syft/syft/cpe/cpe_test.go
William Murphy 878df69330
chore: stop re-exporting wfn.Attributes (#2534)
* chore: stop re-exporting wfn.Attributes

Previously, Syft re-exported wfn.Attributes from the nvdtools package as
a member of the Package struct. However, Syft doesn't own this struct,
and so after Syft 1.0, might be forced to bump a semver major version
due to a breaking change in wfn.Attributes. Rather than incur this risk
going into 1.0, instead replace Syft's use of wfn.Attributes with Syft's
own cpe.CPE type. That type has some pass-through calls to
wfn.Attributes, but hides the dependency from the rest of the
application.

Signed-off-by: Will Murphy <will.murphy@anchore.com>

* chore: make cpe.CPE type a Stringer

Previously, the cpe.CPE type was an alias for wfn.Attributes from
nvdtools. Now that it is a type we control, make the String method take
the CPE as a receiver, rather than as a normal parameter, so that Syft's
cpe.CPE type implements Stringer.

Signed-off-by: Will Murphy <will.murphy@anchore.com>

---------

Signed-off-by: Will Murphy <will.murphy@anchore.com>
2024-01-24 08:59:03 -05:00

229 lines
5.7 KiB
Go

package cpe
import (
"encoding/json"
"fmt"
"github.com/google/go-cmp/cmp"
"os"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_New(t *testing.T) {
tests := []struct {
name string
input string
expected CPE
}{
{
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:*:*`),
},
{
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:*:*`),
},
{
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:*:*`),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actual, err := New(test.input)
if err != nil {
t.Fatalf("got an error while creating CPE: %+v", err)
}
if d := cmp.Diff(actual, test.expected); d != "" {
t.Errorf("CPE mismatch (-want +got):\n%s", d)
}
})
}
}
func Test_normalizeCpeField(t *testing.T) {
tests := []struct {
field string
expected string
}{
{
field: "something",
expected: "something",
},
{
field: "some\\thing",
expected: `some\thing`,
},
{
field: "*",
expected: "",
},
{
field: "",
expected: "",
},
}
for _, test := range tests {
t.Run(test.field, func(t *testing.T) {
assert.Equal(t, test.expected, normalizeField(test.field))
})
}
}
func Test_CPEParser(t *testing.T) {
var testCases []struct {
CPEString string `json:"cpe-string"`
CPEUrl string `json:"cpe-url"`
WFN CPE `json:"wfn"`
}
out, err := os.ReadFile("test-fixtures/cpe-data.json")
require.NoError(t, err)
require.NoError(t, json.Unmarshal(out, &testCases))
for _, test := range testCases {
t.Run(test.CPEString, func(t *testing.T) {
c1, err := New(test.CPEString)
assert.NoError(t, err)
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, test.WFN.String(), test.CPEString)
})
}
}
func Test_InvalidCPE(t *testing.T) {
type testcase struct {
name string
in string
expected string
expectedErr bool
}
tests := []testcase{
{
// 5.3.2: The underscore (x5f) MAY be used, and it SHOULD be used in place of whitespace characters (which SHALL NOT be used)
name: "translates spaces",
in: "cpe:2.3:a:some-vendor:name:1 2:*:*:*:*:*:*:*",
expected: "cpe:2.3:a:some-vendor:name:1_2:*:*:*:*:*:*:*",
},
{
// it isn't easily possible in the string formatted string to detect improper escaping of : (it will fail parsing)
name: "unescaped ':' cannot be helped -- too many fields",
in: "cpe:2.3:a:some-vendor:name:::*:*:*:*:*:*:*",
expectedErr: true,
},
{
name: "too few fields",
in: "cpe:2.3:a:some-vendor:name:*:*:*:*:*:*:*",
expected: "cpe:2.3:a:some-vendor:name:*:*:*:*:*:*:*:*",
},
// Note: though the CPE spec does not allow for ? and * as escaped character input, these seem to be allowed in
// the NVD CPE validator for this reason these edge cases were removed
}
// the wfn library does not account for escapes of . and -
exceptions := ".-"
// it isn't easily possible in the string formatted string to detect improper escaping of : (it will fail parsing)
skip := ":"
// make escape exceptions for section 5.3.2 of the CPE spec (2.3)
for _, char := range allowedCPEPunctuation {
if strings.Contains(skip, string(char)) {
continue
}
in := fmt.Sprintf("cpe:2.3:a:some-vendor:name:*:%s:*:*:*:*:*:*", string(char))
exp := fmt.Sprintf(`cpe:2.3:a:some-vendor:name:*:\%s:*:*:*:*:*:*`, string(char))
if strings.Contains(exceptions, string(char)) {
exp = in
}
tests = append(tests, testcase{
name: fmt.Sprintf("allowes future escape of character (%s)", string(char)),
in: in,
expected: exp,
expectedErr: false,
})
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
c, err := New(test.in)
if test.expectedErr {
assert.Error(t, err)
if t.Failed() {
t.Logf("got CPE: %q details: %+v", c, c)
}
return
}
require.NoError(t, err)
assert.Equal(t, test.expected, c.String())
})
}
}
func Test_RoundTrip(t *testing.T) {
tests := []struct {
name string
cpe string
parsedCPE CPE
}{
{
name: "normal",
cpe: "cpe:2.3:a:some-vendor:name:3.2:*:*:*:*:*:*:*",
parsedCPE: CPE{
Part: "a",
Vendor: "some-vendor",
Product: "name",
Version: "3.2",
},
},
{
name: "escaped colon",
cpe: "cpe:2.3:a:some-vendor:name:1\\:3.2:*:*:*:*:*:*:*",
parsedCPE: CPE{
Part: "a",
Vendor: "some-vendor",
Product: "name",
Version: "1:3.2",
},
},
{
name: "escaped forward slash",
cpe: "cpe:2.3:a:test\\/some-vendor:name:3.2:*:*:*:*:*:*:*",
parsedCPE: CPE{
Part: "a",
Vendor: "test/some-vendor",
Product: "name",
Version: "3.2",
},
},
}
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
assert.Equal(t, test.parsedCPE.String(), test.cpe)
})
}
}