mirror of
https://github.com/anchore/syft
synced 2024-11-10 06:14:16 +00:00
feat: Record where CPEs come from (#2552)
Syft can get CPEs from several source, including generating them based on package data, finding them in the NVD CPE dictionary, or finding them declared in a manifest or existing SBOM. Record where Syft got CPEs so that consumers of SBOMs can reason about how trustworthy they are. Signed-off-by: Will Murphy <will.murphy@anchore.com>
This commit is contained in:
parent
4fe50f4169
commit
b7a6d5e946
47 changed files with 2674 additions and 287 deletions
|
@ -19,7 +19,7 @@ func TestGolangCompilerDetection(t *testing.T) {
|
|||
name: "syft can detect a single golang compiler given the golang base image",
|
||||
image: "image-golang-compiler",
|
||||
expectedCompilers: []string{"go1.18.10"},
|
||||
expectedCPE: []cpe.CPE{cpe.Must("cpe:2.3:a:golang:go:1.18.10:-:*:*:*:*:*:*")},
|
||||
expectedCPE: []cpe.CPE{cpe.Must("cpe:2.3:a:golang:go:1.18.10:-:*:*:*:*:*:*", cpe.GeneratedSource)},
|
||||
expectedPURL: []string{"pkg:golang/stdlib@1.18.10"},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -3,5 +3,5 @@ package internal
|
|||
const (
|
||||
// JSONSchemaVersion is the current schema version output by the JSON encoder
|
||||
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
|
||||
JSONSchemaVersion = "15.0.0"
|
||||
JSONSchemaVersion = "16.0.0"
|
||||
)
|
||||
|
|
|
@ -113,7 +113,7 @@ func NewPackageTask(cfg CatalogingFactoryConfig, c pkg.Cataloger, tags ...string
|
|||
// we might have binary classified CPE already with the package so we want to append here
|
||||
dictionaryCPE, ok := cpe.DictionaryFind(p)
|
||||
if ok {
|
||||
log.Tracef("used CPE dictionary to find CPE for %s package %q: %s", p.Type, p.Name, dictionaryCPE.BindToFmtString())
|
||||
log.Tracef("used CPE dictionary to find CPE for %s package %q: %s", p.Type, p.Name, dictionaryCPE.Attributes.BindToFmtString())
|
||||
p.CPEs = append(p.CPEs, dictionaryCPE)
|
||||
} else {
|
||||
p.CPEs = append(p.CPEs, cpe.Generate(p)...)
|
||||
|
|
2164
schema/json/schema-16.0.0.json
Normal file
2164
schema/json/schema-16.0.0.json
Normal file
File diff suppressed because it is too large
Load diff
39
syft/cpe/by_source_then_specificity.go
Normal file
39
syft/cpe/by_source_then_specificity.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package cpe
|
||||
|
||||
import "sort"
|
||||
|
||||
type BySourceThenSpecificity []CPE
|
||||
|
||||
func (b BySourceThenSpecificity) Len() int {
|
||||
return len(b)
|
||||
}
|
||||
|
||||
func (b BySourceThenSpecificity) Less(i, j int) bool {
|
||||
sourceOrder := map[Source]int{
|
||||
NVDDictionaryLookupSource: 1,
|
||||
DeclaredSource: 2,
|
||||
GeneratedSource: 3,
|
||||
}
|
||||
|
||||
getRank := func(source Source) int {
|
||||
if rank, exists := sourceOrder[source]; exists {
|
||||
return rank
|
||||
}
|
||||
return 4 // Sourced we don't know about can't be assigned special priority, so
|
||||
// are considered ties.
|
||||
}
|
||||
iSource := b[i].Source
|
||||
jSource := b[j].Source
|
||||
rankI, rankJ := getRank(iSource), getRank(jSource)
|
||||
if rankI != rankJ {
|
||||
return rankI < rankJ
|
||||
}
|
||||
|
||||
return isMoreSpecific(b[i].Attributes, b[j].Attributes)
|
||||
}
|
||||
|
||||
func (b BySourceThenSpecificity) Swap(i, j int) {
|
||||
b[i], b[j] = b[j], b[i]
|
||||
}
|
||||
|
||||
var _ sort.Interface = (*BySourceThenSpecificity)(nil)
|
74
syft/cpe/by_source_then_specificity_test.go
Normal file
74
syft/cpe/by_source_then_specificity_test.go
Normal file
|
@ -0,0 +1,74 @@
|
|||
package cpe
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBySourceThenSpecificity(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []CPE
|
||||
want []CPE
|
||||
}{
|
||||
{
|
||||
name: "empty case",
|
||||
},
|
||||
{
|
||||
name: "nvd before generated",
|
||||
input: []CPE{
|
||||
Must("cpe:2.3:a:alpine:alpine_keys:2.3-r1:*:*:*:*:*:*:*", GeneratedSource),
|
||||
Must("cpe:2.3:a:alpine:alpine_keys:2.3-r1:*:*:*:*:*:*:*", NVDDictionaryLookupSource),
|
||||
},
|
||||
want: []CPE{
|
||||
Must("cpe:2.3:a:alpine:alpine_keys:2.3-r1:*:*:*:*:*:*:*", NVDDictionaryLookupSource),
|
||||
Must("cpe:2.3:a:alpine:alpine_keys:2.3-r1:*:*:*:*:*:*:*", GeneratedSource),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "declared before generated",
|
||||
input: []CPE{
|
||||
Must("cpe:2.3:a:alpine:alpine_keys:2.3-r1:*:*:*:*:*:*:*", GeneratedSource),
|
||||
Must("cpe:2.3:a:alpine:alpine_keys:2.3-r1:*:*:*:*:*:*:*", DeclaredSource),
|
||||
},
|
||||
want: []CPE{
|
||||
Must("cpe:2.3:a:alpine:alpine_keys:2.3-r1:*:*:*:*:*:*:*", DeclaredSource),
|
||||
Must("cpe:2.3:a:alpine:alpine_keys:2.3-r1:*:*:*:*:*:*:*", GeneratedSource),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "most specific attributes of equal sources",
|
||||
input: []CPE{
|
||||
Must("cpe:2.3:a:some:package:*:*:*:*:*:*:*:*", NVDDictionaryLookupSource),
|
||||
Must("cpe:2.3:a:some:package:1:*:*:*:*:*:*:*", NVDDictionaryLookupSource),
|
||||
Must("cpe:2.3:a:some:package:1:*:*:*:*:some:*:*", NVDDictionaryLookupSource),
|
||||
},
|
||||
want: []CPE{
|
||||
Must("cpe:2.3:a:some:package:1:*:*:*:*:some:*:*", NVDDictionaryLookupSource),
|
||||
Must("cpe:2.3:a:some:package:1:*:*:*:*:*:*:*", NVDDictionaryLookupSource),
|
||||
Must("cpe:2.3:a:some:package:*:*:*:*:*:*:*:*", NVDDictionaryLookupSource),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "most specific attributes of unknown sources",
|
||||
input: []CPE{
|
||||
Must("cpe:2.3:a:some:package:1:*:*:*:*:*:*:*", ""),
|
||||
Must("cpe:2.3:a:some:package:1:*:*:*:*:some:*:*", "some-other-unknown-source"),
|
||||
Must("cpe:2.3:a:some:package:*:*:*:*:*:*:*:*", "some-unknown-source"),
|
||||
},
|
||||
want: []CPE{
|
||||
Must("cpe:2.3:a:some:package:1:*:*:*:*:some:*:*", "some-other-unknown-source"),
|
||||
Must("cpe:2.3:a:some:package:1:*:*:*:*:*:*:*", ""),
|
||||
Must("cpe:2.3:a:some:package:*:*:*:*:*:*:*:*", "some-unknown-source"),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
sort.Sort(BySourceThenSpecificity(tt.input))
|
||||
assert.Equal(t, tt.want, tt.input)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -6,15 +6,22 @@ import (
|
|||
|
||||
var _ sort.Interface = (*BySpecificity)(nil)
|
||||
|
||||
type BySpecificity []CPE
|
||||
type BySpecificity []Attributes
|
||||
|
||||
func (c BySpecificity) Len() int { return len(c) }
|
||||
|
||||
func (c BySpecificity) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
|
||||
|
||||
func (c BySpecificity) Less(i, j int) bool {
|
||||
iScore := weightedCountForSpecifiedFields(c[i])
|
||||
jScore := weightedCountForSpecifiedFields(c[j])
|
||||
return isMoreSpecific(c[i], c[j])
|
||||
}
|
||||
|
||||
// Returns true if i is more specific than j, with some
|
||||
// tie breaking mechanisms to make sorting equally-specific cpe Attributes
|
||||
// deterministic.
|
||||
func isMoreSpecific(i, j Attributes) bool {
|
||||
iScore := weightedCountForSpecifiedFields(i)
|
||||
jScore := weightedCountForSpecifiedFields(j)
|
||||
|
||||
// check weighted sort first
|
||||
if iScore != jScore {
|
||||
|
@ -22,28 +29,28 @@ func (c BySpecificity) Less(i, j int) bool {
|
|||
}
|
||||
|
||||
// sort longer fields to top
|
||||
if countFieldLength(c[i]) != countFieldLength(c[j]) {
|
||||
return countFieldLength(c[i]) > countFieldLength(c[j])
|
||||
if countFieldLength(i) != countFieldLength(j) {
|
||||
return countFieldLength(i) > countFieldLength(j)
|
||||
}
|
||||
|
||||
// if score and length are equal then text sort
|
||||
// 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()
|
||||
// as we are not encoding/decoding this Attributes string so we don't
|
||||
// need the proper quoted version of the Attributes.
|
||||
return i.BindToFmtString() < j.BindToFmtString()
|
||||
}
|
||||
|
||||
func countFieldLength(cpe CPE) int {
|
||||
func countFieldLength(cpe Attributes) int {
|
||||
return len(cpe.Part + cpe.Vendor + cpe.Product + cpe.Version + cpe.TargetSW)
|
||||
}
|
||||
|
||||
func weightedCountForSpecifiedFields(cpe CPE) int {
|
||||
checksForSpecifiedField := []func(cpe CPE) (bool, int){
|
||||
func(cpe CPE) (bool, int) { return cpe.Part != "", 2 },
|
||||
func(cpe CPE) (bool, int) { return cpe.Vendor != "", 3 },
|
||||
func(cpe CPE) (bool, int) { return cpe.Product != "", 4 },
|
||||
func(cpe CPE) (bool, int) { return cpe.Version != "", 1 },
|
||||
func(cpe CPE) (bool, int) { return cpe.TargetSW != "", 1 },
|
||||
func weightedCountForSpecifiedFields(cpe Attributes) int {
|
||||
checksForSpecifiedField := []func(cpe Attributes) (bool, int){
|
||||
func(cpe Attributes) (bool, int) { return cpe.Part != "", 2 },
|
||||
func(cpe Attributes) (bool, int) { return cpe.Vendor != "", 3 },
|
||||
func(cpe Attributes) (bool, int) { return cpe.Product != "", 4 },
|
||||
func(cpe Attributes) (bool, int) { return cpe.Version != "", 1 },
|
||||
func(cpe Attributes) (bool, int) { return cpe.TargetSW != "", 1 },
|
||||
}
|
||||
|
||||
weightedCount := 0
|
||||
|
|
|
@ -10,81 +10,81 @@ import (
|
|||
func Test_BySpecificity(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []CPE
|
||||
expected []CPE
|
||||
input []Attributes
|
||||
expected []Attributes
|
||||
}{
|
||||
{
|
||||
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:*:*:*:*:*:*:*:*"),
|
||||
input: []Attributes{
|
||||
MustAttributes("cpe:2.3:a:*:package:1:*:*:*:*:*:*:*"),
|
||||
MustAttributes("cpe:2.3:a:some:package:1:*:*:*:*:*:*:*"),
|
||||
MustAttributes("cpe:2.3:a:*:package:1:*:*:*:*:some:*:*"),
|
||||
MustAttributes("cpe:2.3:a:some:package:1:*:*:*:*:some:*:*"),
|
||||
MustAttributes("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:*:*:*:*:*:*:*"),
|
||||
expected: []Attributes{
|
||||
MustAttributes("cpe:2.3:a:some:package:1:*:*:*:*:some:*:*"),
|
||||
MustAttributes("cpe:2.3:a:some:package:1:*:*:*:*:*:*:*"),
|
||||
MustAttributes("cpe:2.3:a:some:package:*:*:*:*:*:*:*:*"),
|
||||
MustAttributes("cpe:2.3:a:*:package:1:*:*:*:*:some:*:*"),
|
||||
MustAttributes("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:*:*"),
|
||||
input: []Attributes{
|
||||
MustAttributes("cpe:2.3:a:1:22:1:*:*:*:*:1:*:*"),
|
||||
MustAttributes("cpe:2.3:a:55555:1:1:*:*:*:*:1:*:*"),
|
||||
MustAttributes("cpe:2.3:a:1:1:333:*:*:*:*:1:*:*"),
|
||||
MustAttributes("cpe:2.3:a:1:666666:1:*:*:*:*:1:*:*"),
|
||||
MustAttributes("cpe:2.3:a:1:1:1:*:*:*:*:1:*:*"),
|
||||
MustAttributes("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:*:*"),
|
||||
expected: []Attributes{
|
||||
MustAttributes("cpe:2.3:a:1:666666:1:*:*:*:*:1:*:*"),
|
||||
MustAttributes("cpe:2.3:a:55555:1:1:*:*:*:*:1:*:*"),
|
||||
MustAttributes("cpe:2.3:a:1:1:1:*:*:*:*:4444:*:*"),
|
||||
MustAttributes("cpe:2.3:a:1:1:333:*:*:*:*:1:*:*"),
|
||||
MustAttributes("cpe:2.3:a:1:22:1:*:*:*:*:1:*:*"),
|
||||
MustAttributes("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:*:*"),
|
||||
input: []Attributes{
|
||||
MustAttributes("cpe:2.3:a:1:666666:*:*:*:*:*:1:*:*"),
|
||||
MustAttributes("cpe:2.3:a:*:1:1:*:*:*:*:4444:*:*"),
|
||||
MustAttributes("cpe:2.3:a:1:*:333:*:*:*:*:*:*:*"),
|
||||
MustAttributes("cpe:2.3:a:1:1:1:*:*:*:*:1:*:*"),
|
||||
MustAttributes("cpe:2.3:a:1:22:1:*:*:*:*:1:*:*"),
|
||||
MustAttributes("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:*:*:*:*:*:*:*"),
|
||||
expected: []Attributes{
|
||||
MustAttributes("cpe:2.3:a:55555:1:1:*:*:*:*:1:*:*"),
|
||||
MustAttributes("cpe:2.3:a:1:22:1:*:*:*:*:1:*:*"),
|
||||
MustAttributes("cpe:2.3:a:1:1:1:*:*:*:*:1:*:*"),
|
||||
MustAttributes("cpe:2.3:a:1:666666:*:*:*:*:*:1:*:*"),
|
||||
MustAttributes("cpe:2.3:a:*:1:1:*:*:*:*:4444:*:*"),
|
||||
MustAttributes("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:*:*:*:*:*:*:*"),
|
||||
input: []Attributes{
|
||||
MustAttributes("cpe:2.3:a:alpine:alpine_keys:2.3-r1:*:*:*:*:*:*:*"),
|
||||
MustAttributes("cpe:2.3:a:alpine_keys:alpine_keys:2.3-r1:*:*:*:*:*:*:*"),
|
||||
MustAttributes("cpe:2.3:a:alpine-keys:alpine_keys:2.3-r1:*:*:*:*:*:*:*"),
|
||||
MustAttributes("cpe:2.3:a:alpine:alpine-keys:2.3-r1:*:*:*:*:*:*:*"),
|
||||
MustAttributes("cpe:2.3:a:alpine-keys:alpine-keys:2.3-r1:*:*:*:*:*:*:*"),
|
||||
MustAttributes("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:*:*:*:*:*:*:*"),
|
||||
expected: []Attributes{
|
||||
MustAttributes("cpe:2.3:a:alpine-keys:alpine-keys:2.3-r1:*:*:*:*:*:*:*"),
|
||||
MustAttributes("cpe:2.3:a:alpine-keys:alpine_keys:2.3-r1:*:*:*:*:*:*:*"),
|
||||
MustAttributes("cpe:2.3:a:alpine_keys:alpine-keys:2.3-r1:*:*:*:*:*:*:*"),
|
||||
MustAttributes("cpe:2.3:a:alpine_keys:alpine_keys:2.3-r1:*:*:*:*:*:*:*"),
|
||||
MustAttributes("cpe:2.3:a:alpine:alpine-keys:2.3-r1:*:*:*:*:*:*:*"),
|
||||
MustAttributes("cpe:2.3:a:alpine:alpine_keys:2.3-r1:*:*:*:*:*:*:*"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -8,9 +8,29 @@ import (
|
|||
"github.com/facebookincubator/nvdtools/wfn"
|
||||
)
|
||||
|
||||
// CPE contains the attributes of an NVD Attributes and a string
|
||||
// describing where Syft got the Attributes, e.g. generated by heuristics
|
||||
// vs looked up in the NVD Attributes dictionary
|
||||
type CPE struct {
|
||||
Attributes Attributes
|
||||
Source Source
|
||||
}
|
||||
|
||||
type Source string
|
||||
|
||||
func (c Source) String() string {
|
||||
return string(c)
|
||||
}
|
||||
|
||||
const (
|
||||
GeneratedSource Source = "syft-generated"
|
||||
NVDDictionaryLookupSource Source = "nvd-cpe-dictionary"
|
||||
DeclaredSource Source = "declared"
|
||||
)
|
||||
|
||||
const Any = ""
|
||||
|
||||
type CPE struct {
|
||||
type Attributes struct {
|
||||
Part string
|
||||
Vendor string
|
||||
Product string
|
||||
|
@ -24,19 +44,19 @@ type CPE struct {
|
|||
Language string
|
||||
}
|
||||
|
||||
func (c CPE) asAttributes() wfn.Attributes {
|
||||
func (c Attributes) asAttributes() wfn.Attributes {
|
||||
return wfn.Attributes(c)
|
||||
}
|
||||
|
||||
func fromAttributes(a wfn.Attributes) CPE {
|
||||
return CPE(a)
|
||||
func fromAttributes(a wfn.Attributes) Attributes {
|
||||
return Attributes(a)
|
||||
}
|
||||
|
||||
func (c CPE) BindToFmtString() string {
|
||||
func (c Attributes) BindToFmtString() string {
|
||||
return c.asAttributes().BindToFmtString()
|
||||
}
|
||||
|
||||
func NewWithAny() CPE {
|
||||
func NewWithAny() Attributes {
|
||||
return fromAttributes(*(wfn.NewAttributesWithAny()))
|
||||
}
|
||||
|
||||
|
@ -46,36 +66,55 @@ const (
|
|||
|
||||
// This regex string is taken from
|
||||
// https://csrc.nist.gov/schema/cpe/2.3/cpe-naming_2.3.xsd which has the official cpe spec
|
||||
// This first part matches CPE urls and the second part matches binding strings
|
||||
// This first part matches Attributes urls and the second part matches binding strings
|
||||
const cpeRegexString = ((`^([c][pP][eE]:/[AHOaho]?(:[A-Za-z0-9\._\-~%]*){0,6})`) +
|
||||
// Or match the CPE binding string
|
||||
// Or match the Attributes binding string
|
||||
// Note that we had to replace '`' with '\x60' to escape the backticks
|
||||
`|(cpe:2\.3:[aho\*\-](:(((\?*|\*?)([a-zA-Z0-9\-\._]|(\\[\\\*\?!"#$$%&'\(\)\+,/:;<=>@\[\]\^\x60\{\|}~]))+(\?*|\*?))|[\*\-])){5}(:(([a-zA-Z]{2,3}(-([a-zA-Z]{2}|[0-9]{3}))?)|[\*\-]))(:(((\?*|\*?)([a-zA-Z0-9\-\._]|(\\[\\\*\?!"#$$%&'\(\)\+,/:;<=>@\[\]\^\x60\{\|}~]))+(\?*|\*?))|[\*\-])){4})$`)
|
||||
|
||||
var cpeRegex = regexp.MustCompile(cpeRegexString)
|
||||
|
||||
// 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 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 := newWithoutValidation(cpeStr)
|
||||
func New(value string, source Source) (CPE, error) {
|
||||
attributes, err := NewAttributes(value)
|
||||
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 ValidateString(c.String()) != nil {
|
||||
return CPE{}, err
|
||||
}
|
||||
return CPE{
|
||||
Attributes: attributes,
|
||||
Source: source,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// we don't return the sanitized string, as this is a concern for later when creating CPE strings. In fact, since
|
||||
// NewAttributes will parse a formatted Attributes string and return a Attributes object. Some input, such as the existence of whitespace
|
||||
// characters is allowed, however, a more strict validation is done after this sanitization process.
|
||||
func NewAttributes(cpeStr string) (Attributes, error) {
|
||||
// get a Attributes 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 := newWithoutValidation(cpeStr)
|
||||
if err != nil {
|
||||
return Attributes{}, fmt.Errorf("unable to parse Attributes string: %w", err)
|
||||
}
|
||||
|
||||
// ensure that this Attributes can be validated after being fully sanitized
|
||||
if ValidateString(c.String()) != nil {
|
||||
return Attributes{}, err
|
||||
}
|
||||
|
||||
// we don't return the sanitized string, as this is a concern for later when creating Attributes strings. In fact, since
|
||||
// sanitization is lossy (whitespace is replaced, not escaped) it's important that the raw values are left as.
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Must returns a CPE or panics if the provided string is not valid
|
||||
func Must(cpeStr string) CPE {
|
||||
c, err := New(cpeStr)
|
||||
func Must(cpeStr string, source Source) CPE {
|
||||
c := MustAttributes(cpeStr)
|
||||
return CPE{
|
||||
Attributes: c,
|
||||
Source: source,
|
||||
}
|
||||
}
|
||||
|
||||
func MustAttributes(cpeStr string) Attributes {
|
||||
c, err := NewAttributes(cpeStr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@ -83,22 +122,22 @@ func Must(cpeStr string) CPE {
|
|||
}
|
||||
|
||||
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
|
||||
// We should filter out all CPEs that do not match the official Attributes regex
|
||||
// The facebook nvdtools parser can sometimes incorrectly parse invalid Attributes strings
|
||||
if !cpeRegex.MatchString(cpeStr) {
|
||||
return fmt.Errorf("failed to parse CPE=%q as it doesn't match the regex=%s", cpeStr, cpeRegexString)
|
||||
return fmt.Errorf("failed to parse Attributes=%q as it doesn't match the regex=%s", cpeStr, cpeRegexString)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func newWithoutValidation(cpeStr string) (CPE, error) {
|
||||
func newWithoutValidation(cpeStr string) (Attributes, error) {
|
||||
value, err := wfn.Parse(cpeStr)
|
||||
if err != nil {
|
||||
return CPE{}, fmt.Errorf("failed to parse CPE=%q: %w", cpeStr, err)
|
||||
return Attributes{}, fmt.Errorf("failed to parse Attributes=%q: %w", cpeStr, err)
|
||||
}
|
||||
|
||||
if value == nil {
|
||||
return CPE{}, fmt.Errorf("failed to parse CPE=%q", cpeStr)
|
||||
return Attributes{}, fmt.Errorf("failed to parse Attributes=%q", cpeStr)
|
||||
}
|
||||
|
||||
syftCPE := fromAttributes(*value)
|
||||
|
@ -120,7 +159,7 @@ func newWithoutValidation(cpeStr string) (CPE, error) {
|
|||
}
|
||||
|
||||
func normalizeField(field string) string {
|
||||
// replace spaces with underscores (per section 5.3.2 of the CPE spec v 2.3)
|
||||
// replace spaces with underscores (per section 5.3.2 of the Attributes spec v 2.3)
|
||||
field = strings.ReplaceAll(field, " ", "_")
|
||||
|
||||
// keep dashes and forward slashes unescaped
|
||||
|
@ -144,8 +183,8 @@ func stripSlashes(s string) string {
|
|||
return sb.String()
|
||||
}
|
||||
|
||||
func (c CPE) String() string {
|
||||
output := CPE{}
|
||||
func (c Attributes) String() string {
|
||||
output := Attributes{}
|
||||
output.Vendor = sanitize(c.Vendor)
|
||||
output.Product = sanitize(c.Product)
|
||||
output.Language = sanitize(c.Language)
|
||||
|
|
|
@ -12,38 +12,38 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_New(t *testing.T) {
|
||||
func Test_NewAttributes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected CPE
|
||||
expected Attributes
|
||||
}{
|
||||
{
|
||||
name: "gocase",
|
||||
input: `cpe:/a:10web:form_maker:1.0.0::~~~wordpress~~`,
|
||||
expected: Must(`cpe:2.3:a:10web:form_maker:1.0.0:*:*:*:*:wordpress:*:*`),
|
||||
expected: MustAttributes(`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(`cpe:2.3:a:7-zip:7-zip:4.56:beta:*:*:*:windows:*:*`),
|
||||
expected: MustAttributes(`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(`cpe:2.3:a:\$0.99_kindle_books_project:\$0.99_kindle_books:6:*:*:*:*:android:*:*`),
|
||||
expected: MustAttributes(`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 := New(test.input)
|
||||
actual, err := NewAttributes(test.input)
|
||||
if err != nil {
|
||||
t.Fatalf("got an error while creating CPE: %+v", err)
|
||||
t.Fatalf("got an error while creating Attributes: %+v", err)
|
||||
}
|
||||
|
||||
if d := cmp.Diff(actual, test.expected); d != "" {
|
||||
t.Errorf("CPE mismatch (-want +got):\n%s", d)
|
||||
t.Errorf("Attributes mismatch (-want +got):\n%s", d)
|
||||
}
|
||||
|
||||
})
|
||||
|
@ -82,9 +82,9 @@ func Test_normalizeCpeField(t *testing.T) {
|
|||
|
||||
func Test_CPEParser(t *testing.T) {
|
||||
var testCases []struct {
|
||||
CPEString string `json:"cpe-string"`
|
||||
CPEUrl string `json:"cpe-url"`
|
||||
WFN CPE `json:"wfn"`
|
||||
CPEString string `json:"cpe-string"`
|
||||
CPEUrl string `json:"cpe-url"`
|
||||
WFN Attributes `json:"wfn"`
|
||||
}
|
||||
out, err := os.ReadFile("test-fixtures/cpe-data.json")
|
||||
require.NoError(t, err)
|
||||
|
@ -92,9 +92,9 @@ func Test_CPEParser(t *testing.T) {
|
|||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.CPEString, func(t *testing.T) {
|
||||
c1, err := New(test.CPEString)
|
||||
c1, err := NewAttributes(test.CPEString)
|
||||
assert.NoError(t, err)
|
||||
c2, err := New(test.CPEUrl)
|
||||
c2, err := NewAttributes(test.CPEUrl)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, c1, c2)
|
||||
assert.Equal(t, c1, test.WFN)
|
||||
|
@ -161,11 +161,11 @@ func Test_InvalidCPE(t *testing.T) {
|
|||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
c, err := New(test.in)
|
||||
c, err := NewAttributes(test.in)
|
||||
if test.expectedErr {
|
||||
assert.Error(t, err)
|
||||
if t.Failed() {
|
||||
t.Logf("got CPE: %q details: %+v", c, c)
|
||||
t.Logf("got Attributes: %q details: %+v", c, c)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@ -179,12 +179,12 @@ func Test_RoundTrip(t *testing.T) {
|
|||
tests := []struct {
|
||||
name string
|
||||
cpe string
|
||||
parsedCPE CPE
|
||||
parsedCPE Attributes
|
||||
}{
|
||||
{
|
||||
name: "normal",
|
||||
cpe: "cpe:2.3:a:some-vendor:name:3.2:*:*:*:*:*:*:*",
|
||||
parsedCPE: CPE{
|
||||
parsedCPE: Attributes{
|
||||
Part: "a",
|
||||
Vendor: "some-vendor",
|
||||
Product: "name",
|
||||
|
@ -194,7 +194,7 @@ func Test_RoundTrip(t *testing.T) {
|
|||
{
|
||||
name: "escaped colon",
|
||||
cpe: "cpe:2.3:a:some-vendor:name:1\\:3.2:*:*:*:*:*:*:*",
|
||||
parsedCPE: CPE{
|
||||
parsedCPE: Attributes{
|
||||
Part: "a",
|
||||
Vendor: "some-vendor",
|
||||
Product: "name",
|
||||
|
@ -204,7 +204,7 @@ func Test_RoundTrip(t *testing.T) {
|
|||
{
|
||||
name: "escaped forward slash",
|
||||
cpe: "cpe:2.3:a:test\\/some-vendor:name:3.2:*:*:*:*:*:*:*",
|
||||
parsedCPE: CPE{
|
||||
parsedCPE: Attributes{
|
||||
Part: "a",
|
||||
Vendor: "test/some-vendor",
|
||||
Product: "name",
|
||||
|
@ -215,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, Must(test.cpe).String())
|
||||
// The parsed CPE must be the same after a round trip
|
||||
assert.Equal(t, Must(test.cpe), Must(Must(test.cpe).String()))
|
||||
// The test case parsed CPE must be the same after parsing the input string
|
||||
assert.Equal(t, test.parsedCPE, Must(test.cpe))
|
||||
// The test case parsed CPE must produce the same string as the input cpe
|
||||
// Attributes string must be preserved through a round trip
|
||||
assert.Equal(t, test.cpe, MustAttributes(test.cpe).String())
|
||||
// The parsed Attributes must be the same after a round trip
|
||||
assert.Equal(t, MustAttributes(test.cpe), MustAttributes(MustAttributes(test.cpe).String()))
|
||||
// The test case parsed Attributes must be the same after parsing the input string
|
||||
assert.Equal(t, test.parsedCPE, MustAttributes(test.cpe))
|
||||
// The test case parsed Attributes must produce the same string as the input cpe
|
||||
assert.Equal(t, test.parsedCPE.String(), test.cpe)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,25 +1,27 @@
|
|||
package cpe
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
)
|
||||
|
||||
func Merge(a, b []CPE) (result []CPE) {
|
||||
aCPEs := make(map[string]CPE)
|
||||
|
||||
// keep all CPEs from a and create a quick string-based lookup
|
||||
for _, aCPE := range a {
|
||||
aCPEs[aCPE.BindToFmtString()] = aCPE
|
||||
result = append(result, aCPE)
|
||||
// Merge returns unique SourcedCPEs that are found in A or B
|
||||
// Two SourcedCPEs are identical if their source and normalized string are identical
|
||||
func Merge(a, b []CPE) []CPE {
|
||||
var result []CPE
|
||||
dedupe := make(map[string]CPE)
|
||||
key := func(scpe CPE) string {
|
||||
return fmt.Sprintf("%s:%s", scpe.Source.String(), scpe.Attributes.BindToFmtString())
|
||||
}
|
||||
|
||||
// keep all unique CPEs from b
|
||||
for _, bCPE := range b {
|
||||
if _, exists := aCPEs[bCPE.BindToFmtString()]; !exists {
|
||||
result = append(result, bCPE)
|
||||
}
|
||||
for _, s := range a {
|
||||
dedupe[key(s)] = s
|
||||
}
|
||||
|
||||
sort.Sort(BySpecificity(result))
|
||||
for _, s := range b {
|
||||
dedupe[key(s)] = s
|
||||
}
|
||||
for _, val := range dedupe {
|
||||
result = append(result, val)
|
||||
}
|
||||
sort.Sort(BySourceThenSpecificity(result))
|
||||
return result
|
||||
}
|
||||
|
|
|
@ -16,18 +16,20 @@ func Test_Merge(t *testing.T) {
|
|||
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:*:package:1:*:*:*:*:*:*:*", NVDDictionaryLookupSource),
|
||||
Must("cpe:2.3:a:*:package:1:*:*:*:*:*:*:*", DeclaredSource),
|
||||
Must("cpe:2.3:a:some:package:*:*:*:*:*:*:*:*", GeneratedSource),
|
||||
},
|
||||
{
|
||||
Must("cpe:2.3:a:some:package:1:*:*:*:*:*:*:*"),
|
||||
Must("cpe:2.3:a:some:package:*:*:*:*:*:*:*:*"),
|
||||
Must("cpe:2.3:a:some:package:1:*:*:*:*:*:*:*", DeclaredSource),
|
||||
Must("cpe:2.3:a:some:package:*:*:*:*:*:*:*:*", GeneratedSource),
|
||||
},
|
||||
},
|
||||
expected: []CPE{
|
||||
Must("cpe:2.3:a:some:package:1:*:*:*:*:*:*:*"),
|
||||
Must("cpe:2.3:a:some:package:*:*:*:*:*:*:*:*"),
|
||||
Must("cpe:2.3:a:*:package:1:*:*:*:*:*:*:*"),
|
||||
Must("cpe:2.3:a:*:package:1:*:*:*:*:*:*:*", NVDDictionaryLookupSource),
|
||||
Must("cpe:2.3:a:some:package:1:*:*:*:*:*:*:*", DeclaredSource),
|
||||
Must("cpe:2.3:a:*:package:1:*:*:*:*:*:*:*", DeclaredSource),
|
||||
Must("cpe:2.3:a:some:package:*:*:*:*:*:*:*:*", GeneratedSource),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -103,7 +103,7 @@ func toOSComponent(distro *linux.Release) []cyclonedx.Component {
|
|||
}
|
||||
|
||||
func formatCPE(cpeString string) string {
|
||||
c, err := cpe.New(cpeString)
|
||||
c, err := cpe.NewAttributes(cpeString)
|
||||
if err != nil {
|
||||
log.Debugf("skipping invalid CPE: %s", cpeString)
|
||||
return ""
|
||||
|
|
|
@ -630,7 +630,7 @@ func findPURLValue(p *spdx.Package) string {
|
|||
func extractCPEs(p *spdx.Package) (cpes []cpe.CPE) {
|
||||
for _, r := range p.PackageExternalReferences {
|
||||
if r.RefType == string(helpers.Cpe23ExternalRefType) {
|
||||
c, err := cpe.New(r.Locator)
|
||||
c, err := cpe.New(r.Locator, cpe.DeclaredSource)
|
||||
if err != nil {
|
||||
log.Warnf("unable to extract SPDX CPE=%q: %+v", r.Locator, err)
|
||||
continue
|
||||
|
|
|
@ -12,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 p.CPEs[0].String()
|
||||
return p.CPEs[0].Attributes.String()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ func encodeCPEs(p pkg.Package) (out []cyclonedx.Property) {
|
|||
}
|
||||
out = append(out, cyclonedx.Property{
|
||||
Name: "syft:cpe23",
|
||||
Value: c.String(),
|
||||
Value: c.Attributes.String(),
|
||||
})
|
||||
}
|
||||
return
|
||||
|
@ -33,7 +33,7 @@ func encodeCPEs(p pkg.Package) (out []cyclonedx.Property) {
|
|||
|
||||
func decodeCPEs(c *cyclonedx.Component) (out []cpe.CPE) {
|
||||
if c.CPE != "" {
|
||||
cp, err := cpe.New(c.CPE)
|
||||
cp, err := cpe.New(c.CPE, cpe.DeclaredSource)
|
||||
if err != nil {
|
||||
log.Warnf("invalid CPE: %s", c.CPE)
|
||||
} else {
|
||||
|
@ -44,7 +44,7 @@ func decodeCPEs(c *cyclonedx.Component) (out []cpe.CPE) {
|
|||
if c.Properties != nil {
|
||||
for _, p := range *c.Properties {
|
||||
if p.Name == "syft:cpe23" {
|
||||
cp, err := cpe.New(p.Value)
|
||||
cp, err := cpe.New(p.Value, cpe.DeclaredSource)
|
||||
if err != nil {
|
||||
log.Warnf("invalid CPE: %s", p.Value)
|
||||
} else {
|
||||
|
|
|
@ -10,8 +10,8 @@ import (
|
|||
)
|
||||
|
||||
func Test_encodeCPE(t *testing.T) {
|
||||
testCPE := cpe.Must("cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*")
|
||||
testCPE2 := cpe.Must("cpe:2.3:a:name:name2:3.2:*:*:*:*:*:*:*")
|
||||
testCPE := cpe.Must("cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*", "test-source")
|
||||
testCPE2 := cpe.Must("cpe:2.3:a:name:name2:3.2:*:*:*:*:*:*:*", "test-source-2")
|
||||
tests := []struct {
|
||||
name string
|
||||
input pkg.Package
|
||||
|
@ -26,7 +26,7 @@ func Test_encodeCPE(t *testing.T) {
|
|||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "single CPE",
|
||||
name: "single Attributes",
|
||||
input: pkg.Package{
|
||||
CPEs: []cpe.CPE{
|
||||
testCPE,
|
||||
|
|
|
@ -222,7 +222,7 @@ func Test_decode(t *testing.T) {
|
|||
if e.cpe != "" {
|
||||
foundCPE := false
|
||||
for _, c := range p.CPEs {
|
||||
cstr := c.BindToFmtString()
|
||||
cstr := c.Attributes.BindToFmtString()
|
||||
if e.cpe == cstr {
|
||||
foundCPE = true
|
||||
break
|
||||
|
|
|
@ -10,7 +10,7 @@ func ExternalRefs(p pkg.Package) (externalRefs []ExternalRef) {
|
|||
for _, c := range p.CPEs {
|
||||
externalRefs = append(externalRefs, ExternalRef{
|
||||
ReferenceCategory: SecurityReferenceCategory,
|
||||
ReferenceLocator: c.String(),
|
||||
ReferenceLocator: c.Attributes.String(),
|
||||
ReferenceType: Cpe23ExternalRefType,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import (
|
|||
)
|
||||
|
||||
func Test_ExternalRefs(t *testing.T) {
|
||||
testCPE := cpe.Must("cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*")
|
||||
testCPE := cpe.Must("cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*", cpe.Source(""))
|
||||
tests := []struct {
|
||||
name string
|
||||
input pkg.Package
|
||||
|
@ -27,7 +27,7 @@ func Test_ExternalRefs(t *testing.T) {
|
|||
expected: []ExternalRef{
|
||||
{
|
||||
ReferenceCategory: SecurityReferenceCategory,
|
||||
ReferenceLocator: testCPE.String(),
|
||||
ReferenceLocator: testCPE.Attributes.String(),
|
||||
ReferenceType: Cpe23ExternalRefType,
|
||||
},
|
||||
{
|
||||
|
|
|
@ -123,7 +123,7 @@ func newDirectoryCatalog() *pkg.Collection {
|
|||
},
|
||||
PURL: "a-purl-2", // intentionally a bad pURL for test fixtures
|
||||
CPEs: []cpe.CPE{
|
||||
cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"),
|
||||
cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*", cpe.Source("")),
|
||||
},
|
||||
})
|
||||
catalog.Add(pkg.Package{
|
||||
|
@ -140,7 +140,7 @@ func newDirectoryCatalog() *pkg.Collection {
|
|||
},
|
||||
PURL: "pkg:deb/debian/package-2@2.0.1",
|
||||
CPEs: []cpe.CPE{
|
||||
cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"),
|
||||
cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*", cpe.Source("")),
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -175,7 +175,7 @@ func newDirectoryCatalogWithAuthorField() *pkg.Collection {
|
|||
},
|
||||
PURL: "a-purl-2", // intentionally a bad pURL for test fixtures
|
||||
CPEs: []cpe.CPE{
|
||||
cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"),
|
||||
cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*", cpe.GeneratedSource),
|
||||
},
|
||||
})
|
||||
catalog.Add(pkg.Package{
|
||||
|
@ -192,7 +192,7 @@ func newDirectoryCatalogWithAuthorField() *pkg.Collection {
|
|||
},
|
||||
PURL: "pkg:deb/debian/package-2@2.0.1",
|
||||
CPEs: []cpe.CPE{
|
||||
cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"),
|
||||
cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*", "another-test-source"),
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
@ -119,7 +119,7 @@ func populateImageCatalog(catalog *pkg.Collection, img *image.Image) {
|
|||
},
|
||||
PURL: "a-purl-1", // intentionally a bad pURL for test fixtures
|
||||
CPEs: []cpe.CPE{
|
||||
cpe.Must("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*"),
|
||||
cpe.Must("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*", cpe.GeneratedSource),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -139,7 +139,7 @@ func populateImageCatalog(catalog *pkg.Collection, img *image.Image) {
|
|||
},
|
||||
PURL: "pkg:deb/debian/package-2@2.0.1",
|
||||
CPEs: []cpe.CPE{
|
||||
cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"),
|
||||
cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
@ -159,11 +159,14 @@ func Test_encodeDecodeFileMetadata(t *testing.T) {
|
|||
Type: "type",
|
||||
CPEs: []cpe.CPE{
|
||||
{
|
||||
Part: "a",
|
||||
Vendor: "vendor",
|
||||
Product: "product",
|
||||
Version: "version",
|
||||
Update: "update",
|
||||
Attributes: cpe.Attributes{
|
||||
Part: "a",
|
||||
Vendor: "vendor",
|
||||
Product: "product",
|
||||
Version: "version",
|
||||
Update: "update",
|
||||
},
|
||||
Source: "test-source",
|
||||
},
|
||||
},
|
||||
PURL: "pkg:generic/pkg@version",
|
||||
|
|
|
@ -152,7 +152,7 @@ func TestEncodeFullJSONDocument(t *testing.T) {
|
|||
},
|
||||
PURL: "a-purl-1",
|
||||
CPEs: []cpe.CPE{
|
||||
cpe.Must("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*"),
|
||||
cpe.Must("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource),
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -173,7 +173,7 @@ func TestEncodeFullJSONDocument(t *testing.T) {
|
|||
},
|
||||
PURL: "a-purl-2",
|
||||
CPEs: []cpe.CPE{
|
||||
cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"),
|
||||
cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*", cpe.GeneratedSource),
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -32,10 +32,17 @@ type PackageBasicData struct {
|
|||
Locations []file.Location `json:"locations"`
|
||||
Licenses licenses `json:"licenses"`
|
||||
Language pkg.Language `json:"language"`
|
||||
CPEs []string `json:"cpes"`
|
||||
CPEs cpes `json:"cpes"`
|
||||
PURL string `json:"purl"`
|
||||
}
|
||||
|
||||
type cpes []CPE
|
||||
|
||||
type CPE struct {
|
||||
Value string `json:"cpe"`
|
||||
Source string `json:"source,omitempty"`
|
||||
}
|
||||
|
||||
type licenses []License
|
||||
|
||||
type License struct {
|
||||
|
@ -74,6 +81,29 @@ func (f *licenses) UnmarshalJSON(b []byte) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func sourcedCPESfromSimpleCPEs(simpleCPEs []string) []CPE {
|
||||
var result []CPE
|
||||
for _, s := range simpleCPEs {
|
||||
result = append(result, CPE{
|
||||
Value: s,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (c *cpes) UnmarshalJSON(b []byte) error {
|
||||
var cs []CPE
|
||||
if err := json.Unmarshal(b, &cs); err != nil {
|
||||
var simpleCPEs []string
|
||||
if err := json.Unmarshal(b, &simpleCPEs); err != nil {
|
||||
return fmt.Errorf("unable to unmarshal cpes: %w", err)
|
||||
}
|
||||
cs = sourcedCPESfromSimpleCPEs(simpleCPEs)
|
||||
}
|
||||
*c = cs
|
||||
return nil
|
||||
}
|
||||
|
||||
// PackageCustomData contains ambiguous values (type-wise) from pkg.Package.
|
||||
type PackageCustomData struct {
|
||||
MetadataType string `json:"metadataType,omitempty"`
|
||||
|
|
|
@ -23,7 +23,9 @@
|
|||
],
|
||||
"language": "python",
|
||||
"cpes": [
|
||||
"cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"
|
||||
{
|
||||
"cpe": "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"
|
||||
}
|
||||
],
|
||||
"purl": "a-purl-2",
|
||||
"metadataType": "python-package",
|
||||
|
@ -56,7 +58,9 @@
|
|||
"licenses": [],
|
||||
"language": "",
|
||||
"cpes": [
|
||||
"cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"
|
||||
{
|
||||
"cpe": "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"
|
||||
}
|
||||
],
|
||||
"purl": "pkg:deb/debian/package-2@2.0.1",
|
||||
"metadataType": "dpkg-db-entry",
|
||||
|
|
|
@ -23,7 +23,10 @@
|
|||
],
|
||||
"language": "python",
|
||||
"cpes": [
|
||||
"cpe:2.3:*:some:package:1:*:*:*:*:*:*:*"
|
||||
{
|
||||
"cpe": "cpe:2.3:*:some:package:1:*:*:*:*:*:*:*",
|
||||
"source": "nvd-cpe-dictionary"
|
||||
}
|
||||
],
|
||||
"purl": "a-purl-1",
|
||||
"metadataType": "python-package",
|
||||
|
@ -51,7 +54,10 @@
|
|||
"licenses": [],
|
||||
"language": "",
|
||||
"cpes": [
|
||||
"cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"
|
||||
{
|
||||
"cpe": "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*",
|
||||
"source": "syft-generated"
|
||||
}
|
||||
],
|
||||
"purl": "a-purl-2",
|
||||
"metadataType": "dpkg-db-entry",
|
||||
|
|
|
@ -24,7 +24,10 @@
|
|||
],
|
||||
"language": "python",
|
||||
"cpes": [
|
||||
"cpe:2.3:*:some:package:1:*:*:*:*:*:*:*"
|
||||
{
|
||||
"cpe": "cpe:2.3:*:some:package:1:*:*:*:*:*:*:*",
|
||||
"source": "syft-generated"
|
||||
}
|
||||
],
|
||||
"purl": "a-purl-1",
|
||||
"metadataType": "python-package",
|
||||
|
@ -53,7 +56,10 @@
|
|||
"licenses": [],
|
||||
"language": "",
|
||||
"cpes": [
|
||||
"cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"
|
||||
{
|
||||
"cpe": "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*",
|
||||
"source": "nvd-cpe-dictionary"
|
||||
}
|
||||
],
|
||||
"purl": "pkg:deb/debian/package-2@2.0.1",
|
||||
"metadataType": "dpkg-db-entry",
|
||||
|
|
|
@ -229,9 +229,13 @@ func toLicenseModel(pkgLicenses []pkg.License) (modelLicenses []model.License) {
|
|||
|
||||
// toPackageModel crates a new Package from the given pkg.Package.
|
||||
func toPackageModel(p pkg.Package, cfg EncoderConfig) model.Package {
|
||||
var cpes = make([]string, len(p.CPEs))
|
||||
var cpes = make([]model.CPE, len(p.CPEs))
|
||||
for i, c := range p.CPEs {
|
||||
cpes[i] = c.String()
|
||||
convertedCPE := model.CPE{
|
||||
Value: c.Attributes.String(),
|
||||
Source: c.Source.String(),
|
||||
}
|
||||
cpes[i] = convertedCPE
|
||||
}
|
||||
|
||||
// we want to make sure all catalogers are
|
||||
|
|
|
@ -301,9 +301,9 @@ func toSyftCatalog(pkgs []model.Package, idAliases map[string]string) *pkg.Colle
|
|||
func toSyftPackage(p model.Package, idAliases map[string]string) pkg.Package {
|
||||
var cpes []cpe.CPE
|
||||
for _, c := range p.CPEs {
|
||||
value, err := cpe.New(c)
|
||||
value, err := cpe.New(c.Value, cpe.Source(c.Source))
|
||||
if err != nil {
|
||||
log.Warnf("excluding invalid CPE %q: %v", c, err)
|
||||
log.Warnf("excluding invalid Attributes %q: %v", c, err)
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
|
@ -1282,7 +1282,7 @@ func TestCatalogerConfig_MarshalJSON(t *testing.T) {
|
|||
Qualifiers: nil,
|
||||
Subpath: "subpath",
|
||||
},
|
||||
CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:some:app:*:*:*:*:*:*:*:*")},
|
||||
CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:some:app:*:*:*:*:*:*:*:*", cpe.GeneratedSource)},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -56,7 +56,7 @@ func (cfg Classifier) MarshalJSON() ([]byte, error) {
|
|||
|
||||
var marshalledCPEs []string
|
||||
for _, c := range cfg.CPEs {
|
||||
marshalledCPEs = append(marshalledCPEs, c.BindToFmtString())
|
||||
marshalledCPEs = append(marshalledCPEs, c.Attributes.BindToFmtString())
|
||||
}
|
||||
|
||||
m := marshalled{
|
||||
|
@ -225,10 +225,11 @@ func getContents(resolver file.Resolver, location file.Location) ([]byte, error)
|
|||
return contents, nil
|
||||
}
|
||||
|
||||
// singleCPE returns a []pkg.CPE based on the cpe string or panics if the CPE is invalid
|
||||
// singleCPE returns a []cpe.CPE with Source: Generated based on the cpe string or panics if the
|
||||
// cpe string cannot be parsed into valid CPE Attributes
|
||||
func singleCPE(cpeString string) []cpe.CPE {
|
||||
return []cpe.CPE{
|
||||
cpe.Must(cpeString),
|
||||
cpe.Must(cpeString, cpe.GeneratedSource),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -30,14 +30,14 @@ func Test_ClassifierCPEs(t *testing.T) {
|
|||
cpes: nil,
|
||||
},
|
||||
{
|
||||
name: "one CPE",
|
||||
name: "one Attributes",
|
||||
fixture: "test-fixtures/version.txt",
|
||||
classifier: Classifier{
|
||||
Package: "some-app",
|
||||
FileGlob: "**/version.txt",
|
||||
EvidenceMatcher: FileContentsVersionMatcher(`(?m)my-verison:(?P<version>[0-9.]+)`),
|
||||
CPEs: []cpe.CPE{
|
||||
cpe.Must("cpe:2.3:a:some:app:*:*:*:*:*:*:*:*"),
|
||||
cpe.Must("cpe:2.3:a:some:app:*:*:*:*:*:*:*:*", cpe.GeneratedSource),
|
||||
},
|
||||
},
|
||||
cpes: []string{
|
||||
|
@ -52,8 +52,8 @@ func Test_ClassifierCPEs(t *testing.T) {
|
|||
FileGlob: "**/version.txt",
|
||||
EvidenceMatcher: FileContentsVersionMatcher(`(?m)my-verison:(?P<version>[0-9.]+)`),
|
||||
CPEs: []cpe.CPE{
|
||||
cpe.Must("cpe:2.3:a:some:app:*:*:*:*:*:*:*:*"),
|
||||
cpe.Must("cpe:2.3:a:some:apps:*:*:*:*:*:*:*:*"),
|
||||
cpe.Must("cpe:2.3:a:some:app:*:*:*:*:*:*:*:*", cpe.GeneratedSource),
|
||||
cpe.Must("cpe:2.3:a:some:apps:*:*:*:*:*:*:*:*", cpe.GeneratedSource),
|
||||
},
|
||||
},
|
||||
cpes: []string{
|
||||
|
@ -79,7 +79,7 @@ func Test_ClassifierCPEs(t *testing.T) {
|
|||
|
||||
var cpes []string
|
||||
for _, c := range p.CPEs {
|
||||
cpes = append(cpes, c.String())
|
||||
cpes = append(cpes, c.Attributes.String())
|
||||
}
|
||||
require.Equal(t, test.cpes, cpes)
|
||||
})
|
||||
|
@ -109,7 +109,7 @@ func TestClassifier_MarshalJSON(t *testing.T) {
|
|||
Qualifiers: nil,
|
||||
Subpath: "subpath",
|
||||
},
|
||||
CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:some:app:*:*:*:*:*:*:*:*")},
|
||||
CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:some:app:*:*:*:*:*:*:*:*", cpe.GeneratedSource)},
|
||||
},
|
||||
want: `{"class":"class","fileGlob":"glob","package":"pkg","purl":"pkg:type/namespace/name@version#subpath","cpes":["cpe:2.3:a:some:app:*:*:*:*:*:*:*:*"]}`,
|
||||
},
|
||||
|
|
|
@ -23,8 +23,8 @@ func DefaultClassifiers() []Classifier {
|
|||
Package: "python",
|
||||
PURL: mustPURL("pkg:generic/python@version"),
|
||||
CPEs: []cpe.CPE{
|
||||
cpe.Must("cpe:2.3:a:python_software_foundation:python:*:*:*:*:*:*:*:*"),
|
||||
cpe.Must("cpe:2.3:a:python:python:*:*:*:*:*:*:*:*"),
|
||||
cpe.Must("cpe:2.3:a:python_software_foundation:python:*:*:*:*:*:*:*:*", cpe.GeneratedSource),
|
||||
cpe.Must("cpe:2.3:a:python:python:*:*:*:*:*:*:*:*", cpe.GeneratedSource),
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -34,8 +34,8 @@ func DefaultClassifiers() []Classifier {
|
|||
Package: "python",
|
||||
PURL: mustPURL("pkg:generic/python@version"),
|
||||
CPEs: []cpe.CPE{
|
||||
cpe.Must("cpe:2.3:a:python_software_foundation:python:*:*:*:*:*:*:*:*"),
|
||||
cpe.Must("cpe:2.3:a:python:python:*:*:*:*:*:*:*:*"),
|
||||
cpe.Must("cpe:2.3:a:python_software_foundation:python:*:*:*:*:*:*:*:*", cpe.GeneratedSource),
|
||||
cpe.Must("cpe:2.3:a:python:python:*:*:*:*:*:*:*:*", cpe.GeneratedSource),
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -93,7 +93,7 @@ func DefaultClassifiers() []Classifier {
|
|||
`(?m)\x00openjdk\x00java\x00(?P<release>[0-9]+[.0-9]*)\x00(?P<version>[0-9]+[^\x00]+)\x00`),
|
||||
Package: "java",
|
||||
PURL: mustPURL("pkg:generic/java@version"),
|
||||
// TODO the updates might need to be part of the CPE, like: 1.8.0:update152
|
||||
// TODO the updates might need to be part of the CPE Attributes, like: 1.8.0:update152
|
||||
CPEs: singleCPE("cpe:2.3:a:oracle:openjdk:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
|
@ -255,8 +255,8 @@ func DefaultClassifiers() []Classifier {
|
|||
Package: "percona-server",
|
||||
PURL: mustPURL("pkg:generic/percona-server@version"),
|
||||
CPEs: []cpe.CPE{
|
||||
cpe.Must("cpe:2.3:a:oracle:mysql:*:*:*:*:*:*:*:*"),
|
||||
cpe.Must("cpe:2.3:a:percona:percona_server:*:*:*:*:*:*:*:*"),
|
||||
cpe.Must("cpe:2.3:a:oracle:mysql:*:*:*:*:*:*:*:*", cpe.GeneratedSource),
|
||||
cpe.Must("cpe:2.3:a:percona:percona_server:*:*:*:*:*:*:*:*", cpe.GeneratedSource),
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -267,9 +267,9 @@ func DefaultClassifiers() []Classifier {
|
|||
Package: "percona-xtradb-cluster",
|
||||
PURL: mustPURL("pkg:generic/percona-xtradb-cluster@version"),
|
||||
CPEs: []cpe.CPE{
|
||||
cpe.Must("cpe:2.3:a:oracle:mysql:*:*:*:*:*:*:*:*"),
|
||||
cpe.Must("cpe:2.3:a:percona:percona_server:*:*:*:*:*:*:*:*"),
|
||||
cpe.Must("cpe:2.3:a:percona:xtradb_cluster:*:*:*:*:*:*:*:*"),
|
||||
cpe.Must("cpe:2.3:a:oracle:mysql:*:*:*:*:*:*:*:*", cpe.GeneratedSource),
|
||||
cpe.Must("cpe:2.3:a:percona:percona_server:*:*:*:*:*:*:*:*", cpe.GeneratedSource),
|
||||
cpe.Must("cpe:2.3:a:percona:xtradb_cluster:*:*:*:*:*:*:*:*", cpe.GeneratedSource),
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -363,8 +363,8 @@ func DefaultClassifiers() []Classifier {
|
|||
Package: "nginx",
|
||||
PURL: mustPURL("pkg:generic/nginx@version"),
|
||||
CPEs: []cpe.CPE{
|
||||
cpe.Must("cpe:2.3:a:f5:nginx:*:*:*:*:*:*:*:*"),
|
||||
cpe.Must("cpe:2.3:a:nginx:nginx:*:*:*:*:*:*:*:*"),
|
||||
cpe.Must("cpe:2.3:a:f5:nginx:*:*:*:*:*:*:*:*", cpe.GeneratedSource),
|
||||
cpe.Must("cpe:2.3:a:nginx:nginx:*:*:*:*:*:*:*:*", cpe.GeneratedSource),
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -21,8 +21,8 @@ func newPackage(classifier Classifier, location file.Location, matchMetadata map
|
|||
|
||||
var cpes []cpe.CPE
|
||||
for _, c := range classifier.CPEs {
|
||||
c.Version = version
|
||||
c.Update = update
|
||||
c.Attributes.Version = version
|
||||
c.Attributes.Update = update
|
||||
cpes = append(cpes, c)
|
||||
}
|
||||
|
||||
|
|
|
@ -127,5 +127,5 @@ func generateStdlibCpe(version string) (stdlibCpe cpe.CPE, err error) {
|
|||
cpeString = fmt.Sprintf("cpe:2.3:a:golang:go:%s:%s:*:*:*:*:*:*", vr, candidate)
|
||||
}
|
||||
|
||||
return cpe.New(cpeString)
|
||||
return cpe.New(cpeString, cpe.GeneratedSource)
|
||||
}
|
||||
|
|
|
@ -81,7 +81,7 @@ func Test_Binary_Cataloger_Stdlib_Cpe(t *testing.T) {
|
|||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := generateStdlibCpe(tc.candidate)
|
||||
assert.NoError(t, err, "expected no err; got %v", err)
|
||||
assert.Equal(t, got.String(), tc.want)
|
||||
assert.Equal(t, got.Attributes.String(), tc.want)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ type upstreamCandidate struct {
|
|||
}
|
||||
|
||||
func upstreamCandidates(m pkg.ApkDBEntry) (candidates []upstreamCandidate) {
|
||||
// Do not consider OriginPackage variations when generating CPE candidates for the child package
|
||||
// Do not consider OriginPackage variations when generating CPE Attributes candidates for the child package
|
||||
// because doing so will result in false positives when matching to vulnerabilities in Grype since
|
||||
// it won't know to lookup apk fix entries using the OriginPackage name.
|
||||
|
||||
|
|
|
@ -90,7 +90,7 @@ func filterCpeList(cpeList CpeList) CpeList {
|
|||
return processedCpeList
|
||||
}
|
||||
|
||||
// normalizeCPE removes the version and update parts of a CPE.
|
||||
// normalizeCPE removes the version and update parts of CPE Attributes.
|
||||
func normalizeCPE(cpe *wfn.Attributes) *wfn.Attributes {
|
||||
cpeCopy := *cpe
|
||||
|
||||
|
|
|
@ -9,8 +9,8 @@ import (
|
|||
|
||||
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 cpe.CPE, p pkg.Package) bool
|
||||
// filterFn instances should return true if the given CPE attributes should be removed from a collection for the given package
|
||||
type filterFn func(cpe cpe.Attributes, p pkg.Package) bool
|
||||
|
||||
var cpeFilters = []filterFn{
|
||||
disallowJiraClientServerMismatch,
|
||||
|
@ -19,7 +19,7 @@ var cpeFilters = []filterFn{
|
|||
disallowNonParseableCPEs,
|
||||
}
|
||||
|
||||
func filter(cpes []cpe.CPE, p pkg.Package, filters ...filterFn) (result []cpe.CPE) {
|
||||
func filter(cpes []cpe.Attributes, p pkg.Package, filters ...filterFn) (result []cpe.Attributes) {
|
||||
cpeLoop:
|
||||
for _, c := range cpes {
|
||||
for _, fn := range filters {
|
||||
|
@ -33,9 +33,9 @@ cpeLoop:
|
|||
return result
|
||||
}
|
||||
|
||||
func disallowNonParseableCPEs(c cpe.CPE, _ pkg.Package) bool {
|
||||
func disallowNonParseableCPEs(c cpe.Attributes, _ pkg.Package) bool {
|
||||
v := c.String()
|
||||
_, err := cpe.New(v)
|
||||
_, err := cpe.NewAttributes(v)
|
||||
|
||||
cannotParse := err != nil
|
||||
|
||||
|
@ -43,15 +43,15 @@ func disallowNonParseableCPEs(c cpe.CPE, _ pkg.Package) bool {
|
|||
}
|
||||
|
||||
// jenkins plugins should not match against jenkins
|
||||
func disallowJenkinsServerCPEForPluginPackage(c cpe.CPE, p pkg.Package) bool {
|
||||
func disallowJenkinsServerCPEForPluginPackage(c cpe.Attributes, p pkg.Package) bool {
|
||||
if p.Type == pkg.JenkinsPluginPkg && c.Product == jenkinsName {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// filter to account that packages that are not for jenkins but have a CPE generated that will match against jenkins
|
||||
func disallowJenkinsCPEsNotAssociatedWithJenkins(c cpe.CPE, p pkg.Package) bool {
|
||||
// filter to account that packages that are not for jenkins but have a Attributes generated that will match against jenkins
|
||||
func disallowJenkinsCPEsNotAssociatedWithJenkins(c cpe.Attributes, p pkg.Package) bool {
|
||||
// jenkins server should only match against a product with the name jenkins
|
||||
if c.Product == jenkinsName && !strings.Contains(strings.ToLower(p.Name), jenkinsName) {
|
||||
if c.Vendor == cpe.Any || c.Vendor == jenkinsName || c.Vendor == "cloudbees" {
|
||||
|
@ -61,8 +61,8 @@ func disallowJenkinsCPEsNotAssociatedWithJenkins(c cpe.CPE, p pkg.Package) bool
|
|||
return false
|
||||
}
|
||||
|
||||
// filter to account for packages which are jira client packages but have a CPE that will match against jira
|
||||
func disallowJiraClientServerMismatch(c cpe.CPE, p pkg.Package) bool {
|
||||
// filter to account for packages which are jira client packages but have a Attributes that will match against jira
|
||||
func disallowJiraClientServerMismatch(c cpe.Attributes, p pkg.Package) bool {
|
||||
// jira / atlassian should not apply to clients
|
||||
if c.Product == "jira" && strings.Contains(strings.ToLower(p.Name), "client") {
|
||||
if c.Vendor == cpe.Any || c.Vendor == "jira" || c.Vendor == "atlassian" {
|
||||
|
|
|
@ -18,7 +18,7 @@ func Test_disallowJenkinsServerCPEForPluginPackage(t *testing.T) {
|
|||
}{
|
||||
{
|
||||
name: "go case (filter out)",
|
||||
cpe: cpe.Must("cpe:2.3:a:name:jenkins:3.2:*:*:*:*:*:*:*"),
|
||||
cpe: cpe.Must("cpe:2.3:a:name:jenkins:3.2:*:*:*:*:*:*:*", cpe.GeneratedSource),
|
||||
pkg: pkg.Package{
|
||||
Type: pkg.JenkinsPluginPkg,
|
||||
},
|
||||
|
@ -26,7 +26,7 @@ func Test_disallowJenkinsServerCPEForPluginPackage(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "ignore jenkins plugins with unique name",
|
||||
cpe: cpe.Must("cpe:2.3:a:name:ci-jenkins:3.2:*:*:*:*:*:*:*"),
|
||||
cpe: cpe.Must("cpe:2.3:a:name:ci-jenkins:3.2:*:*:*:*:*:*:*", cpe.GeneratedSource),
|
||||
pkg: pkg.Package{
|
||||
Type: pkg.JenkinsPluginPkg,
|
||||
},
|
||||
|
@ -34,7 +34,7 @@ func Test_disallowJenkinsServerCPEForPluginPackage(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "ignore java packages",
|
||||
cpe: cpe.Must("cpe:2.3:a:name:jenkins:3.2:*:*:*:*:*:*:*"),
|
||||
cpe: cpe.Must("cpe:2.3:a:name:jenkins:3.2:*:*:*:*:*:*:*", cpe.GeneratedSource),
|
||||
pkg: pkg.Package{
|
||||
Type: pkg.JavaPkg,
|
||||
},
|
||||
|
@ -43,7 +43,7 @@ func Test_disallowJenkinsServerCPEForPluginPackage(t *testing.T) {
|
|||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, disallowJenkinsServerCPEForPluginPackage(test.cpe, test.pkg))
|
||||
assert.Equal(t, test.expected, disallowJenkinsServerCPEForPluginPackage(test.cpe.Attributes, test.pkg))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ func Test_disallowJenkinsCPEsNotAssociatedWithJenkins(t *testing.T) {
|
|||
}{
|
||||
{
|
||||
name: "filter out mismatched name (cloudbees vendor)",
|
||||
cpe: cpe.Must("cpe:2.3:a:cloudbees:jenkins:3.2:*:*:*:*:*:*:*"),
|
||||
cpe: cpe.Must("cpe:2.3:a:cloudbees:jenkins:3.2:*:*:*:*:*:*:*", cpe.GeneratedSource),
|
||||
pkg: pkg.Package{
|
||||
Name: "not-j*nkins",
|
||||
Type: pkg.JavaPkg,
|
||||
|
@ -66,7 +66,7 @@ func Test_disallowJenkinsCPEsNotAssociatedWithJenkins(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "filter out mismatched name (jenkins vendor)",
|
||||
cpe: cpe.Must("cpe:2.3:a:jenkins:jenkins:3.2:*:*:*:*:*:*:*"),
|
||||
cpe: cpe.Must("cpe:2.3:a:jenkins:jenkins:3.2:*:*:*:*:*:*:*", cpe.GeneratedSource),
|
||||
pkg: pkg.Package{
|
||||
Name: "not-j*nkins",
|
||||
Type: pkg.JavaPkg,
|
||||
|
@ -75,7 +75,7 @@ func Test_disallowJenkinsCPEsNotAssociatedWithJenkins(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "filter out mismatched name (any vendor)",
|
||||
cpe: cpe.Must("cpe:2.3:a:*:jenkins:3.2:*:*:*:*:*:*:*"),
|
||||
cpe: cpe.Must("cpe:2.3:a:*:jenkins:3.2:*:*:*:*:*:*:*", cpe.GeneratedSource),
|
||||
pkg: pkg.Package{
|
||||
Name: "not-j*nkins",
|
||||
Type: pkg.JavaPkg,
|
||||
|
@ -84,7 +84,7 @@ func Test_disallowJenkinsCPEsNotAssociatedWithJenkins(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "ignore packages with the name jenkins",
|
||||
cpe: cpe.Must("cpe:2.3:a:*:jenkins:3.2:*:*:*:*:*:*:*"),
|
||||
cpe: cpe.Must("cpe:2.3:a:*:jenkins:3.2:*:*:*:*:*:*:*", cpe.GeneratedSource),
|
||||
pkg: pkg.Package{
|
||||
Name: "jenkins-thing",
|
||||
Type: pkg.JavaPkg,
|
||||
|
@ -93,7 +93,7 @@ func Test_disallowJenkinsCPEsNotAssociatedWithJenkins(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "ignore product names that are not exactly 'jenkins'",
|
||||
cpe: cpe.Must("cpe:2.3:a:*:jenkins-something-else:3.2:*:*:*:*:*:*:*"),
|
||||
cpe: cpe.Must("cpe:2.3:a:*:jenkins-something-else:3.2:*:*:*:*:*:*:*", cpe.GeneratedSource),
|
||||
pkg: pkg.Package{
|
||||
Name: "not-j*nkins",
|
||||
Type: pkg.JavaPkg,
|
||||
|
@ -103,7 +103,7 @@ func Test_disallowJenkinsCPEsNotAssociatedWithJenkins(t *testing.T) {
|
|||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, disallowJenkinsCPEsNotAssociatedWithJenkins(test.cpe, test.pkg))
|
||||
assert.Equal(t, test.expected, disallowJenkinsCPEsNotAssociatedWithJenkins(test.cpe.Attributes, test.pkg))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -117,7 +117,7 @@ func Test_disallowJiraClientServerMismatch(t *testing.T) {
|
|||
}{
|
||||
{
|
||||
name: "filter out mismatched name (atlassian vendor)",
|
||||
cpe: cpe.Must("cpe:2.3:a:atlassian:jira:3.2:*:*:*:*:*:*:*"),
|
||||
cpe: cpe.Must("cpe:2.3:a:atlassian:jira:3.2:*:*:*:*:*:*:*", cpe.GeneratedSource),
|
||||
pkg: pkg.Package{
|
||||
Name: "something-client",
|
||||
Type: pkg.JavaPkg,
|
||||
|
@ -126,7 +126,7 @@ func Test_disallowJiraClientServerMismatch(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "filter out mismatched name (jira vendor)",
|
||||
cpe: cpe.Must("cpe:2.3:a:jira:jira:3.2:*:*:*:*:*:*:*"),
|
||||
cpe: cpe.Must("cpe:2.3:a:jira:jira:3.2:*:*:*:*:*:*:*", cpe.GeneratedSource),
|
||||
pkg: pkg.Package{
|
||||
Name: "something-client",
|
||||
Type: pkg.JavaPkg,
|
||||
|
@ -135,7 +135,7 @@ func Test_disallowJiraClientServerMismatch(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "filter out mismatched name (any vendor)",
|
||||
cpe: cpe.Must("cpe:2.3:a:*:jira:3.2:*:*:*:*:*:*:*"),
|
||||
cpe: cpe.Must("cpe:2.3:a:*:jira:3.2:*:*:*:*:*:*:*", cpe.GeneratedSource),
|
||||
pkg: pkg.Package{
|
||||
Name: "something-client",
|
||||
Type: pkg.JavaPkg,
|
||||
|
@ -144,7 +144,7 @@ func Test_disallowJiraClientServerMismatch(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "ignore package names that do not have 'client'",
|
||||
cpe: cpe.Must("cpe:2.3:a:*:jira:3.2:*:*:*:*:*:*:*"),
|
||||
cpe: cpe.Must("cpe:2.3:a:*:jira:3.2:*:*:*:*:*:*:*", cpe.GeneratedSource),
|
||||
pkg: pkg.Package{
|
||||
Name: "jira-thing",
|
||||
Type: pkg.JavaPkg,
|
||||
|
@ -153,7 +153,7 @@ func Test_disallowJiraClientServerMismatch(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "ignore product names that are not exactly 'jira'",
|
||||
cpe: cpe.Must("cpe:2.3:a:*:jira-something-else:3.2:*:*:*:*:*:*:*"),
|
||||
cpe: cpe.Must("cpe:2.3:a:*:jira-something-else:3.2:*:*:*:*:*:*:*", cpe.GeneratedSource),
|
||||
pkg: pkg.Package{
|
||||
Name: "not-j*ra",
|
||||
Type: pkg.JavaPkg,
|
||||
|
@ -163,7 +163,7 @@ func Test_disallowJiraClientServerMismatch(t *testing.T) {
|
|||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, disallowJiraClientServerMismatch(test.cpe, test.pkg))
|
||||
assert.Equal(t, test.expected, disallowJiraClientServerMismatch(test.cpe.Attributes, test.pkg))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ import (
|
|||
// the CPE database, so they will be preferred over other candidates:
|
||||
var knownVendors = strset.New("apache")
|
||||
|
||||
func newCPE(product, vendor, version, targetSW string) *cpe.CPE {
|
||||
func newCPE(product, vendor, version, targetSW string) *cpe.Attributes {
|
||||
c := cpe.NewWithAny()
|
||||
c.Part = "a"
|
||||
c.Product = product
|
||||
|
@ -61,7 +61,7 @@ func GetIndexedDictionary() (_ *dictionary.Indexed, err error) {
|
|||
func FromDictionaryFind(p pkg.Package) (cpe.CPE, bool) {
|
||||
dict, err := GetIndexedDictionary()
|
||||
if err != nil {
|
||||
log.Debugf("dictionary CPE lookup not available: %+v", err)
|
||||
log.Debugf("CPE dictionary lookup not available: %+v", err)
|
||||
return cpe.CPE{}, false
|
||||
}
|
||||
|
||||
|
@ -96,12 +96,12 @@ func FromDictionaryFind(p pkg.Package) (cpe.CPE, bool) {
|
|||
return cpe.CPE{}, false
|
||||
}
|
||||
|
||||
parsedCPE, err := cpe.New(cpeString)
|
||||
parsedCPE, err := cpe.New(cpeString, cpe.NVDDictionaryLookupSource)
|
||||
if err != nil {
|
||||
return cpe.CPE{}, false
|
||||
}
|
||||
|
||||
parsedCPE.Version = p.Version
|
||||
parsedCPE.Attributes.Version = p.Version
|
||||
|
||||
return parsedCPE, true
|
||||
}
|
||||
|
@ -117,7 +117,7 @@ func FromPackageAttributes(p pkg.Package) []cpe.CPE {
|
|||
}
|
||||
|
||||
keys := strset.New()
|
||||
cpes := make([]cpe.CPE, 0)
|
||||
cpes := make([]cpe.Attributes, 0)
|
||||
for _, product := range products {
|
||||
for _, vendor := range vendors {
|
||||
// prevent duplicate entries...
|
||||
|
@ -137,8 +137,12 @@ func FromPackageAttributes(p pkg.Package) []cpe.CPE {
|
|||
cpes = filter(cpes, p, cpeFilters...)
|
||||
|
||||
sort.Sort(cpe.BySpecificity(cpes))
|
||||
var result []cpe.CPE
|
||||
for _, c := range cpes {
|
||||
result = append(result, cpe.CPE{Attributes: c, Source: cpe.GeneratedSource})
|
||||
}
|
||||
|
||||
return cpes
|
||||
return result
|
||||
}
|
||||
|
||||
func candidateVendors(p pkg.Package) []string {
|
||||
|
|
|
@ -723,11 +723,14 @@ func TestGeneratePackageCPEs(t *testing.T) {
|
|||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
actual := FromPackageAttributes(test.p)
|
||||
expectedCpeSet := set.NewStringSet()
|
||||
for _, cpeStr := range test.expected {
|
||||
expectedCpeSet.Add("syft-generated:" + cpeStr)
|
||||
}
|
||||
|
||||
expectedCpeSet := set.NewStringSet(test.expected...)
|
||||
actualCpeSet := set.NewStringSet()
|
||||
for _, a := range actual {
|
||||
actualCpeSet.Add(a.String())
|
||||
actualCpeSet.Add(fmt.Sprintf("%s:%s", a.Source.String(), a.Attributes.String()))
|
||||
}
|
||||
|
||||
extra := strset.Difference(actualCpeSet, expectedCpeSet).List()
|
||||
|
@ -1007,7 +1010,7 @@ func TestDictionaryFindIsWired(t *testing.T) {
|
|||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, gotExists := FromDictionaryFind(tt.pkg)
|
||||
|
||||
assert.Equal(t, tt.want, got.BindToFmtString())
|
||||
assert.Equal(t, tt.want, got.Attributes.BindToFmtString())
|
||||
assert.Equal(t, tt.wantExists, gotExists)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -117,9 +117,9 @@ func (c *nativeImageCataloger) Name() string {
|
|||
func getPackage(component nativeImageComponent) pkg.Package {
|
||||
var cpes []cpe.CPE
|
||||
for _, property := range component.Properties {
|
||||
c, err := cpe.New(property.Value)
|
||||
c, err := cpe.New(property.Value, cpe.DeclaredSource)
|
||||
if err != nil {
|
||||
log.Debugf("unable to parse CPE: %v", err)
|
||||
log.Debugf("unable to parse Attributes: %v", err)
|
||||
continue
|
||||
}
|
||||
cpes = append(cpes, c)
|
||||
|
|
|
@ -80,22 +80,31 @@ func TestParseNativeImageSbom(t *testing.T) {
|
|||
},
|
||||
CPEs: []cpe.CPE{
|
||||
{
|
||||
Part: "a",
|
||||
Vendor: "codec",
|
||||
Product: "codec",
|
||||
Version: "4.1.73.Final",
|
||||
Attributes: cpe.Attributes{
|
||||
Part: "a",
|
||||
Vendor: "codec",
|
||||
Product: "codec",
|
||||
Version: "4.1.73.Final",
|
||||
},
|
||||
Source: "declared",
|
||||
},
|
||||
{
|
||||
Part: "a",
|
||||
Vendor: "codec",
|
||||
Product: "netty-codec-http2",
|
||||
Version: "4.1.73.Final",
|
||||
Attributes: cpe.Attributes{
|
||||
Part: "a",
|
||||
Vendor: "codec",
|
||||
Product: "netty-codec-http2",
|
||||
Version: "4.1.73.Final",
|
||||
},
|
||||
Source: "declared",
|
||||
},
|
||||
{
|
||||
Part: "a",
|
||||
Vendor: "codec",
|
||||
Product: "netty_codec_http2",
|
||||
Version: "4.1.73.Final",
|
||||
Attributes: cpe.Attributes{
|
||||
Part: "a",
|
||||
Vendor: "codec",
|
||||
Product: "netty_codec_http2",
|
||||
Version: "4.1.73.Final",
|
||||
},
|
||||
Source: "declared",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -14,21 +14,11 @@ import (
|
|||
|
||||
func mustCPEs(s ...string) (c []cpe.CPE) {
|
||||
for _, i := range s {
|
||||
c = append(c, mustCPE(i))
|
||||
c = append(c, cpe.Must(i, ""))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func mustCPE(c string) cpe.CPE {
|
||||
return must(cpe.New(c))
|
||||
}
|
||||
func must(c cpe.CPE, e error) cpe.CPE {
|
||||
if e != nil {
|
||||
panic(e)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func Test_parseSBOM(t *testing.T) {
|
||||
expectedPkgs := []pkg.Package{
|
||||
{
|
||||
|
|
|
@ -368,7 +368,7 @@ func TestCatalog_MergeRecords(t *testing.T) {
|
|||
name: "multiple Locations with shared path",
|
||||
pkgs: []Package{
|
||||
{
|
||||
CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:package:1:1:*:*:*:*:*:*:*")},
|
||||
CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:package:1:1:*:*:*:*:*:*:*", cpe.GeneratedSource)},
|
||||
Locations: file.NewLocationSet(
|
||||
file.NewVirtualLocationFromCoordinates(
|
||||
file.Coordinates{
|
||||
|
@ -381,7 +381,7 @@ func TestCatalog_MergeRecords(t *testing.T) {
|
|||
Type: RpmPkg,
|
||||
},
|
||||
{
|
||||
CPEs: []cpe.CPE{cpe.Must("cpe:2.3:b:package:1:1:*:*:*:*:*:*:*")},
|
||||
CPEs: []cpe.CPE{cpe.Must("cpe:2.3:b:package:1:1:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource)},
|
||||
Locations: file.NewLocationSet(
|
||||
file.NewVirtualLocationFromCoordinates(
|
||||
file.Coordinates{
|
||||
|
|
|
@ -34,7 +34,7 @@ func TestIDUniqueness(t *testing.T) {
|
|||
Language: "math",
|
||||
Type: PythonPkg,
|
||||
CPEs: []cpe.CPE{
|
||||
cpe.Must(`cpe:2.3:a:Archimedes:pi:3.14:*:*:*:*:math:*:*`),
|
||||
cpe.Must(`cpe:2.3:a:Archimedes:pi:3.14:*:*:*:*:math:*:*`, cpe.NVDDictionaryLookupSource),
|
||||
},
|
||||
PURL: "pkg:pypi/pi@3.14",
|
||||
Metadata: PythonPackage{
|
||||
|
@ -256,7 +256,7 @@ func TestPackage_Merge(t *testing.T) {
|
|||
Language: "math",
|
||||
Type: PythonPkg,
|
||||
CPEs: []cpe.CPE{
|
||||
cpe.Must(`cpe:2.3:a:Archimedes:pi:3.14:*:*:*:*:math:*:*`),
|
||||
cpe.Must(`cpe:2.3:a:Archimedes:pi:3.14:*:*:*:*:math:*:*`, cpe.NVDDictionaryLookupSource),
|
||||
},
|
||||
PURL: "pkg:pypi/pi@3.14",
|
||||
Metadata: PythonPackage{
|
||||
|
@ -278,7 +278,7 @@ func TestPackage_Merge(t *testing.T) {
|
|||
Language: "math",
|
||||
Type: PythonPkg,
|
||||
CPEs: []cpe.CPE{
|
||||
cpe.Must(`cpe:2.3:a:DIFFERENT:pi:3.14:*:*:*:*:math:*:*`), // NOTE: difference
|
||||
cpe.Must(`cpe:2.3:a:DIFFERENT:pi:3.14:*:*:*:*:math:*:*`, cpe.NVDDictionaryLookupSource), // NOTE: difference
|
||||
},
|
||||
PURL: "pkg:pypi/pi@3.14",
|
||||
Metadata: PythonPackage{
|
||||
|
@ -301,8 +301,8 @@ func TestPackage_Merge(t *testing.T) {
|
|||
Language: "math",
|
||||
Type: PythonPkg,
|
||||
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!
|
||||
cpe.Must(`cpe:2.3:a:Archimedes:pi:3.14:*:*:*:*:math:*:*`, cpe.NVDDictionaryLookupSource),
|
||||
cpe.Must(`cpe:2.3:a:DIFFERENT:pi:3.14:*:*:*:*:math:*:*`, cpe.NVDDictionaryLookupSource), // NOTE: merge!
|
||||
},
|
||||
PURL: "pkg:pypi/pi@3.14",
|
||||
Metadata: PythonPackage{
|
||||
|
@ -327,7 +327,7 @@ func TestPackage_Merge(t *testing.T) {
|
|||
Language: "math",
|
||||
Type: PythonPkg,
|
||||
CPEs: []cpe.CPE{
|
||||
cpe.Must(`cpe:2.3:a:Archimedes:pi:3.14:*:*:*:*:math:*:*`),
|
||||
cpe.Must(`cpe:2.3:a:Archimedes:pi:3.14:*:*:*:*:math:*:*`, cpe.NVDDictionaryLookupSource),
|
||||
},
|
||||
PURL: "pkg:pypi/pi@3.14",
|
||||
Metadata: PythonPackage{
|
||||
|
@ -349,7 +349,7 @@ func TestPackage_Merge(t *testing.T) {
|
|||
Language: "math",
|
||||
Type: PythonPkg,
|
||||
CPEs: []cpe.CPE{
|
||||
cpe.Must(`cpe:2.3:a:Archimedes:pi:3.14:*:*:*:*:math:*:*`),
|
||||
cpe.Must(`cpe:2.3:a:Archimedes:pi:3.14:*:*:*:*:math:*:*`, cpe.NVDDictionaryLookupSource),
|
||||
},
|
||||
PURL: "pkg:pypi/pi@3.14",
|
||||
Metadata: PythonPackage{
|
||||
|
|
Loading…
Reference in a new issue