Skip to content

Commit 55832c2

Browse files
authored
Merge pull request #49 from puerco/match-functions
Identifier matching functions
2 parents 0eda627 + 2d52680 commit 55832c2

File tree

10 files changed

+501
-26
lines changed

10 files changed

+501
-26
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ require (
2323
)
2424

2525
require (
26+
github.com/package-url/packageurl-go v0.1.1
2627
github.com/shibumi/go-pathspec v1.3.0 // indirect
2728
github.com/stretchr/testify v1.8.4
2829
golang.org/x/sys v0.8.0 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
2121
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
2222
github.com/owenrumney/go-sarif v1.1.1 h1:QNObu6YX1igyFKhdzd7vgzmw7XsWN3/6NMGuDzBgXmE=
2323
github.com/owenrumney/go-sarif v1.1.1/go.mod h1:dNDiPlF04ESR/6fHlPyq7gHKmrM0sHUvAGjsoh8ZH0U=
24+
github.com/package-url/packageurl-go v0.1.1 h1:KTRE0bK3sKbFKAk3yy63DpeskU7Cvs/x/Da5l+RtzyU=
25+
github.com/package-url/packageurl-go v0.1.1/go.mod h1:uQd4a7Rh3ZsVg5j0lNyAfyxIeGde9yrlhjF78GzeW0c=
2426
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
2527
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
2628
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=

pkg/vex/component.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
Copyright 2023 The OpenVEX Authors
3+
SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package vex
7+
8+
import "strings"
9+
10+
// Component abstracts the common construct shared by product and subcomponents
11+
// allowing OpenVEX statements to point to a piece of software by referencing it
12+
// by hash or identifier.
13+
//
14+
// The ID should be an IRI uniquely identifying the product. Software can be
15+
// referenced as a VEX product or subcomponent using only its IRI or it may be
16+
// referenced by its crptographic hashes and/or other identifiers but, in no case,
17+
// must an IRI describe two different pieces of software or used to describe
18+
// a range of software.
19+
type Component struct {
20+
// ID is an IRI identifying the component. It is optional as the component
21+
// can also be identified using hashes or software identifiers.
22+
ID string `json:"@id,omitempty"`
23+
24+
// Hashes is a map of hashes to identify the component using cryptographic
25+
// hashes.
26+
Hashes map[Algorithm]Hash `json:"hashes,omitempty"`
27+
28+
// Identifiers is a list of software identifiers that describe the component.
29+
Identifiers map[IdentifierType]string `json:"identifiers,omitempty"`
30+
31+
// Supplier is an optional machine-readable identifier for the supplier of
32+
// the component. Valid examples include email address or IRIs.
33+
Supplier string `json:"supplier,omitempty"`
34+
}
35+
36+
// Matches returns true if one of the components identifiers match a string.
37+
// All types except purl are checked string vs string. Purls are a special
38+
// case and can match from more generic to more specific.
39+
// Note that a future iterarion of this function will treat CPEs in the same
40+
// way.
41+
func (c *Component) Matches(identifier string) bool {
42+
// If we have an exact match in the ID, match
43+
if c.ID == identifier && c.ID != "" {
44+
return true
45+
} else if strings.HasPrefix(c.ID, "pkg:") {
46+
// ... but the identifier can be a purl. If it is, then do
47+
// a purl comparison:
48+
if PurlMatches(c.ID, identifier) {
49+
return true
50+
}
51+
}
52+
53+
for t, id := range c.Identifiers {
54+
if id == identifier {
55+
return true
56+
}
57+
58+
if t == PURL && strings.HasPrefix(identifier, "pkg:") {
59+
if PurlMatches(id, identifier) {
60+
return true
61+
}
62+
}
63+
}
64+
65+
for _, hashVal := range c.Hashes {
66+
if hashVal == Hash(identifier) {
67+
return true
68+
}
69+
}
70+
71+
return false
72+
}

pkg/vex/component_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
Copyright 2023 The OpenVEX Authors
3+
SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package vex
7+
8+
import (
9+
"fmt"
10+
"testing"
11+
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
func TestComponentMatches(t *testing.T) {
16+
for testCase, tc := range map[string]struct {
17+
identifier string
18+
component *Component
19+
mustMatch bool
20+
}{
21+
"iri": {
22+
"https://example.com/document.spdx.json#node",
23+
&Component{ID: "https://example.com/document.spdx.json#node"},
24+
true,
25+
},
26+
"misc identifier": {
27+
"madeup-2023-12345",
28+
&Component{
29+
Identifiers: map[IdentifierType]string{"customIdentifier": "madeup-2023-12345"},
30+
},
31+
true,
32+
},
33+
"wrong misc identifier": {
34+
"madeup-2023-12345",
35+
&Component{
36+
Identifiers: map[IdentifierType]string{"customIdentifier": "another-string"},
37+
},
38+
false,
39+
},
40+
"same purl": {
41+
"pkg:apk/wolfi/[email protected]?arch=x86_64",
42+
&Component{
43+
Identifiers: map[IdentifierType]string{PURL: "pkg:apk/wolfi/[email protected]?arch=x86_64"},
44+
},
45+
true,
46+
},
47+
"globing purl": {
48+
"pkg:oci/curl@sha256:47fed8868b46b060efb8699dc40e981a0c785650223e03602d8c4493fc75b68c",
49+
&Component{
50+
Identifiers: map[IdentifierType]string{PURL: "pkg:oci/curl"},
51+
},
52+
true,
53+
},
54+
"globing purl (inverse)": {
55+
"pkg:oci/curl",
56+
&Component{
57+
Identifiers: map[IdentifierType]string{
58+
PURL: "pkg:oci/curl@sha256:47fed8868b46b060efb8699dc40e981a0c785650223e03602d8c4493fc75b68c",
59+
},
60+
},
61+
false,
62+
},
63+
"hash": {
64+
"77d86e9752cb933569dfa1f693ee4338e65b28b4",
65+
&Component{
66+
Hashes: map[Algorithm]Hash{
67+
SHA1: "77d86e9752cb933569dfa1f693ee4338e65b28b4",
68+
},
69+
},
70+
true,
71+
},
72+
"wrong hash": {
73+
"77d86e9752cb933569dfa1f693ee4338e65b28b4",
74+
&Component{
75+
Hashes: map[Algorithm]Hash{
76+
SHA1: "b5cc41d90d7ccc195c4a24ceb32656942c9854ea",
77+
},
78+
},
79+
false,
80+
},
81+
} {
82+
require.Equal(t, tc.mustMatch, tc.component.Matches(tc.identifier), fmt.Sprintf("failed: %s", testCase))
83+
}
84+
}

pkg/vex/product.go

Lines changed: 22 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,6 @@ SPDX-License-Identifier: Apache-2.0
55

66
package vex
77

8-
// Component abstracts the common construct shared by product and subcomponents
9-
// allowing OpenVEX statements to point to a piece of software by referencing it
10-
// by hash or identifier.
11-
//
12-
// The ID should be an IRI uniquely identifying the product. Software can be
13-
// referenced as a VEX product or subcomponent using only its IRI or it may be
14-
// referenced by its crptographic hashes and/or other identifiers but, in no case,
15-
// must an IRI describe two different pieces of software or used to describe
16-
// a range of software.
17-
type Component struct {
18-
// ID is an IRI identifying the component. It is optional as the component
19-
// can also be identified using hashes or software identifiers.
20-
ID string `json:"@id,omitempty"`
21-
22-
// Hashes is a map of hashes to identify the component using cryptographic
23-
// hashes.
24-
Hashes map[Algorithm]Hash `json:"hashes,omitempty"`
25-
26-
// Identifiers is a list of software identifiers that describe the component.
27-
Identifiers map[IdentifierType]string `json:"identifiers,omitempty"`
28-
29-
// Supplier is an optional machine-readable identifier for the supplier of
30-
// the component. Valid examples include email address or IRIs.
31-
Supplier string `json:"supplier,omitempty"`
32-
}
33-
348
// Product abstracts the VEX product into a struct that can identify software
359
// through various means. The main one is the ID field which contains an IRI
3610
// identifying the product, possibly pointing to another document with more data,
@@ -48,6 +22,28 @@ type Subcomponent struct {
4822
Component
4923
}
5024

25+
// Product returns true if an identifier and subcomponent identifier match any
26+
// of the identifiers in the product and subcomponents.
27+
func (p *Product) Matches(identifier, subIdentifier string) bool {
28+
if !p.Component.Matches(identifier) {
29+
return false
30+
}
31+
32+
// If the product has no subcomponents or no subcomponent was specified,
33+
// matching the product part is enough:
34+
if len(p.Subcomponents) == 0 || subIdentifier == "" {
35+
return true
36+
}
37+
38+
for _, s := range p.Subcomponents {
39+
if s.Component.Matches(subIdentifier) {
40+
return true
41+
}
42+
}
43+
44+
return false
45+
}
46+
5147
type (
5248
IdentifierLocator string
5349
IdentifierType string

pkg/vex/product_test.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
Copyright 2023 The OpenVEX Authors
3+
SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package vex
7+
8+
import (
9+
"fmt"
10+
"testing"
11+
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
func TestProductMatches(t *testing.T) {
16+
for testCase, tc := range map[string]struct {
17+
sut *Product
18+
product string
19+
subcomponent string
20+
mustMach bool
21+
}{
22+
"identifier only": {
23+
sut: &Product{
24+
Component: Component{ID: "pkg:apk/alpine/[email protected]"},
25+
},
26+
product: "pkg:apk/alpine/[email protected]",
27+
subcomponent: "",
28+
mustMach: true,
29+
},
30+
"purl only": {
31+
sut: &Product{
32+
Component: Component{Identifiers: map[IdentifierType]string{
33+
PURL: "pkg:apk/alpine/[email protected]",
34+
}},
35+
},
36+
product: "pkg:apk/alpine/[email protected]",
37+
subcomponent: "",
38+
mustMach: true,
39+
},
40+
"generic purl only": {
41+
sut: &Product{
42+
Component: Component{Identifiers: map[IdentifierType]string{
43+
PURL: "pkg:apk/alpine/libcrypto3",
44+
}},
45+
},
46+
product: "pkg:apk/alpine/[email protected]",
47+
subcomponent: "",
48+
mustMach: true,
49+
},
50+
"identifier and components in doc and statement": {
51+
sut: &Product{
52+
Component: Component{ID: "pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126"},
53+
Subcomponents: []Subcomponent{
54+
{
55+
Component{ID: "pkg:apk/alpine/[email protected]"},
56+
},
57+
},
58+
},
59+
product: "pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126",
60+
subcomponent: "pkg:apk/alpine/[email protected]",
61+
mustMach: true,
62+
},
63+
"identifier and no components in query": {
64+
sut: &Product{
65+
Component: Component{ID: "pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126"},
66+
Subcomponents: []Subcomponent{
67+
{
68+
Component{ID: "pkg:apk/alpine/[email protected]"},
69+
},
70+
},
71+
},
72+
product: "pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126",
73+
subcomponent: "",
74+
mustMach: true,
75+
},
76+
"identifier and no components in document": {
77+
sut: &Product{
78+
Component: Component{ID: "pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126"},
79+
Subcomponents: []Subcomponent{},
80+
},
81+
product: "pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126",
82+
subcomponent: "pkg:apk/alpine/[email protected]",
83+
mustMach: true,
84+
},
85+
"identifier + multicomponent doc": {
86+
sut: &Product{
87+
Component: Component{ID: "pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126"},
88+
Subcomponents: []Subcomponent{
89+
{Component{ID: "pkg:apk/alpine/[email protected]"}},
90+
{Component{ID: "pkg:apk/alpine/[email protected]"}},
91+
},
92+
},
93+
product: "pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126",
94+
subcomponent: "pkg:apk/alpine/[email protected]",
95+
mustMach: true,
96+
},
97+
} {
98+
require.Equal(t, tc.mustMach, tc.sut.Matches(tc.product, tc.subcomponent), fmt.Sprintf("failed: %s", testCase))
99+
}
100+
}

pkg/vex/statement.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,3 +172,37 @@ func SortStatements(stmts []Statement, documentTimestamp time.Time) {
172172
return iTime.Before(*jTime)
173173
})
174174
}
175+
176+
// Matches returns true if the statement matches the specified vulnerability
177+
// identifier, the VEX product and any of the identifiers from the received list.
178+
func (stmt *Statement) Matches(vuln, product string, subcomponents []string) bool {
179+
if !stmt.Vulnerability.Matches(vuln) {
180+
return false
181+
}
182+
183+
for i := range stmt.Products {
184+
if len(subcomponents) == 0 {
185+
if stmt.Products[i].Matches(product, "") {
186+
return true
187+
}
188+
}
189+
190+
for _, sc := range subcomponents {
191+
if stmt.Products[i].Matches(product, sc) {
192+
return true
193+
}
194+
}
195+
}
196+
return false
197+
}
198+
199+
// MatchesProduct returns true if the statement matches the identifier string
200+
// with an optional subcomponent identifier
201+
func (stmt *Statement) MatchesProduct(identifier, subidentifier string) bool {
202+
for _, p := range stmt.Products {
203+
if p.Matches(identifier, subidentifier) {
204+
return true
205+
}
206+
}
207+
return false
208+
}

0 commit comments

Comments
 (0)