From 300ba7d4e9d416cf08a925d9b0125ab393e103d2 Mon Sep 17 00:00:00 2001 From: Felipe Zipitria Date: Sun, 22 Sep 2024 15:30:41 -0300 Subject: [PATCH] fix: use both phases for getting FPs Signed-off-by: Felipe Zipitria --- internal/quantitative/local_engine.go | 28 ++-- internal/quantitative/stats.go | 15 +- internal/quantitative/stats_test.go | 192 +++++++++++++------------- 3 files changed, 124 insertions(+), 111 deletions(-) diff --git a/internal/quantitative/local_engine.go b/internal/quantitative/local_engine.go index d1f60ee..e3563e4 100644 --- a/internal/quantitative/local_engine.go +++ b/internal/quantitative/local_engine.go @@ -88,28 +88,27 @@ func (e *localEngine) CrsCall(payload string) (int, map[int]string) { if e.waf == nil { log.Fatal().Msg("local engine not initialized") } + // we use the payload in the URI to rules in phase:1 can catch it + uri := fmt.Sprintf("/get?payload=%s", url.QueryEscape(payload)) + tx := e.waf.NewTransaction() tx.ProcessConnection("127.0.0.1", 8080, "127.0.0.1", 8080) - tx.ProcessURI("/post", "POST", "HTTP/1.1") + tx.ProcessURI(uri, "GET", "HTTP/1.1") tx.AddRequestHeader("Host", "localhost") tx.AddRequestHeader("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75. 0.3770.100 Safari/537.36") - tx.AddRequestHeader("Accept", "application/json") - tx.AddRequestHeader("Content-Type", "application/x-www-form-urlencoded") - body := []byte("payload=" + url.QueryEscape(payload)) - - tx.AddRequestHeader("Content-Length", strconv.Itoa(len(body))) - tx.ProcessRequestHeaders() - if _, _, err := tx.WriteRequestBody(body); err != nil { - log.Error().Err(err).Msg("failed to write request body") - } - it, err := tx.ProcessRequestBody() - if err != nil { - log.Error().Err(err).Msg("failed to process request body") + tx.AddRequestHeader("Accept", "*/*") + + // we need to check also for phase:1 rules only + it := tx.ProcessRequestHeaders() + if it == nil { // if no interruption, we check phase:2 + it, _ = tx.ProcessRequestBody() } - if it != nil { // execution was interrupted + + if it != nil { status = obtainStatusCodeFromInterruptionOrDefault(it, http.StatusOK) matchedRules = getMatchedRules(tx) } + // We don't care about the response body for now, nor logging. if err := tx.Close(); err != nil { log.Error().Err(err).Msg("failed to close transaction") @@ -161,6 +160,7 @@ func crsWAF(prefix string, paranoiaLevel int) coraza.WAF { } func obtainStatusCodeFromInterruptionOrDefault(it *types.Interruption, defaultStatusCode int) int { + log.Debug().Msgf("Interruption: %s", it.Action) if it.Action == "deny" { statusCode := it.Status if statusCode == 0 { diff --git a/internal/quantitative/stats.go b/internal/quantitative/stats.go index edd93aa..637988a 100644 --- a/internal/quantitative/stats.go +++ b/internal/quantitative/stats.go @@ -5,6 +5,7 @@ package quantitative import ( "encoding/json" + "sort" "time" "github.com/rs/zerolog/log" @@ -45,7 +46,19 @@ func (s *QuantitativeRunStats) printSummary(out *output.Output) { ratio := float64(s.falsePositives) / float64(s.count_) out.Println("Run %d payloads in %s", s.count_, s.totalTime) out.Println("Total False positive ratio: %d/%d = %.4f", s.falsePositives, s.count_, ratio) - out.Println("False positives per rule: %+v", s.falsePositivesPerRule) + out.Println("False positives per rule id:") + // Extract and sort the keys + rules := make([]int, 0, len(s.falsePositivesPerRule)) + for rule := range s.falsePositivesPerRule { + rules = append(rules, rule) + } + sort.Ints(rules) + + // Print the sorted map + for _, rule := range rules { + count := s.falsePositivesPerRule[rule] + out.Println(" %d: %d false positives", rule, count) + } } } else { out.Println("No false positives detected with the passed corpus") diff --git a/internal/quantitative/stats_test.go b/internal/quantitative/stats_test.go index b615ebb..8ff9f45 100644 --- a/internal/quantitative/stats_test.go +++ b/internal/quantitative/stats_test.go @@ -4,128 +4,128 @@ package quantitative import ( - "bytes" - "testing" - "time" + "bytes" + "testing" + "time" - "github.com/stretchr/testify/suite" + "github.com/stretchr/testify/suite" - "github.com/coreruleset/go-ftw/output" + "github.com/coreruleset/go-ftw/output" ) type statsTestSuite struct { - suite.Suite + suite.Suite } func TestStatsTestSuite(t *testing.T) { - suite.Run(t, new(statsTestSuite)) + suite.Run(t, new(statsTestSuite)) } func (s *statsTestSuite) TestNewQuantitativeStats() { - tests := []struct { - name string - want *QuantitativeRunStats - }{ - { - name: "Test 1", - want: &QuantitativeRunStats{ - count_: 0, - falsePositives: 0, - falsePositivesPerRule: make(map[int]int), - totalTime: 0, - }, - }, - } - for _, tt := range tests { - s.Run(tt.name, func() { - got := NewQuantitativeStats() - s.Require().Equal(got, tt.want) - }) - } + tests := []struct { + name string + want *QuantitativeRunStats + }{ + { + name: "Test 1", + want: &QuantitativeRunStats{ + count_: 0, + falsePositives: 0, + falsePositivesPerRule: make(map[int]int), + totalTime: 0, + }, + }, + } + for _, tt := range tests { + s.Run(tt.name, func() { + got := NewQuantitativeStats() + s.Require().Equal(got, tt.want) + }) + } } func (s *statsTestSuite) TestQuantitativeRunStats_MarshalJSON() { - type fields struct { - count_ int - totalTime time.Duration - falsePositives int - falsePositivesPerRule map[int]int - } - tests := []struct { - name string - fields fields - want []byte - wantErr bool - }{ - { - name: "Test 1", - fields: fields{ - count_: 1, - totalTime: 1, - falsePositives: 1, - falsePositivesPerRule: map[int]int{920010: 1}, - }, - want: []byte(`{"count":1,"falsePositives":1,"falsePositivesPerRule":{"920010":1},"totalTime":1}`), - wantErr: false, - }, - { - name: "Test 2", - fields: fields{ - count_: 2, - totalTime: 2, - falsePositives: 2, - falsePositivesPerRule: map[int]int{933100: 2}, - }, - want: []byte(`{"count":2,"falsePositives":2,"falsePositivesPerRule":{"933100":2},"totalTime":2}`), - wantErr: false, - }, - } - for _, tt := range tests { - s.Run(tt.name, func() { - q := &QuantitativeRunStats{ - count_: tt.fields.count_, - totalTime: tt.fields.totalTime, - falsePositives: tt.fields.falsePositives, - falsePositivesPerRule: tt.fields.falsePositivesPerRule, - } - got, err := q.MarshalJSON() - s.Require().NoError(err) - s.Require().Equal(got, tt.want) - }) - } + type fields struct { + count_ int + totalTime time.Duration + falsePositives int + falsePositivesPerRule map[int]int + } + tests := []struct { + name string + fields fields + want []byte + wantErr bool + }{ + { + name: "Test 1", + fields: fields{ + count_: 1, + totalTime: 1, + falsePositives: 1, + falsePositivesPerRule: map[int]int{920010: 1}, + }, + want: []byte(`{"count":1,"falsePositives":1,"falsePositivesPerRule":{"920010":1},"totalTime":1}`), + wantErr: false, + }, + { + name: "Test 2", + fields: fields{ + count_: 2, + totalTime: 2, + falsePositives: 2, + falsePositivesPerRule: map[int]int{933100: 2}, + }, + want: []byte(`{"count":2,"falsePositives":2,"falsePositivesPerRule":{"933100":2},"totalTime":2}`), + wantErr: false, + }, + } + for _, tt := range tests { + s.Run(tt.name, func() { + q := &QuantitativeRunStats{ + count_: tt.fields.count_, + totalTime: tt.fields.totalTime, + falsePositives: tt.fields.falsePositives, + falsePositivesPerRule: tt.fields.falsePositivesPerRule, + } + got, err := q.MarshalJSON() + s.Require().NoError(err) + s.Require().Equal(got, tt.want) + }) + } } func (s *statsTestSuite) TestQuantitativeRunStats_functions() { - q := NewQuantitativeStats() + q := NewQuantitativeStats() - q.incrementRun() - s.Require().Equal(q.Count(), 1) + q.incrementRun() + s.Require().Equal(q.Count(), 1) - q.addFalsePositive(920100) - s.Require().Equal(q.FalsePositives(), 1) + q.addFalsePositive(920100) + s.Require().Equal(q.FalsePositives(), 1) - q.incrementRun() - s.Require().Equal(q.Count(), 2) + q.incrementRun() + s.Require().Equal(q.Count(), 2) - q.addFalsePositive(920200) - s.Require().Equal(q.FalsePositives(), 2) + q.addFalsePositive(920200) + s.Require().Equal(q.FalsePositives(), 2) - duration := time.Duration(5000) - q.SetTotalTime(duration) - s.Require().Equal(q.TotalTime(), duration) + duration := time.Duration(5000) + q.SetTotalTime(duration) + s.Require().Equal(q.TotalTime(), duration) } func (s *statsTestSuite) TestQuantitativeRunStats_printSummary() { - var b bytes.Buffer - out := output.NewOutput("plain", &b) - q := NewQuantitativeStats() + var b bytes.Buffer + out := output.NewOutput("plain", &b) + q := NewQuantitativeStats() - q.incrementRun() - s.Require().Equal(q.Count(), 1) + q.incrementRun() + s.Require().Equal(q.Count(), 1) - q.addFalsePositive(920100) - s.Require().Equal(q.FalsePositives(), 1) + q.addFalsePositive(920100) + s.Require().Equal(q.FalsePositives(), 1) - q.printSummary(out) - s.Require().Equal(b.String(), "Run 1 payloads in 0s\nTotal False positive ratio: 1/1 = 1.0000\nFalse positives per rule: map[920100:1]\n") + q.printSummary(out) + s.Require().Equal("Run 1 payloads in 0s\nTotal False positive ratio: 1/1 = 1.0000\nFalse positives per rule id:\n 920100: 1 false positives\n", b.String()) }