github.com/nextlinux/gosbom@v0.81.1-0.20230627115839-1ff50c281391/gosbom/cpe/cpe_test.go (about) 1 package cpe 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "os" 7 "strings" 8 "testing" 9 10 "github.com/stretchr/testify/assert" 11 "github.com/stretchr/testify/require" 12 ) 13 14 func Test_New(t *testing.T) { 15 tests := []struct { 16 name string 17 input string 18 expected CPE 19 }{ 20 { 21 name: "gocase", 22 input: `cpe:/a:10web:form_maker:1.0.0::~~~wordpress~~`, 23 expected: Must(`cpe:2.3:a:10web:form_maker:1.0.0:*:*:*:*:wordpress:*:*`), 24 }, 25 { 26 name: "dashes", 27 input: `cpe:/a:7-zip:7-zip:4.56:beta:~~~windows~~`, 28 expected: Must(`cpe:2.3:a:7-zip:7-zip:4.56:beta:*:*:*:windows:*:*`), 29 }, 30 { 31 name: "URL escape characters", 32 input: `cpe:/a:%240.99_kindle_books_project:%240.99_kindle_books:6::~~~android~~`, 33 expected: Must(`cpe:2.3:a:\$0.99_kindle_books_project:\$0.99_kindle_books:6:*:*:*:*:android:*:*`), 34 }, 35 } 36 37 for _, test := range tests { 38 t.Run(test.name, func(t *testing.T) { 39 actual, err := New(test.input) 40 if err != nil { 41 t.Fatalf("got an error while creating CPE: %+v", err) 42 } 43 44 if String(actual) != String(test.expected) { 45 t.Errorf("mismatched entries:\n\texpected:%+v\n\t actual:%+v\n", String(test.expected), String(actual)) 46 } 47 48 }) 49 } 50 } 51 52 func Test_normalizeCpeField(t *testing.T) { 53 54 tests := []struct { 55 field string 56 expected string 57 }{ 58 { 59 field: "something", 60 expected: "something", 61 }, 62 { 63 field: "some\\thing", 64 expected: `some\thing`, 65 }, 66 { 67 field: "*", 68 expected: "", 69 }, 70 { 71 field: "", 72 expected: "", 73 }, 74 } 75 for _, test := range tests { 76 t.Run(test.field, func(t *testing.T) { 77 assert.Equal(t, test.expected, normalizeField(test.field)) 78 }) 79 } 80 } 81 82 func Test_CPEParser(t *testing.T) { 83 var testCases []struct { 84 CPEString string `json:"cpe-string"` 85 CPEUrl string `json:"cpe-url"` 86 WFN CPE `json:"wfn"` 87 } 88 out, err := os.ReadFile("test-fixtures/cpe-data.json") 89 require.NoError(t, err) 90 require.NoError(t, json.Unmarshal(out, &testCases)) 91 92 for _, test := range testCases { 93 t.Run(test.CPEString, func(t *testing.T) { 94 c1, err := New(test.CPEString) 95 assert.NoError(t, err) 96 c2, err := New(test.CPEUrl) 97 assert.NoError(t, err) 98 assert.Equal(t, c1, c2) 99 assert.Equal(t, c1, test.WFN) 100 assert.Equal(t, c2, test.WFN) 101 assert.Equal(t, String(test.WFN), test.CPEString) 102 }) 103 } 104 } 105 106 func Test_InvalidCPE(t *testing.T) { 107 type testcase struct { 108 name string 109 in string 110 expected string 111 expectedErr bool 112 } 113 114 tests := []testcase{ 115 { 116 // 5.3.2: The underscore (x5f) MAY be used, and it SHOULD be used in place of whitespace characters (which SHALL NOT be used) 117 name: "translates spaces", 118 in: "cpe:2.3:a:some-vendor:name:1 2:*:*:*:*:*:*:*", 119 expected: "cpe:2.3:a:some-vendor:name:1_2:*:*:*:*:*:*:*", 120 }, 121 { 122 // it isn't easily possible in the string formatted string to detect improper escaping of : (it will fail parsing) 123 name: "unescaped ':' cannot be helped -- too many fields", 124 in: "cpe:2.3:a:some-vendor:name:::*:*:*:*:*:*:*", 125 expectedErr: true, 126 }, 127 { 128 name: "too few fields", 129 in: "cpe:2.3:a:some-vendor:name:*:*:*:*:*:*:*", 130 expected: "cpe:2.3:a:some-vendor:name:*:*:*:*:*:*:*:*", 131 }, 132 // Note: though the CPE spec does not allow for ? and * as escaped character input, these seem to be allowed in 133 // the NVD CPE validator for this reason these edge cases were removed 134 } 135 136 // the wfn library does not account for escapes of . and - 137 exceptions := ".-" 138 // it isn't easily possible in the string formatted string to detect improper escaping of : (it will fail parsing) 139 skip := ":" 140 141 // make escape exceptions for section 5.3.2 of the CPE spec (2.3) 142 for _, char := range allowedCPEPunctuation { 143 if strings.Contains(skip, string(char)) { 144 continue 145 } 146 147 in := fmt.Sprintf("cpe:2.3:a:some-vendor:name:*:%s:*:*:*:*:*:*", string(char)) 148 exp := fmt.Sprintf(`cpe:2.3:a:some-vendor:name:*:\%s:*:*:*:*:*:*`, string(char)) 149 if strings.Contains(exceptions, string(char)) { 150 exp = in 151 } 152 153 tests = append(tests, testcase{ 154 name: fmt.Sprintf("allowes future escape of character (%s)", string(char)), 155 in: in, 156 expected: exp, 157 expectedErr: false, 158 }) 159 } 160 161 for _, test := range tests { 162 t.Run(test.name, func(t *testing.T) { 163 c, err := New(test.in) 164 if test.expectedErr { 165 assert.Error(t, err) 166 if t.Failed() { 167 t.Logf("got CPE: %q details: %+v", String(c), c) 168 } 169 return 170 } 171 require.NoError(t, err) 172 assert.Equal(t, test.expected, String(c)) 173 }) 174 } 175 } 176 177 func Test_RoundTrip(t *testing.T) { 178 tests := []struct { 179 name string 180 cpe string 181 parsedCPE CPE 182 }{ 183 { 184 name: "normal", 185 cpe: "cpe:2.3:a:some-vendor:name:3.2:*:*:*:*:*:*:*", 186 parsedCPE: CPE{ 187 Part: "a", 188 Vendor: "some-vendor", 189 Product: "name", 190 Version: "3.2", 191 }, 192 }, 193 { 194 name: "escaped colon", 195 cpe: "cpe:2.3:a:some-vendor:name:1\\:3.2:*:*:*:*:*:*:*", 196 parsedCPE: CPE{ 197 Part: "a", 198 Vendor: "some-vendor", 199 Product: "name", 200 Version: "1:3.2", 201 }, 202 }, 203 { 204 name: "escaped forward slash", 205 cpe: "cpe:2.3:a:test\\/some-vendor:name:3.2:*:*:*:*:*:*:*", 206 parsedCPE: CPE{ 207 Part: "a", 208 Vendor: "test/some-vendor", 209 Product: "name", 210 Version: "3.2", 211 }, 212 }, 213 } 214 215 for _, test := range tests { 216 t.Run(test.name, func(t *testing.T) { 217 // CPE string must be preserved through a round trip 218 assert.Equal(t, test.cpe, String(Must(test.cpe))) 219 // The parsed CPE must be the same after a round trip 220 assert.Equal(t, Must(test.cpe), Must(String(Must(test.cpe)))) 221 // The test case parsed CPE must be the same after parsing the input string 222 assert.Equal(t, test.parsedCPE, Must(test.cpe)) 223 // The test case parsed CPE must produce the same string as the input cpe 224 assert.Equal(t, String(test.parsedCPE), test.cpe) 225 }) 226 } 227 }