Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: refactor basic CPE functionality to its own package #1436

Merged
merged 2 commits into from
Jan 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions syft/pkg/cpe_by_specificity.go → syft/cpe/by_specificity.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
package pkg
package cpe

import (
"sort"

"github.com/facebookincubator/nvdtools/wfn"
)

var _ sort.Interface = (*CPEBySpecificity)(nil)
var _ sort.Interface = (*BySpecificity)(nil)

type CPEBySpecificity []wfn.Attributes
type BySpecificity []wfn.Attributes

func (c CPEBySpecificity) Len() int { return len(c) }
func (c BySpecificity) Len() int { return len(c) }

func (c CPEBySpecificity) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
func (c BySpecificity) Swap(i, j int) { c[i], c[j] = c[j], c[i] }

func (c CPEBySpecificity) Less(i, j int) bool {
func (c BySpecificity) Less(i, j int) bool {
iScore := weightedCountForSpecifiedFields(c[i])
jScore := weightedCountForSpecifiedFields(c[j])

Expand All @@ -29,7 +29,7 @@ func (c CPEBySpecificity) Less(i, j int) bool {
}

// if score and length are equal then text sort
// note that we are not using CPEString from the syft pkg
// note that we are not using String from the syft pkg
// as we are not encoding/decoding this CPE string so we don't
// need the proper quoted version of the CPE.
return c[i].BindToFmtString() < c[j].BindToFmtString()
Expand Down
98 changes: 98 additions & 0 deletions syft/cpe/by_specificity_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package cpe

import (
"sort"
"testing"

"github.com/stretchr/testify/assert"
)

func Test_BySpecificity(t *testing.T) {
tests := []struct {
name string
input []CPE
expected []CPE
}{
{
name: "sort strictly by wfn *",
input: []CPE{
Must("cpe:2.3:a:*:package:1:*:*:*:*:*:*:*"),
Must("cpe:2.3:a:some:package:1:*:*:*:*:*:*:*"),
Must("cpe:2.3:a:*:package:1:*:*:*:*:some:*:*"),
Must("cpe:2.3:a:some:package:1:*:*:*:*:some:*:*"),
Must("cpe:2.3:a:some:package:*:*:*:*:*:*:*:*"),
},
expected: []CPE{
Must("cpe:2.3:a:some:package:1:*:*:*:*:some:*:*"),
Must("cpe:2.3:a:some:package:1:*:*:*:*:*:*:*"),
Must("cpe:2.3:a:some:package:*:*:*:*:*:*:*:*"),
Must("cpe:2.3:a:*:package:1:*:*:*:*:some:*:*"),
Must("cpe:2.3:a:*:package:1:*:*:*:*:*:*:*"),
},
},
{
name: "sort strictly by field length",
input: []CPE{
Must("cpe:2.3:a:1:22:1:*:*:*:*:1:*:*"),
Must("cpe:2.3:a:55555:1:1:*:*:*:*:1:*:*"),
Must("cpe:2.3:a:1:1:333:*:*:*:*:1:*:*"),
Must("cpe:2.3:a:1:666666:1:*:*:*:*:1:*:*"),
Must("cpe:2.3:a:1:1:1:*:*:*:*:1:*:*"),
Must("cpe:2.3:a:1:1:1:*:*:*:*:4444:*:*"),
},
expected: []CPE{
Must("cpe:2.3:a:1:666666:1:*:*:*:*:1:*:*"),
Must("cpe:2.3:a:55555:1:1:*:*:*:*:1:*:*"),
Must("cpe:2.3:a:1:1:1:*:*:*:*:4444:*:*"),
Must("cpe:2.3:a:1:1:333:*:*:*:*:1:*:*"),
Must("cpe:2.3:a:1:22:1:*:*:*:*:1:*:*"),
Must("cpe:2.3:a:1:1:1:*:*:*:*:1:*:*"),
},
},
{
name: "sort by mix of field length and specificity",
input: []CPE{
Must("cpe:2.3:a:1:666666:*:*:*:*:*:1:*:*"),
Must("cpe:2.3:a:*:1:1:*:*:*:*:4444:*:*"),
Must("cpe:2.3:a:1:*:333:*:*:*:*:*:*:*"),
Must("cpe:2.3:a:1:1:1:*:*:*:*:1:*:*"),
Must("cpe:2.3:a:1:22:1:*:*:*:*:1:*:*"),
Must("cpe:2.3:a:55555:1:1:*:*:*:*:1:*:*"),
},
expected: []CPE{
Must("cpe:2.3:a:55555:1:1:*:*:*:*:1:*:*"),
Must("cpe:2.3:a:1:22:1:*:*:*:*:1:*:*"),
Must("cpe:2.3:a:1:1:1:*:*:*:*:1:*:*"),
Must("cpe:2.3:a:1:666666:*:*:*:*:*:1:*:*"),
Must("cpe:2.3:a:*:1:1:*:*:*:*:4444:*:*"),
Must("cpe:2.3:a:1:*:333:*:*:*:*:*:*:*"),
},
},
{
name: "sort by mix of field length, specificity, dash",
input: []CPE{
Must("cpe:2.3:a:alpine:alpine_keys:2.3-r1:*:*:*:*:*:*:*"),
Must("cpe:2.3:a:alpine_keys:alpine_keys:2.3-r1:*:*:*:*:*:*:*"),
Must("cpe:2.3:a:alpine-keys:alpine_keys:2.3-r1:*:*:*:*:*:*:*"),
Must("cpe:2.3:a:alpine:alpine-keys:2.3-r1:*:*:*:*:*:*:*"),
Must("cpe:2.3:a:alpine-keys:alpine-keys:2.3-r1:*:*:*:*:*:*:*"),
Must("cpe:2.3:a:alpine_keys:alpine-keys:2.3-r1:*:*:*:*:*:*:*"),
},
expected: []CPE{
Must("cpe:2.3:a:alpine-keys:alpine-keys:2.3-r1:*:*:*:*:*:*:*"),
Must("cpe:2.3:a:alpine-keys:alpine_keys:2.3-r1:*:*:*:*:*:*:*"),
Must("cpe:2.3:a:alpine_keys:alpine-keys:2.3-r1:*:*:*:*:*:*:*"),
Must("cpe:2.3:a:alpine_keys:alpine_keys:2.3-r1:*:*:*:*:*:*:*"),
Must("cpe:2.3:a:alpine:alpine-keys:2.3-r1:*:*:*:*:*:*:*"),
Must("cpe:2.3:a:alpine:alpine_keys:2.3-r1:*:*:*:*:*:*:*"),
},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
sort.Sort(BySpecificity(test.input))
assert.Equal(t, test.expected, test.input)
})
}
}
57 changes: 29 additions & 28 deletions syft/pkg/cpe.go → syft/cpe/cpe.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package pkg
package cpe

import (
"fmt"
Expand All @@ -24,17 +24,17 @@ const cpeRegexString = ((`^([c][pP][eE]:/[AHOaho]?(:[A-Za-z0-9\._\-~%]*){0,6})`)

var cpeRegex = regexp.MustCompile(cpeRegexString)

// NewCPE will parse a formatted CPE string and return a CPE object. Some input, such as the existence of whitespace
// New will parse a formatted CPE string and return a CPE object. Some input, such as the existence of whitespace
// characters is allowed, however, a more strict validation is done after this sanitization process.
func NewCPE(cpeStr string) (CPE, error) {
func New(cpeStr string) (CPE, error) {
// get a CPE object based on the given string --don't validate yet since it may be possible to escape select cases on the callers behalf
c, err := newCPEWithoutValidation(cpeStr)
c, err := newWithoutValidation(cpeStr)
if err != nil {
return CPE{}, fmt.Errorf("unable to parse CPE string: %w", err)
}

// ensure that this CPE can be validated after being fully sanitized
if ValidateCPEString(CPEString(c)) != nil {
if ValidateString(String(c)) != nil {
return CPE{}, err
}

Expand All @@ -43,7 +43,16 @@ func NewCPE(cpeStr string) (CPE, error) {
return c, nil
}

func ValidateCPEString(cpeStr string) error {
// Must returns a CPE or panics if the provided string is not valid
func Must(cpeStr string) CPE {
c, err := New(cpeStr)
if err != nil {
panic(err)
}
return c
}

func ValidateString(cpeStr string) error {
// We should filter out all CPEs that do not match the official CPE regex
// The facebook nvdtools parser can sometimes incorrectly parse invalid CPE strings
if !cpeRegex.MatchString(cpeStr) {
Expand All @@ -52,7 +61,7 @@ func ValidateCPEString(cpeStr string) error {
return nil
}

func newCPEWithoutValidation(cpeStr string) (CPE, error) {
func newWithoutValidation(cpeStr string) (CPE, error) {
value, err := wfn.Parse(cpeStr)
if err != nil {
return CPE{}, fmt.Errorf("failed to parse CPE=%q: %w", cpeStr, err)
Expand All @@ -63,30 +72,22 @@ func newCPEWithoutValidation(cpeStr string) (CPE, error) {
}

// we need to compare the raw data since we are constructing CPEs in other locations
value.Vendor = normalizeCpeField(value.Vendor)
value.Product = normalizeCpeField(value.Product)
value.Language = normalizeCpeField(value.Language)
value.Version = normalizeCpeField(value.Version)
value.TargetSW = normalizeCpeField(value.TargetSW)
value.Part = normalizeCpeField(value.Part)
value.Edition = normalizeCpeField(value.Edition)
value.Other = normalizeCpeField(value.Other)
value.SWEdition = normalizeCpeField(value.SWEdition)
value.TargetHW = normalizeCpeField(value.TargetHW)
value.Update = normalizeCpeField(value.Update)
value.Vendor = normalizeField(value.Vendor)
value.Product = normalizeField(value.Product)
value.Language = normalizeField(value.Language)
value.Version = normalizeField(value.Version)
value.TargetSW = normalizeField(value.TargetSW)
value.Part = normalizeField(value.Part)
value.Edition = normalizeField(value.Edition)
value.Other = normalizeField(value.Other)
value.SWEdition = normalizeField(value.SWEdition)
value.TargetHW = normalizeField(value.TargetHW)
value.Update = normalizeField(value.Update)

return *value, nil
}

func MustCPE(cpeStr string) CPE {
c, err := NewCPE(cpeStr)
if err != nil {
panic(err)
}
return c
}

func normalizeCpeField(field string) string {
func normalizeField(field string) string {
// replace spaces with underscores (per section 5.3.2 of the CPE spec v 2.3)
field = strings.ReplaceAll(field, " ", "_")

Expand All @@ -112,7 +113,7 @@ func stripSlashes(s string) string {
return sb.String()
}

func CPEString(c CPE) string {
func String(c CPE) string {
output := CPE{}
output.Vendor = sanitize(c.Vendor)
output.Product = sanitize(c.Product)
Expand Down
45 changes: 19 additions & 26 deletions syft/pkg/cpe_test.go → syft/cpe/cpe_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package pkg
package cpe

import (
"encoding/json"
Expand All @@ -11,14 +11,7 @@ import (
"github.com/stretchr/testify/require"
)

func must(c CPE, e error) CPE {
if e != nil {
panic(e)
}
return c
}

func TestNewCPE(t *testing.T) {
func Test_New(t *testing.T) {
tests := []struct {
name string
input string
Expand All @@ -27,29 +20,29 @@ func TestNewCPE(t *testing.T) {
{
name: "gocase",
input: `cpe:/a:10web:form_maker:1.0.0::~~~wordpress~~`,
expected: must(NewCPE(`cpe:2.3:a:10web:form_maker:1.0.0:*:*:*:*:wordpress:*:*`)),
expected: Must(`cpe:2.3:a:10web:form_maker:1.0.0:*:*:*:*:wordpress:*:*`),
},
{
name: "dashes",
input: `cpe:/a:7-zip:7-zip:4.56:beta:~~~windows~~`,
expected: must(NewCPE(`cpe:2.3:a:7-zip:7-zip:4.56:beta:*:*:*:windows:*:*`)),
expected: Must(`cpe:2.3:a:7-zip:7-zip:4.56:beta:*:*:*:windows:*:*`),
},
{
name: "URL escape characters",
input: `cpe:/a:%240.99_kindle_books_project:%240.99_kindle_books:6::~~~android~~`,
expected: must(NewCPE(`cpe:2.3:a:\$0.99_kindle_books_project:\$0.99_kindle_books:6:*:*:*:*:android:*:*`)),
expected: Must(`cpe:2.3:a:\$0.99_kindle_books_project:\$0.99_kindle_books:6:*:*:*:*:android:*:*`),
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actual, err := NewCPE(test.input)
actual, err := New(test.input)
if err != nil {
t.Fatalf("got an error while creating CPE: %+v", err)
}

if CPEString(actual) != CPEString(test.expected) {
t.Errorf("mismatched entries:\n\texpected:%+v\n\t actual:%+v\n", CPEString(test.expected), CPEString(actual))
if String(actual) != String(test.expected) {
t.Errorf("mismatched entries:\n\texpected:%+v\n\t actual:%+v\n", String(test.expected), String(actual))
}

})
Expand Down Expand Up @@ -81,7 +74,7 @@ func Test_normalizeCpeField(t *testing.T) {
}
for _, test := range tests {
t.Run(test.field, func(t *testing.T) {
assert.Equal(t, test.expected, normalizeCpeField(test.field))
assert.Equal(t, test.expected, normalizeField(test.field))
})
}
}
Expand All @@ -98,14 +91,14 @@ func Test_CPEParser(t *testing.T) {

for _, test := range testCases {
t.Run(test.CPEString, func(t *testing.T) {
c1, err := NewCPE(test.CPEString)
c1, err := New(test.CPEString)
assert.NoError(t, err)
c2, err := NewCPE(test.CPEUrl)
c2, err := New(test.CPEUrl)
assert.NoError(t, err)
assert.Equal(t, c1, c2)
assert.Equal(t, c1, test.WFN)
assert.Equal(t, c2, test.WFN)
assert.Equal(t, CPEString(test.WFN), test.CPEString)
assert.Equal(t, String(test.WFN), test.CPEString)
})
}
}
Expand Down Expand Up @@ -167,16 +160,16 @@ func Test_InvalidCPE(t *testing.T) {

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
c, err := NewCPE(test.in)
c, err := New(test.in)
if test.expectedErr {
assert.Error(t, err)
if t.Failed() {
t.Logf("got CPE: %q details: %+v", CPEString(c), c)
t.Logf("got CPE: %q details: %+v", String(c), c)
}
return
}
require.NoError(t, err)
assert.Equal(t, test.expected, CPEString(c))
assert.Equal(t, test.expected, String(c))
})
}
}
Expand Down Expand Up @@ -222,13 +215,13 @@ func Test_RoundTrip(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// CPE string must be preserved through a round trip
assert.Equal(t, test.cpe, CPEString(MustCPE(test.cpe)))
assert.Equal(t, test.cpe, String(Must(test.cpe)))
// The parsed CPE must be the same after a round trip
assert.Equal(t, MustCPE(test.cpe), MustCPE(CPEString(MustCPE(test.cpe))))
assert.Equal(t, Must(test.cpe), Must(String(Must(test.cpe))))
// The test case parsed CPE must be the same after parsing the input string
assert.Equal(t, test.parsedCPE, MustCPE(test.cpe))
assert.Equal(t, test.parsedCPE, Must(test.cpe))
// The test case parsed CPE must produce the same string as the input cpe
assert.Equal(t, CPEString(test.parsedCPE), test.cpe)
assert.Equal(t, String(test.parsedCPE), test.cpe)
})
}
}
6 changes: 3 additions & 3 deletions syft/pkg/merge_cpes.go → syft/cpe/merge_cpes.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package pkg
package cpe

import (
"sort"
)

func mergeCPEs(a, b []CPE) (result []CPE) {
func Merge(a, b []CPE) (result []CPE) {
aCPEs := make(map[string]CPE)

// keep all CPEs from a and create a quick string-based lookup
Expand All @@ -20,6 +20,6 @@ func mergeCPEs(a, b []CPE) (result []CPE) {
}
}

sort.Sort(CPEBySpecificity(result))
sort.Sort(BySpecificity(result))
return result
}
Loading