Skip to content

Commit 7a07e3d

Browse files
authored
create ignore regexs conditionally (#2805)
Signed-off-by: Alex Goodman <[email protected]>
1 parent a9fb8f1 commit 7a07e3d

File tree

3 files changed

+309
-11
lines changed

3 files changed

+309
-11
lines changed

grype/db/v6/models_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
package v6
22

33
import (
4-
"github.com/google/go-cmp/cmp"
54
"testing"
65

6+
"github.com/google/go-cmp/cmp"
77
"github.com/stretchr/testify/assert"
88
"github.com/stretchr/testify/require"
99
)

grype/match/ignore.go

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -222,13 +222,21 @@ func packageNameRegex(packageName string) (*regexp.Regexp, error) {
222222
}
223223

224224
func ifPackageNameApplies(name string) ignoreCondition {
225-
pattern, err := packageNameRegex(name)
226-
if err != nil {
227-
return func(Match) bool { return false }
228-
}
225+
// with enough ignore rules, we could end up needlessly creating a lot of regexes, which is not ideal.
226+
// instead lets detect if the input string is a regex or not, and if it is, then compile it...
227+
// otherwise, we can just do a simple string comparison
228+
if isLikelyARegex(name) {
229+
pattern, err := packageNameRegex(name)
230+
if err != nil || pattern == nil {
231+
return func(Match) bool { return false }
232+
}
229233

234+
return func(match Match) bool {
235+
return pattern.MatchString(match.Package.Name)
236+
}
237+
}
230238
return func(match Match) bool {
231-
return pattern.MatchString(match.Package.Name)
239+
return name == match.Package.Name
232240
}
233241
}
234242

@@ -257,21 +265,43 @@ func ifPackageLocationApplies(location string) ignoreCondition {
257265
}
258266

259267
func ifUpstreamPackageNameApplies(name string) ignoreCondition {
260-
pattern, err := packageNameRegex(name)
261-
if err != nil {
262-
log.WithFields("name", name, "error", err).Debug("unable to parse name expression")
263-
return func(Match) bool { return false }
268+
// with enough ignore rules, we could end up needlessly creating a lot of regexes, which is not ideal.
269+
// instead lets detect if the input string is a regex or not, and if it is, then compile it...
270+
// otherwise, we can just do a simple string comparison
271+
if isLikelyARegex(name) {
272+
pattern, err := packageNameRegex(name)
273+
if err != nil {
274+
log.WithFields("name", name, "error", err).Debug("unable to parse name expression")
275+
return func(Match) bool { return false }
276+
}
277+
return func(match Match) bool {
278+
for _, upstream := range match.Package.Upstreams {
279+
if pattern.MatchString(upstream.Name) {
280+
return true
281+
}
282+
}
283+
return false
284+
}
264285
}
265286
return func(match Match) bool {
266287
for _, upstream := range match.Package.Upstreams {
267-
if pattern.MatchString(upstream.Name) {
288+
if name == upstream.Name {
268289
return true
269290
}
270291
}
271292
return false
272293
}
273294
}
274295

296+
// isRegexPattern is a compiled regex that matches common regex characters. We intentionally leave out
297+
// the '.' character, as it is a common character in package names and versions, and we do not want to
298+
// treat it as a regex unless there is other evidence that it is a regex.
299+
var isRegexPattern = regexp.MustCompile(`[\^\$\*\+\?\[\]\(\)\{\}\|\\]|\\[dDwWsSnrtfv]`)
300+
301+
func isLikelyARegex(s string) bool {
302+
return isRegexPattern.MatchString(s)
303+
}
304+
275305
func ifMatchTypeApplies(matchType Type) ignoreCondition {
276306
return func(match Match) bool {
277307
for _, mType := range match.Details.Types() {

grype/match/ignore_test.go

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -833,6 +833,274 @@ var (
833833
}
834834
)
835835

836+
func TestIsRegex(t *testing.T) {
837+
tests := []struct {
838+
name string
839+
input string
840+
expected bool
841+
}{
842+
// simple strings that should NOT be detected as regex
843+
{
844+
name: "simple string",
845+
input: "hello",
846+
expected: false,
847+
},
848+
{
849+
name: "alphanumeric with dashes",
850+
input: "kernel-headers",
851+
expected: false,
852+
},
853+
{
854+
name: "alphanumeric with underscores",
855+
input: "my_package_name",
856+
expected: false,
857+
},
858+
{
859+
name: "version numbers",
860+
input: "1.2.3",
861+
expected: false, // dots are no longer considered regex metacharacters
862+
},
863+
{
864+
name: "empty string",
865+
input: "",
866+
expected: false,
867+
},
868+
{
869+
name: "spaces only",
870+
input: " ",
871+
expected: false,
872+
},
873+
{
874+
name: "numbers only",
875+
input: "12345",
876+
expected: false,
877+
},
878+
{
879+
name: "letters and numbers",
880+
input: "abc123",
881+
expected: false,
882+
},
883+
{
884+
name: "with slashes",
885+
input: "path/to/file",
886+
expected: false,
887+
},
888+
{
889+
name: "with colons",
890+
input: "namespace:package",
891+
expected: false,
892+
},
893+
{
894+
name: "with at symbol",
895+
896+
expected: false, // dots are no longer considered regex metacharacters
897+
},
898+
899+
// strings with regex metacharacters that SHOULD be detected as regex
900+
{
901+
name: "caret at start",
902+
input: "^start",
903+
expected: true,
904+
},
905+
{
906+
name: "dollar at end",
907+
input: "end$",
908+
expected: true,
909+
},
910+
{
911+
name: "asterisk wildcard",
912+
input: "test*",
913+
expected: true,
914+
},
915+
{
916+
name: "plus quantifier",
917+
input: "test+",
918+
expected: true,
919+
},
920+
{
921+
name: "question mark",
922+
input: "test?",
923+
expected: true,
924+
},
925+
{
926+
name: "dot wildcard",
927+
input: "test.",
928+
expected: false, // dots are no longer considered regex metacharacters
929+
},
930+
{
931+
name: "square brackets",
932+
input: "test[abc]",
933+
expected: true,
934+
},
935+
{
936+
name: "parentheses grouping",
937+
input: "(test)",
938+
expected: true,
939+
},
940+
{
941+
name: "curly braces quantifier",
942+
input: "test{1,3}",
943+
expected: true,
944+
},
945+
{
946+
name: "pipe alternation",
947+
input: "test|other",
948+
expected: true,
949+
},
950+
{
951+
name: "backslash escape",
952+
input: "test\\",
953+
expected: true,
954+
},
955+
{
956+
name: "multiple metacharacters",
957+
input: "^test.*$",
958+
expected: true,
959+
},
960+
{
961+
name: "complex regex pattern",
962+
input: "kernel-headers.*",
963+
expected: true,
964+
},
965+
{
966+
name: "anchored regex",
967+
input: "^kernel-headers$",
968+
expected: true,
969+
},
970+
{
971+
name: "character class",
972+
input: "test[0-9]",
973+
expected: true,
974+
},
975+
976+
// escaped character classes
977+
{
978+
name: "escaped digit",
979+
input: "\\d",
980+
expected: true,
981+
},
982+
{
983+
name: "escaped non-digit",
984+
input: "\\D",
985+
expected: true,
986+
},
987+
{
988+
name: "escaped word character",
989+
input: "\\w",
990+
expected: true,
991+
},
992+
{
993+
name: "escaped non-word character",
994+
input: "\\W",
995+
expected: true,
996+
},
997+
{
998+
name: "escaped whitespace",
999+
input: "\\s",
1000+
expected: true,
1001+
},
1002+
{
1003+
name: "escaped non-whitespace",
1004+
input: "\\S",
1005+
expected: true,
1006+
},
1007+
{
1008+
name: "escaped newline",
1009+
input: "\\n",
1010+
expected: true,
1011+
},
1012+
{
1013+
name: "escaped carriage return",
1014+
input: "\\r",
1015+
expected: true,
1016+
},
1017+
{
1018+
name: "escaped tab",
1019+
input: "\\t",
1020+
expected: true,
1021+
},
1022+
{
1023+
name: "escaped form feed",
1024+
input: "\\f",
1025+
expected: true,
1026+
},
1027+
{
1028+
name: "escaped vertical tab",
1029+
input: "\\v",
1030+
expected: true,
1031+
},
1032+
{
1033+
name: "escaped character classes in longer string",
1034+
input: "prefix\\dpostfix",
1035+
expected: true,
1036+
},
1037+
{
1038+
name: "multiple escaped classes",
1039+
input: "\\w+\\s*\\d+",
1040+
expected: true,
1041+
},
1042+
1043+
// edge cases
1044+
{
1045+
name: "single backslash",
1046+
input: "\\",
1047+
expected: true,
1048+
},
1049+
{
1050+
name: "single caret",
1051+
input: "^",
1052+
expected: true,
1053+
},
1054+
{
1055+
name: "single dollar",
1056+
input: "$",
1057+
expected: true,
1058+
},
1059+
{
1060+
name: "single dot",
1061+
input: ".",
1062+
expected: false, // dots are no longer considered regex metacharacters
1063+
},
1064+
{
1065+
name: "backslash followed by regular character",
1066+
input: "\\a",
1067+
expected: true, // backslash is still a metacharacter
1068+
},
1069+
{
1070+
name: "backslash at end",
1071+
input: "test\\",
1072+
expected: true,
1073+
},
1074+
{
1075+
name: "mixed metacharacters and escaped classes",
1076+
input: "^\\w+\\.\\d{2,}$",
1077+
expected: true,
1078+
},
1079+
{
1080+
name: "real world package patterns",
1081+
input: "linux-.*",
1082+
expected: true,
1083+
},
1084+
{
1085+
name: "real world upstream patterns",
1086+
input: "linux.*",
1087+
expected: true,
1088+
},
1089+
{
1090+
name: "real world header patterns",
1091+
input: "linux-.*-headers-.*",
1092+
expected: true,
1093+
},
1094+
}
1095+
1096+
for _, tt := range tests {
1097+
t.Run(tt.name, func(t *testing.T) {
1098+
got := isLikelyARegex(tt.input)
1099+
assert.Equal(t, tt.expected, got)
1100+
})
1101+
}
1102+
}
1103+
8361104
func TestShouldIgnore(t *testing.T) {
8371105
cases := []struct {
8381106
name string

0 commit comments

Comments
 (0)