Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/go.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.22.2'
go-version: '1.26.2'

- name: Test
run: go run mage.go test
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.so
coverage.*
33 changes: 22 additions & 11 deletions configstore/configstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ type modelPluginConfig struct {
remote bool
training bool
TrainingData TrainingData
sanitize bool
}

// DecisionPluginConfig stores the configuration of a decision plugin
Expand All @@ -95,12 +96,13 @@ type decisionPluginConfig struct {

// ConfigStore stores all wacecore configuration from the config file.
type ConfigStore struct {
ModelPlugins map[string]modelPluginConfig
DecisionPlugins map[string]decisionPluginConfig
LogPath string
LogLevel logging.LogLevel
NatsURL string
ApplicationId string
ModelPlugins map[string]modelPluginConfig
DecisionPlugins map[string]decisionPluginConfig
LogPath string
LogLevel logging.LogLevel
NatsURL string
ApplicationId string
CredentialHeaders []string
}

var config *ConfigStore
Expand Down Expand Up @@ -138,6 +140,7 @@ type configFileModelPlugin struct {
Remote bool
Training bool
TrainingData TrainingData `yaml:"training_data"`
Sanitize bool
}

type configFileDecisionPlugin struct {
Expand All @@ -147,11 +150,12 @@ type configFileDecisionPlugin struct {
}

type ConfigFileData struct {
Logpath string
Loglevel string
Modelplugins []configFileModelPlugin
Decisionplugins []configFileDecisionPlugin
NatsURL string
Logpath string
Loglevel string
Modelplugins []configFileModelPlugin
Decisionplugins []configFileDecisionPlugin
NatsURL string
CredentialHeaders []string `yaml:"credential_headers"`
}

// IsAsync returns true if the model plugin is async
Expand All @@ -169,6 +173,10 @@ func (c *ConfigStore) IsInTraining(modelID string) bool {
return c.ModelPlugins[modelID].training
}

func (c *ConfigStore) ShouldSanitize(modelID string) bool {
return c.ModelPlugins[modelID].sanitize
}

// CheckLogging verifies if the log path is valid
func checkLogging(inConf ConfigFileData) error {
// check logpath
Expand Down Expand Up @@ -258,6 +266,7 @@ func (cs *ConfigStore) SetConfig(inConf ConfigFileData) error {
modelConfig.remote = modelP.Remote
modelConfig.training = modelP.Training
modelConfig.TrainingData = modelP.TrainingData
modelConfig.sanitize = modelP.Sanitize
if err != nil {
return err
}
Expand All @@ -275,5 +284,7 @@ func (cs *ConfigStore) SetConfig(inConf ConfigFileData) error {

cs.NatsURL = inConf.NatsURL

cs.CredentialHeaders = inConf.CredentialHeaders

return nil
}
101 changes: 101 additions & 0 deletions configstore/configstore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package configstore
import (
"fmt"
"os"
"reflect"
"testing"

"gopkg.in/yaml.v3"
Expand Down Expand Up @@ -675,6 +676,106 @@ modelplugins:
}
}

func TestShouldSanitize(t *testing.T) {
tests := []struct {
name string
sanitize bool
wantSanitize bool
}{
{"sanitize omitted defaults to false", false, false},
{"sanitize: true propagates correctly", true, true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cs, err := New()
if err != nil {
t.Fatal(err)
}
defer Clean()

config := fmt.Sprintf(`---
loglevel: ERROR
logpath: /dev/null
modelplugins:
- id: "testplugin"
path: "../testdata/plugins/model/trivial.so"
plugintype: "RequestHeaders"
sanitize: %v
`, tt.sanitize)
if err := initialize([]byte(config)); err != nil {
t.Fatalf("initialize: %v", err)
}

if got := cs.ShouldSanitize("testplugin"); got != tt.wantSanitize {
t.Errorf("ShouldSanitize = %v, want %v", got, tt.wantSanitize)
}
})
}
}

func TestShouldSanitizeUnknownModel(t *testing.T) {
cs, err := New()
if err != nil {
t.Fatal(err)
}
defer Clean()

if err := initialize(validConfig); err != nil {
t.Fatalf("initialize: %v", err)
}

if cs.ShouldSanitize("nonexistent") {
t.Error("ShouldSanitize for unknown model ID should return false")
}
}

func TestCredentialHeaders(t *testing.T) {
tests := []struct {
name string
config string
wantHeaders []string
}{
{
name: "no credential_headers field defaults to nil",
config: `---
loglevel: ERROR
logpath: /dev/null
`,
wantHeaders: nil,
},
{
name: "credential_headers values are stored",
config: `---
loglevel: ERROR
logpath: /dev/null
credential_headers:
- x-api-key
- x-secret-token
`,
wantHeaders: []string{"x-api-key", "x-secret-token"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cs, err := New()
if err != nil {
t.Fatal(err)
}
defer Clean()

if err := initialize([]byte(tt.config)); err != nil {
t.Fatalf("initialize: %v", err)
}

if !reflect.DeepEqual(cs.CredentialHeaders, tt.wantHeaders) {
t.Errorf("CredentialHeaders = %v, want %v", cs.CredentialHeaders, tt.wantHeaders)
}
})
}
}

func TestNatsURL(t *testing.T) {
tests := []struct {
name string
Expand Down
46 changes: 46 additions & 0 deletions sanitize.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package wace

import (
"regexp"
"slices"
"strings"

"github.com/tilsor/ModSecIntl_wace_lib/waceapi"
)

const passwordRegex = `(?i)((new|old|form_|)(v)?password|clave|pass(|2))=([^*&\n]*)`

var (
credentialRegex = regexp.MustCompile(passwordRegex)
credentialHeaders = []string{"authorization", "cookie", "set-cookie"}
)

// sanitizeCredentials replaces password-like fields in the request body and
// known credential headers (Authorization, Cookie, Set-Cookie) with "********".
func sanitizeCredentials(p waceapi.HTTPPayload) waceapi.HTTPPayload {
p.RequestBody = string(credentialRegex.ReplaceAll([]byte(p.RequestBody), []byte("$1=********")))

p.URI = string(credentialRegex.ReplaceAll([]byte(p.URI), []byte("$1=********")))

for i := range p.RequestHeaders {
if slices.Contains(credentialHeaders, strings.ToLower(p.RequestHeaders[i].Key)) {
p.RequestHeaders[i] = waceapi.HTTPHeader{Key: p.RequestHeaders[i].Key, Value: "********"}
}
}

for i := range p.ResponseHeaders {
if slices.Contains(credentialHeaders, strings.ToLower(p.ResponseHeaders[i].Key)) {
p.ResponseHeaders[i] = waceapi.HTTPHeader{Key: p.ResponseHeaders[i].Key, Value: "********"}
}
}

return p
}

// setCredentialHeaders allows the user to change the list of headers to be sanitized.
func setCredentialHeaders(headers []string) {
for i := range headers {
headers[i] = strings.ToLower(headers[i])
}
credentialHeaders = headers
}
Loading
Loading