diff --git a/syft/pkg/cpe_by_specificity.go b/syft/cpe/by_specificity.go similarity index 81% rename from syft/pkg/cpe_by_specificity.go rename to syft/cpe/by_specificity.go index 73cce8d04c1..ad1a9adb8e5 100644 --- a/syft/pkg/cpe_by_specificity.go +++ b/syft/cpe/by_specificity.go @@ -1,4 +1,4 @@ -package pkg +package cpe import ( "sort" @@ -6,15 +6,15 @@ import ( "github.com/facebookincubator/nvdtools/wfn" ) -var _ sort.Interface = (*CPEBySpecificity)(nil) +var _ sort.Interface = (*BySpecificity)(nil) -type CPEBySpecificity []wfn.Attributes +type BySpecificity []wfn.Attributes -func (c CPEBySpecificity) Len() int { return len(c) } +func (c BySpecificity) Len() int { return len(c) } -func (c CPEBySpecificity) Swap(i, j int) { c[i], c[j] = c[j], c[i] } +func (c BySpecificity) Swap(i, j int) { c[i], c[j] = c[j], c[i] } -func (c CPEBySpecificity) Less(i, j int) bool { +func (c BySpecificity) Less(i, j int) bool { iScore := weightedCountForSpecifiedFields(c[i]) jScore := weightedCountForSpecifiedFields(c[j]) @@ -29,7 +29,7 @@ func (c CPEBySpecificity) Less(i, j int) bool { } // if score and length are equal then text sort - // note that we are not using CPEString from the syft pkg + // note that we are not using String from the syft pkg // as we are not encoding/decoding this CPE string so we don't // need the proper quoted version of the CPE. return c[i].BindToFmtString() < c[j].BindToFmtString() diff --git a/syft/cpe/by_specificity_test.go b/syft/cpe/by_specificity_test.go new file mode 100644 index 00000000000..33c0d33368c --- /dev/null +++ b/syft/cpe/by_specificity_test.go @@ -0,0 +1,98 @@ +package cpe + +import ( + "sort" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_BySpecificity(t *testing.T) { + tests := []struct { + name string + input []CPE + expected []CPE + }{ + { + name: "sort strictly by wfn *", + input: []CPE{ + Must("cpe:2.3:a:*:package:1:*:*:*:*:*:*:*"), + Must("cpe:2.3:a:some:package:1:*:*:*:*:*:*:*"), + Must("cpe:2.3:a:*:package:1:*:*:*:*:some:*:*"), + Must("cpe:2.3:a:some:package:1:*:*:*:*:some:*:*"), + Must("cpe:2.3:a:some:package:*:*:*:*:*:*:*:*"), + }, + expected: []CPE{ + Must("cpe:2.3:a:some:package:1:*:*:*:*:some:*:*"), + Must("cpe:2.3:a:some:package:1:*:*:*:*:*:*:*"), + Must("cpe:2.3:a:some:package:*:*:*:*:*:*:*:*"), + Must("cpe:2.3:a:*:package:1:*:*:*:*:some:*:*"), + Must("cpe:2.3:a:*:package:1:*:*:*:*:*:*:*"), + }, + }, + { + name: "sort strictly by field length", + input: []CPE{ + Must("cpe:2.3:a:1:22:1:*:*:*:*:1:*:*"), + Must("cpe:2.3:a:55555:1:1:*:*:*:*:1:*:*"), + Must("cpe:2.3:a:1:1:333:*:*:*:*:1:*:*"), + Must("cpe:2.3:a:1:666666:1:*:*:*:*:1:*:*"), + Must("cpe:2.3:a:1:1:1:*:*:*:*:1:*:*"), + Must("cpe:2.3:a:1:1:1:*:*:*:*:4444:*:*"), + }, + expected: []CPE{ + Must("cpe:2.3:a:1:666666:1:*:*:*:*:1:*:*"), + Must("cpe:2.3:a:55555:1:1:*:*:*:*:1:*:*"), + Must("cpe:2.3:a:1:1:1:*:*:*:*:4444:*:*"), + Must("cpe:2.3:a:1:1:333:*:*:*:*:1:*:*"), + Must("cpe:2.3:a:1:22:1:*:*:*:*:1:*:*"), + Must("cpe:2.3:a:1:1:1:*:*:*:*:1:*:*"), + }, + }, + { + name: "sort by mix of field length and specificity", + input: []CPE{ + Must("cpe:2.3:a:1:666666:*:*:*:*:*:1:*:*"), + Must("cpe:2.3:a:*:1:1:*:*:*:*:4444:*:*"), + Must("cpe:2.3:a:1:*:333:*:*:*:*:*:*:*"), + Must("cpe:2.3:a:1:1:1:*:*:*:*:1:*:*"), + Must("cpe:2.3:a:1:22:1:*:*:*:*:1:*:*"), + Must("cpe:2.3:a:55555:1:1:*:*:*:*:1:*:*"), + }, + expected: []CPE{ + Must("cpe:2.3:a:55555:1:1:*:*:*:*:1:*:*"), + Must("cpe:2.3:a:1:22:1:*:*:*:*:1:*:*"), + Must("cpe:2.3:a:1:1:1:*:*:*:*:1:*:*"), + Must("cpe:2.3:a:1:666666:*:*:*:*:*:1:*:*"), + Must("cpe:2.3:a:*:1:1:*:*:*:*:4444:*:*"), + Must("cpe:2.3:a:1:*:333:*:*:*:*:*:*:*"), + }, + }, + { + name: "sort by mix of field length, specificity, dash", + input: []CPE{ + Must("cpe:2.3:a:alpine:alpine_keys:2.3-r1:*:*:*:*:*:*:*"), + Must("cpe:2.3:a:alpine_keys:alpine_keys:2.3-r1:*:*:*:*:*:*:*"), + Must("cpe:2.3:a:alpine-keys:alpine_keys:2.3-r1:*:*:*:*:*:*:*"), + Must("cpe:2.3:a:alpine:alpine-keys:2.3-r1:*:*:*:*:*:*:*"), + Must("cpe:2.3:a:alpine-keys:alpine-keys:2.3-r1:*:*:*:*:*:*:*"), + Must("cpe:2.3:a:alpine_keys:alpine-keys:2.3-r1:*:*:*:*:*:*:*"), + }, + expected: []CPE{ + Must("cpe:2.3:a:alpine-keys:alpine-keys:2.3-r1:*:*:*:*:*:*:*"), + Must("cpe:2.3:a:alpine-keys:alpine_keys:2.3-r1:*:*:*:*:*:*:*"), + Must("cpe:2.3:a:alpine_keys:alpine-keys:2.3-r1:*:*:*:*:*:*:*"), + Must("cpe:2.3:a:alpine_keys:alpine_keys:2.3-r1:*:*:*:*:*:*:*"), + Must("cpe:2.3:a:alpine:alpine-keys:2.3-r1:*:*:*:*:*:*:*"), + Must("cpe:2.3:a:alpine:alpine_keys:2.3-r1:*:*:*:*:*:*:*"), + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + sort.Sort(BySpecificity(test.input)) + assert.Equal(t, test.expected, test.input) + }) + } +} diff --git a/syft/pkg/cpe.go b/syft/cpe/cpe.go similarity index 81% rename from syft/pkg/cpe.go rename to syft/cpe/cpe.go index 1d640272695..b716eef55bf 100644 --- a/syft/pkg/cpe.go +++ b/syft/cpe/cpe.go @@ -1,4 +1,4 @@ -package pkg +package cpe import ( "fmt" @@ -24,17 +24,17 @@ const cpeRegexString = ((`^([c][pP][eE]:/[AHOaho]?(:[A-Za-z0-9\._\-~%]*){0,6})`) var cpeRegex = regexp.MustCompile(cpeRegexString) -// NewCPE will parse a formatted CPE string and return a CPE object. Some input, such as the existence of whitespace +// New will parse a formatted CPE string and return a CPE object. Some input, such as the existence of whitespace // characters is allowed, however, a more strict validation is done after this sanitization process. -func NewCPE(cpeStr string) (CPE, error) { +func New(cpeStr string) (CPE, error) { // get a CPE object based on the given string --don't validate yet since it may be possible to escape select cases on the callers behalf - c, err := newCPEWithoutValidation(cpeStr) + c, err := newWithoutValidation(cpeStr) if err != nil { return CPE{}, fmt.Errorf("unable to parse CPE string: %w", err) } // ensure that this CPE can be validated after being fully sanitized - if ValidateCPEString(CPEString(c)) != nil { + if ValidateString(String(c)) != nil { return CPE{}, err } @@ -43,7 +43,16 @@ func NewCPE(cpeStr string) (CPE, error) { return c, nil } -func ValidateCPEString(cpeStr string) error { +// Must returns a CPE or panics if the provided string is not valid +func Must(cpeStr string) CPE { + c, err := New(cpeStr) + if err != nil { + panic(err) + } + return c +} + +func ValidateString(cpeStr string) error { // We should filter out all CPEs that do not match the official CPE regex // The facebook nvdtools parser can sometimes incorrectly parse invalid CPE strings if !cpeRegex.MatchString(cpeStr) { @@ -52,7 +61,7 @@ func ValidateCPEString(cpeStr string) error { return nil } -func newCPEWithoutValidation(cpeStr string) (CPE, error) { +func newWithoutValidation(cpeStr string) (CPE, error) { value, err := wfn.Parse(cpeStr) if err != nil { return CPE{}, fmt.Errorf("failed to parse CPE=%q: %w", cpeStr, err) @@ -63,30 +72,22 @@ func newCPEWithoutValidation(cpeStr string) (CPE, error) { } // we need to compare the raw data since we are constructing CPEs in other locations - value.Vendor = normalizeCpeField(value.Vendor) - value.Product = normalizeCpeField(value.Product) - value.Language = normalizeCpeField(value.Language) - value.Version = normalizeCpeField(value.Version) - value.TargetSW = normalizeCpeField(value.TargetSW) - value.Part = normalizeCpeField(value.Part) - value.Edition = normalizeCpeField(value.Edition) - value.Other = normalizeCpeField(value.Other) - value.SWEdition = normalizeCpeField(value.SWEdition) - value.TargetHW = normalizeCpeField(value.TargetHW) - value.Update = normalizeCpeField(value.Update) + value.Vendor = normalizeField(value.Vendor) + value.Product = normalizeField(value.Product) + value.Language = normalizeField(value.Language) + value.Version = normalizeField(value.Version) + value.TargetSW = normalizeField(value.TargetSW) + value.Part = normalizeField(value.Part) + value.Edition = normalizeField(value.Edition) + value.Other = normalizeField(value.Other) + value.SWEdition = normalizeField(value.SWEdition) + value.TargetHW = normalizeField(value.TargetHW) + value.Update = normalizeField(value.Update) return *value, nil } -func MustCPE(cpeStr string) CPE { - c, err := NewCPE(cpeStr) - if err != nil { - panic(err) - } - return c -} - -func normalizeCpeField(field string) string { +func normalizeField(field string) string { // replace spaces with underscores (per section 5.3.2 of the CPE spec v 2.3) field = strings.ReplaceAll(field, " ", "_") @@ -112,7 +113,7 @@ func stripSlashes(s string) string { return sb.String() } -func CPEString(c CPE) string { +func String(c CPE) string { output := CPE{} output.Vendor = sanitize(c.Vendor) output.Product = sanitize(c.Product) diff --git a/syft/pkg/cpe_test.go b/syft/cpe/cpe_test.go similarity index 81% rename from syft/pkg/cpe_test.go rename to syft/cpe/cpe_test.go index 1f610541c47..38d6112ae68 100644 --- a/syft/pkg/cpe_test.go +++ b/syft/cpe/cpe_test.go @@ -1,4 +1,4 @@ -package pkg +package cpe import ( "encoding/json" @@ -11,14 +11,7 @@ import ( "github.com/stretchr/testify/require" ) -func must(c CPE, e error) CPE { - if e != nil { - panic(e) - } - return c -} - -func TestNewCPE(t *testing.T) { +func Test_New(t *testing.T) { tests := []struct { name string input string @@ -27,29 +20,29 @@ func TestNewCPE(t *testing.T) { { name: "gocase", input: `cpe:/a:10web:form_maker:1.0.0::~~~wordpress~~`, - expected: must(NewCPE(`cpe:2.3:a:10web:form_maker:1.0.0:*:*:*:*:wordpress:*:*`)), + expected: Must(`cpe:2.3:a:10web:form_maker:1.0.0:*:*:*:*:wordpress:*:*`), }, { name: "dashes", input: `cpe:/a:7-zip:7-zip:4.56:beta:~~~windows~~`, - expected: must(NewCPE(`cpe:2.3:a:7-zip:7-zip:4.56:beta:*:*:*:windows:*:*`)), + expected: Must(`cpe:2.3:a:7-zip:7-zip:4.56:beta:*:*:*:windows:*:*`), }, { name: "URL escape characters", input: `cpe:/a:%240.99_kindle_books_project:%240.99_kindle_books:6::~~~android~~`, - expected: must(NewCPE(`cpe:2.3:a:\$0.99_kindle_books_project:\$0.99_kindle_books:6:*:*:*:*:android:*:*`)), + expected: Must(`cpe:2.3:a:\$0.99_kindle_books_project:\$0.99_kindle_books:6:*:*:*:*:android:*:*`), }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - actual, err := NewCPE(test.input) + actual, err := New(test.input) if err != nil { t.Fatalf("got an error while creating CPE: %+v", err) } - if CPEString(actual) != CPEString(test.expected) { - t.Errorf("mismatched entries:\n\texpected:%+v\n\t actual:%+v\n", CPEString(test.expected), CPEString(actual)) + if String(actual) != String(test.expected) { + t.Errorf("mismatched entries:\n\texpected:%+v\n\t actual:%+v\n", String(test.expected), String(actual)) } }) @@ -81,7 +74,7 @@ func Test_normalizeCpeField(t *testing.T) { } for _, test := range tests { t.Run(test.field, func(t *testing.T) { - assert.Equal(t, test.expected, normalizeCpeField(test.field)) + assert.Equal(t, test.expected, normalizeField(test.field)) }) } } @@ -98,14 +91,14 @@ func Test_CPEParser(t *testing.T) { for _, test := range testCases { t.Run(test.CPEString, func(t *testing.T) { - c1, err := NewCPE(test.CPEString) + c1, err := New(test.CPEString) assert.NoError(t, err) - c2, err := NewCPE(test.CPEUrl) + c2, err := New(test.CPEUrl) assert.NoError(t, err) assert.Equal(t, c1, c2) assert.Equal(t, c1, test.WFN) assert.Equal(t, c2, test.WFN) - assert.Equal(t, CPEString(test.WFN), test.CPEString) + assert.Equal(t, String(test.WFN), test.CPEString) }) } } @@ -167,16 +160,16 @@ func Test_InvalidCPE(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - c, err := NewCPE(test.in) + c, err := New(test.in) if test.expectedErr { assert.Error(t, err) if t.Failed() { - t.Logf("got CPE: %q details: %+v", CPEString(c), c) + t.Logf("got CPE: %q details: %+v", String(c), c) } return } require.NoError(t, err) - assert.Equal(t, test.expected, CPEString(c)) + assert.Equal(t, test.expected, String(c)) }) } } @@ -222,13 +215,13 @@ func Test_RoundTrip(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { // CPE string must be preserved through a round trip - assert.Equal(t, test.cpe, CPEString(MustCPE(test.cpe))) + assert.Equal(t, test.cpe, String(Must(test.cpe))) // The parsed CPE must be the same after a round trip - assert.Equal(t, MustCPE(test.cpe), MustCPE(CPEString(MustCPE(test.cpe)))) + assert.Equal(t, Must(test.cpe), Must(String(Must(test.cpe)))) // The test case parsed CPE must be the same after parsing the input string - assert.Equal(t, test.parsedCPE, MustCPE(test.cpe)) + assert.Equal(t, test.parsedCPE, Must(test.cpe)) // The test case parsed CPE must produce the same string as the input cpe - assert.Equal(t, CPEString(test.parsedCPE), test.cpe) + assert.Equal(t, String(test.parsedCPE), test.cpe) }) } } diff --git a/syft/pkg/merge_cpes.go b/syft/cpe/merge_cpes.go similarity index 80% rename from syft/pkg/merge_cpes.go rename to syft/cpe/merge_cpes.go index cb766a72fed..5c5c7daf276 100644 --- a/syft/pkg/merge_cpes.go +++ b/syft/cpe/merge_cpes.go @@ -1,10 +1,10 @@ -package pkg +package cpe import ( "sort" ) -func mergeCPEs(a, b []CPE) (result []CPE) { +func Merge(a, b []CPE) (result []CPE) { aCPEs := make(map[string]CPE) // keep all CPEs from a and create a quick string-based lookup @@ -20,6 +20,6 @@ func mergeCPEs(a, b []CPE) (result []CPE) { } } - sort.Sort(CPEBySpecificity(result)) + sort.Sort(BySpecificity(result)) return result } diff --git a/syft/cpe/merge_cpes_test.go b/syft/cpe/merge_cpes_test.go new file mode 100644 index 00000000000..80ef32e7ab0 --- /dev/null +++ b/syft/cpe/merge_cpes_test.go @@ -0,0 +1,41 @@ +package cpe + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_Merge(t *testing.T) { + tests := []struct { + name string + input [][]CPE + expected []CPE + }{ + { + name: "merge, removing duplicates and ordered", + input: [][]CPE{ + { + Must("cpe:2.3:a:*:package:1:*:*:*:*:*:*:*"), + Must("cpe:2.3:a:some:package:*:*:*:*:*:*:*:*"), + }, + { + Must("cpe:2.3:a:some:package:1:*:*:*:*:*:*:*"), + Must("cpe:2.3:a:some:package:*:*:*:*:*:*:*:*"), + }, + }, + expected: []CPE{ + Must("cpe:2.3:a:some:package:1:*:*:*:*:*:*:*"), + Must("cpe:2.3:a:some:package:*:*:*:*:*:*:*:*"), + Must("cpe:2.3:a:*:package:1:*:*:*:*:*:*:*"), + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + out := Merge(test.input[0], test.input[1]) + assert.Equal(t, test.expected, out) + }) + } +} diff --git a/syft/pkg/test-fixtures/cpe-data.json b/syft/cpe/test-fixtures/cpe-data.json similarity index 100% rename from syft/pkg/test-fixtures/cpe-data.json rename to syft/cpe/test-fixtures/cpe-data.json diff --git a/syft/formats/common/cyclonedxhelpers/cpe.go b/syft/formats/common/cyclonedxhelpers/cpe.go index ecee0dc3918..89813e8aabb 100644 --- a/syft/formats/common/cyclonedxhelpers/cpe.go +++ b/syft/formats/common/cyclonedxhelpers/cpe.go @@ -4,6 +4,7 @@ import ( "github.com/CycloneDX/cyclonedx-go" "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/pkg" ) @@ -11,7 +12,7 @@ func encodeSingleCPE(p pkg.Package) string { // Since the CPEs in a package are sorted by specificity // we can extract the first CPE as the one to output in cyclonedx if len(p.CPEs) > 0 { - return pkg.CPEString(p.CPEs[0]) + return cpe.String(p.CPEs[0]) } return "" } @@ -24,15 +25,15 @@ func encodeCPEs(p pkg.Package) (out []cyclonedx.Property) { } out = append(out, cyclonedx.Property{ Name: "syft:cpe23", - Value: pkg.CPEString(c), + Value: cpe.String(c), }) } return } -func decodeCPEs(c *cyclonedx.Component) (out []pkg.CPE) { +func decodeCPEs(c *cyclonedx.Component) (out []cpe.CPE) { if c.CPE != "" { - cp, err := pkg.NewCPE(c.CPE) + cp, err := cpe.New(c.CPE) if err != nil { log.Warnf("invalid CPE: %s", c.CPE) } else { @@ -43,7 +44,7 @@ func decodeCPEs(c *cyclonedx.Component) (out []pkg.CPE) { if c.Properties != nil { for _, p := range *c.Properties { if p.Name == "syft:cpe23" { - cp, err := pkg.NewCPE(p.Value) + cp, err := cpe.New(p.Value) if err != nil { log.Warnf("invalid CPE: %s", p.Value) } else { diff --git a/syft/formats/common/cyclonedxhelpers/cpe_test.go b/syft/formats/common/cyclonedxhelpers/cpe_test.go index 1eff79ac6b0..7f5f537456c 100644 --- a/syft/formats/common/cyclonedxhelpers/cpe_test.go +++ b/syft/formats/common/cyclonedxhelpers/cpe_test.go @@ -5,12 +5,13 @@ import ( "github.com/stretchr/testify/assert" + "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/pkg" ) func Test_encodeCPE(t *testing.T) { - testCPE := pkg.MustCPE("cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*") - testCPE2 := pkg.MustCPE("cpe:2.3:a:name:name2:3.2:*:*:*:*:*:*:*") + testCPE := cpe.Must("cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*") + testCPE2 := cpe.Must("cpe:2.3:a:name:name2:3.2:*:*:*:*:*:*:*") tests := []struct { name string input pkg.Package @@ -20,14 +21,14 @@ func Test_encodeCPE(t *testing.T) { // note: since this is an optional field, no value is preferred over NONE or NOASSERTION name: "no metadata", input: pkg.Package{ - CPEs: []pkg.CPE{}, + CPEs: []cpe.CPE{}, }, expected: "", }, { name: "single CPE", input: pkg.Package{ - CPEs: []pkg.CPE{ + CPEs: []cpe.CPE{ testCPE, }, }, @@ -36,7 +37,7 @@ func Test_encodeCPE(t *testing.T) { { name: "multiple CPEs", input: pkg.Package{ - CPEs: []pkg.CPE{ + CPEs: []cpe.CPE{ testCPE2, testCPE, }, diff --git a/syft/formats/common/cyclonedxhelpers/format.go b/syft/formats/common/cyclonedxhelpers/format.go index e4bd055e20c..0894d67b36e 100644 --- a/syft/formats/common/cyclonedxhelpers/format.go +++ b/syft/formats/common/cyclonedxhelpers/format.go @@ -9,6 +9,7 @@ import ( "github.com/anchore/syft/internal" "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/linux" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/sbom" @@ -100,12 +101,12 @@ func toOSComponent(distro *linux.Release) []cyclonedx.Component { } func formatCPE(cpeString string) string { - cpe, err := pkg.NewCPE(cpeString) + c, err := cpe.New(cpeString) if err != nil { log.Debugf("skipping invalid CPE: %s", cpeString) return "" } - return pkg.CPEString(cpe) + return cpe.String(c) } // NewBomDescriptor returns a new BomDescriptor tailored for the current time and "syft" tool details. diff --git a/syft/formats/common/spdxhelpers/external_refs.go b/syft/formats/common/spdxhelpers/external_refs.go index d114c2ab279..03172621007 100644 --- a/syft/formats/common/spdxhelpers/external_refs.go +++ b/syft/formats/common/spdxhelpers/external_refs.go @@ -1,6 +1,7 @@ package spdxhelpers import ( + "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/pkg" ) @@ -10,7 +11,7 @@ func ExternalRefs(p pkg.Package) (externalRefs []ExternalRef) { for _, c := range p.CPEs { externalRefs = append(externalRefs, ExternalRef{ ReferenceCategory: SecurityReferenceCategory, - ReferenceLocator: pkg.CPEString(c), + ReferenceLocator: cpe.String(c), ReferenceType: Cpe23ExternalRefType, }) } diff --git a/syft/formats/common/spdxhelpers/external_refs_test.go b/syft/formats/common/spdxhelpers/external_refs_test.go index 1ae0a224c4a..08c4ab5c47c 100644 --- a/syft/formats/common/spdxhelpers/external_refs_test.go +++ b/syft/formats/common/spdxhelpers/external_refs_test.go @@ -5,11 +5,12 @@ import ( "github.com/stretchr/testify/assert" + "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/pkg" ) func Test_ExternalRefs(t *testing.T) { - testCPE := pkg.MustCPE("cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*") + testCPE := cpe.Must("cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*") tests := []struct { name string input pkg.Package @@ -18,7 +19,7 @@ func Test_ExternalRefs(t *testing.T) { { name: "cpe + purl", input: pkg.Package{ - CPEs: []pkg.CPE{ + CPEs: []cpe.CPE{ testCPE, }, PURL: "a-purl", @@ -26,7 +27,7 @@ func Test_ExternalRefs(t *testing.T) { expected: []ExternalRef{ { ReferenceCategory: SecurityReferenceCategory, - ReferenceLocator: pkg.CPEString(testCPE), + ReferenceLocator: cpe.String(testCPE), ReferenceType: Cpe23ExternalRefType, }, { diff --git a/syft/formats/common/spdxhelpers/to_syft_model.go b/syft/formats/common/spdxhelpers/to_syft_model.go index c8b88c5db48..2f671157f23 100644 --- a/syft/formats/common/spdxhelpers/to_syft_model.go +++ b/syft/formats/common/spdxhelpers/to_syft_model.go @@ -11,6 +11,7 @@ import ( "github.com/anchore/packageurl-go" "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/formats/common/util" "github.com/anchore/syft/syft/linux" @@ -381,15 +382,15 @@ func findPURLValue(p *spdx.Package) string { return "" } -func extractCPEs(p *spdx.Package) (cpes []pkg.CPE) { +func extractCPEs(p *spdx.Package) (cpes []cpe.CPE) { for _, r := range p.PackageExternalReferences { if r.RefType == string(Cpe23ExternalRefType) { - cpe, err := pkg.NewCPE(r.Locator) + c, err := cpe.New(r.Locator) if err != nil { log.Warnf("unable to extract SPDX CPE=%q: %+v", r.Locator, err) continue } - cpes = append(cpes, cpe) + cpes = append(cpes, c) } } return cpes diff --git a/syft/formats/common/testutils/utils.go b/syft/formats/common/testutils/utils.go index 2104f53b06b..39090d29192 100644 --- a/syft/formats/common/testutils/utils.go +++ b/syft/formats/common/testutils/utils.go @@ -16,6 +16,7 @@ import ( "github.com/anchore/stereoscope/pkg/image" "github.com/anchore/stereoscope/pkg/imagetest" "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/linux" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/sbom" @@ -179,8 +180,8 @@ func populateImageCatalog(catalog *pkg.Catalog, img *image.Image) { Version: "1.0.1", }, PURL: "a-purl-1", // intentionally a bad pURL for test fixtures - CPEs: []pkg.CPE{ - pkg.MustCPE("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*"), + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*"), }, }) catalog.Add(pkg.Package{ @@ -197,8 +198,8 @@ func populateImageCatalog(catalog *pkg.Catalog, img *image.Image) { Version: "2.0.1", }, PURL: "pkg:deb/debian/package-2@2.0.1", - CPEs: []pkg.CPE{ - pkg.MustCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"), + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"), }, }) } @@ -259,8 +260,8 @@ func newDirectoryCatalog() *pkg.Catalog { }, }, PURL: "a-purl-2", // intentionally a bad pURL for test fixtures - CPEs: []pkg.CPE{ - pkg.MustCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"), + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"), }, }) catalog.Add(pkg.Package{ @@ -277,8 +278,8 @@ func newDirectoryCatalog() *pkg.Catalog { Version: "2.0.1", }, PURL: "pkg:deb/debian/package-2@2.0.1", - CPEs: []pkg.CPE{ - pkg.MustCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"), + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"), }, }) diff --git a/syft/formats/syftjson/encoder_test.go b/syft/formats/syftjson/encoder_test.go index d7d3da95e6d..558fe2da62a 100644 --- a/syft/formats/syftjson/encoder_test.go +++ b/syft/formats/syftjson/encoder_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/formats/common/testutils" "github.com/anchore/syft/syft/linux" @@ -59,8 +60,8 @@ func TestEncodeFullJSONDocument(t *testing.T) { Files: []pkg.PythonFileRecord{}, }, PURL: "a-purl-1", - CPEs: []pkg.CPE{ - pkg.MustCPE("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*"), + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*"), }, } @@ -83,8 +84,8 @@ func TestEncodeFullJSONDocument(t *testing.T) { Files: []pkg.DpkgFileRecord{}, }, PURL: "a-purl-2", - CPEs: []pkg.CPE{ - pkg.MustCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"), + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"), }, } diff --git a/syft/formats/syftjson/to_format_model.go b/syft/formats/syftjson/to_format_model.go index 51d8f87dfd8..00c62fc0db5 100644 --- a/syft/formats/syftjson/to_format_model.go +++ b/syft/formats/syftjson/to_format_model.go @@ -8,6 +8,7 @@ import ( "github.com/anchore/syft/internal" "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/formats/syftjson/model" "github.com/anchore/syft/syft/linux" @@ -159,7 +160,7 @@ func toPackageModels(catalog *pkg.Catalog) []model.Package { func toPackageModel(p pkg.Package) model.Package { var cpes = make([]string, len(p.CPEs)) for i, c := range p.CPEs { - cpes[i] = pkg.CPEString(c) + cpes[i] = cpe.String(c) } var licenses = make([]string, 0) diff --git a/syft/formats/syftjson/to_syft_model.go b/syft/formats/syftjson/to_syft_model.go index c010393e4e8..14baab89eb1 100644 --- a/syft/formats/syftjson/to_syft_model.go +++ b/syft/formats/syftjson/to_syft_model.go @@ -7,6 +7,7 @@ import ( "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/formats/syftjson/model" "github.com/anchore/syft/syft/linux" "github.com/anchore/syft/syft/pkg" @@ -174,9 +175,9 @@ func toSyftCatalog(pkgs []model.Package, idAliases map[string]string) *pkg.Catal } func toSyftPackage(p model.Package, idAliases map[string]string) pkg.Package { - var cpes []pkg.CPE + var cpes []cpe.CPE for _, c := range p.CPEs { - value, err := pkg.NewCPE(c) + value, err := cpe.New(c) if err != nil { log.Warnf("excluding invalid CPE %q: %v", c, err) continue diff --git a/syft/pkg/catalog_test.go b/syft/pkg/catalog_test.go index 046938cebaa..62513203988 100644 --- a/syft/pkg/catalog_test.go +++ b/syft/pkg/catalog_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/require" "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/source" ) @@ -324,7 +325,7 @@ func TestCatalog_MergeRecords(t *testing.T) { name: "multiple Locations with shared path", pkgs: []Package{ { - CPEs: []CPE{MustCPE("cpe:2.3:a:package:1:1:*:*:*:*:*:*:*")}, + CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:package:1:1:*:*:*:*:*:*:*")}, Locations: source.NewLocationSet( source.Location{ Coordinates: source.Coordinates{ @@ -337,7 +338,7 @@ func TestCatalog_MergeRecords(t *testing.T) { Type: RpmPkg, }, { - CPEs: []CPE{MustCPE("cpe:2.3:b:package:1:1:*:*:*:*:*:*:*")}, + CPEs: []cpe.CPE{cpe.Must("cpe:2.3:b:package:1:1:*:*:*:*:*:*:*")}, Locations: source.NewLocationSet( source.Location{ Coordinates: source.Coordinates{ diff --git a/syft/pkg/cataloger/binary/classifier.go b/syft/pkg/cataloger/binary/classifier.go index 2ce4c1255f5..959a151fa4b 100644 --- a/syft/pkg/cataloger/binary/classifier.go +++ b/syft/pkg/cataloger/binary/classifier.go @@ -10,6 +10,7 @@ import ( "github.com/anchore/packageurl-go" "github.com/anchore/syft/internal" + "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg/cataloger/internal/unionreader" "github.com/anchore/syft/syft/source" @@ -44,7 +45,7 @@ type classifier struct { PURL packageurl.PackageURL // CPEs are the specific CPEs we want to include for this binary with updated version information - CPEs []pkg.CPE + CPEs []cpe.CPE } // evidenceMatcher is a function called to catalog Packages that match some sort of evidence @@ -114,11 +115,11 @@ func singlePackage(classifier classifier, reader source.LocationReadCloser, matc update := matchMetadata["update"] - var cpes []pkg.CPE - for _, cpe := range classifier.CPEs { - cpe.Version = version - cpe.Update = update - cpes = append(cpes, cpe) + var cpes []cpe.CPE + for _, c := range classifier.CPEs { + c.Version = version + c.Update = update + cpes = append(cpes, c) } p := pkg.Package{ @@ -172,8 +173,8 @@ func getContents(reader source.LocationReadCloser) ([]byte, error) { } // singleCPE returns a []pkg.CPE based on the cpe string or panics if the CPE is invalid -func singleCPE(cpe string) []pkg.CPE { - return []pkg.CPE{ - pkg.MustCPE(cpe), +func singleCPE(cpeString string) []cpe.CPE { + return []cpe.CPE{ + cpe.Must(cpeString), } } diff --git a/syft/pkg/cataloger/binary/classifier_test.go b/syft/pkg/cataloger/binary/classifier_test.go index 35cec08eb16..c0c9520af52 100644 --- a/syft/pkg/cataloger/binary/classifier_test.go +++ b/syft/pkg/cataloger/binary/classifier_test.go @@ -5,7 +5,7 @@ import ( "github.com/stretchr/testify/require" - "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/source" ) @@ -23,7 +23,7 @@ func Test_ClassifierCPEs(t *testing.T) { Package: "some-app", FileGlob: ".*/version.txt", EvidenceMatcher: fileContentsVersionMatcher(`(?m)my-verison:(?P[0-9.]+)`), - CPEs: []pkg.CPE{}, + CPEs: []cpe.CPE{}, }, cpes: nil, }, @@ -34,8 +34,8 @@ func Test_ClassifierCPEs(t *testing.T) { Package: "some-app", FileGlob: ".*/version.txt", EvidenceMatcher: fileContentsVersionMatcher(`(?m)my-verison:(?P[0-9.]+)`), - CPEs: []pkg.CPE{ - pkg.MustCPE("cpe:2.3:a:some:app:*:*:*:*:*:*:*:*"), + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:a:some:app:*:*:*:*:*:*:*:*"), }, }, cpes: []string{ @@ -49,9 +49,9 @@ func Test_ClassifierCPEs(t *testing.T) { Package: "some-app", FileGlob: ".*/version.txt", EvidenceMatcher: fileContentsVersionMatcher(`(?m)my-verison:(?P[0-9.]+)`), - CPEs: []pkg.CPE{ - pkg.MustCPE("cpe:2.3:a:some:app:*:*:*:*:*:*:*:*"), - pkg.MustCPE("cpe:2.3:a:some:apps:*:*:*:*:*:*:*:*"), + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:a:some:app:*:*:*:*:*:*:*:*"), + cpe.Must("cpe:2.3:a:some:apps:*:*:*:*:*:*:*:*"), }, }, cpes: []string{ @@ -79,7 +79,7 @@ func Test_ClassifierCPEs(t *testing.T) { var cpes []string for _, c := range p.CPEs { - cpes = append(cpes, pkg.CPEString(c)) + cpes = append(cpes, cpe.String(c)) } require.Equal(t, test.cpes, cpes) }) diff --git a/syft/pkg/cataloger/common/cpe/filter.go b/syft/pkg/cataloger/common/cpe/filter.go index 69be278c1cf..5f252a7d43f 100644 --- a/syft/pkg/cataloger/common/cpe/filter.go +++ b/syft/pkg/cataloger/common/cpe/filter.go @@ -5,13 +5,14 @@ import ( "github.com/facebookincubator/nvdtools/wfn" + "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/pkg" ) const jenkinsName = "jenkins" // filterFn instances should return true if the given CPE should be removed from a collection for the given package -type filterFn func(cpe pkg.CPE, p pkg.Package) bool +type filterFn func(cpe cpe.CPE, p pkg.Package) bool var cpeFilters = []filterFn{ disallowJiraClientServerMismatch, @@ -20,23 +21,23 @@ var cpeFilters = []filterFn{ disallowNonParseableCPEs, } -func filter(cpes []pkg.CPE, p pkg.Package, filters ...filterFn) (result []pkg.CPE) { +func filter(cpes []cpe.CPE, p pkg.Package, filters ...filterFn) (result []cpe.CPE) { cpeLoop: - for _, cpe := range cpes { + for _, c := range cpes { for _, fn := range filters { - if fn(cpe, p) { + if fn(c, p) { continue cpeLoop } } // all filter functions passed on filtering this CPE - result = append(result, cpe) + result = append(result, c) } return result } -func disallowNonParseableCPEs(cpe pkg.CPE, _ pkg.Package) bool { - v := pkg.CPEString(cpe) - _, err := pkg.NewCPE(v) +func disallowNonParseableCPEs(c cpe.CPE, _ pkg.Package) bool { + v := cpe.String(c) + _, err := cpe.New(v) cannotParse := err != nil @@ -44,7 +45,7 @@ func disallowNonParseableCPEs(cpe pkg.CPE, _ pkg.Package) bool { } // jenkins plugins should not match against jenkins -func disallowJenkinsServerCPEForPluginPackage(cpe pkg.CPE, p pkg.Package) bool { +func disallowJenkinsServerCPEForPluginPackage(cpe cpe.CPE, p pkg.Package) bool { if p.Type == pkg.JenkinsPluginPkg && cpe.Product == jenkinsName { return true } @@ -52,7 +53,7 @@ func disallowJenkinsServerCPEForPluginPackage(cpe pkg.CPE, p pkg.Package) bool { } // filter to account that packages that are not for jenkins but have a CPE generated that will match against jenkins -func disallowJenkinsCPEsNotAssociatedWithJenkins(cpe pkg.CPE, p pkg.Package) bool { +func disallowJenkinsCPEsNotAssociatedWithJenkins(cpe cpe.CPE, p pkg.Package) bool { // jenkins server should only match against a product with the name jenkins if cpe.Product == jenkinsName && !strings.Contains(strings.ToLower(p.Name), jenkinsName) { if cpe.Vendor == wfn.Any || cpe.Vendor == jenkinsName || cpe.Vendor == "cloudbees" { @@ -63,7 +64,7 @@ func disallowJenkinsCPEsNotAssociatedWithJenkins(cpe pkg.CPE, p pkg.Package) boo } // filter to account for packages which are jira client packages but have a CPE that will match against jira -func disallowJiraClientServerMismatch(cpe pkg.CPE, p pkg.Package) bool { +func disallowJiraClientServerMismatch(cpe cpe.CPE, p pkg.Package) bool { // jira / atlassian should not apply to clients if cpe.Product == "jira" && strings.Contains(strings.ToLower(p.Name), "client") { if cpe.Vendor == wfn.Any || cpe.Vendor == "jira" || cpe.Vendor == "atlassian" { diff --git a/syft/pkg/cataloger/common/cpe/filter_test.go b/syft/pkg/cataloger/common/cpe/filter_test.go index f0727a7c950..2c68bc2c3bc 100644 --- a/syft/pkg/cataloger/common/cpe/filter_test.go +++ b/syft/pkg/cataloger/common/cpe/filter_test.go @@ -5,19 +5,20 @@ import ( "github.com/stretchr/testify/assert" + "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/pkg" ) func Test_disallowJenkinsServerCPEForPluginPackage(t *testing.T) { tests := []struct { name string - cpe pkg.CPE + cpe cpe.CPE pkg pkg.Package expected bool }{ { name: "go case (filter out)", - cpe: pkg.MustCPE("cpe:2.3:a:name:jenkins:3.2:*:*:*:*:*:*:*"), + cpe: cpe.Must("cpe:2.3:a:name:jenkins:3.2:*:*:*:*:*:*:*"), pkg: pkg.Package{ Type: pkg.JenkinsPluginPkg, }, @@ -25,7 +26,7 @@ func Test_disallowJenkinsServerCPEForPluginPackage(t *testing.T) { }, { name: "ignore jenkins plugins with unique name", - cpe: pkg.MustCPE("cpe:2.3:a:name:ci-jenkins:3.2:*:*:*:*:*:*:*"), + cpe: cpe.Must("cpe:2.3:a:name:ci-jenkins:3.2:*:*:*:*:*:*:*"), pkg: pkg.Package{ Type: pkg.JenkinsPluginPkg, }, @@ -33,7 +34,7 @@ func Test_disallowJenkinsServerCPEForPluginPackage(t *testing.T) { }, { name: "ignore java packages", - cpe: pkg.MustCPE("cpe:2.3:a:name:jenkins:3.2:*:*:*:*:*:*:*"), + cpe: cpe.Must("cpe:2.3:a:name:jenkins:3.2:*:*:*:*:*:*:*"), pkg: pkg.Package{ Type: pkg.JavaPkg, }, @@ -50,13 +51,13 @@ func Test_disallowJenkinsServerCPEForPluginPackage(t *testing.T) { func Test_disallowJenkinsCPEsNotAssociatedWithJenkins(t *testing.T) { tests := []struct { name string - cpe pkg.CPE + cpe cpe.CPE pkg pkg.Package expected bool }{ { name: "filter out mismatched name (cloudbees vendor)", - cpe: pkg.MustCPE("cpe:2.3:a:cloudbees:jenkins:3.2:*:*:*:*:*:*:*"), + cpe: cpe.Must("cpe:2.3:a:cloudbees:jenkins:3.2:*:*:*:*:*:*:*"), pkg: pkg.Package{ Name: "not-j*nkins", Type: pkg.JavaPkg, @@ -65,7 +66,7 @@ func Test_disallowJenkinsCPEsNotAssociatedWithJenkins(t *testing.T) { }, { name: "filter out mismatched name (jenkins vendor)", - cpe: pkg.MustCPE("cpe:2.3:a:jenkins:jenkins:3.2:*:*:*:*:*:*:*"), + cpe: cpe.Must("cpe:2.3:a:jenkins:jenkins:3.2:*:*:*:*:*:*:*"), pkg: pkg.Package{ Name: "not-j*nkins", Type: pkg.JavaPkg, @@ -74,7 +75,7 @@ func Test_disallowJenkinsCPEsNotAssociatedWithJenkins(t *testing.T) { }, { name: "filter out mismatched name (any vendor)", - cpe: pkg.MustCPE("cpe:2.3:a:*:jenkins:3.2:*:*:*:*:*:*:*"), + cpe: cpe.Must("cpe:2.3:a:*:jenkins:3.2:*:*:*:*:*:*:*"), pkg: pkg.Package{ Name: "not-j*nkins", Type: pkg.JavaPkg, @@ -83,7 +84,7 @@ func Test_disallowJenkinsCPEsNotAssociatedWithJenkins(t *testing.T) { }, { name: "ignore packages with the name jenkins", - cpe: pkg.MustCPE("cpe:2.3:a:*:jenkins:3.2:*:*:*:*:*:*:*"), + cpe: cpe.Must("cpe:2.3:a:*:jenkins:3.2:*:*:*:*:*:*:*"), pkg: pkg.Package{ Name: "jenkins-thing", Type: pkg.JavaPkg, @@ -92,7 +93,7 @@ func Test_disallowJenkinsCPEsNotAssociatedWithJenkins(t *testing.T) { }, { name: "ignore product names that are not exactly 'jenkins'", - cpe: pkg.MustCPE("cpe:2.3:a:*:jenkins-something-else:3.2:*:*:*:*:*:*:*"), + cpe: cpe.Must("cpe:2.3:a:*:jenkins-something-else:3.2:*:*:*:*:*:*:*"), pkg: pkg.Package{ Name: "not-j*nkins", Type: pkg.JavaPkg, @@ -110,13 +111,13 @@ func Test_disallowJenkinsCPEsNotAssociatedWithJenkins(t *testing.T) { func Test_disallowJiraClientServerMismatch(t *testing.T) { tests := []struct { name string - cpe pkg.CPE + cpe cpe.CPE pkg pkg.Package expected bool }{ { name: "filter out mismatched name (atlassian vendor)", - cpe: pkg.MustCPE("cpe:2.3:a:atlassian:jira:3.2:*:*:*:*:*:*:*"), + cpe: cpe.Must("cpe:2.3:a:atlassian:jira:3.2:*:*:*:*:*:*:*"), pkg: pkg.Package{ Name: "something-client", Type: pkg.JavaPkg, @@ -125,7 +126,7 @@ func Test_disallowJiraClientServerMismatch(t *testing.T) { }, { name: "filter out mismatched name (jira vendor)", - cpe: pkg.MustCPE("cpe:2.3:a:jira:jira:3.2:*:*:*:*:*:*:*"), + cpe: cpe.Must("cpe:2.3:a:jira:jira:3.2:*:*:*:*:*:*:*"), pkg: pkg.Package{ Name: "something-client", Type: pkg.JavaPkg, @@ -134,7 +135,7 @@ func Test_disallowJiraClientServerMismatch(t *testing.T) { }, { name: "filter out mismatched name (any vendor)", - cpe: pkg.MustCPE("cpe:2.3:a:*:jira:3.2:*:*:*:*:*:*:*"), + cpe: cpe.Must("cpe:2.3:a:*:jira:3.2:*:*:*:*:*:*:*"), pkg: pkg.Package{ Name: "something-client", Type: pkg.JavaPkg, @@ -143,7 +144,7 @@ func Test_disallowJiraClientServerMismatch(t *testing.T) { }, { name: "ignore package names that do not have 'client'", - cpe: pkg.MustCPE("cpe:2.3:a:*:jira:3.2:*:*:*:*:*:*:*"), + cpe: cpe.Must("cpe:2.3:a:*:jira:3.2:*:*:*:*:*:*:*"), pkg: pkg.Package{ Name: "jira-thing", Type: pkg.JavaPkg, @@ -152,7 +153,7 @@ func Test_disallowJiraClientServerMismatch(t *testing.T) { }, { name: "ignore product names that are not exactly 'jira'", - cpe: pkg.MustCPE("cpe:2.3:a:*:jira-something-else:3.2:*:*:*:*:*:*:*"), + cpe: cpe.Must("cpe:2.3:a:*:jira-something-else:3.2:*:*:*:*:*:*:*"), pkg: pkg.Package{ Name: "not-j*ra", Type: pkg.JavaPkg, diff --git a/syft/pkg/cataloger/common/cpe/generate.go b/syft/pkg/cataloger/common/cpe/generate.go index a48e5784a8a..a67f7a9eb06 100644 --- a/syft/pkg/cataloger/common/cpe/generate.go +++ b/syft/pkg/cataloger/common/cpe/generate.go @@ -10,26 +10,27 @@ import ( "github.com/facebookincubator/nvdtools/wfn" "github.com/anchore/syft/internal" + "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/pkg" ) func newCPE(product, vendor, version, targetSW string) *wfn.Attributes { - cpe := *(wfn.NewAttributesWithAny()) - cpe.Part = "a" - cpe.Product = product - cpe.Vendor = vendor - cpe.Version = version - cpe.TargetSW = targetSW - if pkg.ValidateCPEString(pkg.CPEString(cpe)) != nil { + c := *(wfn.NewAttributesWithAny()) + c.Part = "a" + c.Product = product + c.Vendor = vendor + c.Version = version + c.TargetSW = targetSW + if cpe.ValidateString(cpe.String(c)) != nil { return nil } - return &cpe + return &c } // Generate Create a list of CPEs for a given package, trying to guess the vendor, product tuple. We should be trying to // generate the minimal set of representative CPEs, which implies that optional fields should not be included // (such as target SW). -func Generate(p pkg.Package) []pkg.CPE { +func Generate(p pkg.Package) []cpe.CPE { vendors := candidateVendors(p) products := candidateProducts(p) if len(products) == 0 { @@ -37,7 +38,7 @@ func Generate(p pkg.Package) []pkg.CPE { } keys := internal.NewStringSet() - cpes := make([]pkg.CPE, 0) + cpes := make([]cpe.CPE, 0) for _, product := range products { for _, vendor := range vendors { // prevent duplicate entries... @@ -47,8 +48,8 @@ func Generate(p pkg.Package) []pkg.CPE { } keys.Add(key) // add a new entry... - if cpe := newCPE(product, vendor, p.Version, wfn.Any); cpe != nil { - cpes = append(cpes, *cpe) + if c := newCPE(product, vendor, p.Version, wfn.Any); c != nil { + cpes = append(cpes, *c) } } } @@ -56,7 +57,7 @@ func Generate(p pkg.Package) []pkg.CPE { // filter out any known combinations that don't accurately represent this package cpes = filter(cpes, p, cpeFilters...) - sort.Sort(pkg.CPEBySpecificity(cpes)) + sort.Sort(cpe.BySpecificity(cpes)) return cpes } diff --git a/syft/pkg/cataloger/common/cpe/generate_test.go b/syft/pkg/cataloger/common/cpe/generate_test.go index 187302b9dd8..e078444f348 100644 --- a/syft/pkg/cataloger/common/cpe/generate_test.go +++ b/syft/pkg/cataloger/common/cpe/generate_test.go @@ -10,6 +10,7 @@ import ( "github.com/scylladb/go-set/strset" "github.com/stretchr/testify/assert" + "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/pkg" ) @@ -696,7 +697,7 @@ func TestGeneratePackageCPEs(t *testing.T) { expectedCpeSet := set.NewStringSet(test.expected...) actualCpeSet := set.NewStringSet() for _, a := range actual { - actualCpeSet.Add(pkg.CPEString(a)) + actualCpeSet.Add(cpe.String(a)) } extra := strset.Difference(actualCpeSet, expectedCpeSet).List() diff --git a/syft/pkg/cataloger/sbom/cataloger_test.go b/syft/pkg/cataloger/sbom/cataloger_test.go index 3918cbf7875..5886fc17a51 100644 --- a/syft/pkg/cataloger/sbom/cataloger_test.go +++ b/syft/pkg/cataloger/sbom/cataloger_test.go @@ -6,6 +6,7 @@ import ( "github.com/stretchr/testify/require" "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/formats/syftjson" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" @@ -13,17 +14,17 @@ import ( "github.com/anchore/syft/syft/source" ) -func mustCPEs(s ...string) (c []pkg.CPE) { +func mustCPEs(s ...string) (c []cpe.CPE) { for _, i := range s { c = append(c, mustCPE(i)) } return } -func mustCPE(c string) pkg.CPE { - return must(pkg.NewCPE(c)) +func mustCPE(c string) cpe.CPE { + return must(cpe.New(c)) } -func must(c pkg.CPE, e error) pkg.CPE { +func must(c cpe.CPE, e error) cpe.CPE { if e != nil { panic(e) } diff --git a/syft/pkg/cpe_by_specificity_test.go b/syft/pkg/cpe_by_specificity_test.go deleted file mode 100644 index 54f8e9f1308..00000000000 --- a/syft/pkg/cpe_by_specificity_test.go +++ /dev/null @@ -1,103 +0,0 @@ -package pkg - -import ( - "sort" - "testing" - - "github.com/stretchr/testify/assert" -) - -func mustCPE(c string) CPE { - return must(NewCPE(c)) -} - -func TestCPESpecificity(t *testing.T) { - tests := []struct { - name string - input []CPE - expected []CPE - }{ - { - name: "sort strictly by wfn *", - input: []CPE{ - mustCPE("cpe:2.3:a:*:package:1:*:*:*:*:*:*:*"), - mustCPE("cpe:2.3:a:some:package:1:*:*:*:*:*:*:*"), - mustCPE("cpe:2.3:a:*:package:1:*:*:*:*:some:*:*"), - mustCPE("cpe:2.3:a:some:package:1:*:*:*:*:some:*:*"), - mustCPE("cpe:2.3:a:some:package:*:*:*:*:*:*:*:*"), - }, - expected: []CPE{ - mustCPE("cpe:2.3:a:some:package:1:*:*:*:*:some:*:*"), - mustCPE("cpe:2.3:a:some:package:1:*:*:*:*:*:*:*"), - mustCPE("cpe:2.3:a:some:package:*:*:*:*:*:*:*:*"), - mustCPE("cpe:2.3:a:*:package:1:*:*:*:*:some:*:*"), - mustCPE("cpe:2.3:a:*:package:1:*:*:*:*:*:*:*"), - }, - }, - { - name: "sort strictly by field length", - input: []CPE{ - mustCPE("cpe:2.3:a:1:22:1:*:*:*:*:1:*:*"), - mustCPE("cpe:2.3:a:55555:1:1:*:*:*:*:1:*:*"), - mustCPE("cpe:2.3:a:1:1:333:*:*:*:*:1:*:*"), - mustCPE("cpe:2.3:a:1:666666:1:*:*:*:*:1:*:*"), - mustCPE("cpe:2.3:a:1:1:1:*:*:*:*:1:*:*"), - mustCPE("cpe:2.3:a:1:1:1:*:*:*:*:4444:*:*"), - }, - expected: []CPE{ - mustCPE("cpe:2.3:a:1:666666:1:*:*:*:*:1:*:*"), - mustCPE("cpe:2.3:a:55555:1:1:*:*:*:*:1:*:*"), - mustCPE("cpe:2.3:a:1:1:1:*:*:*:*:4444:*:*"), - mustCPE("cpe:2.3:a:1:1:333:*:*:*:*:1:*:*"), - mustCPE("cpe:2.3:a:1:22:1:*:*:*:*:1:*:*"), - mustCPE("cpe:2.3:a:1:1:1:*:*:*:*:1:*:*"), - }, - }, - { - name: "sort by mix of field length and specificity", - input: []CPE{ - mustCPE("cpe:2.3:a:1:666666:*:*:*:*:*:1:*:*"), - mustCPE("cpe:2.3:a:*:1:1:*:*:*:*:4444:*:*"), - mustCPE("cpe:2.3:a:1:*:333:*:*:*:*:*:*:*"), - mustCPE("cpe:2.3:a:1:1:1:*:*:*:*:1:*:*"), - mustCPE("cpe:2.3:a:1:22:1:*:*:*:*:1:*:*"), - mustCPE("cpe:2.3:a:55555:1:1:*:*:*:*:1:*:*"), - }, - expected: []CPE{ - mustCPE("cpe:2.3:a:55555:1:1:*:*:*:*:1:*:*"), - mustCPE("cpe:2.3:a:1:22:1:*:*:*:*:1:*:*"), - mustCPE("cpe:2.3:a:1:1:1:*:*:*:*:1:*:*"), - mustCPE("cpe:2.3:a:1:666666:*:*:*:*:*:1:*:*"), - mustCPE("cpe:2.3:a:*:1:1:*:*:*:*:4444:*:*"), - mustCPE("cpe:2.3:a:1:*:333:*:*:*:*:*:*:*"), - }, - }, - { - name: "sort by mix of field length, specificity, dash", - input: []CPE{ - mustCPE("cpe:2.3:a:alpine:alpine_keys:2.3-r1:*:*:*:*:*:*:*"), - mustCPE("cpe:2.3:a:alpine_keys:alpine_keys:2.3-r1:*:*:*:*:*:*:*"), - mustCPE("cpe:2.3:a:alpine-keys:alpine_keys:2.3-r1:*:*:*:*:*:*:*"), - mustCPE("cpe:2.3:a:alpine:alpine-keys:2.3-r1:*:*:*:*:*:*:*"), - mustCPE("cpe:2.3:a:alpine-keys:alpine-keys:2.3-r1:*:*:*:*:*:*:*"), - mustCPE("cpe:2.3:a:alpine_keys:alpine-keys:2.3-r1:*:*:*:*:*:*:*"), - }, - expected: []CPE{ - mustCPE("cpe:2.3:a:alpine-keys:alpine-keys:2.3-r1:*:*:*:*:*:*:*"), - mustCPE("cpe:2.3:a:alpine-keys:alpine_keys:2.3-r1:*:*:*:*:*:*:*"), - mustCPE("cpe:2.3:a:alpine_keys:alpine-keys:2.3-r1:*:*:*:*:*:*:*"), - mustCPE("cpe:2.3:a:alpine_keys:alpine_keys:2.3-r1:*:*:*:*:*:*:*"), - mustCPE("cpe:2.3:a:alpine:alpine-keys:2.3-r1:*:*:*:*:*:*:*"), - mustCPE("cpe:2.3:a:alpine:alpine_keys:2.3-r1:*:*:*:*:*:*:*"), - }, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - sort.Sort(CPEBySpecificity(test.input)) - assert.Equal(t, test.expected, test.input) - }) - } - -} diff --git a/syft/pkg/package.go b/syft/pkg/package.go index 5bf278af589..88351f2174d 100644 --- a/syft/pkg/package.go +++ b/syft/pkg/package.go @@ -10,6 +10,7 @@ import ( "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/source" ) @@ -24,7 +25,7 @@ type Package struct { Licenses []string // licenses discovered with the package metadata Language Language `hash:"ignore" cyclonedx:"language"` // the language ecosystem this package belongs to (e.g. JavaScript, Python, etc) Type Type `cyclonedx:"type"` // the package type (e.g. Npm, Yarn, Python, Rpm, Deb, etc) - CPEs []CPE `hash:"ignore"` // all possible Common Platform Enumerators (note: this is NOT included in the definition of the ID since all fields on a CPE are derived from other fields) + CPEs []cpe.CPE `hash:"ignore"` // all possible Common Platform Enumerators (note: this is NOT included in the definition of the ID since all fields on a CPE are derived from other fields) PURL string `hash:"ignore"` // the Package URL (see https://github.com/package-url/purl-spec) MetadataType MetadataType `cyclonedx:"metadataType"` // the shape of the additional data in the "metadata" field Metadata interface{} // additional data found while parsing the package source @@ -64,7 +65,7 @@ func (p *Package) merge(other Package) error { p.Locations.Add(other.Locations.ToSlice()...) - p.CPEs = mergeCPEs(p.CPEs, other.CPEs) + p.CPEs = cpe.Merge(p.CPEs, other.CPEs) if p.PURL == "" { p.PURL = other.PURL diff --git a/syft/pkg/package_test.go b/syft/pkg/package_test.go index 7a0a3334067..51854c061ed 100644 --- a/syft/pkg/package_test.go +++ b/syft/pkg/package_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/require" "github.com/anchore/stereoscope/pkg/file" + "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/source" ) @@ -32,8 +33,8 @@ func TestIDUniqueness(t *testing.T) { }, Language: "math", Type: PythonPkg, - CPEs: []CPE{ - must(NewCPE(`cpe:2.3:a:Archimedes:pi:3.14:*:*:*:*:math:*:*`)), + CPEs: []cpe.CPE{ + cpe.Must(`cpe:2.3:a:Archimedes:pi:3.14:*:*:*:*:math:*:*`), }, PURL: "pkg:pypi/pi@3.14", MetadataType: PythonPackageMetadataType, @@ -169,7 +170,7 @@ func TestIDUniqueness(t *testing.T) { { name: "CPEs is ignored", transform: func(pkg Package) Package { - pkg.CPEs = []CPE{} + pkg.CPEs = []cpe.CPE{} return pkg }, expectedIDComparison: assert.Equal, @@ -269,8 +270,8 @@ func TestPackage_Merge(t *testing.T) { }, Language: "math", Type: PythonPkg, - CPEs: []CPE{ - must(NewCPE(`cpe:2.3:a:Archimedes:pi:3.14:*:*:*:*:math:*:*`)), + CPEs: []cpe.CPE{ + cpe.Must(`cpe:2.3:a:Archimedes:pi:3.14:*:*:*:*:math:*:*`), }, PURL: "pkg:pypi/pi@3.14", MetadataType: PythonPackageMetadataType, @@ -297,8 +298,8 @@ func TestPackage_Merge(t *testing.T) { }, Language: "math", Type: PythonPkg, - CPEs: []CPE{ - must(NewCPE(`cpe:2.3:a:DIFFERENT:pi:3.14:*:*:*:*:math:*:*`)), // NOTE: difference + CPEs: []cpe.CPE{ + cpe.Must(`cpe:2.3:a:DIFFERENT:pi:3.14:*:*:*:*:math:*:*`), // NOTE: difference }, PURL: "pkg:pypi/pi@3.14", MetadataType: PythonPackageMetadataType, @@ -326,9 +327,9 @@ func TestPackage_Merge(t *testing.T) { }, Language: "math", Type: PythonPkg, - CPEs: []CPE{ - must(NewCPE(`cpe:2.3:a:Archimedes:pi:3.14:*:*:*:*:math:*:*`)), - must(NewCPE(`cpe:2.3:a:DIFFERENT:pi:3.14:*:*:*:*:math:*:*`)), // NOTE: merge! + CPEs: []cpe.CPE{ + cpe.Must(`cpe:2.3:a:Archimedes:pi:3.14:*:*:*:*:math:*:*`), + cpe.Must(`cpe:2.3:a:DIFFERENT:pi:3.14:*:*:*:*:math:*:*`), // NOTE: merge! }, PURL: "pkg:pypi/pi@3.14", MetadataType: PythonPackageMetadataType, @@ -358,8 +359,8 @@ func TestPackage_Merge(t *testing.T) { }, Language: "math", Type: PythonPkg, - CPEs: []CPE{ - must(NewCPE(`cpe:2.3:a:Archimedes:pi:3.14:*:*:*:*:math:*:*`)), + CPEs: []cpe.CPE{ + cpe.Must(`cpe:2.3:a:Archimedes:pi:3.14:*:*:*:*:math:*:*`), }, PURL: "pkg:pypi/pi@3.14", MetadataType: PythonPackageMetadataType, @@ -386,8 +387,8 @@ func TestPackage_Merge(t *testing.T) { }, Language: "math", Type: PythonPkg, - CPEs: []CPE{ - must(NewCPE(`cpe:2.3:a:Archimedes:pi:3.14:*:*:*:*:math:*:*`)), + CPEs: []cpe.CPE{ + cpe.Must(`cpe:2.3:a:Archimedes:pi:3.14:*:*:*:*:math:*:*`), }, PURL: "pkg:pypi/pi@3.14", MetadataType: PythonPackageMetadataType,