podman
296 строк · 9.1 Кб
1package sbom
2
3import (
4"encoding/json"
5"fmt"
6"io"
7"os"
8"sort"
9
10"github.com/containers/buildah/define"
11)
12
13// getComponentNameVersionPurl extracts the "name", "version", and "purl"
14// fields of a CycloneDX component record
15func getComponentNameVersionPurl(anyComponent any) (string, string, error) {
16if component, ok := anyComponent.(map[string]any); ok {
17// read the "name" field
18anyName, ok := component["name"]
19if !ok {
20return "", "", fmt.Errorf("no name in component %v", anyComponent)
21}
22name, ok := anyName.(string)
23if !ok {
24return "", "", fmt.Errorf("name %v is not a string", anyName)
25}
26// read the optional "version" field
27var version string
28anyVersion, ok := component["version"]
29if ok {
30if version, ok = anyVersion.(string); !ok {
31return "", "", fmt.Errorf("version %v is not a string", anyVersion)
32}
33}
34// combine them
35nameWithVersion := name
36if version != "" {
37nameWithVersion += ("@" + version)
38}
39// read the optional "purl" field
40var purl string
41anyPurl, ok := component["purl"]
42if ok {
43if purl, ok = anyPurl.(string); !ok {
44return "", "", fmt.Errorf("purl %v is not a string", anyPurl)
45}
46}
47return nameWithVersion, purl, nil
48}
49return "", "", fmt.Errorf("component %v is not an object", anyComponent)
50}
51
52// getPackageNameVersionInfoPurl extracts the "name", "versionInfo", and "purl"
53// fields of an SPDX package record
54func getPackageNameVersionInfoPurl(anyPackage any) (string, string, error) {
55if pkg, ok := anyPackage.(map[string]any); ok {
56// read the "name" field
57anyName, ok := pkg["name"]
58if !ok {
59return "", "", fmt.Errorf("no name in package %v", anyPackage)
60}
61name, ok := anyName.(string)
62if !ok {
63return "", "", fmt.Errorf("name %v is not a string", anyName)
64}
65// read the optional "versionInfo" field
66var versionInfo string
67if anyVersionInfo, ok := pkg["versionInfo"]; ok {
68if versionInfo, ok = anyVersionInfo.(string); !ok {
69return "", "", fmt.Errorf("versionInfo %v is not a string", anyVersionInfo)
70}
71}
72// combine them
73nameWithVersionInfo := name
74if versionInfo != "" {
75nameWithVersionInfo += ("@" + versionInfo)
76}
77// now look for optional externalRefs[].purl if "referenceCategory"
78// is "PACKAGE-MANAGER" and "referenceType" is "purl"
79var purl string
80if anyExternalRefs, ok := pkg["externalRefs"]; ok {
81if externalRefs, ok := anyExternalRefs.([]any); ok {
82for _, anyExternalRef := range externalRefs {
83if externalRef, ok := anyExternalRef.(map[string]any); ok {
84anyReferenceCategory, ok := externalRef["referenceCategory"]
85if !ok {
86continue
87}
88if referenceCategory, ok := anyReferenceCategory.(string); !ok || referenceCategory != "PACKAGE-MANAGER" {
89continue
90}
91anyReferenceType, ok := externalRef["referenceType"]
92if !ok {
93continue
94}
95if referenceType, ok := anyReferenceType.(string); !ok || referenceType != "purl" {
96continue
97}
98if anyReferenceLocator, ok := externalRef["referenceLocator"]; ok {
99if purl, ok = anyReferenceLocator.(string); !ok {
100return "", "", fmt.Errorf("purl %v is not a string", anyReferenceLocator)
101}
102}
103}
104}
105}
106}
107return nameWithVersionInfo, purl, nil
108}
109return "", "", fmt.Errorf("package %v is not an object", anyPackage)
110}
111
112// getLicenseID extracts the "licenseId" field of an SPDX license record
113func getLicenseID(anyLicense any) (string, error) {
114var licenseID string
115if lic, ok := anyLicense.(map[string]any); ok {
116anyID, ok := lic["licenseId"]
117if !ok {
118return "", fmt.Errorf("no licenseId in license %v", anyID)
119}
120id, ok := anyID.(string)
121if !ok {
122return "", fmt.Errorf("licenseId %v is not a string", anyID)
123}
124licenseID = id
125}
126return licenseID, nil
127}
128
129// mergeSlicesWithoutDuplicates merges a named slice in "base" with items from
130// the same slice in "merge", so long as getKey() returns values for them that
131// it didn't for items from the "base" slice
132func mergeSlicesWithoutDuplicates(base, merge map[string]any, sliceField string, getKey func(record any) (string, error)) error {
133uniqueKeys := make(map[string]struct{})
134// go through all of the values in the base slice, grab their
135// keys, and note them
136baseRecords := base[sliceField]
137baseRecordsSlice, ok := baseRecords.([]any)
138if !ok {
139baseRecordsSlice = []any{}
140}
141for _, anyRecord := range baseRecordsSlice {
142key, err := getKey(anyRecord)
143if err != nil {
144return err
145}
146uniqueKeys[key] = struct{}{}
147}
148// go through all of the record values in the merge doc, grab their
149// associated keys, and append them to the base records slice if we
150// haven't seen the key yet
151mergeRecords := merge[sliceField]
152mergeRecordsSlice, ok := mergeRecords.([]any)
153if !ok {
154mergeRecordsSlice = []any{}
155}
156for _, anyRecord := range mergeRecordsSlice {
157key, err := getKey(anyRecord)
158if err != nil {
159return err
160}
161if _, present := uniqueKeys[key]; !present {
162baseRecordsSlice = append(baseRecordsSlice, anyRecord)
163uniqueKeys[key] = struct{}{}
164}
165}
166if len(baseRecordsSlice) > 0 {
167base[sliceField] = baseRecordsSlice
168}
169return nil
170}
171
172// decodeJSON decodes a file into a map
173func decodeJSON(inputFile string, document *map[string]any) error {
174src, err := os.Open(inputFile)
175if err != nil {
176return err
177}
178defer src.Close()
179if err = json.NewDecoder(src).Decode(document); err != nil {
180return fmt.Errorf("decoding JSON document from %q: %w", inputFile, err)
181}
182return nil
183}
184
185// encodeJSON encodes a map and saves it to a file
186func encodeJSON(outputFile string, document any) error {
187dst, err := os.Create(outputFile)
188if err != nil {
189return err
190}
191defer dst.Close()
192if err = json.NewEncoder(dst).Encode(document); err != nil {
193return fmt.Errorf("writing JSON document to %q: %w", outputFile, err)
194}
195return nil
196}
197
198// Merge adds the contents of inputSBOM to inputOutputSBOM using one of a
199// handful of named strategies.
200func Merge(mergeStrategy define.SBOMMergeStrategy, inputOutputSBOM, inputSBOM, outputPURL string) (err error) {
201type purlImageContents struct {
202Dependencies []string `json:"dependencies,omitempty"`
203}
204type purlDocument struct {
205ImageContents purlImageContents `json:"image_contents,omitempty"`
206}
207purls := []string{}
208seenPurls := make(map[string]struct{})
209
210switch mergeStrategy {
211case define.SBOMMergeStrategyCycloneDXByComponentNameAndVersion:
212var base, merge map[string]any
213if err = decodeJSON(inputOutputSBOM, &base); err != nil {
214return fmt.Errorf("reading first SBOM to be merged from %q: %w", inputOutputSBOM, err)
215}
216if err = decodeJSON(inputSBOM, &merge); err != nil {
217return fmt.Errorf("reading second SBOM to be merged from %q: %w", inputSBOM, err)
218}
219
220// merge the "components" lists based on unique combinations of
221// "name" and "version" fields, and save unique package URL
222// values
223err = mergeSlicesWithoutDuplicates(base, merge, "components", func(anyPackage any) (string, error) {
224nameWithVersion, purl, err := getComponentNameVersionPurl(anyPackage)
225if purl != "" {
226if _, seen := seenPurls[purl]; !seen {
227purls = append(purls, purl)
228seenPurls[purl] = struct{}{}
229}
230}
231return nameWithVersion, err
232})
233if err != nil {
234return fmt.Errorf("merging the %q field of CycloneDX SBOMs: %w", "components", err)
235}
236
237// save the updated doc
238err = encodeJSON(inputOutputSBOM, base)
239
240case define.SBOMMergeStrategySPDXByPackageNameAndVersionInfo:
241var base, merge map[string]any
242if err = decodeJSON(inputOutputSBOM, &base); err != nil {
243return fmt.Errorf("reading first SBOM to be merged from %q: %w", inputOutputSBOM, err)
244}
245if err = decodeJSON(inputSBOM, &merge); err != nil {
246return fmt.Errorf("reading second SBOM to be merged from %q: %w", inputSBOM, err)
247}
248
249// merge the "packages" lists based on unique combinations of
250// "name" and "versionInfo" fields, and save unique package URL
251// values
252err = mergeSlicesWithoutDuplicates(base, merge, "packages", func(anyPackage any) (string, error) {
253nameWithVersionInfo, purl, err := getPackageNameVersionInfoPurl(anyPackage)
254if purl != "" {
255if _, seen := seenPurls[purl]; !seen {
256purls = append(purls, purl)
257seenPurls[purl] = struct{}{}
258}
259}
260return nameWithVersionInfo, err
261})
262if err != nil {
263return fmt.Errorf("merging the %q field of SPDX SBOMs: %w", "packages", err)
264}
265
266// merge the "hasExtractedLicensingInfos" lists based on unique
267// "licenseId" values
268err = mergeSlicesWithoutDuplicates(base, merge, "hasExtractedLicensingInfos", getLicenseID)
269if err != nil {
270return fmt.Errorf("merging the %q field of SPDX SBOMs: %w", "hasExtractedLicensingInfos", err)
271}
272
273// save the updated doc
274err = encodeJSON(inputOutputSBOM, base)
275
276case define.SBOMMergeStrategyCat:
277dst, err := os.OpenFile(inputOutputSBOM, os.O_RDWR|os.O_APPEND, 0o644)
278if err != nil {
279return err
280}
281defer dst.Close()
282src, err := os.Open(inputSBOM)
283if err != nil {
284return err
285}
286defer src.Close()
287if _, err = io.Copy(dst, src); err != nil {
288return err
289}
290}
291if err == nil {
292sort.Strings(purls)
293err = encodeJSON(outputPURL, &purlDocument{purlImageContents{Dependencies: purls}})
294}
295return err
296}
297