From 5ebeb89f93a220bb0e86a2825f02c3e8d52b403d Mon Sep 17 00:00:00 2001 From: Tigah <88289044+TBX3D@users.noreply.github.com> Date: Wed, 22 Jul 2026 12:51:39 -0700 Subject: [PATCH] feat(modules): add case-insensitive word matcher option (#319) --- internal/modules/executor.go | 17 +++++++++++++---- internal/modules/matchers_test.go | 27 ++++++++++++++++----------- internal/modules/module.go | 2 ++ 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/internal/modules/executor.go b/internal/modules/executor.go index 529534c..8e606b2 100644 --- a/internal/modules/executor.go +++ b/internal/modules/executor.go @@ -383,7 +383,7 @@ func checkMatcher(m *Matcher, resp *http.Response, body string) bool { return false case "word": - return checkWords(getPart(m.Part, resp, body), m.Words, m.Condition) + return checkWords(getPart(m.Part, resp, body), m.Words, m.Condition, m.CaseInsensitive) case "regex": return checkRegex(getPart(m.Part, resp, body), m.Regex, m.Condition) @@ -436,10 +436,19 @@ func getPart(part string, resp *http.Response, body string) string { } // checkWords checks if any/all words are found. -func checkWords(content string, words []string, condition string) bool { +func checkWords(content string, words []string, condition string, caseInsensitive bool) bool { + if caseInsensitive { + content = strings.ToLower(content) + } + fold := func(w string) string { + if caseInsensitive { + return strings.ToLower(w) + } + return w + } if condition == "or" { for _, word := range words { - if strings.Contains(content, word) { + if strings.Contains(content, fold(word)) { return true } } @@ -447,7 +456,7 @@ func checkWords(content string, words []string, condition string) bool { } // Default to AND for _, word := range words { - if !strings.Contains(content, word) { + if !strings.Contains(content, fold(word)) { return false } } diff --git a/internal/modules/matchers_test.go b/internal/modules/matchers_test.go index 30834f1..46edf60 100644 --- a/internal/modules/matchers_test.go +++ b/internal/modules/matchers_test.go @@ -197,22 +197,27 @@ func TestCheckWords(t *testing.T) { const content = "alpha beta gamma" tests := []struct { - name string - words []string - condition string - expect bool + name string + content string + words []string + condition string + caseInsens bool + expect bool }{ - {name: "and all present", words: []string{"alpha", "gamma"}, condition: "and", expect: true}, - {name: "and missing", words: []string{"alpha", "delta"}, condition: "and", expect: false}, - {name: "or present", words: []string{"delta", "beta"}, condition: "or", expect: true}, - {name: "or absent", words: []string{"delta", "epsilon"}, condition: "or", expect: false}, - {name: "empty under and matches vacuously", words: nil, condition: "and", expect: true}, - {name: "empty under or matches nothing", words: nil, condition: "or", expect: false}, + {name: "and all present", content: content, words: []string{"alpha", "gamma"}, condition: "and", expect: true}, + {name: "and missing", content: content, words: []string{"alpha", "delta"}, condition: "and", expect: false}, + {name: "or present", content: content, words: []string{"delta", "beta"}, condition: "or", expect: true}, + {name: "or absent", content: content, words: []string{"delta", "epsilon"}, condition: "or", expect: false}, + {name: "empty under and matches vacuously", content: content, words: nil, condition: "and", expect: true}, + {name: "empty under or matches nothing", content: content, words: nil, condition: "or", expect: false}, + {name: "case mismatch fails by default", content: "AdMiN panel", words: []string{"admin"}, condition: "or", expect: false}, + {name: "case-insensitive folds both sides", content: "AdMiN panel", words: []string{"admin"}, condition: "or", caseInsens: true, expect: true}, + {name: "case-insensitive folds mixed needle", content: "admin panel", words: []string{"AdMiN"}, condition: "or", caseInsens: true, expect: true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := checkWords(content, tt.words, tt.condition); got != tt.expect { + if got := checkWords(tt.content, tt.words, tt.condition, tt.caseInsens); got != tt.expect { t.Errorf("checkWords = %v, want %v", got, tt.expect) } }) diff --git a/internal/modules/module.go b/internal/modules/module.go index 1a45e5c..7adeb34 100644 --- a/internal/modules/module.go +++ b/internal/modules/module.go @@ -95,6 +95,8 @@ type Matcher struct { Hash []int64 `yaml:"hash,omitempty"` // favicon: shodan mmh3 hashes (signed or unsigned) Condition string `yaml:"condition"` // and, or Negative bool `yaml:"negative"` + // CaseInsensitive folds word matching to lower-case when set (word matcher only). + CaseInsensitive bool `yaml:"case-insensitive,omitempty"` } // Extractor defines data extraction from responses.