mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2026-11-07 17:43:04 -10:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2bd54cb66 | ||
|
|
c6c4e054ca | ||
|
|
8002ba3395 | ||
|
|
5182bf6a8e | ||
|
|
606e1a6567 | ||
|
|
16740c6e74 | ||
|
|
6b08a393d1 | ||
|
|
3132b20ba1 | ||
|
|
3836892f8a | ||
|
|
98b28ca24e | ||
|
|
dc29347909 |
No files matched your search
@@ -44,16 +44,19 @@ func (matcher *Matcher) CompileMatchers() error {
|
||||
|
||||
// Compile the regexes (with shared cache)
|
||||
for _, regex := range matcher.Regex {
|
||||
if cached, err := cache.Regex().GetIFPresent(regex); err == nil && cached != nil {
|
||||
matcher.regexCompiled = append(matcher.regexCompiled, cached)
|
||||
continue
|
||||
}
|
||||
compiled, err := regexp.Compile(regex)
|
||||
compiled, err := compileRegex(regex)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not compile regex: %s", regex)
|
||||
}
|
||||
_ = cache.Regex().Set(regex, compiled)
|
||||
matcher.regexCompiled = append(matcher.regexCompiled, compiled)
|
||||
|
||||
if matcher.Offset != nil {
|
||||
offsetCompiled, err := compileOffsetRegex(regex, *matcher.Offset)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not compile regex: %s", regex)
|
||||
}
|
||||
matcher.offsetRegexCompiled = append(matcher.offsetRegexCompiled, offsetCompiled)
|
||||
}
|
||||
}
|
||||
|
||||
// Compile and validate binary Values in matcher
|
||||
@@ -100,6 +103,33 @@ func (matcher *Matcher) CompileMatchers() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// compileRegex compiles pattern through the shared regex cache
|
||||
func compileRegex(pattern string) (*regexp.Regexp, error) {
|
||||
if cached, err := cache.Regex().GetIFPresent(pattern); err == nil && cached != nil {
|
||||
return cached, nil
|
||||
}
|
||||
compiled, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = cache.Regex().Set(pattern, compiled)
|
||||
return compiled, nil
|
||||
}
|
||||
|
||||
// compileOffsetRegex compiles the variant of pattern used when a matcher is
|
||||
// pinned to a byte offset. The pattern is anchored so a single match attempt
|
||||
// answers whether it matches at the requested position, and for offsets past
|
||||
// the start of the corpus it also consumes the rune preceding the offset. That
|
||||
// leading rune is matched against the real bytes of the corpus, so ^, \A, \b
|
||||
// and \B keep seeing the context they would see during a normal scan.
|
||||
func compileOffsetRegex(pattern string, offset int) (*regexp.Regexp, error) {
|
||||
anchored := `\A(?:` + pattern + `)`
|
||||
if offset > 0 {
|
||||
anchored = `\A(?s:.)(?:` + pattern + `)`
|
||||
}
|
||||
return compileRegex(anchored)
|
||||
}
|
||||
|
||||
// GetType returns the condition type of the matcher
|
||||
// todo: the field should be exposed natively
|
||||
func (matcher *Matcher) GetCondition() ConditionType {
|
||||
|
||||
+147
-31
@@ -2,7 +2,9 @@ package matchers
|
||||
|
||||
import (
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/antchfx/htmlquery"
|
||||
"github.com/antchfx/xmlquery"
|
||||
@@ -60,7 +62,14 @@ func (matcher *Matcher) MatchWords(corpus string, data map[string]interface{}) (
|
||||
|
||||
// MatchWordsWithOptions matches words with the current scan's DSL network policy.
|
||||
func (matcher *Matcher) MatchWordsWithOptions(corpus string, data map[string]interface{}, options *types.Options) (bool, []string) {
|
||||
if matcher.CaseInsensitive {
|
||||
return matcher.MatchWordsAt(corpus, data, options, 0)
|
||||
}
|
||||
|
||||
// MatchWordsAt matches words against a corpus that starts at byte position
|
||||
// base of the full response, so Offset stays absolute when the response is
|
||||
// matched in chunks.
|
||||
func (matcher *Matcher) MatchWordsAt(corpus string, data map[string]interface{}, options *types.Options, base int) (bool, []string) {
|
||||
if matcher.CaseInsensitive && matcher.Offset == nil {
|
||||
corpus = strings.ToLower(corpus)
|
||||
}
|
||||
|
||||
@@ -84,7 +93,11 @@ func (matcher *Matcher) MatchWordsWithOptions(corpus string, data map[string]int
|
||||
}
|
||||
}
|
||||
// Continue if the word doesn't match
|
||||
if !strings.Contains(corpus, word) {
|
||||
matched := matcher.matchStringAt(corpus, word, base)
|
||||
if matcher.CaseInsensitive && matcher.Offset != nil {
|
||||
matched = matcher.matchFoldStringAt(corpus, word, base)
|
||||
}
|
||||
if !matched {
|
||||
// If we are in an AND request and a match failed,
|
||||
// return false as the AND condition fails on any single mismatch.
|
||||
switch matcher.condition {
|
||||
@@ -114,36 +127,18 @@ func (matcher *Matcher) MatchWordsWithOptions(corpus string, data map[string]int
|
||||
|
||||
// MatchRegex matches a regex check against a corpus
|
||||
func (matcher *Matcher) MatchRegex(corpus string) (bool, []string) {
|
||||
return matcher.MatchRegexAt(corpus, 0)
|
||||
}
|
||||
|
||||
// MatchRegexAt matches regexes against a corpus that starts at byte position
|
||||
// base of the full response, so Offset stays absolute when the response is
|
||||
// matched in chunks.
|
||||
func (matcher *Matcher) MatchRegexAt(corpus string, base int) (bool, []string) {
|
||||
var matchedRegexes []string
|
||||
// Iterate over all the regexes accepted as valid
|
||||
for i, regex := range matcher.regexCompiled {
|
||||
// Literal prefix short-circuit
|
||||
rstr := regex.String()
|
||||
if !strings.Contains(rstr, "(?i") { // covers (?i) and (?i:
|
||||
if prefix, ok := regex.LiteralPrefix(); ok && prefix != "" {
|
||||
if !strings.Contains(corpus, prefix) {
|
||||
switch matcher.condition {
|
||||
case ANDCondition:
|
||||
return false, []string{}
|
||||
case ORCondition:
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fast OR-path: return first match without full scan
|
||||
if matcher.condition == ORCondition && !matcher.MatchAll {
|
||||
m := regex.FindAllString(corpus, 1)
|
||||
if len(m) == 0 {
|
||||
continue
|
||||
}
|
||||
return true, m
|
||||
}
|
||||
|
||||
// Single scan: get all matches directly
|
||||
currentMatches := regex.FindAllString(corpus, -1)
|
||||
if len(currentMatches) == 0 {
|
||||
currentMatches, matched := matcher.findRegexMatches(corpus, i, regex, base)
|
||||
if !matched {
|
||||
switch matcher.condition {
|
||||
case ANDCondition:
|
||||
return false, []string{}
|
||||
@@ -152,7 +147,10 @@ func (matcher *Matcher) MatchRegex(corpus string) (bool, []string) {
|
||||
}
|
||||
}
|
||||
|
||||
// If the condition was an OR (and MatchAll true), we still need to gather all
|
||||
// If the condition was an OR, return on the first match.
|
||||
if matcher.condition == ORCondition && !matcher.MatchAll {
|
||||
return true, currentMatches
|
||||
}
|
||||
matchedRegexes = append(matchedRegexes, currentMatches...)
|
||||
|
||||
// If we are at the end of the regex, return with true
|
||||
@@ -166,12 +164,98 @@ func (matcher *Matcher) MatchRegex(corpus string) (bool, []string) {
|
||||
return false, []string{}
|
||||
}
|
||||
|
||||
func (matcher *Matcher) findRegexMatches(corpus string, index int, regex *regexp.Regexp, base int) ([]string, bool) {
|
||||
if matcher.Offset != nil {
|
||||
return matcher.findRegexMatchAtOffset(corpus, index, regex, *matcher.Offset-base)
|
||||
}
|
||||
|
||||
// Literal prefix short-circuit
|
||||
rstr := regex.String()
|
||||
if !strings.Contains(rstr, "(?i") { // covers (?i) and (?i:
|
||||
if prefix, ok := regex.LiteralPrefix(); ok && prefix != "" {
|
||||
if !strings.Contains(corpus, prefix) {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if matcher.condition == ORCondition && !matcher.MatchAll {
|
||||
m := regex.FindAllString(corpus, 1)
|
||||
if len(m) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
return m, true
|
||||
}
|
||||
|
||||
currentMatches := regex.FindAllString(corpus, -1)
|
||||
if len(currentMatches) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
return currentMatches, true
|
||||
}
|
||||
|
||||
// findRegexMatchAtOffset reports whether the regex has a match starting exactly
|
||||
// at offset, which is matcher.Offset made relative to the corpus. Scanning the corpus for every match is both wasteful and
|
||||
// wrong here: leftmost matching hides overlapping candidates, so a regex like
|
||||
// `ab|b` against `ab` never reports the `b` starting at offset 1. Instead the
|
||||
// anchored variant of the regex is run once over the corpus suffix that starts
|
||||
// one rune before the offset, which keeps ^, \A and word boundaries evaluated
|
||||
// against the original surrounding bytes.
|
||||
func (matcher *Matcher) findRegexMatchAtOffset(corpus string, index int, regex *regexp.Regexp, offset int) ([]string, bool) {
|
||||
if offset < 0 || offset > len(corpus) {
|
||||
return nil, false
|
||||
}
|
||||
anchored := matcher.offsetRegex(index, regex, offset)
|
||||
if anchored == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
start := offset
|
||||
if offset > 0 {
|
||||
_, backward := utf8.DecodeLastRuneInString(corpus[:offset])
|
||||
start = offset - backward
|
||||
// a match can only begin on a rune boundary, so an offset landing
|
||||
// inside a multi byte rune never matches
|
||||
if _, forward := utf8.DecodeRuneInString(corpus[start:]); forward != backward {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
loc := anchored.FindStringIndex(corpus[start:])
|
||||
if loc == nil {
|
||||
return nil, false
|
||||
}
|
||||
return []string{corpus[offset : start+loc[1]]}, true
|
||||
}
|
||||
|
||||
// offsetRegex returns the anchored variant of the regex at the given index,
|
||||
// compiling it on demand when the matcher was not compiled with an offset or
|
||||
// when the relative offset needs the other variant (offset 0 in a later chunk).
|
||||
func (matcher *Matcher) offsetRegex(index int, regex *regexp.Regexp, offset int) *regexp.Regexp {
|
||||
sameVariant := matcher.Offset != nil && (*matcher.Offset > 0) == (offset > 0)
|
||||
if sameVariant && index >= 0 && index < len(matcher.offsetRegexCompiled) {
|
||||
return matcher.offsetRegexCompiled[index]
|
||||
}
|
||||
compiled, err := compileOffsetRegex(regex.String(), offset)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return compiled
|
||||
}
|
||||
|
||||
// MatchBinary matches a binary check against a corpus
|
||||
func (matcher *Matcher) MatchBinary(corpus string) (bool, []string) {
|
||||
return matcher.MatchBinaryAt(corpus, 0)
|
||||
}
|
||||
|
||||
// MatchBinaryAt matches binary values against a corpus that starts at byte
|
||||
// position base of the full response, so Offset stays absolute when the
|
||||
// response is matched in chunks.
|
||||
func (matcher *Matcher) MatchBinaryAt(corpus string, base int) (bool, []string) {
|
||||
var matchedBinary []string
|
||||
// Iterate over all the words accepted as valid
|
||||
for i, binary := range matcher.binaryDecoded {
|
||||
if !strings.Contains(corpus, binary) {
|
||||
if !matcher.matchStringAt(corpus, binary, base) {
|
||||
// If we are in an AND request and a match failed,
|
||||
// return false as the AND condition fails on any single mismatch.
|
||||
switch matcher.condition {
|
||||
@@ -197,6 +281,38 @@ func (matcher *Matcher) MatchBinary(corpus string) (bool, []string) {
|
||||
return false, []string{}
|
||||
}
|
||||
|
||||
// matchStringAt reports whether needle is present in corpus, optionally pinned
|
||||
// to matcher.Offset when that field is set. base is the position of corpus in
|
||||
// the full response.
|
||||
func (matcher *Matcher) matchStringAt(corpus, needle string, base int) bool {
|
||||
if matcher.Offset == nil {
|
||||
return strings.Contains(corpus, needle)
|
||||
}
|
||||
offset := *matcher.Offset - base
|
||||
if offset < 0 || offset > len(corpus) || offset+len(needle) > len(corpus) {
|
||||
return false
|
||||
}
|
||||
return corpus[offset:offset+len(needle)] == needle
|
||||
}
|
||||
|
||||
// matchFoldStringAt compares the same number of Unicode code points at the
|
||||
// configured byte offset without lowercasing the corpus and shifting offsets.
|
||||
func (matcher *Matcher) matchFoldStringAt(corpus, needle string, base int) bool {
|
||||
offset := *matcher.Offset - base
|
||||
if offset < 0 || offset > len(corpus) {
|
||||
return false
|
||||
}
|
||||
end := offset
|
||||
for range needle {
|
||||
if end == len(corpus) {
|
||||
return false
|
||||
}
|
||||
_, size := utf8.DecodeRuneInString(corpus[end:])
|
||||
end += size
|
||||
}
|
||||
return strings.EqualFold(corpus[offset:end], needle)
|
||||
}
|
||||
|
||||
// MatchDSL matches on a generic map result
|
||||
func (matcher *Matcher) MatchDSL(data map[string]interface{}) bool {
|
||||
return matcher.MatchDSLWithOptions(data, nil)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package matchers
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/govaluate"
|
||||
@@ -435,6 +436,208 @@ func TestMatchWords_CaseInsensitive_DynamicValue(t *testing.T) {
|
||||
require.Equal(t, []string{"example.com"}, matched)
|
||||
}
|
||||
|
||||
func TestMatchOffset(t *testing.T) {
|
||||
offset0 := 0
|
||||
offset2 := 2
|
||||
|
||||
t.Run("word at start", func(t *testing.T) {
|
||||
m := &Matcher{Type: MatcherTypeHolder{MatcherType: WordsMatcher}, Words: []string{"MZ"}, Offset: &offset0}
|
||||
require.NoError(t, m.CompileMatchers())
|
||||
ok, snippets := m.MatchWords("MZ....", nil)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, []string{"MZ"}, snippets)
|
||||
ok, _ = m.MatchWords("xMZ...", nil)
|
||||
require.False(t, ok)
|
||||
})
|
||||
|
||||
t.Run("word at mid offset", func(t *testing.T) {
|
||||
m := &Matcher{Type: MatcherTypeHolder{MatcherType: WordsMatcher}, Words: []string{"AB"}, Offset: &offset2}
|
||||
require.NoError(t, m.CompileMatchers())
|
||||
ok, _ := m.MatchWords("xxAByy", nil)
|
||||
require.True(t, ok)
|
||||
ok, _ = m.MatchWords("ABxxxx", nil)
|
||||
require.False(t, ok)
|
||||
ok, _ = m.MatchWords("xx--AB", nil)
|
||||
require.False(t, ok)
|
||||
})
|
||||
|
||||
t.Run("case insensitive word preserves byte offset", func(t *testing.T) {
|
||||
m := &Matcher{
|
||||
Type: MatcherTypeHolder{MatcherType: WordsMatcher},
|
||||
Words: []string{"mz"},
|
||||
CaseInsensitive: true,
|
||||
Offset: &offset2,
|
||||
}
|
||||
require.NoError(t, m.CompileMatchers())
|
||||
ok, snippets := m.MatchWords("İMZ", nil)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, []string{"mz"}, snippets)
|
||||
})
|
||||
|
||||
t.Run("binary at start", func(t *testing.T) {
|
||||
m := &Matcher{Type: MatcherTypeHolder{MatcherType: BinaryMatcher}, Binary: []string{"4d5a"}, Offset: &offset0}
|
||||
require.NoError(t, m.CompileMatchers())
|
||||
ok, _ := m.MatchBinary("MZ....")
|
||||
require.True(t, ok)
|
||||
ok, _ = m.MatchBinary("xMZ...")
|
||||
require.False(t, ok)
|
||||
})
|
||||
|
||||
t.Run("binary at mid offset", func(t *testing.T) {
|
||||
m := &Matcher{Type: MatcherTypeHolder{MatcherType: BinaryMatcher}, Binary: []string{"4d5a"}, Offset: &offset2}
|
||||
require.NoError(t, m.CompileMatchers())
|
||||
ok, _ := m.MatchBinary("xxMZ....")
|
||||
require.True(t, ok)
|
||||
ok, _ = m.MatchBinary("xx--MZ")
|
||||
require.False(t, ok)
|
||||
})
|
||||
|
||||
t.Run("regex must start at offset", func(t *testing.T) {
|
||||
m := &Matcher{Type: MatcherTypeHolder{MatcherType: RegexMatcher}, Regex: []string{"MZ"}, Offset: &offset0}
|
||||
require.NoError(t, m.CompileMatchers())
|
||||
ok, snippets := m.MatchRegex("MZPE")
|
||||
require.True(t, ok)
|
||||
require.Equal(t, []string{"MZ"}, snippets)
|
||||
ok, _ = m.MatchRegex("xxMZ")
|
||||
require.False(t, ok)
|
||||
})
|
||||
|
||||
t.Run("regex preserves assertion context", func(t *testing.T) {
|
||||
m := &Matcher{Type: MatcherTypeHolder{MatcherType: RegexMatcher}, Regex: []string{"^MZ"}, Offset: &offset2}
|
||||
require.NoError(t, m.CompileMatchers())
|
||||
ok, _ := m.MatchRegex("xxMZ")
|
||||
require.False(t, ok)
|
||||
})
|
||||
|
||||
t.Run("negative offset rejected", func(t *testing.T) {
|
||||
neg := -1
|
||||
m := &Matcher{Type: MatcherTypeHolder{MatcherType: WordsMatcher}, Words: []string{"MZ"}, Offset: &neg}
|
||||
require.Error(t, m.CompileMatchers())
|
||||
})
|
||||
}
|
||||
|
||||
func newOffsetRegexMatcher(t *testing.T, offset int, regexes ...string) *Matcher {
|
||||
t.Helper()
|
||||
|
||||
m := &Matcher{Type: MatcherTypeHolder{MatcherType: RegexMatcher}, Regex: regexes, Offset: &offset}
|
||||
require.NoError(t, m.CompileMatchers())
|
||||
return m
|
||||
}
|
||||
|
||||
func TestMatchRegexOffset(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
regex string
|
||||
corpus string
|
||||
offset int
|
||||
matched bool
|
||||
snippet string
|
||||
}{
|
||||
{name: "overlapping alternative at offset", regex: "ab|b", corpus: "ab", offset: 1, matched: true, snippet: "b"},
|
||||
{name: "leftmost alternative at offset", regex: "ab|b", corpus: "ab", offset: 0, matched: true, snippet: "ab"},
|
||||
{name: "match before offset is ignored", regex: "ab", corpus: "abab", offset: 1, matched: false},
|
||||
{name: "match after offset is ignored", regex: "b", corpus: "ab", offset: 0, matched: false},
|
||||
{name: "greedy match keeps full span", regex: "a+", corpus: "baaa", offset: 1, matched: true, snippet: "aaa"},
|
||||
{name: "start of text anchor at offset zero", regex: "^MZ", corpus: "MZxx", offset: 0, matched: true, snippet: "MZ"},
|
||||
{name: "start of text anchor mid corpus", regex: "^MZ", corpus: "xxMZ", offset: 2, matched: false},
|
||||
{name: "absolute start anchor mid corpus", regex: `\AMZ`, corpus: "xxMZ", offset: 2, matched: false},
|
||||
{name: "multiline anchor after newline", regex: "(?m)^MZ", corpus: "xx\nMZ", offset: 3, matched: true, snippet: "MZ"},
|
||||
{name: "multiline anchor without newline", regex: "(?m)^MZ", corpus: "xxMZ", offset: 2, matched: false},
|
||||
{name: "word boundary after word character", regex: `\bfoo`, corpus: "xfoo", offset: 1, matched: false},
|
||||
{name: "word boundary after separator", regex: `\bfoo`, corpus: " foo", offset: 1, matched: true, snippet: "foo"},
|
||||
{name: "non word boundary inside word", regex: `\Bar`, corpus: "bar", offset: 1, matched: true, snippet: "ar"},
|
||||
{name: "non word boundary after separator", regex: `\Bar`, corpus: "-ar", offset: 1, matched: false},
|
||||
{name: "end of text anchor honored", regex: "b$", corpus: "ab", offset: 1, matched: true, snippet: "b"},
|
||||
{name: "end of text anchor rejected", regex: "b$", corpus: "abc", offset: 1, matched: false},
|
||||
{name: "case insensitive flag preserved", regex: "(?i)mz", corpus: "xxMZ", offset: 2, matched: true, snippet: "MZ"},
|
||||
{name: "offset inside multi byte rune", regex: "MZ", corpus: "éMZ", offset: 1, matched: false},
|
||||
{name: "offset after multi byte rune", regex: "MZ", corpus: "éMZ", offset: 2, matched: true, snippet: "MZ"},
|
||||
{name: "offset past corpus", regex: "MZ", corpus: "MZ", offset: 5, matched: false},
|
||||
{name: "zero width match at corpus end", regex: "x*", corpus: "ab", offset: 2, matched: true, snippet: ""},
|
||||
{name: "invalid utf8 byte as context", regex: "MZ", corpus: "\xffMZ", offset: 1, matched: true, snippet: "MZ"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
m := newOffsetRegexMatcher(t, test.offset, test.regex)
|
||||
ok, snippets := m.MatchRegex(test.corpus)
|
||||
require.Equal(t, test.matched, ok)
|
||||
if test.matched {
|
||||
require.Equal(t, []string{test.snippet}, snippets)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("multiple regexes keep their anchored variant", func(t *testing.T) {
|
||||
offset := 1
|
||||
m := &Matcher{
|
||||
Type: MatcherTypeHolder{MatcherType: RegexMatcher},
|
||||
Regex: []string{"b", "bc"},
|
||||
Condition: "and",
|
||||
Offset: &offset,
|
||||
}
|
||||
require.NoError(t, m.CompileMatchers())
|
||||
ok, snippets := m.MatchRegex("abc")
|
||||
require.True(t, ok)
|
||||
require.Equal(t, []string{"b", "bc"}, snippets)
|
||||
|
||||
ok, _ = m.MatchRegex("xbc")
|
||||
require.True(t, ok)
|
||||
ok, _ = m.MatchRegex("bcx")
|
||||
require.False(t, ok)
|
||||
})
|
||||
|
||||
t.Run("anchored variant compiled on demand", func(t *testing.T) {
|
||||
m := newOffsetRegexMatcher(t, 1, "ab|b")
|
||||
m.offsetRegexCompiled = nil
|
||||
ok, snippets := m.MatchRegex("ab")
|
||||
require.True(t, ok)
|
||||
require.Equal(t, []string{"b"}, snippets)
|
||||
})
|
||||
|
||||
t.Run("allocations do not grow with corpus size", func(t *testing.T) {
|
||||
m := newOffsetRegexMatcher(t, 4, "a{3}")
|
||||
small := strings.Repeat("a", 1<<10)
|
||||
large := strings.Repeat("a", 1<<20)
|
||||
|
||||
smallAllocs := testing.AllocsPerRun(20, func() { _, _ = m.MatchRegex(small) })
|
||||
largeAllocs := testing.AllocsPerRun(20, func() { _, _ = m.MatchRegex(large) })
|
||||
require.LessOrEqual(t, largeAllocs, smallAllocs+1, "offset matching should not allocate per corpus match")
|
||||
require.LessOrEqual(t, largeAllocs, float64(8), "offset matching should allocate a constant amount")
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkMatchRegexOffset(b *testing.B) {
|
||||
dense := strings.Repeat("ab", 1<<19)
|
||||
|
||||
benchmarks := []struct {
|
||||
name string
|
||||
regex string
|
||||
corpus string
|
||||
offset int
|
||||
}{
|
||||
{name: "match at start of dense corpus", regex: "ab", corpus: dense, offset: 0},
|
||||
{name: "overlapping alternative", regex: "ab|b", corpus: dense, offset: 1},
|
||||
{name: "match near end of dense corpus", regex: "ab", corpus: dense, offset: len(dense) - 2},
|
||||
{name: "no match in dense corpus", regex: "zz", corpus: dense, offset: 3},
|
||||
}
|
||||
|
||||
for _, bm := range benchmarks {
|
||||
b.Run(bm.name, func(b *testing.B) {
|
||||
offset := bm.offset
|
||||
m := &Matcher{Type: MatcherTypeHolder{MatcherType: RegexMatcher}, Regex: []string{bm.regex}, Offset: &offset}
|
||||
if err := m.CompileMatchers(); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = m.MatchRegex(bm.corpus)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatcher_MatchDSL_ErrorHandling(t *testing.T) {
|
||||
// First expression errors (division by zero), second is true
|
||||
m := &Matcher{Type: MatcherTypeHolder{MatcherType: DSLMatcher}, Condition: "or", DSL: []string{"1 / 0", "1 == 1"}}
|
||||
|
||||
@@ -105,6 +105,15 @@ type Matcher struct {
|
||||
// []string{"//a[@target=\"_blank\"]"}
|
||||
XPath []string `yaml:"xpath,omitempty" json:"xpath,omitempty" jsonschema:"title=xpath queries to match in response,description=xpath are the XPath queries that will be evaluated against the response part of nuclei matching rules"`
|
||||
// description: |
|
||||
// Offset is an optional byte index where the matcher must match.
|
||||
// When set, word/binary values must appear exactly at this offset, and
|
||||
// regex matches must start at this offset. When omitted, matching uses
|
||||
// the default anywhere-in-corpus behavior.
|
||||
// examples:
|
||||
// - name: Match MZ magic at start of file
|
||||
// value: "0"
|
||||
Offset *int `yaml:"offset,omitempty" json:"offset,omitempty" jsonschema:"title=byte offset to match at,description=Optional byte offset where the matcher must match"`
|
||||
// description: |
|
||||
// Prompt is the natural-language question for an llm matcher. The model
|
||||
// judges the selected response part and returns a verdict; the matcher
|
||||
// fires when the verdict equals Expect with at least MinConfidence.
|
||||
@@ -179,8 +188,11 @@ type Matcher struct {
|
||||
matcherType MatcherType
|
||||
binaryDecoded []string
|
||||
regexCompiled []*regexp.Regexp
|
||||
dslCompiled []*govaluate.EvaluableExpression
|
||||
llmClient LLMClient
|
||||
// offsetRegexCompiled holds the anchored variant of each regex used when
|
||||
// Offset is set, in the same order as regexCompiled
|
||||
offsetRegexCompiled []*regexp.Regexp
|
||||
dslCompiled []*govaluate.EvaluableExpression
|
||||
llmClient LLMClient
|
||||
}
|
||||
|
||||
// ConditionType is the type of condition for matcher
|
||||
|
||||
@@ -47,13 +47,13 @@ func (matcher *Matcher) Validate() error {
|
||||
expectedFields = append(commonExpectedFields, "Size", "Part")
|
||||
case WordsMatcher:
|
||||
requiredField, valueCount = "words", len(matcher.Words)
|
||||
expectedFields = append(commonExpectedFields, "Words", "Part", "Encoding", "CaseInsensitive")
|
||||
expectedFields = append(commonExpectedFields, "Words", "Part", "Encoding", "CaseInsensitive", "Offset")
|
||||
case BinaryMatcher:
|
||||
requiredField, valueCount = "binary", len(matcher.Binary)
|
||||
expectedFields = append(commonExpectedFields, "Binary", "Part", "Encoding", "CaseInsensitive")
|
||||
expectedFields = append(commonExpectedFields, "Binary", "Part", "Encoding", "CaseInsensitive", "Offset")
|
||||
case RegexMatcher:
|
||||
requiredField, valueCount = "regex", len(matcher.Regex)
|
||||
expectedFields = append(commonExpectedFields, "Regex", "Part", "Encoding", "CaseInsensitive")
|
||||
expectedFields = append(commonExpectedFields, "Regex", "Part", "Encoding", "CaseInsensitive", "Offset")
|
||||
case XPathMatcher:
|
||||
requiredField, valueCount = "xpath", len(matcher.XPath)
|
||||
expectedFields = append(commonExpectedFields, "XPath", "Part")
|
||||
@@ -70,6 +70,9 @@ func (matcher *Matcher) Validate() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if matcher.Offset != nil && *matcher.Offset < 0 {
|
||||
return fmt.Errorf("offset must be >= 0 (got %d)", *matcher.Offset)
|
||||
}
|
||||
if requiredField != "" && valueCount == 0 {
|
||||
return fmt.Errorf("%s matcher requires at least one %s value", matcher.matcherType, requiredField)
|
||||
}
|
||||
|
||||
@@ -34,6 +34,10 @@ var (
|
||||
chunkSize, _ = units.FromHumanSize("100Mb")
|
||||
)
|
||||
|
||||
// chunkStartKey holds the byte position of the current chunk in the file, so
|
||||
// matcher offsets are measured from the start of the file
|
||||
const chunkStartKey = "file-chunk-start"
|
||||
|
||||
// Request contains a File matching mechanism for local disk operations.
|
||||
type Request struct {
|
||||
// Operators for the current request go here.
|
||||
@@ -119,6 +123,7 @@ var RequestPartDefinitions = map[string]string{
|
||||
"matched": "Matched is the input which was matched upon",
|
||||
"path": "Path is the path of file on local filesystem",
|
||||
"type": "Type is the type of request made",
|
||||
"filesize": "Size of the file on disk in bytes",
|
||||
"raw,body,all,data": "Raw contains the raw file contents",
|
||||
}
|
||||
|
||||
|
||||
@@ -20,15 +20,18 @@ func (request *Request) Match(data map[string]interface{}, matcher *matchers.Mat
|
||||
return false, []string{}
|
||||
}
|
||||
|
||||
// chunkStart keeps matcher offsets absolute when the file is read in chunks
|
||||
chunkStart, _ := data[chunkStartKey].(int)
|
||||
|
||||
switch matcher.GetType() {
|
||||
case matchers.SizeMatcher:
|
||||
return matcher.Result(matcher.MatchSize(len(itemStr))), []string{}
|
||||
case matchers.WordsMatcher:
|
||||
return matcher.ResultWithMatchedSnippet(matcher.MatchWordsWithOptions(itemStr, nil, request.options.GetOptions()))
|
||||
return matcher.ResultWithMatchedSnippet(matcher.MatchWordsAt(itemStr, nil, request.options.GetOptions(), chunkStart))
|
||||
case matchers.RegexMatcher:
|
||||
return matcher.ResultWithMatchedSnippet(matcher.MatchRegex(itemStr))
|
||||
return matcher.ResultWithMatchedSnippet(matcher.MatchRegexAt(itemStr, chunkStart))
|
||||
case matchers.BinaryMatcher:
|
||||
return matcher.ResultWithMatchedSnippet(matcher.MatchBinary(itemStr))
|
||||
return matcher.ResultWithMatchedSnippet(matcher.MatchBinaryAt(itemStr, chunkStart))
|
||||
case matchers.DSLMatcher:
|
||||
return matcher.Result(matcher.MatchDSLWithOptions(data, request.options.GetOptions())), []string{}
|
||||
case matchers.XPathMatcher:
|
||||
@@ -79,12 +82,13 @@ func (request *Request) getMatchPart(part string, data output.InternalEvent) (st
|
||||
}
|
||||
|
||||
// responseToDSLMap converts a file chunk elaboration to a map for use in DSL matching
|
||||
func (request *Request) responseToDSLMap(raw, inputFilePath, matchedFileName string) output.InternalEvent {
|
||||
func (request *Request) responseToDSLMap(raw, inputFilePath, matchedFileName string, fileSize int64) output.InternalEvent {
|
||||
return output.InternalEvent{
|
||||
"path": inputFilePath,
|
||||
"matched": matchedFileName,
|
||||
"raw": raw,
|
||||
"type": request.Type().String(),
|
||||
"filesize": fileSize,
|
||||
"template-id": request.options.TemplateID,
|
||||
"template-info": request.options.TemplateInfo,
|
||||
"template-path": request.options.TemplatePath,
|
||||
|
||||
@@ -49,9 +49,10 @@ func TestResponseToDSLMap(t *testing.T) {
|
||||
require.Nil(t, err, "could not compile file request")
|
||||
|
||||
resp := "test-data\r\n"
|
||||
event := request.responseToDSLMap(resp, "one.one.one.one", "one.one.one.one")
|
||||
require.Len(t, event, 7, "could not get correct number of items in dsl map")
|
||||
event := request.responseToDSLMap(resp, "one.one.one.one", "one.one.one.one", 11)
|
||||
require.Len(t, event, 8, "could not get correct number of items in dsl map")
|
||||
require.Equal(t, resp, event["raw"], "could not get correct resp")
|
||||
require.Equal(t, int64(11), event["filesize"])
|
||||
}
|
||||
|
||||
func TestFileOperatorMatch(t *testing.T) {
|
||||
@@ -75,8 +76,8 @@ func TestFileOperatorMatch(t *testing.T) {
|
||||
require.Nil(t, err, "could not compile file request")
|
||||
|
||||
resp := "test-data\r\n1.1.1.1\r\n"
|
||||
event := request.responseToDSLMap(resp, "one.one.one.one", "one.one.one.one")
|
||||
require.Len(t, event, 7, "could not get correct number of items in dsl map")
|
||||
event := request.responseToDSLMap(resp, "one.one.one.one", "one.one.one.one", 11)
|
||||
require.Len(t, event, 8, "could not get correct number of items in dsl map")
|
||||
require.Equal(t, resp, event["raw"], "could not get correct resp")
|
||||
|
||||
t.Run("valid", func(t *testing.T) {
|
||||
@@ -124,8 +125,8 @@ func TestFileOperatorMatch(t *testing.T) {
|
||||
|
||||
t.Run("caseInsensitive", func(t *testing.T) {
|
||||
resp := "TEST-DATA\r\n1.1.1.1\r\n"
|
||||
event := request.responseToDSLMap(resp, "one.one.one.one", "one.one.one.one")
|
||||
require.Len(t, event, 7, "could not get correct number of items in dsl map")
|
||||
event := request.responseToDSLMap(resp, "one.one.one.one", "one.one.one.one", 11)
|
||||
require.Len(t, event, 8, "could not get correct number of items in dsl map")
|
||||
require.Equal(t, resp, event["raw"], "could not get correct resp")
|
||||
|
||||
matcher := &matchers.Matcher{
|
||||
@@ -164,8 +165,8 @@ func TestFileOperatorExtract(t *testing.T) {
|
||||
require.Nil(t, err, "could not compile file request")
|
||||
|
||||
resp := "test-data\r\n1.1.1.1\r\n"
|
||||
event := request.responseToDSLMap(resp, "one.one.one.one", "one.one.one.one")
|
||||
require.Len(t, event, 7, "could not get correct number of items in dsl map")
|
||||
event := request.responseToDSLMap(resp, "one.one.one.one", "one.one.one.one", 11)
|
||||
require.Len(t, event, 8, "could not get correct number of items in dsl map")
|
||||
require.Equal(t, resp, event["raw"], "could not get correct resp")
|
||||
|
||||
t.Run("extract", func(t *testing.T) {
|
||||
@@ -283,8 +284,8 @@ func testFileMakeResult(t *testing.T, matchers []*matchers.Matcher, matcherCondi
|
||||
matchedFileName := "test.txt"
|
||||
fileContent := "test-data\r\n1.1.1.1\r\n"
|
||||
|
||||
event := request.responseToDSLMap(fileContent, "/tmp", matchedFileName)
|
||||
require.Len(t, event, 7, "could not get correct number of items in dsl map")
|
||||
event := request.responseToDSLMap(fileContent, "/tmp", matchedFileName, 11)
|
||||
require.Len(t, event, 8, "could not get correct number of items in dsl map")
|
||||
require.Equal(t, fileContent, event["raw"], "could not get correct resp")
|
||||
|
||||
finalEvent := &output.InternalWrappedEvent{InternalEvent: event}
|
||||
|
||||
@@ -258,7 +258,7 @@ func (request *Request) processReader(reader io.Reader, filePath string, input *
|
||||
}
|
||||
|
||||
// build event structure to interface with internal logic
|
||||
return request.buildEvent(input.MetaInput.Input, filePath, fileMatches, opResult, previousInternalEvent), fileMatches, nil
|
||||
return request.buildEvent(input.MetaInput.Input, filePath, totalBytes, fileMatches, opResult, previousInternalEvent), fileMatches, nil
|
||||
}
|
||||
|
||||
func (request *Request) findMatchesWithReader(reader io.Reader, input *contextargs.Context, filePath string, totalBytes int64, previous output.InternalEvent) ([]FileMatch, *operators.Result) {
|
||||
@@ -269,9 +269,13 @@ func (request *Request) findMatchesWithReader(reader io.Reader, input *contextar
|
||||
// we are forced to check if the whole file needs to be elaborated
|
||||
// - matchers-condition option set to AND
|
||||
hasAndCondition := request.CompiledOperators.GetMatchersCondition() == matchers.ANDCondition
|
||||
// - any matcher has AND condition
|
||||
hasOffsetMatcher := false
|
||||
// - any matcher has AND condition or an absolute offset
|
||||
for _, matcher := range request.CompiledOperators.Matchers {
|
||||
if hasAndCondition {
|
||||
if matcher.Offset != nil {
|
||||
hasOffsetMatcher = true
|
||||
}
|
||||
if hasAndCondition && hasOffsetMatcher {
|
||||
break
|
||||
}
|
||||
if matcher.GetCondition() == matchers.ANDCondition {
|
||||
@@ -281,11 +285,13 @@ func (request *Request) findMatchesWithReader(reader io.Reader, input *contextar
|
||||
|
||||
scanner := bufio.NewScanner(reader)
|
||||
buffer := []byte{}
|
||||
if hasAndCondition {
|
||||
if hasAndCondition || hasOffsetMatcher {
|
||||
scanner.Buffer(buffer, int(defaultMaxReadSize))
|
||||
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||
defaultMaxReadSizeInt := int(defaultMaxReadSize)
|
||||
if len(data) > defaultMaxReadSizeInt {
|
||||
// the scanner buffer never grows past defaultMaxReadSize, so a full
|
||||
// buffer must already yield a chunk or the scan fails as too long
|
||||
if len(data) >= defaultMaxReadSizeInt {
|
||||
return defaultMaxReadSizeInt, data[0:defaultMaxReadSizeInt], nil
|
||||
}
|
||||
if !atEOF {
|
||||
@@ -308,8 +314,9 @@ func (request *Request) findMatchesWithReader(reader io.Reader, input *contextar
|
||||
processedBytes := units.BytesSize(float64(currentBytes))
|
||||
|
||||
gologger.Verbose().Msgf("[%s] Processing file %s chunk %s/%s", request.options.TemplateID, filePath, processedBytes, totalBytesString)
|
||||
dslMap := request.responseToDSLMap(lineContent, input.MetaInput.Input, filePath)
|
||||
dslMap := request.responseToDSLMap(lineContent, input.MetaInput.Input, filePath, totalBytes)
|
||||
maps.Copy(dslMap, previous)
|
||||
dslMap[chunkStartKey] = bytesCount
|
||||
// add vars to template context
|
||||
request.options.AddTemplateVars(input.MetaInput, request.Type(), request.ID, dslMap)
|
||||
// add template context variables to DSL map
|
||||
@@ -374,10 +381,10 @@ func (request *Request) findMatchesWithReader(reader io.Reader, input *contextar
|
||||
return fileMatches, opResult
|
||||
}
|
||||
|
||||
func (request *Request) buildEvent(input, filePath string, fileMatches []FileMatch, operatorResult *operators.Result, previous output.InternalEvent) *output.InternalWrappedEvent {
|
||||
func (request *Request) buildEvent(input, filePath string, fileSize int64, fileMatches []FileMatch, operatorResult *operators.Result, previous output.InternalEvent) *output.InternalWrappedEvent {
|
||||
exprLines := make(map[string][]int)
|
||||
exprBytes := make(map[string][]int)
|
||||
internalEvent := request.responseToDSLMap("", input, filePath)
|
||||
internalEvent := request.responseToDSLMap("", input, filePath, fileSize)
|
||||
maps.Copy(internalEvent, previous)
|
||||
for _, fileMatch := range fileMatches {
|
||||
exprLines[fileMatch.Expr] = append(exprLines[fileMatch.Expr], fileMatch.Line)
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/internal/tests/testutils"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/model"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/severity"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/operators"
|
||||
@@ -21,7 +22,6 @@ import (
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/operators/matchers"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
|
||||
"github.com/projectdiscovery/nuclei/v3/internal/tests/testutils"
|
||||
permissionutil "github.com/projectdiscovery/utils/permission"
|
||||
)
|
||||
|
||||
@@ -212,3 +212,160 @@ func TestFileProtocolConcurrentExecution(t *testing.T) {
|
||||
timesMutex.Lock()
|
||||
defer timesMutex.Unlock()
|
||||
}
|
||||
|
||||
func TestFileFilesizeAndOffset(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "nuclei-filesize-*")
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = os.RemoveAll(tempDir) }()
|
||||
|
||||
content := []byte("MZPAYLOADDATA")
|
||||
filePath := filepath.Join(tempDir, "sample.bin")
|
||||
require.NoError(t, os.WriteFile(filePath, content, permissionutil.TempFilePermission))
|
||||
|
||||
options := testutils.DefaultOptions
|
||||
testutils.Init(options)
|
||||
templateID := "testing-file-filesize-offset"
|
||||
executerOpts := testutils.NewMockExecuterOptions(options, &testutils.TemplateInfo{
|
||||
ID: templateID,
|
||||
Info: model.Info{SeverityHolder: severity.Holder{Severity: severity.Low}, Name: "test"},
|
||||
})
|
||||
|
||||
offset0 := 0
|
||||
request := &Request{
|
||||
ID: templateID,
|
||||
MaxSize: "1Gb",
|
||||
Extensions: []string{"all"},
|
||||
Operators: operators.Operators{
|
||||
MatchersCondition: "and",
|
||||
Matchers: []*matchers.Matcher{
|
||||
{
|
||||
Type: matchers.MatcherTypeHolder{MatcherType: matchers.WordsMatcher},
|
||||
Part: "raw",
|
||||
Words: []string{"MZ"},
|
||||
Offset: &offset0,
|
||||
},
|
||||
{
|
||||
Type: matchers.MatcherTypeHolder{MatcherType: matchers.DSLMatcher},
|
||||
DSL: []string{"filesize == 13"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
require.NoError(t, request.Compile(executerOpts))
|
||||
|
||||
var matched bool
|
||||
ctxArgs := contextargs.NewWithInput(context.Background(), tempDir)
|
||||
err = request.ExecuteWithResults(ctxArgs, nil, nil, func(event *output.InternalWrappedEvent) {
|
||||
if event.OperatorsResult != nil && event.OperatorsResult.Matched {
|
||||
matched = true
|
||||
require.Equal(t, int64(len(content)), event.InternalEvent["filesize"])
|
||||
}
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.True(t, matched)
|
||||
}
|
||||
|
||||
func TestFileOffsetUsesAbsolutePosition(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "nuclei-offset-*")
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = os.RemoveAll(tempDir) }()
|
||||
|
||||
filePath := filepath.Join(tempDir, "sample.txt")
|
||||
require.NoError(t, os.WriteFile(filePath, []byte("header\nMZ"), permissionutil.TempFilePermission))
|
||||
|
||||
options := testutils.DefaultOptions
|
||||
testutils.Init(options)
|
||||
templateID := "testing-file-absolute-offset"
|
||||
executerOpts := testutils.NewMockExecuterOptions(options, &testutils.TemplateInfo{
|
||||
ID: templateID,
|
||||
Info: model.Info{SeverityHolder: severity.Holder{Severity: severity.Low}, Name: "test"},
|
||||
})
|
||||
|
||||
offset0 := 0
|
||||
request := &Request{
|
||||
ID: templateID,
|
||||
MaxSize: "1Gb",
|
||||
Extensions: []string{"txt"},
|
||||
Operators: operators.Operators{
|
||||
Matchers: []*matchers.Matcher{{
|
||||
Type: matchers.MatcherTypeHolder{MatcherType: matchers.WordsMatcher},
|
||||
Part: "raw",
|
||||
Words: []string{"MZ"},
|
||||
Offset: &offset0,
|
||||
}},
|
||||
},
|
||||
}
|
||||
require.NoError(t, request.Compile(executerOpts))
|
||||
|
||||
var matched bool
|
||||
ctxArgs := contextargs.NewWithInput(context.Background(), tempDir)
|
||||
err = request.ExecuteWithResults(ctxArgs, nil, nil, func(event *output.InternalWrappedEvent) {
|
||||
matched = event.OperatorsResult != nil && event.OperatorsResult.Matched
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.False(t, matched)
|
||||
}
|
||||
|
||||
func TestFileOffsetAbsoluteAcrossChunks(t *testing.T) {
|
||||
// shrink the chunk size so the file is read in several chunks
|
||||
previousMaxReadSize := defaultMaxReadSize
|
||||
defaultMaxReadSize = 5
|
||||
defer func() { defaultMaxReadSize = previousMaxReadSize }()
|
||||
|
||||
tempDir, err := os.MkdirTemp("", "nuclei-offset-chunks-*")
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = os.RemoveAll(tempDir) }()
|
||||
|
||||
// chunks are "01234", "56789" and "MZabc"
|
||||
filePath := filepath.Join(tempDir, "sample.bin")
|
||||
require.NoError(t, os.WriteFile(filePath, []byte("0123456789MZabc"), permissionutil.TempFilePermission))
|
||||
|
||||
options := testutils.DefaultOptions
|
||||
testutils.Init(options)
|
||||
|
||||
run := func(t *testing.T, matcher *matchers.Matcher) bool {
|
||||
templateID := "testing-file-offset-chunks"
|
||||
executerOpts := testutils.NewMockExecuterOptions(options, &testutils.TemplateInfo{
|
||||
ID: templateID,
|
||||
Info: model.Info{SeverityHolder: severity.Holder{Severity: severity.Low}, Name: "test"},
|
||||
})
|
||||
request := &Request{
|
||||
ID: templateID,
|
||||
MaxSize: "1Kb",
|
||||
Extensions: []string{"all"},
|
||||
Operators: operators.Operators{Matchers: []*matchers.Matcher{matcher}},
|
||||
}
|
||||
require.NoError(t, request.Compile(executerOpts))
|
||||
|
||||
var matched bool
|
||||
ctxArgs := contextargs.NewWithInput(context.Background(), tempDir)
|
||||
err := request.ExecuteWithResults(ctxArgs, nil, nil, func(event *output.InternalWrappedEvent) {
|
||||
if event.OperatorsResult != nil && event.OperatorsResult.Matched {
|
||||
matched = true
|
||||
}
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return matched
|
||||
}
|
||||
offset := func(v int) *int { return &v }
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
matcher *matchers.Matcher
|
||||
want bool
|
||||
}{
|
||||
{"word at absolute offset", &matchers.Matcher{Type: matchers.MatcherTypeHolder{MatcherType: matchers.WordsMatcher}, Words: []string{"MZ"}, Offset: offset(10)}, true},
|
||||
{"word at chunk relative offset", &matchers.Matcher{Type: matchers.MatcherTypeHolder{MatcherType: matchers.WordsMatcher}, Words: []string{"MZ"}, Offset: offset(0)}, false},
|
||||
{"word inside second chunk", &matchers.Matcher{Type: matchers.MatcherTypeHolder{MatcherType: matchers.WordsMatcher}, Words: []string{"78"}, Offset: offset(7)}, true},
|
||||
{"case insensitive word", &matchers.Matcher{Type: matchers.MatcherTypeHolder{MatcherType: matchers.WordsMatcher}, Words: []string{"mz"}, CaseInsensitive: true, Offset: offset(10)}, true},
|
||||
{"binary at absolute offset", &matchers.Matcher{Type: matchers.MatcherTypeHolder{MatcherType: matchers.BinaryMatcher}, Binary: []string{"4d5a"}, Offset: offset(10)}, true},
|
||||
{"regex at absolute offset", &matchers.Matcher{Type: matchers.MatcherTypeHolder{MatcherType: matchers.RegexMatcher}, Regex: []string{"M[A-Z]"}, Offset: offset(10)}, true},
|
||||
{"regex at chunk relative offset", &matchers.Matcher{Type: matchers.MatcherTypeHolder{MatcherType: matchers.RegexMatcher}, Regex: []string{"M[A-Z]"}, Offset: offset(0)}, false},
|
||||
{"regex inside last chunk", &matchers.Matcher{Type: matchers.MatcherTypeHolder{MatcherType: matchers.RegexMatcher}, Regex: []string{"Z[a-z]+"}, Offset: offset(11)}, true},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require.Equal(t, tc.want, run(t, tc.matcher))
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user