Skip to content

Commit b04e185

Browse files
author
Syin Wu
authored
Merge pull request #361 from corazawaf/seclang-datasets
add dataset support
2 parents 4d6009f + c7ec811 commit b04e185

File tree

9 files changed

+173
-14
lines changed

9 files changed

+173
-14
lines changed

SECURITY.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ Versions currently being supported with security updates.
66

77
| Version | Supported | EOL |
88
| ------- | ------------------ | ------------- |
9-
| v1.2.x | :white_check_mark: | Jun 1st 2022 |
10-
| v2.x | :white_check_mark: | Not defined |
9+
| v1.2.x | :x: | Jun 1st 2022 |
10+
| v2.x | :white_check_mark: | TBD |
11+
| v3.x | :white_check_mark: | Not defined |
1112

1213
## Reporting a Vulnerability
1314

go.mod

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,20 @@ module github.com/corazawaf/coraza/v3
22

33
go 1.18
44

5+
// Testing dependencies:
6+
// - go-mockdns
7+
// - go-modsecurity (optional)
8+
9+
// Development dependencies:
10+
// - mage
11+
12+
// Build dependencies:
13+
// - libinjection-go
14+
// - aho-corasick
15+
16+
// Tinygo dependencies:
17+
// - gjson
18+
519
require (
620
github.com/anuraaga/go-modsecurity v0.0.0-20220824035035-b9a4099778df
721
github.com/corazawaf/libinjection-go v0.0.0-20220207031228-44e9c4250eb5

operators/pm_from_dataset.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package operators
5+
6+
import (
7+
"fmt"
8+
9+
"github.com/corazawaf/coraza/v3"
10+
ahocorasick "github.com/petar-dambovaliev/aho-corasick"
11+
)
12+
13+
// TODO according to coraza researchs, re2 matching is faster than ahocorasick
14+
// maybe we should switch in the future
15+
// pmFromDataset is always lowercase
16+
type pmFromDataset struct {
17+
matcher ahocorasick.AhoCorasick
18+
}
19+
20+
func (o *pmFromDataset) Init(options coraza.RuleOperatorOptions) error {
21+
data := options.Arguments
22+
dataset, ok := options.Datasets[data]
23+
if !ok {
24+
return fmt.Errorf("Dataset %q not found", data)
25+
}
26+
builder := ahocorasick.NewAhoCorasickBuilder(ahocorasick.Opts{
27+
AsciiCaseInsensitive: true,
28+
MatchOnlyWholeWords: false,
29+
MatchKind: ahocorasick.LeftMostLongestMatch,
30+
DFA: true,
31+
})
32+
33+
// TODO this operator is supposed to support snort data syntax: "@pmFromDataset A|42|C|44|F"
34+
o.matcher = builder.Build(dataset)
35+
return nil
36+
}
37+
38+
func (o *pmFromDataset) Evaluate(tx *coraza.Transaction, value string) bool {
39+
if tx.Capture {
40+
matches := o.matcher.FindAll(value)
41+
for i, match := range matches {
42+
if i == 10 {
43+
return true
44+
}
45+
tx.CaptureField(i, value[match.Start():match.End()])
46+
}
47+
return len(matches) > 0
48+
}
49+
iter := o.matcher.Iter(value)
50+
return iter.Next() != nil
51+
}
52+
53+
var _ coraza.RuleOperator = (*pmFromDataset)(nil)

operators/pm_from_dataset_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package operators
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"testing"
10+
11+
"github.com/corazawaf/coraza/v3"
12+
)
13+
14+
func TestPmFromDataset(t *testing.T) {
15+
pm := &pmFromDataset{}
16+
opts := coraza.RuleOperatorOptions{
17+
Arguments: "test_1",
18+
Datasets: map[string][]string{
19+
"test_1": {"test_1", "test_2"},
20+
},
21+
}
22+
23+
if err := pm.Init(opts); err != nil {
24+
t.Error(err)
25+
}
26+
waf := coraza.NewWaf()
27+
tx := waf.NewTransaction(context.Background())
28+
tx.Capture = true
29+
res := pm.Evaluate(tx, "test_1")
30+
if !res {
31+
t.Error("pmFromDataset failed")
32+
}
33+
opts.Datasets = map[string][]string{}
34+
if err := pm.Init(opts); err == nil {
35+
t.Error(fmt.Errorf("pmFromDataset should have failed"))
36+
}
37+
}

rule.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ type RuleOperatorOptions struct {
5252

5353
// Path is used to store a list of possible data paths
5454
Path []string
55+
56+
// Datasets contains input datasets or dictionaries
57+
Datasets map[string][]string
5558
}
5659

5760
// RuleOperator interface is used to define rule @operators

