diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..65f96759a --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +testdata/geoip_db/IP2LOCATION-LITE-DB11.IPV6.BIN filter=lfs diff=lfs merge=lfs -text +testdata/geoip_db/GeoLite2-City.mmdb filter=lfs diff=lfs merge=lfs -text diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d16373b09..a16b35afc 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -20,6 +20,9 @@ jobs: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.43.0 go install golang.org/x/lint/golint@latest - uses: actions/checkout@v2 + with: + lfs: true + fetch-depth: 0 - uses: actions/setup-python@v2 - uses: pre-commit/action@v2.0.3 with: diff --git a/go.mod b/go.mod index ea8c403fd..de655fc13 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,8 @@ go 1.16 require ( github.com/cloudflare/ahocorasick v0.0.0-20210425175752-730270c3e184 github.com/corazawaf/libinjection-go v0.0.0-20220207031228-44e9c4250eb5 + github.com/ip2location/ip2location-go/v9 v9.2.0 + github.com/oschwald/geoip2-golang v1.6.1 go.uber.org/zap v1.20.0 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 gopkg.in/yaml.v2 v2.4.0 diff --git a/go.sum b/go.sum index 2b06e8bd3..112db8962 100644 --- a/go.sum +++ b/go.sum @@ -7,17 +7,24 @@ github.com/corazawaf/libinjection-go v0.0.0-20220207031228-44e9c4250eb5/go.mod h github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/ip2location/ip2location-go/v9 v9.2.0 h1:ZWURUspTpe2gxMQGtcQScocBd73gZKUEWlHCFUJYLHk= +github.com/ip2location/ip2location-go/v9 v9.2.0/go.mod h1:s5SV6YZL10TpfPpXw//7fEJC65G/yH7Oh+Tjq9JcQEQ= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/oschwald/geoip2-golang v1.6.1 h1:GKxT3yaWWNXSb7vj6D7eoJBns+lGYgx08QO0UcNm0YY= +github.com/oschwald/geoip2-golang v1.6.1/go.mod h1:xdvYt5xQzB8ORWFqPnqMwZpCpgNagttWdoZLlJQzg7s= +github.com/oschwald/maxminddb-golang v1.8.0 h1:Uh/DSnGoxsyp/KYbY1AuP0tYEwfs0sCph9p/UMXK/Hk= +github.com/oschwald/maxminddb-golang v1.8.0/go.mod h1:RXZtst0N6+FY/3qCNmZMBApR19cdQj43/NM9VkrNAis= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= @@ -42,8 +49,10 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191224085550-c709ea063b76/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/operators/eq.go b/operators/eq.go index d55ac0ac0..7a9c47791 100644 --- a/operators/eq.go +++ b/operators/eq.go @@ -34,11 +34,11 @@ func (o *eq) Init(data string) error { } func (o *eq) Evaluate(tx *coraza.Transaction, value string) bool { - d1, err := strconv.Atoi(o.data.Expand(tx)) + d1, err := strconv.ParseFloat(o.data.Expand(tx), 64) if err != nil { d1 = 0 } - d2, err := strconv.Atoi(value) + d2, err := strconv.ParseFloat(value, 64) if err != nil { d2 = 0 } diff --git a/operators/ge.go b/operators/ge.go index bdbcff9fd..9a9949ea2 100644 --- a/operators/ge.go +++ b/operators/ge.go @@ -35,14 +35,13 @@ func (o *ge) Init(data string) error { } func (o *ge) Evaluate(tx *engine.Transaction, value string) bool { - v, err := strconv.Atoi(value) + v, err := strconv.ParseFloat(value, 64) if err != nil { v = 0 } - data := o.data.Expand(tx) - dataint, err := strconv.Atoi(data) + d, err := strconv.ParseFloat(o.data.Expand(tx), 64) if err != nil { - dataint = 0 + d = 0 } - return v >= dataint + return v >= d } diff --git a/operators/geo_lookup.go b/operators/geo_lookup.go index 1df68aa5b..988b52905 100644 --- a/operators/geo_lookup.go +++ b/operators/geo_lookup.go @@ -16,6 +16,19 @@ package operators import ( "github.com/corazawaf/coraza/v2" + "github.com/corazawaf/coraza/v2/types/variables" + "strconv" +) + +const ( + countryCode = "country_code" + countryName = "country_name" + countryContinent = "country_continent" + region = "region" + city = "city" + postalCode = "postal_code" + latitude = "latitude" + longitude = "longitude" ) type geoLookup struct{} @@ -25,7 +38,19 @@ func (o *geoLookup) Init(data string) error { } func (o *geoLookup) Evaluate(tx *coraza.Transaction, value string) bool { - // kept for compatibility, it requires a plugin. + r, err := tx.Waf.GeoDB.Get(value) + if err != nil { + return false + } + + tx.GetCollection(variables.Geo).Set(countryCode, []string{r.CountryCode}) + tx.GetCollection(variables.Geo).Set(countryName, []string{r.CountryName}) + tx.GetCollection(variables.Geo).Set(countryContinent, []string{r.CountryContinent}) + tx.GetCollection(variables.Geo).Set(region, []string{r.Region}) + tx.GetCollection(variables.Geo).Set(city, []string{r.City}) + tx.GetCollection(variables.Geo).Set(postalCode, []string{r.PostalCode}) + tx.GetCollection(variables.Geo).Set(latitude, []string{strconv.FormatFloat(r.Latitude, 'f', 10, 64)}) + tx.GetCollection(variables.Geo).Set(longitude, []string{strconv.FormatFloat(r.Longitude, 'f', 10, 64)}) return true } diff --git a/operators/geo_lookup_test.go b/operators/geo_lookup_test.go new file mode 100644 index 000000000..35f9654bd --- /dev/null +++ b/operators/geo_lookup_test.go @@ -0,0 +1,69 @@ +package operators + +import ( + engine "github.com/corazawaf/coraza/v2" + "github.com/corazawaf/coraza/v2/types/variables" + "github.com/corazawaf/coraza/v2/utils/geoip" + "strconv" + "testing" +) + +func Test_geoLookup(t *testing.T) { + geoLookupTest := geoLookup{} + if err := geoLookupTest.Init(""); err != nil { + t.Errorf("Error init geolookup") + } + + var err error + waf := engine.NewWaf() + db := &geoip.MaxMinddb{} + err = db.Init("../testdata/geoip_db/GeoLite2-City.mmdb") + if err != nil { + t.Error(err) + } + waf.GeoDB = db + if err != nil { + t.Errorf("geoip db init error: %s", err) + } + + tx := waf.NewTransaction() + if !geoLookupTest.Evaluate(tx, "81.2.69.142") { + t.Errorf("Invalid result for @geoLookup operator") + } + + if tx.GetCollection(variables.Geo).GetFirstString(countryCode) != "GB" { + t.Errorf("Invalid `COUNTRY_CODE` key for @geoLookup operator") + } + + if tx.GetCollection(variables.Geo).GetFirstString(countryName) != "United Kingdom" { + t.Errorf("Invalid `COUNTRY_NAME` key for @geoLookup operator") + } + + if tx.GetCollection(variables.Geo).GetFirstString(countryContinent) != "Europe" { + t.Errorf("Invalid `COUNTRY_CONTINENT` key for @geoLookup operator") + } + + if tx.GetCollection(variables.Geo).GetFirstString(city) != "Chingford" { + t.Errorf("Invalid `CITY` key for @geoLookup operator") + } + + if tx.GetCollection(variables.Geo).GetFirstString(postalCode) != "E4" { + t.Errorf("Invalid `POSTAL_CODE` key for @geoLookup operator") + } + + latitude, err := strconv.ParseFloat(tx.GetCollection(variables.Geo).GetFirstString(latitude), 64) + if err != nil { + t.Errorf("latitude data parse error: %s", err.Error()) + } + if latitude != 51.6311 { + t.Errorf("Invalid `LATITUDE` key for @geoLookup operator") + } + + longitude, err := strconv.ParseFloat(tx.GetCollection(variables.Geo).GetFirstString(longitude), 64) + if err != nil { + t.Errorf("longitude data parse error: %s", err.Error()) + } + if longitude != 0.0018 { + t.Errorf("Invalid `LONGITUDE` key for @geoLookup operator") + } +} diff --git a/operators/gt.go b/operators/gt.go index 3cf4b0ab8..4d07630e7 100644 --- a/operators/gt.go +++ b/operators/gt.go @@ -34,14 +34,13 @@ func (o *gt) Init(data string) error { } func (o *gt) Evaluate(tx *coraza.Transaction, value string) bool { - v, err := strconv.Atoi(value) + v, err := strconv.ParseFloat(value, 64) if err != nil { v = 0 } - data := o.data.Expand(tx) - k, err := strconv.Atoi(data) + d, err := strconv.ParseFloat(o.data.Expand(tx), 64) if err != nil { - k = 0 + d = 0 } - return k < v + return d < v } diff --git a/operators/le.go b/operators/le.go index 32a8be7b5..307e74469 100644 --- a/operators/le.go +++ b/operators/le.go @@ -34,9 +34,11 @@ func (o *le) Init(data string) error { } func (o *le) Evaluate(tx *coraza.Transaction, value string) bool { - data := o.data.Expand(tx) - d, _ := strconv.Atoi(data) - v, err := strconv.Atoi(value) + d, err := strconv.ParseFloat(o.data.Expand(tx), 64) + if err != nil { + d = 0 + } + v, err := strconv.ParseFloat(value, 64) if err != nil { v = 0 } diff --git a/operators/lt.go b/operators/lt.go index f0a6bbb78..14124458c 100644 --- a/operators/lt.go +++ b/operators/lt.go @@ -34,12 +34,11 @@ func (o *lt) Init(data string) error { } func (o *lt) Evaluate(tx *coraza.Transaction, value string) bool { - vv := o.data.Expand(tx) - data, err := strconv.Atoi(vv) + data, err := strconv.ParseFloat(o.data.Expand(tx), 64) if err != nil { data = 0 } - v, err := strconv.Atoi(value) + v, err := strconv.ParseFloat(value, 64) if err != nil { v = 0 } diff --git a/seclang/directives.go b/seclang/directives.go index 8aeb79a45..37ffe5015 100644 --- a/seclang/directives.go +++ b/seclang/directives.go @@ -17,6 +17,7 @@ package seclang import ( "errors" "fmt" + "github.com/corazawaf/coraza/v2/utils/geoip" "io/fs" "regexp" "strconv" @@ -251,6 +252,40 @@ func directiveSecGsbLookupDb(w *coraza.Waf, opts string) error { return nil } +func directiveSecGeoLookupDb(w *coraza.Waf, opts string) error { + spl := strings.Split(opts, " ") + if len(spl) > 2 { + return fmt.Errorf("error configuration for SecGeoLookupDb directive") + } + if w.Config.Get("geoip_file_type", "non-configure") == spl[0] && + w.Config.Get("geoip_file", "non-configure") == spl[1] { + return nil + } + w.Config.Set("geoip_file_type", spl[0]) + w.Config.Set("geoip_file", spl[1]) + + var db geoip.GeoDb + switch spl[0] { + case "maxminddb": + db = &geoip.MaxMinddb{} + err := db.Init(spl[1]) + if err != nil { + return err + } + case "ip2location": + db = &geoip.IP2Location{} + err := db.Init(spl[1]) + if err != nil { + return err + } + default: + return fmt.Errorf("invalid geoip engine: %s", spl[0]) + } + + w.GeoDB = db + return nil +} + func directiveSecHashMethodPm(w *coraza.Waf, opts string) error { return nil } @@ -537,6 +572,7 @@ var ( _ directive = directiveSecRemoteRules _ directive = directiveSecSensorID _ directive = directiveSecRuleUpdateTargetByID + _ directive = directiveSecGeoLookupDb ) var directivesMap = map[string]directive{ @@ -575,6 +611,7 @@ var directivesMap = map[string]directive{ "sechashkey": directiveSecHashKey, "sechashengine": directiveSecHashEngine, "secgsblookupdb": directiveSecGsbLookupDb, + "secgeolookupdb": directiveSecGeoLookupDb, "secdefaultaction": directiveSecDefaultAction, "secdatadir": directiveSecDataDir, "seccontentinjection": directiveSecContentInjection, diff --git a/seclang/rules_test.go b/seclang/rules_test.go index a210f36e1..48a41a3b4 100644 --- a/seclang/rules_test.go +++ b/seclang/rules_test.go @@ -432,3 +432,78 @@ func TestDirectiveSecAuditLog(t *testing.T) { t.Errorf("Why is the number of matches %d", c) } } + +// https://github.com/corazawaf/coraza/issues/160 +func TestIssue160(t *testing.T) { + waf := coraza.NewWaf() + parser, err := NewParser(waf) + if err != nil { + t.Error(err) + } + + // test case 3 + err = parser.FromString(` + SecRequestBodyAccess On + SecAction "id:900330,phase:1,nolog,pass,t:none,setvar:tx.total_arg_length=10" + + SecRule &TX:TOTAL_ARG_LENGTH "@eq 1" "id:920390,phase:2,deny,t:none,msg:'Total arguments size exceeded',logdata:'%{MATCHED_VAR_NAME}=%{MATCHED_VAR}',ver:'OWASP_CRS/3.4.0-dev',severity:'CRITICAL',chain" + SecRule ARGS_COMBINED_SIZE "@gt %{tx.total_arg_length}" "t:none" + `) + if err != nil { + t.Error(err) + } + + tx := waf.NewTransaction() + reader := strings.NewReader(`POST / HTTP/1.1 +Host: localhost +User-Agent: curl/7.77.0 +Accept: */* +Content-Length: 154 +Content-Type: application/x-www-form-urlencoded + +foo=111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111`) + it, err := tx.ParseRequestReader(reader) + if err != nil { + t.Error(err) + } + + if it.RuleID != 920390 { + t.Error("Failed to test limit arguments") + } + + // test case 4 + err = parser.FromString(` + SecRequestBodyAccess On + SecAction "id:900350,phase:1,nolog,pass,t:none,setvar:tx.combined_file_sizes=10" + + SecRule &TX:COMBINED_FILE_SIZES "@eq 1" "id:920410,phase:2,deny,t:none,msg:'Total uploaded files size too large',logdata:'%{MATCHED_VAR_NAME}=%{MATCHED_VAR}',ver:'OWASP_CRS/3.4.0-dev',severity:'CRITICAL',chain" + SecRule FILES_COMBINED_SIZE "@gt %{tx.combined_file_sizes}" "t:none,setvar:'tx.anomaly_score_pl1=+%{tx.critical_anomaly_score}'" + `) + if err != nil { + t.Log(err) + } + + tx = waf.NewTransaction() + reader = strings.NewReader(`POST / HTTP/1.1 +Host: localhost +User-Agent: curl/7.77.0 +Accept: */* +Content-Length: 284 +Content-Type: multipart/form-data; boundary=------------------------6e0011d57082257a + +--------------------------6e0011d57082257a +Content-Disposition: form-data; name="file"; filename="1.txt" +Content-Type: text/plain + +qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq + +--------------------------6e0011d57082257a--`) + it, err = tx.ParseRequestReader(reader) + if err != nil { + t.Error(err) + } + + if it.RuleID != 920410 { + t.Error("Failed to test limit file size") + } +} diff --git a/testdata/engine/geoip_ip2location.yaml b/testdata/engine/geoip_ip2location.yaml new file mode 100644 index 000000000..b31defc0f --- /dev/null +++ b/testdata/engine/geoip_ip2location.yaml @@ -0,0 +1,43 @@ +--- +meta: + author: Xinyu Wu + description: This is a mix of many tests used to check geoip ip2location engine. + enabled: true + name: geoip_ip2location.yaml +tests: + - + test_title: geoip rules test + stages: + - + stage: + debug: true + input: + dest_addr: "81.2.69.142" + output: + triggered_rules: + - 8 + - 11 + non_triggered_rules: + - 9 + - 10 + - 12 + - 13 +rules: | + SecGeoLookupDb ip2location ../testdata/geoip_db/IP2LOCATION-LITE-DB11.IPV6.BIN + SecRule SERVER_ADDR "@geoLookup" "chain,id:8,log,phase:1" + SecRule GEO:COUNTRY_CODE "@streq GB" + + SecRule SERVER_ADDR "@geoLookup" "chain,id:9,log,phase:1" + SecRule GEO:COUNTRY_NAME "@streq United Kingdom" + + SecRule SERVER_ADDR "@geoLookup" "chain,id:10,log,phase:1" + SecRule GEO:Region "!@streq England" + + SecRule SERVER_ADDR "@geoLookup" "chain,id:11,log,phase:1" + SecRule GEO:City "@streq Bracknell" + + SecRule SERVER_ADDR "@geoLookup" "chain,id:12,log,phase:1" + SecRule GEO:LATITUDE "@lt 50" + + SecRule SERVER_ADDR "@geoLookup" "chain,id:13,log,phase:1" + SecRule GEO:LONGITUDE "@lt -20.1" \ No newline at end of file diff --git a/testdata/engine/geoip_maxminddb.yaml b/testdata/engine/geoip_maxminddb.yaml new file mode 100644 index 000000000..66d5c7aa3 --- /dev/null +++ b/testdata/engine/geoip_maxminddb.yaml @@ -0,0 +1,48 @@ +--- + meta: + author: Xinyu Wu + description: This is a mix of many tests used to check geoip maxminddb engine. + enabled: true + name: geoip_maxminddb.yaml + tests: + - + test_title: geoip rules test + stages: + - + stage: + debug: true + input: + dest_addr: "81.2.69.142" + output: + triggered_rules: + - 1 + - 3 + - 4 + - 5 + - 7 + non_triggered_rules: + - 2 + - 6 + rules: | + SecGeoLookupDb maxminddb ../testdata/geoip_db/GeoLite2-City.mmdb + SecRule SERVER_ADDR "@geoLookup" "chain,id:1,log,phase:1" + SecRule GEO:COUNTRY_CODE "@streq GB" + + SecRule SERVER_ADDR "@geoLookup" "chain,id:2,log,phase:1" + SecRule GEO:COUNTRY_NAME "!@streq United Kingdom" + + SecGeoLookupDb maxminddb ../testdata/geoip_db/GeoLite2-City.mmdb + SecRule SERVER_ADDR "@geoLookup" "chain,id:3,log,phase:1" + SecRule GEO:COUNTRY_CONTINENT "!@streq europe" + + SecRule SERVER_ADDR "@geoLookup" "chain,id:4,log,phase:1" + SecRule GEO:CITY "@streq Chingford" + + SecRule SERVER_ADDR "@geoLookup" "chain,id:5,log,phase:1" + SecRule GEO:POSTAL_CODE "@streq E4" + + SecRule SERVER_ADDR "@geoLookup" "chain,id:6,log,phase:1" + SecRule GEO:LATITUDE "@lt 50" + + SecRule SERVER_ADDR "@geoLookup" "chain,id:7,log,phase:1" + SecRule GEO:LONGITUDE "@gt -20" \ No newline at end of file diff --git a/testdata/geoip_db/GeoLite2-City.mmdb b/testdata/geoip_db/GeoLite2-City.mmdb new file mode 100644 index 000000000..40c6c4cc3 --- /dev/null +++ b/testdata/geoip_db/GeoLite2-City.mmdb @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a13ca38d9c34766c9064c1fc7604ccbe6474aac3c453e143315279f9f2d5755e +size 70647093 diff --git a/testdata/geoip_db/IP2LOCATION-LITE-DB11.IPV6.BIN b/testdata/geoip_db/IP2LOCATION-LITE-DB11.IPV6.BIN new file mode 100755 index 000000000..71f7547c1 --- /dev/null +++ b/testdata/geoip_db/IP2LOCATION-LITE-DB11.IPV6.BIN @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9bff1f1376ccab6fd7c75e8d93d36004f37615f3e39ebd4f9c40054efbc541af +size 158076688 diff --git a/utils/geoip/geoip.go b/utils/geoip/geoip.go new file mode 100644 index 000000000..d165ca8be --- /dev/null +++ b/utils/geoip/geoip.go @@ -0,0 +1,34 @@ +// Copyright 2022 Xinyu Wu +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package geoip + +// GeoData is used to store the information by the geoip engine +type GeoData struct { + CountryCode string + CountryName string + CountryContinent string + Region string + City string + PostalCode string + Latitude float64 + Longitude float64 +} + +// GeoDb defines the geoip engine interface +type GeoDb interface { + Init(file string) error + Get(address string) (GeoData, error) + Close() error +} diff --git a/utils/geoip/ip2location.go b/utils/geoip/ip2location.go new file mode 100644 index 000000000..8ef476326 --- /dev/null +++ b/utils/geoip/ip2location.go @@ -0,0 +1,61 @@ +// Copyright 2022 Xinyu Wu +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package geoip + +import ( + "fmt" + "github.com/ip2location/ip2location-go/v9" +) + +// IP2Location engine for geoip +type IP2Location struct { + db *ip2location.DB +} + +// Init db for engine +func (i2l *IP2Location) Init(file string) error { + if file == "" { + return fmt.Errorf("[file=path] required for maxminddb geoip database") + } + + var err error + i2l.db, err = ip2location.OpenDB(file) + return err +} + +// Get address related information +func (i2l *IP2Location) Get(address string) (GeoData, error) { + r, err := i2l.db.Get_all(address) + if err != nil { + return GeoData{}, err + } + + return GeoData{ + CountryCode: r.Country_short, + CountryName: r.Country_long, + CountryContinent: "", + Region: r.Region, + City: r.City, + PostalCode: "", + Latitude: float64(r.Latitude), + Longitude: float64(r.Longitude), + }, nil +} + +// Close engine +func (i2l *IP2Location) Close() error { + i2l.db.Close() + return nil +} diff --git a/utils/geoip/maxminddb.go b/utils/geoip/maxminddb.go new file mode 100644 index 000000000..b1e3592ed --- /dev/null +++ b/utils/geoip/maxminddb.go @@ -0,0 +1,62 @@ +// Copyright 2022 Xinyu Wu +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package geoip + +import ( + "fmt" + "github.com/oschwald/geoip2-golang" + "net" +) + +// MaxMinddb engine for geoip +type MaxMinddb struct { + db *geoip2.Reader +} + +// Init db for engine +func (m *MaxMinddb) Init(file string) error { + if file == "" { + return fmt.Errorf("[file=path] required for maxminddb geoip database") + } + + var err error + m.db, err = geoip2.Open(file) + return err +} + +// Get address related information +func (m *MaxMinddb) Get(address string) (GeoData, error) { + ip := net.ParseIP(address) + r, err := m.db.City(ip) + if err != nil { + return GeoData{}, err + } + + return GeoData{ + CountryCode: r.Country.IsoCode, + CountryName: r.Country.Names["en"], + CountryContinent: r.Continent.Names["en"], + Region: "", + City: r.City.Names["en"], + PostalCode: r.Postal.Code, + Latitude: r.Location.Latitude, + Longitude: r.Location.Longitude, + }, nil +} + +// Close engine +func (m *MaxMinddb) Close() error { + return m.db.Close() +} diff --git a/waf.go b/waf.go index 414b36a39..a1d295718 100644 --- a/waf.go +++ b/waf.go @@ -16,6 +16,7 @@ package coraza import ( "fmt" + "github.com/corazawaf/coraza/v2/utils/geoip" "io/fs" "os" "regexp" @@ -158,6 +159,9 @@ type Waf struct { // AuditLogWriter is used to write audit logs AuditLogWriter loggers.LogWriter + + // GeoDb reader interface + GeoDB geoip.GeoDb } // NewTransaction Creates a new initialized transaction for this WAF instance