-
Notifications
You must be signed in to change notification settings - Fork 29
Expand file tree
/
Copy pathpath_pattern_test.go
More file actions
202 lines (182 loc) · 7.11 KB
/
path_pattern_test.go
File metadata and controls
202 lines (182 loc) · 7.11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
// Copyright (C) 2026 l3montree GmbH
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package dtos
import (
"testing"
"github.com/l3montree-dev/devguard/normalize"
"github.com/stretchr/testify/assert"
)
func TestIsWildcard(t *testing.T) {
tests := []struct {
name string
elem string
expected bool
}{
{"single wildcard", "*", true},
{"multi wildcard", "**", false},
{"literal A", "A", false},
{"literal pkg:npm/foo", "pkg:npm/foo@1.0.0", false},
{"empty string", "", false},
{"triple star", "***", false},
{"ROOT is not a wildcard", normalize.GraphRootNodeID, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.expected, IsWildcard(tt.elem))
})
}
}
func TestPathPattern_ContainsWildcard(t *testing.T) {
tests := []struct {
name string
pattern PathPattern
expected bool
}{
{"empty pattern", PathPattern{}, false},
{"no wildcards", PathPattern{"A", "B", "C"}, false},
{"single wildcard", PathPattern{"A", "*", "C"}, true},
{"only wildcard", PathPattern{"*"}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.expected, tt.pattern.ContainsWildcard())
})
}
}
func TestPathPattern_MatchesSuffix_ExactMatch(t *testing.T) {
tests := []struct {
name string
pattern PathPattern
path []string
expected bool
}{
// Empty pattern matches everything
{"empty pattern matches empty path", PathPattern{}, []string{}, true},
{"empty pattern matches any path", PathPattern{}, []string{"A", "B", "C"}, true},
// Exact suffix match (no wildcards)
{"exact match single element", PathPattern{"C"}, []string{"A", "B", "C"}, true},
{"exact match two elements", PathPattern{"B", "C"}, []string{"A", "B", "C"}, true},
{"exact match full path", PathPattern{"A", "B", "C"}, []string{"A", "B", "C"}, true},
{"no match wrong suffix", PathPattern{"X", "Y"}, []string{"A", "B", "C"}, false},
{"no match pattern longer than path", PathPattern{"A", "B", "C", "D"}, []string{"A", "B", "C"}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.expected, tt.pattern.MatchesSuffix(tt.path))
})
}
}
func TestRootPathPattern(t *testing.T) {
tests := []struct {
name string
pattern PathPattern
path []string
expected bool
}{
// 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 {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.expected, tt.pattern.MatchesSuffix(tt.path))
})
}
}
func TestPathPattern_MatchesSuffix_Wildcard(t *testing.T) {
tests := []struct {
name string
pattern PathPattern
path []string
expected bool
}{
// Single wildcard (*) matches zero or more elements
{"* matches single element", PathPattern{"*"}, []string{"A"}, true},
{"* matches multiple elements", PathPattern{"*"}, []string{"A", "B", "C"}, true},
{"* matches empty path", PathPattern{"*"}, []string{}, true},
// Wildcard at start
{"* then literal matches suffix", PathPattern{"*", "C"}, []string{"A", "B", "C"}, true},
{"* then literal matches just literal", PathPattern{"*", "C"}, []string{"C"}, true},
{"* then literal no match", PathPattern{"*", "X"}, []string{"A", "B", "C"}, false},
// Wildcard at end
{"literal then * matches", PathPattern{"B", "*"}, []string{"A", "B", "C"}, true},
{"literal then * matches just literal", PathPattern{"C", "*"}, []string{"A", "B", "C"}, true},
// Wildcard in middle
{"A * C matches A B C", PathPattern{"A", "*", "C"}, []string{"A", "B", "C"}, true},
{"A * C matches A C (zero elements)", PathPattern{"A", "*", "C"}, []string{"A", "C"}, true},
{"A * C matches A X Y Z C", PathPattern{"A", "*", "C"}, []string{"A", "X", "Y", "Z", "C"}, true},
// Multiple wildcards
{"* * matches anything", PathPattern{"*", "*"}, []string{"A", "B", "C"}, true},
{"* A * matches X A Y", PathPattern{"*", "A", "*"}, []string{"X", "A", "Y"}, true},
{"* A * matches A (zero on both sides)", PathPattern{"*", "A", "*"}, []string{"A"}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.expected, tt.pattern.MatchesSuffix(tt.path))
})
}
}
func TestPathPattern_MatchesSuffix_RealWorldExamples(t *testing.T) {
// Simulating real vulnerability paths like:
// ["pkg:npm/app@1.0.0", "pkg:npm/lodash@4.17.0", "pkg:npm/vulnerable@1.0.0"]
tests := []struct {
name string
pattern PathPattern
path []string
expected bool
}{
{
"match specific vulnerable package at end",
PathPattern{"pkg:npm/vulnerable@1.0.0"},
[]string{"pkg:npm/app@1.0.0", "pkg:npm/lodash@4.17.0", "pkg:npm/vulnerable@1.0.0"},
true,
},
{
"match any path to vulnerable package",
PathPattern{"*", "pkg:npm/vulnerable@1.0.0"},
[]string{"pkg:npm/app@1.0.0", "pkg:npm/lodash@4.17.0", "pkg:npm/vulnerable@1.0.0"},
true,
},
{
"match lodash leading to vulnerable",
PathPattern{"pkg:npm/lodash@4.17.0", "pkg:npm/vulnerable@1.0.0"},
[]string{"pkg:npm/app@1.0.0", "pkg:npm/lodash@4.17.0", "pkg:npm/vulnerable@1.0.0"},
true,
},
{
"match any intermediate deps to vulnerable",
PathPattern{"*", "pkg:npm/vulnerable@1.0.0"},
[]string{"pkg:npm/app@1.0.0", "pkg:npm/lodash@4.17.0", "pkg:npm/vulnerable@1.0.0"},
true,
},
{
"no match different root",
PathPattern{"pkg:npm/other@1.0.0", "*", "pkg:npm/vulnerable@1.0.0"},
[]string{"pkg:npm/app@1.0.0", "pkg:npm/lodash@4.17.0", "pkg:npm/vulnerable@1.0.0"},
false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.expected, tt.pattern.MatchesSuffix(tt.path))
})
}
}