package cpe import ( "encoding/json" "fmt" "io/ioutil" "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 String(actual) != String(test.expected) { t.Errorf("mismatched entries:\n\texpected:%+v\n\t actual:%+v\n", String(test.expected), String(actual)) } }) } } 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) { testCases := []struct { CPEString string `json:"cpe-string"` CPEUrl string `json:"cpe-url"` WFN CPE `json:"wfn"` }{} out, err := ioutil.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, String(test.WFN), 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", String(c), c) } return } require.NoError(t, err) assert.Equal(t, test.expected, String(c)) }) } } 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, String(Must(test.cpe))) // The parsed CPE must be the same after a round trip 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, Must(test.cpe)) // The test case parsed CPE must produce the same string as the input cpe assert.Equal(t, String(test.parsedCPE), test.cpe) }) } }