Skip to content
Open
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
15 changes: 10 additions & 5 deletions dtos/path_pattern_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func TestIsWildcard(t *testing.T) {
{"literal pkg:npm/foo", "pkg:npm/foo@1.0.0", false},
{"empty string", "", false},
{"triple star", "***", false},
{"ROOT is wildcard", normalize.GraphRootNodeID, true},
{"ROOT is not a wildcard", normalize.GraphRootNodeID, false},
}

for _, tt := range tests {
Expand Down Expand Up @@ -96,10 +96,15 @@ func TestRootPathPattern(t *testing.T) {
path []string
expected bool
}{
{"ROOT matches ROOT", PathPattern{normalize.GraphRootNodeID}, []string{normalize.GraphRootNodeID}, true},
{"ROOT matches any path with ROOT at end", PathPattern{normalize.GraphRootNodeID}, []string{"A", "B", normalize.GraphRootNodeID}, true},
{"ROOT DOES match path without ROOT", PathPattern{normalize.GraphRootNodeID}, []string{"A", "B", "C"}, true},
{"ROOT does not lead to all paths matching", PathPattern{normalize.GraphRootNodeID, "X"}, []string{"A", "B", "C"}, false},
// ROOT is a stop marker: [root, pkg:A] matches only direct dependencies.
// VulnerabilityPath never contains ROOT, so ROOT in the pattern consumes
// zero path elements and anchors the match to position 0 (no suffix scan).
{"root pkg:A matches direct dependency", PathPattern{normalize.GraphRootNodeID, "pkg:A"}, []string{"pkg:A"}, true},
{"root pkg:A does not match transitive dependency", PathPattern{normalize.GraphRootNodeID, "pkg:A"}, []string{"pkg:B", "pkg:A"}, false},
// [*, ROOT, pkg:A] is equivalent to [ROOT, pkg:A]: ROOT absorbs the wildcard prefix.
{"wildcard root pkg:A matches direct dependency", PathPattern{"*", normalize.GraphRootNodeID, "pkg:A"}, []string{"pkg:A"}, true},
{"wildcard root pkg:A does not match transitive dependency", PathPattern{"*", normalize.GraphRootNodeID, "pkg:A"}, []string{"pkg:B", "pkg:A"}, false},
{"wildcard ROOT pkg matches direct dependency path", PathPattern{"*", normalize.GraphRootNodeID, "pkg:golang/go-jose@v4"}, []string{"pkg:golang/go-jose@v4"}, true},
}

for _, tt := range tests {
Expand Down
29 changes: 25 additions & 4 deletions dtos/vex_rule_dto.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,7 @@ type PathPattern []string

// IsWildcard returns true if the element is a wildcard (*).
func IsWildcard(elem string) bool {
// ROOT is a special element in the path
// this means, THE CURRENT application does not call the vulnerable code
// therefore, we can just replace it with a wildcard for matching purposes
return elem == PathPatternWildcard || elem == normalize.GraphRootNodeID
return elem == PathPatternWildcard
}

// MatchesSuffix checks if the given path's suffix matches this pattern using suffix matching.
Expand All @@ -57,6 +54,14 @@ func (p PathPattern) MatchesSuffix(path []string) bool {
return true
}

// ROOT is a stop marker meaning "direct dependency only". When the pattern
// contains ROOT, skip suffix scanning and match the full path from position 0.
for _, elem := range p {
if elem == normalize.GraphRootNodeID {
return matchPatternExact(p, path)
}
}

// For suffix matching, we try increasingly longer suffixes
// Count non-wildcard elements to determine minimum suffix length
minLen := 0
Expand Down Expand Up @@ -98,6 +103,15 @@ func matchPatternExact(pattern, path []string) bool {
return true
}

// If the next pattern element is ROOT (stop marker), try a zero-length
// match for this wildcard. [*, ROOT, pkg:A] is equivalent to [ROOT, pkg:A]:
// ROOT anchors the remainder to the current path position.
if pattern[pIdx+1] == normalize.GraphRootNodeID {
if matchPatternExact(pattern[pIdx+1:], path[pathIdx:]) {
return true
}
}

// Try to find the next pattern element in the path
nextPattern := pattern[pIdx+1]

Expand Down Expand Up @@ -125,6 +139,13 @@ func matchPatternExact(pattern, path []string) bool {
return pathIdx == len(path)
}

// ROOT is a stop marker — it is never stored in VulnerabilityPath,
// so skip it in the pattern without consuming a path element.
if pattern[pIdx] == normalize.GraphRootNodeID {
pIdx++
continue
}

// Literal match
if pathIdx >= len(path) || pattern[pIdx] != path[pathIdx] {
return false
Expand Down