Skip to content
Merged
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
16 changes: 16 additions & 0 deletions internal/detector/rules/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ func fileResult(t *testing.T, scan model.RuleScan, ruleID string) model.RuleFile
func TestScanRegexMatch(t *testing.T) {
dir := t.TempDir()
writeFile(t, dir, "proj/.github/setup.js", "const x = eval(atob('benign'))\n")
// Decoy: same filename, wrong directory — must be rejected by the full-path check.
writeFile(t, dir, "proj/other/setup.js", "const x = eval(atob('benign'))\n")

rs := prep(t, RuleSet{Rules: []Rule{{
ID: "dropper", Revision: "rev1", FileGlobs: []string{"**/.github/setup.js"},
Expand Down Expand Up @@ -161,6 +163,20 @@ func TestScanExistenceOnly(t *testing.T) {
}
}

func TestScan_WildcardFilenameGlob(t *testing.T) {
dir := t.TempDir()
writeFile(t, dir, "nested/payload.mjs", "anything")

rs := prep(t, RuleSet{Rules: []Rule{{
ID: "wildcard", FileGlobs: []string{"**/*.mjs", "**/payload.mjs"},
}}})
scan := newTestEngine(t, DefaultCaps()).Scan(context.Background(), rs, []string{dir})
fm := fileResult(t, scan, "wildcard")
if fm.MatchedGlob != "**/*.mjs" {
t.Errorf("MatchedGlob got %q, want %q", fm.MatchedGlob, "**/*.mjs")
}
}

func TestScanSizeGuard(t *testing.T) {
dir := t.TempDir()
big := strings.Repeat("A", 2048)
Expand Down
40 changes: 28 additions & 12 deletions internal/detector/rules/roots.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import (
"context"
"errors"
"io/fs"
"path"
"path/filepath"
"strings"
)

// errWalkStop unwinds filepath.WalkDir when a global budget is hit or the
Expand Down Expand Up @@ -45,16 +47,25 @@ type relMatcher struct {
}

// walkRoots performs one TCC-aware walk per search root, matching each regular
// file against every relative glob. Symlinks are never followed (the walk does
// not descend into symlinked directories, and symlinked files are not regular,
// so they are skipped) — the symlink-escape guard. Sets st.globalStop if a
// global file/time budget is hit.
// file against relative globs indexed by fixed filename. A bundle containing a
// wildcard filename falls back to the original all-glob loop. Symlinks are
// never followed (the walk does not descend into symlinked directories, and
// symlinked files are not regular, so they are skipped) — the symlink-escape
// guard. Sets st.globalStop if a global file/time budget is hit.
func (e *Engine) walkRoots(ctx context.Context, st *scanState, searchDirs []string) {
matchersByName := make(map[string][]relMatcher)
var matchers []relMatcher
for _, rstate := range st.states {
for _, cg := range rstate.rule.globs {
if !cg.absolute {
matchers = append(matchers, relMatcher{rstate: rstate, cg: cg})
matcher := relMatcher{rstate: rstate, cg: cg}
matchers = append(matchers, matcher)
name := path.Base(cg.raw)
if strings.ContainsAny(name, "*?") {
matchersByName = nil // preserve original ordering for wildcard filenames
} else if matchersByName != nil {
matchersByName[name] = append(matchersByName[name], matcher)
}
}
}
}
Expand All @@ -66,7 +77,7 @@ func (e *Engine) walkRoots(ctx context.Context, st *scanState, searchDirs []stri
if root == "" {
continue
}
if e.walkOneRoot(ctx, st, root, matchers) {
if e.walkOneRoot(ctx, st, root, matchersByName, matchers) {
st.globalStop = true
return
}
Expand All @@ -75,7 +86,7 @@ func (e *Engine) walkRoots(ctx context.Context, st *scanState, searchDirs []stri

// walkOneRoot walks a single root. Returns true if the whole scan should stop
// (global budget or context cancellation).
func (e *Engine) walkOneRoot(ctx context.Context, st *scanState, root string, matchers []relMatcher) (stopped bool) {
func (e *Engine) walkOneRoot(ctx context.Context, st *scanState, root string, matchersByName map[string][]relMatcher, matchers []relMatcher) (stopped bool) {
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return nil // unreadable entry — skip, never fail the run
Expand All @@ -92,17 +103,22 @@ func (e *Engine) walkOneRoot(ctx context.Context, st *scanState, root string, ma
if !d.Type().IsRegular() {
return nil // skip symlinks/sockets/etc.
}
candidates := matchers
if matchersByName != nil {
candidates = matchersByName[d.Name()]
if len(candidates) == 0 {
return nil
}
}

rel, rerr := filepath.Rel(root, path)
if rerr != nil {
return nil
}
relSlashed := filepath.ToSlash(rel)
for _, m := range matchers {
if m.cg.re.MatchString(relSlashed) {
if e.evaluate(st, m.rstate, path, m.cg.raw) {
return errWalkStop
}
for _, m := range candidates {
if m.cg.re.MatchString(relSlashed) && e.evaluate(st, m.rstate, path, m.cg.raw) {
return errWalkStop
}
}
return nil
Expand Down
Loading