mirror of
https://github.com/anchore/syft.git
synced 2026-11-01 15:06:36 +01: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:
@@ -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)
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
+23
-16
@@ -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:*:*:*:*:*:*:*"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
+69
-30
@@ -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)
|
||||
|
||||
+26
-26
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
+17
-15
@@ -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),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user