seclang/directives.go

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@ import (
1818

1919
// DirectiveOptions contains the parsed options for a directive
2020
type DirectiveOptions struct {
21-
Waf *coraza.Waf
22-
Config types.Config
23-
Opts string
24-
Path []string
21+
Waf *coraza.Waf
22+
Config types.Config
23+
Opts string
24+
Path []string
25+
Datasets map[string][]string
2526
}
2627

2728
type directive = func(options *DirectiveOptions) error
@@ -470,6 +471,28 @@ func directiveSecIgnoreRuleCompilationErrors(options *DirectiveOptions) error {
470471
return nil
471472
}
472473

474+
func directiveSecDataset(options *DirectiveOptions) error {
475+
spl := strings.SplitN(options.Opts, " ", 2)
476+
if len(spl) != 2 {
477+
return errors.New("syntax error: SecDataset name `\n...\n`")
478+
}
479+
name := spl[0]
480+
if _, ok := options.Datasets[name]; ok {
481+
options.Waf.Logger.Warn("Dataset %s already exists, overwriting", name)
482+
}
483+
arr := []string{}
484+
data := strings.Trim(spl[1], "`")
485+
for _, s := range strings.Split(data, "\n") {
486+
s = strings.TrimSpace(s)
487+
if s == "" || s[0] == '#' {
488+
continue
489+
}
490+
arr = append(arr, s)
491+
}
492+
options.Datasets[name] = arr
493+
return nil
494+
}
495+
473496
func newCompileRuleError(err error, opts string) error {
474497
return fmt.Errorf("failed to compile rule (%s): %s", err, opts)
475498
}
@@ -573,6 +596,7 @@ var directivesMap = map[string]directive{
573596
"secauditlogfilemode": directiveSecAuditLogFileMode,
574597
"secauditlogdirmode": directiveSecAuditLogDirMode,
575598
"secignorerulecompilationerrors": directiveSecIgnoreRuleCompilationErrors,
599+
"secdataset": directiveSecDataset,
576600

577601
// Unsupported Directives
578602
"secargumentseparator": directiveUnsupported,

seclang/directives_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,19 @@ func TestInvalidRulesWithIgnoredErrors(t *testing.T) {
181181
t.Error("failed to error on invalid rule")
182182
}
183183
}
184+
185+
func TestSecDataset(t *testing.T) {
186+
waf := coraza.NewWaf()
187+
p, _ := NewParser(waf)
188+
if err := p.FromString("" +
189+
"SecDataset test `\n123\n456\n`\n"); err != nil {
190+
t.Error(err)
191+
}
192+
ds := p.options.Datasets["test"]
193+
if len(ds) != 2 {
194+
t.Errorf("failed to add dataset, got %d records", len(ds))
195+
}
196+
if ds[0] != "123" || ds[1] != "456" {
197+
t.Error("failed to add dataset")
198+
}
199+
}

seclang/parser.go

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -76,19 +76,29 @@ func (p *Parser) FromString(data string) error {
7676
scanner := bufio.NewScanner(strings.NewReader(data))
7777
var linebuffer = ""
7878
pattern := regexp.MustCompile(`\\(\s+)?$`)
79+
inQuotes := false
7980
for scanner.Scan() {
8081
p.currentLine++
81-
line := scanner.Text()
82-
linebuffer += strings.TrimSpace(line)
82+
line := strings.TrimSpace(scanner.Text())
83+
if !inQuotes && len(line) > 0 && line[len(line)-1] == '`' {
84+
inQuotes = true
85+
} else if inQuotes && len(line) > 0 && line[0] == '`' {
86+
inQuotes = false
87+
}
88+
if inQuotes {
89+
linebuffer += line + "\n"
90+
} else {
91+
linebuffer += line
92+
}
93+
8394
// Check if line ends with \
84-
match := pattern.MatchString(line)
85-
if !match {
95+
if !pattern.MatchString(line) && !inQuotes {
8696
err := p.evaluate(linebuffer)
8797
if err != nil {
8898
return err
8999
}
90100
linebuffer = ""
91-
} else {
101+
} else if !inQuotes {
92102
linebuffer = strings.TrimSuffix(linebuffer, "\\")
93103
}
94104
}
@@ -158,8 +168,9 @@ func NewParser(waf *coraza.Waf) (*Parser, error) {
158168
}
159169
p := &Parser{
160170
options: &DirectiveOptions{
161-
Waf: waf,
162-
Config: make(types.Config),
171+
Waf: waf,
172+
Config: make(types.Config),
173+
Datasets: make(map[string][]string),
163174
},
164175
}
165176
return p, nil

sonar-project.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ sonar.organization=jptosso
33

44
# This is the name and version displayed in the SonarCloud UI.
55
sonar.projectName=coraza-waf
6-
sonar.projectVersion=v2.0.0-dev
6+
sonar.projectVersion=v3.0.0-dev
77

88
# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows.
99
#sonar.sources=.

0 commit comments

Comments
 (0)