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