Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(experimental): adds seclang parser. #1101

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
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
9 changes: 6 additions & 3 deletions examples/http-server/go.mod
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
module github.com/corazawaf/coraza/v3/examples/http-server

go 1.22
go 1.22.3

require github.com/corazawaf/coraza/v3 v3.2.1
require github.com/corazawaf/coraza/v3 v3.0.0-00010101000000-000000000000

require (
github.com/corazawaf/libinjection-go v0.2.1 // indirect
Expand All @@ -11,8 +11,11 @@ require (
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/valllabh/ocsf-schema-golang v1.0.3 // indirect
golang.org/x/net v0.30.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/tools v0.22.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
rsc.io/binaryregexp v0.2.0 // indirect
)

replace github.com/corazawaf/coraza/v3 => ../../
10 changes: 8 additions & 2 deletions examples/http-server/go.sum
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
github.com/corazawaf/coraza/v3 v3.2.1 h1:zBIji4ut9FtFe8lXdqFwXMAkUoDJZ7HsOlEUYWERLI8=
github.com/corazawaf/coraza/v3 v3.2.1/go.mod h1:fVndCGdUHJWl9c26VZPcORQRzUYwMPnRkC6TyTkhbUg=
github.com/corazawaf/libinjection-go v0.2.1 h1:vNJ7L6c4xkhRgYU6sIO0Tl54TmeCQv/yfxBma30Dy/Y=
github.com/corazawaf/libinjection-go v0.2.1/go.mod h1:OP4TM7xdJ2skyXqNX1AN1wN5nNZEmJNuWbNPOItn7aw=
github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI=
github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
Expand All @@ -17,6 +17,8 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/valllabh/ocsf-schema-golang v1.0.3 h1:eR8k/3jP/OOqB8LRCtdJ4U+vlgd/gk5y3KMXoodrsrw=
github.com/valllabh/ocsf-schema-golang v1.0.3/go.mod h1:sZ3as9xqm1SSK5feFWIR2CuGeGRhsM7TR1MbpBctzPk=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
Expand All @@ -27,5 +29,9 @@ golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
2 changes: 1 addition & 1 deletion examples/http-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (

func exampleHandler(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "text/plain")
resBody := "Hello world, transaction not disrupted."
resBody := "Hello world, transaction not disrupted.\n"

if body := os.Getenv("RESPONSE_BODY"); body != "" {
resBody = body
Expand Down
23 changes: 23 additions & 0 deletions examples/parser/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
module github.com/corazawaf/coraza/v3/examples/parser

go 1.22.3

require (
github.com/corazawaf/coraza/v3 v3.0.0-00010101000000-000000000000
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
)

require (
github.com/corazawaf/libinjection-go v0.2.1 // indirect
github.com/petar-dambovaliev/aho-corasick v0.0.0-20240411101913-e07a1f0e8eb4 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/valllabh/ocsf-schema-golang v1.0.3 // indirect
golang.org/x/net v0.30.0 // indirect
golang.org/x/sync v0.8.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
rsc.io/binaryregexp v0.2.0 // indirect
)

replace github.com/corazawaf/coraza/v3 => ../../
37 changes: 37 additions & 0 deletions examples/parser/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
github.com/corazawaf/libinjection-go v0.2.1 h1:vNJ7L6c4xkhRgYU6sIO0Tl54TmeCQv/yfxBma30Dy/Y=
github.com/corazawaf/libinjection-go v0.2.1/go.mod h1:OP4TM7xdJ2skyXqNX1AN1wN5nNZEmJNuWbNPOItn7aw=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI=
github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=
github.com/petar-dambovaliev/aho-corasick v0.0.0-20240411101913-e07a1f0e8eb4 h1:1Kw2vDBXmjop+LclnzCb/fFy+sgb3gYARwfmoUcQe6o=
github.com/petar-dambovaliev/aho-corasick v0.0.0-20240411101913-e07a1f0e8eb4/go.mod h1:EHPiTAKtiFmrMldLUNswFwfZ2eJIYBHktdaUTZxYWRw=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/valllabh/ocsf-schema-golang v1.0.3 h1:eR8k/3jP/OOqB8LRCtdJ4U+vlgd/gk5y3KMXoodrsrw=
github.com/valllabh/ocsf-schema-golang v1.0.3/go.mod h1:sZ3as9xqm1SSK5feFWIR2CuGeGRhsM7TR1MbpBctzPk=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
17 changes: 17 additions & 0 deletions examples/parser/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package main

import (
"embed"

"github.com/corazawaf/coraza/v3/experimental/seclang"
"github.com/davecgh/go-spew/spew"
)

//go:embed rules/*.conf
var rulesDir embed.FS

func main() {
p := seclang.NewParser(seclang.NewParserConfig().WithRoot(rulesDir))
err := p.FromFile("rules/incorrect.conf")
spew.Dump(err)
}
9 changes: 9 additions & 0 deletions examples/parser/rules/incorrect.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
SecDebugLogLevel 9
SecDebugLog /dev/stdout

SecUnknown LoL

SecRule ARGS:id "@eq 0" "id:1, phase:1,deny, status:403,msg:'Invalid id',log,auditlog"

SecRequestBodyAccess On
SecRule REQUEST_BODY "@contains password" "id:100, phase:2,deny, status:403,msg:'Invalid request body',log,auditlog"
4 changes: 2 additions & 2 deletions experimental/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
## Experimental
# Experimental

This is an experimental Go package that provides some new functionality or feature. It's currently under development and should be considered unstable and subject to change. Use at your own risk.
This is an experimental Go package that provides some new functionality or feature. It's currently under development and should be considered unstable and subject to change. Use at your own risk.
74 changes: 74 additions & 0 deletions experimental/seclang/parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright 2024 Juan Pablo Tosso and the OWASP Coraza contributors
// SPDX-License-Identifier: Apache-2.0

package seclang

import (
"errors"
"io/fs"

"github.com/corazawaf/coraza/v3/internal/corazawaf"
"github.com/corazawaf/coraza/v3/internal/io"
"github.com/corazawaf/coraza/v3/internal/seclang"
)

// Parser is an interface for parsing SecLang rules
type Parser interface {
FromFile(string) error
FromString(string) error
}

type parser struct {
Parser
}

func unwrapErr(err error) error {
if err == nil {
return nil
}

if uErr := errors.Unwrap(err); uErr != nil {
if pErr, ok := uErr.(seclang.ParsingError); ok {
return pErr
}
}

return err
}

func (p parser) FromFile(profilePath string) error {
return unwrapErr(p.Parser.FromFile(profilePath))
}

func (p parser) FromString(data string) error {
return unwrapErr(p.Parser.FromString(data))
}

// ParserConfig is an interface for configuring the parser
type ParserConfig interface {
WithRoot(root fs.FS) ParserConfig
}

// NewParser creates a new SecLang parser
func NewParser(config ParserConfig) Parser {
p := seclang.NewParser(corazawaf.NewWAF())
p.SetRoot(config.(*parserConfig).root)
return parser{p}
}

type parserConfig struct {
root fs.FS
}

func (c *parserConfig) WithRoot(root fs.FS) ParserConfig {
ret := &parserConfig{}
ret.root = root
return ret
}

// NewParserConfig creates a new parser configuration
func NewParserConfig() ParserConfig {
return &parserConfig{
root: io.OSFS{},
}
}
1 change: 1 addition & 0 deletions go.work
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ go 1.22.3
use (
.
./examples/http-server
./examples/parser
./testing/coreruleset
)
26 changes: 20 additions & 6 deletions internal/seclang/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ type Parser struct {
includeCount int
}

type ParsingError struct {
Line int
File string
Err error
}

func (e ParsingError) Error() string {
return fmt.Sprintf("syntax error, %s on %s:%d", e.Err.Error(), e.File, e.Line)
}

// FromFile imports directives from a file
// It will return error if any directive fails to parse
// or the file does not exist.
Expand Down Expand Up @@ -61,15 +71,15 @@ func (p *Parser) FromFile(profilePath string) error {
// we don't use defer for this as tinygo does not seem to like it
p.currentDir = originalDir
p.currentFile = ""
return fmt.Errorf("failed to readfile: %s", err.Error())
return fmt.Errorf("failed to read file: %s", err.Error())
}

err = p.parseString(string(file))
if err != nil {
// we don't use defer for this as tinygo does not seem to like it
p.currentDir = originalDir
p.currentFile = ""
return fmt.Errorf("failed to parse string: %s", err.Error())
return fmt.Errorf("failed to parse string: %w", err)
}
// restore the lastDir post processing all includes
p.currentDir = lastDir
Expand All @@ -92,10 +102,14 @@ func (p *Parser) FromString(data string) error {
return err
}

// parseString parses a string and evaluates each line
// It will return a ParsingError if any directive fails to parse
func (p *Parser) parseString(data string) error {
scanner := bufio.NewScanner(strings.NewReader(data))
var linebuffer strings.Builder
inBackticks := false
var (
linebuffer strings.Builder
inBackticks bool
)
for scanner.Scan() {
p.currentLine++
line := strings.TrimSpace(scanner.Text())
Expand Down Expand Up @@ -129,13 +143,13 @@ func (p *Parser) parseString(data string) error {
linebuffer.WriteString(line)
err := p.evaluateLine(linebuffer.String())
if err != nil {
return err
return ParsingError{Line: p.currentLine, File: p.currentFile, Err: err}
}
linebuffer.Reset()
}
}
if inBackticks {
return errors.New("backticks left open")
return ParsingError{Line: p.currentLine, File: p.currentFile, Err: errors.New("backticks left open")}
}
return nil
}
Expand Down
14 changes: 7 additions & 7 deletions internal/seclang/rule_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
package seclang

import (
"errors"
"reflect"
"strings"
"testing"
Expand Down Expand Up @@ -111,19 +110,20 @@ func TestSecRuleUpdateTargetVariableNegation(t *testing.T) {
SecRule REQUEST_URI|REQUEST_COOKIES "abc" "id:8,phase:2"
SecRuleUpdateTargetById 8 "!REQUEST_HEADERS:"
`)
expectedErr := errors.New("unknown variable")
if errors.Unwrap(err).Error() != expectedErr.Error() {
t.Fatalf("unexpexted error, want %q, have %q", expectedErr, errors.Unwrap(err).Error())

expectedErrMsg := "unknown variable"
if !strings.Contains(err.Error(), expectedErrMsg) {
t.Fatalf("unexpexted error, want to contain %q, have %q", expectedErrMsg, err)
}

// Try to update undefined rule
err = p.FromString(`
SecRule REQUEST_URI|REQUEST_COOKIES "abc" "id:9,phase:2"
SecRuleUpdateTargetById 99 "!REQUEST_HEADERS:xyz"
`)
expectedErr = errors.New("SecRuleUpdateTargetById: rule \"99\" not found")
if errors.Unwrap(err).Error() != expectedErr.Error() {
t.Fatalf("unexpected error, want %q, have %q", expectedErr, errors.Unwrap(err).Error())
expectedErrMsg = "SecRuleUpdateTargetById: rule \"99\" not found"
if !strings.Contains(err.Error(), expectedErrMsg) {
t.Fatalf("unexpexted error, want to contain %q, have %q", expectedErrMsg, err)
}
}

Expand Down
Loading