Skip to content

Commit 5dfa450

Browse files
committed
Code Reorg: Split vex package
This commit rearranges the code into new _functions.go files to get them out of the main vex.go file. Tests are moved into their corresponding files but no other changes are done. Signed-off-by: Adolfo García Veytia (Puerco) <[email protected]>
1 parent 308feef commit 5dfa450

File tree

6 files changed

+448
-313
lines changed

6 files changed

+448
-313
lines changed

pkg/vex/functions_documents.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
Copyright 2023 The OpenVEX Authors
3+
SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package vex
7+
8+
import (
9+
"sort"
10+
)
11+
12+
// SortDocuments sorts and returns a slice of documents based on their date.
13+
// VEXes should be applied sequentially in chronological order as they capture
14+
// knowledge about an artifact as it changes over time.
15+
func SortDocuments(docs []*VEX) []*VEX {
16+
sort.Slice(docs, func(i, j int) bool {
17+
if docs[j].Timestamp == nil {
18+
return true
19+
}
20+
if docs[i].Timestamp == nil {
21+
return false
22+
}
23+
return docs[i].Timestamp.Before(*(docs[j].Timestamp))
24+
})
25+
return docs
26+
}

pkg/vex/functions_documents_test.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
Copyright 2023 The OpenVEX Authors
3+
SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package vex
7+
8+
import (
9+
"testing"
10+
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
func TestMerge(t *testing.T) {
15+
doc1, err := Open("testdata/v001-1.vex.json")
16+
require.NoError(t, err)
17+
doc2, err := Open("testdata/v001-2.vex.json")
18+
require.NoError(t, err)
19+
20+
doc3, err := Open("testdata/v020-1.vex.json")
21+
require.NoError(t, err)
22+
doc4, err := Open("testdata/v020-2.vex.json")
23+
require.NoError(t, err)
24+
25+
for _, tc := range []struct {
26+
opts MergeOptions
27+
docs []*VEX
28+
expectedDoc *VEX
29+
shouldErr bool
30+
}{
31+
// Zero docs should fail
32+
{
33+
opts: MergeOptions{},
34+
docs: []*VEX{},
35+
expectedDoc: &VEX{},
36+
shouldErr: true,
37+
},
38+
// One doc results in the same doc
39+
{
40+
opts: MergeOptions{},
41+
docs: []*VEX{doc1},
42+
expectedDoc: doc1,
43+
shouldErr: false,
44+
},
45+
// Two docs, as they are
46+
{
47+
opts: MergeOptions{},
48+
docs: []*VEX{doc1, doc2},
49+
expectedDoc: &VEX{
50+
Metadata: Metadata{},
51+
Statements: []Statement{
52+
doc1.Statements[0],
53+
doc2.Statements[0],
54+
},
55+
},
56+
shouldErr: false,
57+
},
58+
// Two docs, filter product
59+
{
60+
opts: MergeOptions{
61+
Products: []string{"pkg:apk/wolfi/[email protected]"},
62+
},
63+
docs: []*VEX{doc3, doc4},
64+
expectedDoc: &VEX{
65+
Metadata: Metadata{},
66+
Statements: []Statement{
67+
doc4.Statements[0],
68+
},
69+
},
70+
shouldErr: false,
71+
},
72+
// Two docs, filter vulnerability
73+
{
74+
opts: MergeOptions{
75+
Vulnerabilities: []string{"CVE-9876-54321"},
76+
},
77+
docs: []*VEX{doc3, doc4},
78+
expectedDoc: &VEX{
79+
Metadata: Metadata{},
80+
Statements: []Statement{
81+
doc3.Statements[0],
82+
},
83+
},
84+
shouldErr: false,
85+
},
86+
} {
87+
doc, err := MergeDocuments(&tc.opts, tc.docs)
88+
if tc.shouldErr {
89+
require.Error(t, err)
90+
continue
91+
}
92+
93+
// Check doc
94+
require.Len(t, doc.Statements, len(tc.expectedDoc.Statements))
95+
require.Equal(t, doc.Statements, tc.expectedDoc.Statements)
96+
}
97+
}

pkg/vex/functions_files.go

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
/*
2+
Copyright 2023 The OpenVEX Authors
3+
SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package vex
7+
8+
import (
9+
"bytes"
10+
"encoding/json"
11+
"fmt"
12+
"os"
13+
"strings"
14+
"time"
15+
16+
"github.com/openvex/go-vex/pkg/csaf"
17+
"github.com/sirupsen/logrus"
18+
"gopkg.in/yaml.v3"
19+
)
20+
21+
// Load reads the VEX document file at the given path and returns a decoded VEX
22+
// object. If Load is unable to read the file or decode the document, it returns
23+
// an error.
24+
func Load(path string) (*VEX, error) {
25+
data, err := os.ReadFile(path)
26+
if err != nil {
27+
return nil, fmt.Errorf("loading VEX file: %w", err)
28+
}
29+
30+
return Parse(data)
31+
}
32+
33+
// Parse parses an OpenVEX document in the latest version from the data byte array.
34+
func Parse(data []byte) (*VEX, error) {
35+
vexDoc := &VEX{}
36+
if err := json.Unmarshal(data, vexDoc); err != nil {
37+
return nil, fmt.Errorf("%s: %w", errMsgParse, err)
38+
}
39+
return vexDoc, nil
40+
}
41+
42+
// OpenYAML opens a VEX file in YAML format.
43+
func OpenYAML(path string) (*VEX, error) {
44+
data, err := os.ReadFile(path)
45+
if err != nil {
46+
return nil, fmt.Errorf("opening YAML file: %w", err)
47+
}
48+
vexDoc := New()
49+
if err := yaml.Unmarshal(data, &vexDoc); err != nil {
50+
return nil, fmt.Errorf("unmarshalling VEX data: %w", err)
51+
}
52+
return &vexDoc, nil
53+
}
54+
55+
// OpenJSON opens an OpenVEX file in JSON format.
56+
func OpenJSON(path string) (*VEX, error) {
57+
data, err := os.ReadFile(path)
58+
if err != nil {
59+
return nil, fmt.Errorf("opening JSON file: %w", err)
60+
}
61+
vexDoc := New()
62+
if err := json.Unmarshal(data, &vexDoc); err != nil {
63+
return nil, fmt.Errorf("unmarshalling VEX data: %w", err)
64+
}
65+
return &vexDoc, nil
66+
}
67+
68+
// parseContext light parses a JSON document to look for the OpenVEX context locator
69+
func parseContext(rawDoc []byte) (string, error) {
70+
pd := struct {
71+
Context string `json:"@context"`
72+
}{}
73+
74+
if err := json.Unmarshal(rawDoc, &pd); err != nil {
75+
return "", fmt.Errorf("parsing context from json data: %w", err)
76+
}
77+
78+
if strings.HasPrefix(pd.Context, Context) {
79+
return pd.Context, nil
80+
}
81+
return "", nil
82+
}
83+
84+
// Open tries to autodetect the vex format and open it
85+
func Open(path string) (*VEX, error) {
86+
data, err := os.ReadFile(path)
87+
if err != nil {
88+
return nil, fmt.Errorf("opening VEX file: %w", err)
89+
}
90+
91+
documentContextLocator, err := parseContext(data)
92+
if err != nil {
93+
return nil, err
94+
}
95+
96+
if documentContextLocator == ContextLocator() {
97+
return Parse(data)
98+
} else if documentContextLocator != "" {
99+
version := strings.TrimPrefix(documentContextLocator, Context)
100+
version = strings.TrimPrefix(version, "/")
101+
102+
// If version is nil, then we assume v0.0.1
103+
if version == "" {
104+
version = "v0.0.1"
105+
}
106+
107+
parser := getLegacyVersionParser(version)
108+
if parser == nil {
109+
return nil, fmt.Errorf("unable to get parser for version %s", version)
110+
}
111+
112+
doc, err := parser(data)
113+
if err != nil {
114+
return nil, fmt.Errorf("parsing document: %w", err)
115+
}
116+
117+
return doc, nil
118+
}
119+
120+
if bytes.Contains(data, []byte(`"csaf_version"`)) {
121+
logrus.Info("Abriendo CSAF")
122+
123+
doc, err := OpenCSAF(path, []string{})
124+
if err != nil {
125+
return nil, fmt.Errorf("attempting to open csaf doc: %w", err)
126+
}
127+
return doc, nil
128+
}
129+
130+
return nil, fmt.Errorf("unable to detect document format reading %s", path)
131+
}
132+
133+
// OpenCSAF opens a CSAF document and builds a VEX object from it.
134+
func OpenCSAF(path string, products []string) (*VEX, error) {
135+
csafDoc, err := csaf.Open(path)
136+
if err != nil {
137+
return nil, fmt.Errorf("opening csaf doc: %w", err)
138+
}
139+
140+
productDict := map[string]string{}
141+
filterDict := map[string]string{}
142+
for _, pid := range products {
143+
filterDict[pid] = pid
144+
}
145+
146+
prods := csafDoc.ProductTree.ListProducts()
147+
for _, sp := range prods {
148+
// Check if we need to filter
149+
if len(filterDict) > 0 {
150+
foundID := false
151+
for _, i := range sp.IdentificationHelper {
152+
if _, ok := filterDict[i]; ok {
153+
foundID = true
154+
break
155+
}
156+
}
157+
_, ok := filterDict[sp.ID]
158+
if !foundID && !ok {
159+
continue
160+
}
161+
}
162+
163+
for _, h := range sp.IdentificationHelper {
164+
productDict[sp.ID] = h
165+
}
166+
}
167+
168+
// Create the vex doc
169+
v := &VEX{
170+
Metadata: Metadata{
171+
ID: csafDoc.Document.Tracking.ID,
172+
Author: "",
173+
AuthorRole: "",
174+
Timestamp: &time.Time{},
175+
},
176+
Statements: []Statement{},
177+
}
178+
179+
// Cycle the CSAF vulns list and get those that apply
180+
for i := range csafDoc.Vulnerabilities {
181+
for status, docProducts := range csafDoc.Vulnerabilities[i].ProductStatus {
182+
for _, productID := range docProducts {
183+
if _, ok := productDict[productID]; ok {
184+
// Check we have a valid status
185+
if StatusFromCSAF(status) == "" {
186+
return nil, fmt.Errorf("invalid status for product %s", productID)
187+
}
188+
189+
// TODO search the threats struct for justification, etc
190+
just := ""
191+
for _, t := range csafDoc.Vulnerabilities[i].Threats {
192+
// Search the threats for a justification
193+
for _, p := range t.ProductIDs {
194+
if p == productID {
195+
just = t.Details
196+
}
197+
}
198+
}
199+
200+
v.Statements = append(v.Statements, Statement{
201+
Vulnerability: Vulnerability{Name: VulnerabilityID(csafDoc.Vulnerabilities[i].CVE)},
202+
Status: StatusFromCSAF(status),
203+
Justification: "", // Justifications are not machine readable in csaf, it seems
204+
ActionStatement: just,
205+
Products: []Product{
206+
{
207+
Component: Component{
208+
ID: productID,
209+
},
210+
},
211+
},
212+
})
213+
}
214+
}
215+
}
216+
}
217+
218+
return v, nil
219+
}

0 commit comments

Comments
 (0)