diff --git a/internal/coreruleset/coreruleset_test.go b/internal/coreruleset/coreruleset_test.go index f695303..65ef102 100644 --- a/internal/coreruleset/coreruleset_test.go +++ b/internal/coreruleset/coreruleset_test.go @@ -25,14 +25,37 @@ import ( //nolint:gochecknoglobals // a constant cannot be a list var defaultDisabledRules = []int{920340, 920420, 920440, 920640, 930130, 930140} -// newCoreRuleSet returns the Core Rule Set at paranoia level level, with -// the rules in disabled switched off. -func newCoreRuleSet(t *testing.T, level int, disabled ...int) *coreruleset.CoreRuleSet { +// loaded holds a loader of the Core Rule Set for each set of settings +// the tests have asked for, by the settings, so that each is loaded once. +// +//nolint:gochecknoglobals // the tests share it +var loaded = struct { + sync.Mutex + + loaders map[string]func() (*coreruleset.CoreRuleSet, error) +}{loaders: map[string]func() (*coreruleset.CoreRuleSet, error){}} + +// shared returns the Core Rule Set with params, loaded once for every +// test that asks for the same settings. Loading takes most of the time +// the tests take. +func shared(t *testing.T, params coreruleset.Params) *coreruleset.CoreRuleSet { t.Helper() - crs, err := coreruleset.New(coreruleset.Params{ - ParanoiaLevel: level, DisabledRules: disabled, - }) + settings := fmt.Sprintf("%+v", params) + + loaded.Lock() + + load, found := loaded.loaders[settings] + if !found { + load = sync.OnceValues(func() (*coreruleset.CoreRuleSet, error) { + return coreruleset.New(params) + }) + loaded.loaders[settings] = load + } + + loaded.Unlock() + + crs, err := load() if err != nil { t.Fatalf("load the Core Rule Set: %v", err) } @@ -40,6 +63,14 @@ func newCoreRuleSet(t *testing.T, level int, disabled ...int) *coreruleset.CoreR return crs } +// newCoreRuleSet returns the Core Rule Set at paranoia level level, with +// the rules in disabled switched off. +func newCoreRuleSet(t *testing.T, level int, disabled ...int) *coreruleset.CoreRuleSet { + t.Helper() + + return shared(t, coreruleset.Params{ParanoiaLevel: level, DisabledRules: disabled}) +} + // TestConcurrentRequestsKeepTheirOwnExclusions inspects a Drupal and a // WordPress request at once, many times over, on one Core Rule Set, as // sww's server does. Each passes only while its own exclusions apply. @@ -657,14 +688,9 @@ func recordedRequests(t *testing.T, name string) []recordedRequest { func readingBodiesUpTo64K(t *testing.T) *coreruleset.CoreRuleSet { t.Helper() - crs, err := coreruleset.New(coreruleset.Params{ + return shared(t, coreruleset.Params{ ParanoiaLevel: 1, DisabledRules: defaultDisabledRules, BodyLimit: 64<<10 - 1, }) - if err != nil { - t.Fatalf("load the Core Rule Set: %v", err) - } - - return crs } // wordpressAttackTargets returns attack in a query parameter no exclusion @@ -730,19 +756,23 @@ func TestOrdinaryWordPressRequestsPassAndAttacksAreRefused(t *testing.T) { defaults, bodies := atDefaults(t), readingBodiesUpTo64K(t) for _, recorded := range recordedRequests(t, "wordpress-requests.txt") { - r := request{recorded.Method, recorded.Target, recorded.Headers} + t.Run(recorded.Method+" "+recorded.Target, func(t *testing.T) { + t.Parallel() - crsSets := []*coreruleset.CoreRuleSet{defaults} - if recorded.Body != "" { - crsSets = append(crsSets, bodies) - } + r := request{recorded.Method, recorded.Target, recorded.Headers} - for _, crs := range crsSets { - got, _ := inspectBody(t, crs, r, recorded.Body) - if got.Score >= threshold { - t.Errorf("%s %s %.200q refused: %+v", r.method, r.target, recorded.Body, got) + crsSets := []*coreruleset.CoreRuleSet{defaults} + if recorded.Body != "" { + crsSets = append(crsSets, bodies) } - } + + for _, crs := range crsSets { + got, _ := inspectBody(t, crs, r, recorded.Body) + if got.Score >= threshold { + t.Errorf("%s %s %.200q refused: %+v", r.method, r.target, recorded.Body, got) + } + } + }) } for _, attack := range []string{ @@ -1289,14 +1319,9 @@ const injection = "1' OR '1'='1" func readingBodies(t *testing.T) *coreruleset.CoreRuleSet { t.Helper() - crs, err := coreruleset.New(coreruleset.Params{ + return shared(t, coreruleset.Params{ ParanoiaLevel: 1, DisabledRules: defaultDisabledRules, BodyLimit: bodyLimit, }) - if err != nil { - t.Fatalf("load the Core Rule Set: %v", err) - } - - return crs } // post is a POST request for / with a body of the type contentType, a @@ -1581,10 +1606,19 @@ func TestCorazaWritesNoFile(t *testing.T) { // refuses to load, and could not write a file of a multipart body. t.Setenv("TMPDIR", filepath.Join(t.TempDir(), "missing")) + // The test loads its own Core Rule Set, since loading is part of what + // it checks. + crs, err := coreruleset.New(coreruleset.Params{ + ParanoiaLevel: 1, DisabledRules: defaultDisabledRules, BodyLimit: bodyLimit, + }) + if err != nil { + t.Fatalf("load the Core Rule Set: %v", err) + } + body := "--b\r\nContent-Disposition: form-data; name=\"file\"; " + "filename=\"notes.txt\"\r\nContent-Type: text/plain\r\n\r\n" + strings.Repeat("a", 1000) + "\r\n" + field("q", injection) + end - wantBody(t, readingBodies(t), post(multipart), body, matched(942100), body) + wantBody(t, crs, post(multipart), body, matched(942100), body) } func TestBodyLimitOf1GLoads(t *testing.T) { diff --git a/internal/coreruleset/drupal_test.go b/internal/coreruleset/drupal_test.go index 22bebfc..7e80f27 100644 --- a/internal/coreruleset/drupal_test.go +++ b/internal/coreruleset/drupal_test.go @@ -21,18 +21,22 @@ func TestOrdinaryDrupalRequestsPass(t *testing.T) { defaults, bodies := atDefaults(t), readingBodies(t) for _, recorded := range recordedRequests(t, "drupal-requests.txt") { - r := request{recorded.Method, recorded.Target, recorded.Headers} + t.Run(recorded.Method+" "+recorded.Target, func(t *testing.T) { + t.Parallel() - got, _ := inspectBody(t, defaults, r, recorded.Body) - if got.Score >= drupalThreshold { - t.Errorf("%s %s refused: %+v", r.method, r.target, got) - } + r := request{recorded.Method, recorded.Target, recorded.Headers} - got, _ = inspectBody(t, bodies, r, recorded.Body) - if got.Score >= drupalThreshold { - t.Errorf("%s %s %.200q refused while bodies are read: %+v", - r.method, r.target, recorded.Body, got) - } + got, _ := inspectBody(t, defaults, r, recorded.Body) + if got.Score >= drupalThreshold { + t.Errorf("%s %s refused: %+v", r.method, r.target, got) + } + + got, _ = inspectBody(t, bodies, r, recorded.Body) + if got.Score >= drupalThreshold { + t.Errorf("%s %s %.200q refused while bodies are read: %+v", + r.method, r.target, recorded.Body, got) + } + }) } }