@@ -6,9 +6,122 @@ SPDX-License-Identifier: Apache-2.0
6
6
package vex
7
7
8
8
import (
9
+ "crypto/sha256"
10
+ "errors"
11
+ "fmt"
9
12
"sort"
13
+ "strings"
10
14
)
11
15
16
+ type MergeOptions struct {
17
+ DocumentID string // ID to use in the new document
18
+ Author string // Author to use in the new document
19
+ AuthorRole string // Role of the document author
20
+ Products []string // Product IDs to consider
21
+ Vulnerabilities []string // IDs of vulnerabilities to merge
22
+ }
23
+
24
+ // MergeDocuments is a convenience wrapper over MergeDocumentsWithOptions
25
+ // that does not take options.
26
+ func MergeDocuments (docs []* VEX ) (* VEX , error ) {
27
+ return MergeDocumentsWithOptions (& MergeOptions {}, docs )
28
+ }
29
+
30
+ // Merge combines the statements from a number of documents into
31
+ // a new one, preserving time context from each of them.
32
+ func MergeDocumentsWithOptions (mergeOpts * MergeOptions , docs []* VEX ) (* VEX , error ) {
33
+ if len (docs ) == 0 {
34
+ return nil , fmt .Errorf ("at least one vex document is required to merge" )
35
+ }
36
+
37
+ docID := mergeOpts .DocumentID
38
+ // If no document id is specified we compute a
39
+ // deterministic ID using the merged docs
40
+ if docID == "" {
41
+ ids := []string {}
42
+ for i , d := range docs {
43
+ if d .ID == "" {
44
+ ids = append (ids , fmt .Sprintf ("VEX-DOC-%d" , i ))
45
+ } else {
46
+ ids = append (ids , d .ID )
47
+ }
48
+ }
49
+
50
+ sort .Strings (ids )
51
+ h := sha256 .New ()
52
+ h .Write ([]byte (strings .Join (ids , ":" )))
53
+ // Hash the sorted IDs list
54
+ docID = fmt .Sprintf ("merged-vex-%x" , h .Sum (nil ))
55
+ }
56
+
57
+ newDoc := New ()
58
+
59
+ newDoc .ID = docID
60
+ if author := mergeOpts .Author ; author != "" {
61
+ newDoc .Author = author
62
+ }
63
+ if authorRole := mergeOpts .AuthorRole ; authorRole != "" {
64
+ newDoc .AuthorRole = authorRole
65
+ }
66
+
67
+ ss := []Statement {}
68
+
69
+ // Create an inverse dict of products and vulnerabilities to filter
70
+ // these will only be used if ids to filter on are defined in the options.
71
+ iProds := map [string ]struct {}{}
72
+ iVulns := map [string ]struct {}{}
73
+ for _ , id := range mergeOpts .Products {
74
+ iProds [id ] = struct {}{}
75
+ }
76
+ for _ , id := range mergeOpts .Vulnerabilities {
77
+ iVulns [id ] = struct {}{}
78
+ }
79
+
80
+ for _ , doc := range docs {
81
+ for _ , s := range doc .Statements { //nolint:gocritic // this IS supposed to copy
82
+ matchesProduct := false
83
+ for id := range iProds {
84
+ if s .MatchesProduct (id , "" ) {
85
+ matchesProduct = true
86
+ break
87
+ }
88
+ }
89
+ if len (iProds ) > 0 && ! matchesProduct {
90
+ continue
91
+ }
92
+
93
+ matchesVuln := false
94
+ for id := range iVulns {
95
+ if s .Vulnerability .Matches (id ) {
96
+ matchesVuln = true
97
+ break
98
+ }
99
+ }
100
+ if len (iVulns ) > 0 && ! matchesVuln {
101
+ continue
102
+ }
103
+
104
+ // If statement does not have a timestamp, cascade
105
+ // the timestamp down from the document.
106
+ // See https://github.com/chainguard-dev/vex/issues/49
107
+ if s .Timestamp == nil {
108
+ if doc .Timestamp == nil {
109
+ return nil , errors .New ("unable to cascade timestamp from doc to timeless statement" )
110
+ }
111
+ s .Timestamp = doc .Timestamp
112
+ }
113
+
114
+ ss = append (ss , s )
115
+ }
116
+ }
117
+
118
+ SortStatements (ss , * newDoc .Metadata .Timestamp )
119
+
120
+ newDoc .Statements = ss
121
+
122
+ return & newDoc , nil
123
+ }
124
+
12
125
// SortDocuments sorts and returns a slice of documents based on their date.
13
126
// VEXes should be applied sequentially in chronological order as they capture
14
127
// knowledge about an artifact as it changes over time.
0 commit comments