Skip to content

Commit ee1f5cd

Browse files
authored
Merge pull request #156 from puerco/index
2 parents 12ace4a + 9d1b30d commit ee1f5cd

File tree

6 files changed

+444
-0
lines changed

6 files changed

+444
-0
lines changed

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ go 1.24.8
44

55
require (
66
github.com/google/go-cmp v0.7.0
7+
github.com/in-toto/attestation v1.1.2
78
github.com/in-toto/in-toto-golang v0.9.0
89
github.com/owenrumney/go-sarif v1.1.1
910
gopkg.in/yaml.v3 v3.0.1
@@ -18,6 +19,7 @@ require (
1819
github.com/zclconf/go-cty v1.10.0 // indirect
1920
golang.org/x/crypto v0.17.0 // indirect
2021
golang.org/x/text v0.14.0 // indirect
22+
google.golang.org/protobuf v1.36.6 // indirect
2123
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
2224
)
2325

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaW
1010
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
1111
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
1212
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
13+
github.com/in-toto/attestation v1.1.2 h1:MBFn6lsMq6dptQZJBhalXTcWMb/aJy3V+GX3VYj/V1E=
14+
github.com/in-toto/attestation v1.1.2/go.mod h1:gYFddHMZj3DiQ0b62ltNi1Vj5rC879bTmBbrv9CRHpM=
1315
github.com/in-toto/in-toto-golang v0.9.0 h1:tHny7ac4KgtsfrG6ybU8gVOZux2H8jN05AXJ9EBM1XU=
1416
github.com/in-toto/in-toto-golang v0.9.0/go.mod h1:xsBVrVsHNsB61++S6Dy2vWosKhuA3lUTQd+eF9HdeMo=
1517
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -58,6 +60,8 @@ golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
5860
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
5961
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
6062
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
63+
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
64+
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
6165
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
6266
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
6367
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=

pkg/index/filters.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Copyright 2025 The OpenVEX Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package index
5+
6+
import "github.com/openvex/go-vex/pkg/vex"
7+
8+
// Filter is an internal object that abstracs a function that
9+
// when called, extracts vex statements from an index, returning them
10+
// in a slice ordered by pointers so the matching vex statements.
11+
//
12+
// Filters are used by the index `Matches()` function which calls the
13+
// filters, deduplicates the results and returns the collection of matching
14+
// statements.
15+
type Filter func() map[*vex.Statement]struct{}
16+
17+
// A FilterFunc is a function that returns a Filter when called. FilterFuncs are
18+
// meant to be used as arguments to the `Matches()` index function.
19+
type FilterFunc func(*StatementIndex) Filter
20+
21+
// WithVulnerability returns a filter that matches a vulnerability.
22+
func WithVulnerability(vuln *vex.Vulnerability) FilterFunc {
23+
return func(si *StatementIndex) Filter {
24+
return func() map[*vex.Statement]struct{} {
25+
ret := map[*vex.Statement]struct{}{}
26+
ids := []vex.VulnerabilityID{}
27+
if vuln.Name != "" {
28+
ids = append(ids, vuln.Name)
29+
}
30+
ids = append(ids, vuln.Aliases...)
31+
32+
for _, id := range ids {
33+
for _, s := range si.vulnIndex[string(id)] {
34+
ret[s] = struct{}{}
35+
}
36+
}
37+
return ret
38+
}
39+
}
40+
}
41+
42+
// WithProduct returns a filter that indexes a product by its ID,
43+
// identifiers and hashes.
44+
func WithProduct(prod *vex.Product) FilterFunc {
45+
return func(si *StatementIndex) Filter {
46+
return func() map[*vex.Statement]struct{} {
47+
ret := map[*vex.Statement]struct{}{}
48+
ids := []string{}
49+
if prod.ID != "" {
50+
ids = append(ids, prod.ID)
51+
}
52+
for _, id := range prod.Identifiers {
53+
ids = append(ids, id)
54+
}
55+
for _, h := range prod.Hashes {
56+
ids = append(ids, string(h))
57+
}
58+
59+
for _, id := range ids {
60+
for _, s := range si.prodIndex[id] {
61+
ret[s] = struct{}{}
62+
}
63+
}
64+
65+
return ret
66+
}
67+
}
68+
}
69+
70+
// WithSubcomponent adds a subcomponent filter to the search criteria, indexing
71+
// by ID, identifiers and hashes.
72+
func WithSubcomponent(subc *vex.Subcomponent) FilterFunc {
73+
return func(si *StatementIndex) Filter {
74+
return func() map[*vex.Statement]struct{} {
75+
ret := map[*vex.Statement]struct{}{}
76+
ids := []string{}
77+
if subc.ID != "" {
78+
ids = append(ids, subc.ID)
79+
}
80+
for _, id := range subc.Identifiers {
81+
ids = append(ids, id)
82+
}
83+
for _, h := range subc.Hashes {
84+
ids = append(ids, string(h))
85+
}
86+
87+
for _, id := range ids {
88+
for _, s := range si.subIndex[id] {
89+
ret[s] = struct{}{}
90+
}
91+
}
92+
93+
return ret
94+
}
95+
}
96+
}

pkg/index/index.go

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
// Copyright 2025 The OpenVEX Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package index
5+
6+
import (
7+
"fmt"
8+
"slices"
9+
10+
"github.com/openvex/go-vex/pkg/vex"
11+
)
12+
13+
// New creates a new VEX index with the specified functions
14+
func New(funcs ...constructorFunc) (*StatementIndex, error) {
15+
si := &StatementIndex{}
16+
for _, fn := range funcs {
17+
if err := fn(si); err != nil {
18+
return nil, err
19+
}
20+
}
21+
return si, nil
22+
}
23+
24+
type constructorFunc func(*StatementIndex) error
25+
26+
// WithDocument adds all the statements in a document to the index
27+
func WithDocument(doc *vex.VEX) constructorFunc {
28+
return func(si *StatementIndex) error {
29+
statements := []*vex.Statement{}
30+
for i := range doc.Statements {
31+
statements = append(statements, &doc.Statements[i])
32+
}
33+
si.IndexStatements(statements)
34+
return nil
35+
}
36+
}
37+
38+
// WithStatements adds statements to a newly created index
39+
func WithStatements(statements []*vex.Statement) constructorFunc {
40+
return func(si *StatementIndex) error {
41+
si.IndexStatements(statements)
42+
return nil
43+
}
44+
}
45+
46+
// StatementIndex is the OpenVEX statement indexer. An index reads into memory
47+
// vex statements and catalogs them by the fields in their components
48+
// (vulnerability, product, subcomponents).
49+
//
50+
// The index exposes a StatementIndex.Match() function that takes in Filters
51+
// to return indexed statements that match the filter criteria.
52+
type StatementIndex struct {
53+
vulnIndex map[string][]*vex.Statement
54+
prodIndex map[string][]*vex.Statement
55+
subIndex map[string][]*vex.Statement
56+
}
57+
58+
// IndexStatements indexes all the passed statements by cataloguing the
59+
// fields in the product, vulnerability and subcomponents.
60+
func (si *StatementIndex) IndexStatements(statements []*vex.Statement) {
61+
si.vulnIndex = map[string][]*vex.Statement{}
62+
si.prodIndex = map[string][]*vex.Statement{}
63+
si.subIndex = map[string][]*vex.Statement{}
64+
65+
for _, s := range statements {
66+
for _, p := range s.Products {
67+
if p.ID != "" {
68+
si.prodIndex[p.ID] = append(si.prodIndex[p.ID], s)
69+
}
70+
for _, id := range p.Identifiers {
71+
if !slices.Contains(si.prodIndex[id], s) {
72+
si.prodIndex[id] = append(si.prodIndex[id], s)
73+
}
74+
}
75+
for algo, h := range p.Hashes {
76+
if !slices.Contains(si.prodIndex[string(h)], s) {
77+
si.prodIndex[string(h)] = append(si.prodIndex[string(h)], s)
78+
}
79+
if !slices.Contains(si.prodIndex[fmt.Sprintf("%s:%s", algo, h)], s) {
80+
si.prodIndex[fmt.Sprintf("%s:%s", algo, h)] = append(si.prodIndex[fmt.Sprintf("%s:%s", algo, h)], s)
81+
}
82+
intotoAlgo := algo.ToInToto()
83+
if intotoAlgo == "" {
84+
continue
85+
}
86+
if !slices.Contains(si.prodIndex[fmt.Sprintf("%s:%s", intotoAlgo, h)], s) {
87+
si.prodIndex[fmt.Sprintf("%s:%s", intotoAlgo, h)] = append(si.prodIndex[fmt.Sprintf("%s:%s", intotoAlgo, h)], s)
88+
}
89+
}
90+
91+
// Index the subcomponents
92+
for _, sc := range p.Subcomponents {
93+
// Match by ID too
94+
if sc.ID != "" && !slices.Contains(si.subIndex[sc.ID], s) {
95+
si.subIndex[sc.ID] = append(si.subIndex[sc.ID], s)
96+
}
97+
for _, id := range sc.Identifiers {
98+
if !slices.Contains(si.subIndex[id], s) {
99+
si.subIndex[id] = append(si.subIndex[id], s)
100+
}
101+
}
102+
for _, h := range sc.Hashes {
103+
if !slices.Contains(si.subIndex[string(h)], s) {
104+
si.subIndex[string(h)] = append(si.subIndex[string(h)], s)
105+
}
106+
}
107+
}
108+
}
109+
110+
if s.Vulnerability.Name != "" {
111+
if !slices.Contains(si.vulnIndex[string(s.Vulnerability.Name)], s) {
112+
si.vulnIndex[string(s.Vulnerability.Name)] = append(si.vulnIndex[string(s.Vulnerability.Name)], s)
113+
}
114+
}
115+
for _, alias := range s.Vulnerability.Aliases {
116+
if !slices.Contains(si.vulnIndex[string(alias)], s) {
117+
si.vulnIndex[string(alias)] = append(si.vulnIndex[string(alias)], s)
118+
}
119+
}
120+
}
121+
}
122+
123+
// unionIndexResults
124+
func unionIndexResults(results []map[*vex.Statement]struct{}) []*vex.Statement {
125+
if len(results) == 0 {
126+
return []*vex.Statement{}
127+
}
128+
preret := map[*vex.Statement]struct{}{}
129+
// Since we're looking for statements in all results, we can just
130+
// cycle the shortest list against the others
131+
slices.SortFunc(results, func(a, b map[*vex.Statement]struct{}) int {
132+
if len(a) == len(b) {
133+
return 0
134+
}
135+
if len(a) < len(b) {
136+
return -1
137+
}
138+
return 1
139+
})
140+
141+
var found bool
142+
for s := range results[0] {
143+
// if this is present in all lists, we're in
144+
found = true
145+
for i := range results[1:] {
146+
if _, ok := results[i][s]; !ok {
147+
found = false
148+
break
149+
}
150+
}
151+
if found {
152+
preret[s] = struct{}{}
153+
}
154+
}
155+
156+
// Now assemble the list
157+
ret := []*vex.Statement{}
158+
for s := range preret {
159+
ret = append(ret, s)
160+
}
161+
return ret
162+
}
163+
164+
// Matches applies filters to the index to look for matching statements
165+
func (si *StatementIndex) Matches(filterfunc ...FilterFunc) []*vex.Statement {
166+
lists := []map[*vex.Statement]struct{}{}
167+
for _, ffunc := range filterfunc {
168+
filter := ffunc(si)
169+
lists = append(lists, filter())
170+
}
171+
return unionIndexResults(lists)
172+
}

0 commit comments

Comments
 (0)