Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
module github.com/openvex/go-vex

go 1.22
go 1.24.8

require (
github.com/google/go-cmp v0.7.0
github.com/in-toto/attestation v1.1.2
github.com/in-toto/in-toto-golang v0.9.0
github.com/owenrumney/go-sarif v1.1.1
gopkg.in/yaml.v3 v3.0.1
Expand All @@ -18,6 +19,7 @@ require (
github.com/zclconf/go-cty v1.10.0 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
)

Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaW
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/in-toto/attestation v1.1.2 h1:MBFn6lsMq6dptQZJBhalXTcWMb/aJy3V+GX3VYj/V1E=
github.com/in-toto/attestation v1.1.2/go.mod h1:gYFddHMZj3DiQ0b62ltNi1Vj5rC879bTmBbrv9CRHpM=
github.com/in-toto/in-toto-golang v0.9.0 h1:tHny7ac4KgtsfrG6ybU8gVOZux2H8jN05AXJ9EBM1XU=
github.com/in-toto/in-toto-golang v0.9.0/go.mod h1:xsBVrVsHNsB61++S6Dy2vWosKhuA3lUTQd+eF9HdeMo=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
Expand Down Expand Up @@ -58,6 +60,8 @@ golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
Expand Down
96 changes: 96 additions & 0 deletions pkg/index/filters.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Copyright 2025 The OpenVEX Authors
// SPDX-License-Identifier: Apache-2.0

package index

import "github.com/openvex/go-vex/pkg/vex"

// Filter is an internal object that abstracs a function that
// when called, extracts vex statements from an index, returning them
// in a slice ordered by pointers so the matching vex statements.
//
// Filters are used by the index `Matches()` function which calls the
// filters, deduplicates the results and returns the collection of matching
// statements.
type Filter func() map[*vex.Statement]struct{}

// A FilterFunc is a function that returns a Filter when called. FilterFuncs are
// meant to be used as arguments to the `Matches()` index function.
type FilterFunc func(*StatementIndex) Filter

// WithVulnerability returns a filter that matches a vulnerability.
func WithVulnerability(vuln *vex.Vulnerability) FilterFunc {
return func(si *StatementIndex) Filter {
return func() map[*vex.Statement]struct{} {
ret := map[*vex.Statement]struct{}{}
ids := []vex.VulnerabilityID{}
if vuln.Name != "" {
ids = append(ids, vuln.Name)
}
ids = append(ids, vuln.Aliases...)

for _, id := range ids {
for _, s := range si.vulnIndex[string(id)] {
ret[s] = struct{}{}
}
}
return ret
}
}
}

// WithProduct returns a filter that indexes a product by its ID,
// identifiers and hashes.
func WithProduct(prod *vex.Product) FilterFunc {
return func(si *StatementIndex) Filter {
return func() map[*vex.Statement]struct{} {
ret := map[*vex.Statement]struct{}{}
ids := []string{}
if prod.ID != "" {
ids = append(ids, prod.ID)
}
for _, id := range prod.Identifiers {
ids = append(ids, id)
}
for _, h := range prod.Hashes {
ids = append(ids, string(h))
}

for _, id := range ids {
for _, s := range si.prodIndex[id] {
ret[s] = struct{}{}
}
}

return ret
}
}
}

// WithSubcomponent adds a subcomponent filter to the search criteria, indexing
// by ID, identifiers and hashes.
func WithSubcomponent(subc *vex.Subcomponent) FilterFunc {
return func(si *StatementIndex) Filter {
return func() map[*vex.Statement]struct{} {
ret := map[*vex.Statement]struct{}{}
ids := []string{}
if subc.ID != "" {
ids = append(ids, subc.ID)
}
for _, id := range subc.Identifiers {
ids = append(ids, id)
}
for _, h := range subc.Hashes {
ids = append(ids, string(h))
}

for _, id := range ids {
for _, s := range si.subIndex[id] {
ret[s] = struct{}{}
}
}

return ret
}
}
}
172 changes: 172 additions & 0 deletions pkg/index/index.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// Copyright 2025 The OpenVEX Authors
// SPDX-License-Identifier: Apache-2.0

package index

import (
"fmt"
"slices"

"github.com/openvex/go-vex/pkg/vex"
)

// New creates a new VEX index with the specified functions
func New(funcs ...constructorFunc) (*StatementIndex, error) {
si := &StatementIndex{}
for _, fn := range funcs {
if err := fn(si); err != nil {
return nil, err
}
}
return si, nil
}

type constructorFunc func(*StatementIndex) error

// WithDocument adds all the statements in a document to the index
func WithDocument(doc *vex.VEX) constructorFunc {
return func(si *StatementIndex) error {
statements := []*vex.Statement{}
for i := range doc.Statements {
statements = append(statements, &doc.Statements[i])
}
si.IndexStatements(statements)
return nil
}
}

// WithStatements adds statements to a newly created index
func WithStatements(statements []*vex.Statement) constructorFunc {
return func(si *StatementIndex) error {
si.IndexStatements(statements)
return nil
}
}

// StatementIndex is the OpenVEX statement indexer. An index reads into memory
// vex statements and catalogs them by the fields in their components
// (vulnerability, product, subcomponents).
//
// The index exposes a StatementIndex.Match() function that takes in Filters
// to return indexed statements that match the filter criteria.
type StatementIndex struct {
vulnIndex map[string][]*vex.Statement
prodIndex map[string][]*vex.Statement
subIndex map[string][]*vex.Statement
}

// IndexStatements indexes all the passed statements by cataloguing the
// fields in the product, vulnerability and subcomponents.
func (si *StatementIndex) IndexStatements(statements []*vex.Statement) {
si.vulnIndex = map[string][]*vex.Statement{}
si.prodIndex = map[string][]*vex.Statement{}
si.subIndex = map[string][]*vex.Statement{}

for _, s := range statements {
for _, p := range s.Products {
if p.ID != "" {
si.prodIndex[p.ID] = append(si.prodIndex[p.ID], s)
}
for _, id := range p.Identifiers {
if !slices.Contains(si.prodIndex[id], s) {
si.prodIndex[id] = append(si.prodIndex[id], s)
}
}
for algo, h := range p.Hashes {
if !slices.Contains(si.prodIndex[string(h)], s) {
si.prodIndex[string(h)] = append(si.prodIndex[string(h)], s)
}
if !slices.Contains(si.prodIndex[fmt.Sprintf("%s:%s", algo, h)], s) {
si.prodIndex[fmt.Sprintf("%s:%s", algo, h)] = append(si.prodIndex[fmt.Sprintf("%s:%s", algo, h)], s)
}
intotoAlgo := algo.ToInToto()
if intotoAlgo == "" {
continue
}
if !slices.Contains(si.prodIndex[fmt.Sprintf("%s:%s", intotoAlgo, h)], s) {
si.prodIndex[fmt.Sprintf("%s:%s", intotoAlgo, h)] = append(si.prodIndex[fmt.Sprintf("%s:%s", intotoAlgo, h)], s)
}
}

// Index the subcomponents
for _, sc := range p.Subcomponents {
// Match by ID too
if sc.ID != "" && !slices.Contains(si.subIndex[sc.ID], s) {
si.subIndex[sc.ID] = append(si.subIndex[sc.ID], s)
}
for _, id := range sc.Identifiers {
if !slices.Contains(si.subIndex[id], s) {
si.subIndex[id] = append(si.subIndex[id], s)
}
}
for _, h := range sc.Hashes {
if !slices.Contains(si.subIndex[string(h)], s) {
si.subIndex[string(h)] = append(si.subIndex[string(h)], s)
}
}
}
}

if s.Vulnerability.Name != "" {
if !slices.Contains(si.vulnIndex[string(s.Vulnerability.Name)], s) {
si.vulnIndex[string(s.Vulnerability.Name)] = append(si.vulnIndex[string(s.Vulnerability.Name)], s)
}
}
for _, alias := range s.Vulnerability.Aliases {
if !slices.Contains(si.vulnIndex[string(alias)], s) {
si.vulnIndex[string(alias)] = append(si.vulnIndex[string(alias)], s)
}
}
}
}

// unionIndexResults
func unionIndexResults(results []map[*vex.Statement]struct{}) []*vex.Statement {
if len(results) == 0 {
return []*vex.Statement{}
}
preret := map[*vex.Statement]struct{}{}
// Since we're looking for statements in all results, we can just
// cycle the shortest list against the others
slices.SortFunc(results, func(a, b map[*vex.Statement]struct{}) int {
if len(a) == len(b) {
return 0
}
if len(a) < len(b) {
return -1
}
return 1
})

var found bool
for s := range results[0] {
// if this is present in all lists, we're in
found = true
for i := range results[1:] {
if _, ok := results[i][s]; !ok {
found = false
break
}
}
if found {
preret[s] = struct{}{}
}
}

// Now assemble the list
ret := []*vex.Statement{}
for s := range preret {
ret = append(ret, s)
}
return ret
}

// Matches applies filters to the index to look for matching statements
func (si *StatementIndex) Matches(filterfunc ...FilterFunc) []*vex.Statement {
lists := []map[*vex.Statement]struct{}{}
for _, ffunc := range filterfunc {
filter := ffunc(si)
lists = append(lists, filter())
}
return unionIndexResults(lists)
}
Loading