podman

Форк
0
296 строк · 9.1 Кб
1
package sbom
2

3
import (
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
15
func getComponentNameVersionPurl(anyComponent any) (string, string, error) {
16
	if component, ok := anyComponent.(map[string]any); ok {
17
		// read the "name" field
18
		anyName, ok := component["name"]
19
		if !ok {
20
			return "", "", fmt.Errorf("no name in component %v", anyComponent)
21
		}
22
		name, ok := anyName.(string)
23
		if !ok {
24
			return "", "", fmt.Errorf("name %v is not a string", anyName)
25
		}
26
		// read the optional "version" field
27
		var version string
28
		anyVersion, ok := component["version"]
29
		if ok {
30
			if version, ok = anyVersion.(string); !ok {
31
				return "", "", fmt.Errorf("version %v is not a string", anyVersion)
32
			}
33
		}
34
		// combine them
35
		nameWithVersion := name
36
		if version != "" {
37
			nameWithVersion += ("@" + version)
38
		}
39
		// read the optional "purl" field
40
		var purl string
41
		anyPurl, ok := component["purl"]
42
		if ok {
43
			if purl, ok = anyPurl.(string); !ok {
44
				return "", "", fmt.Errorf("purl %v is not a string", anyPurl)
45
			}
46
		}
47
		return nameWithVersion, purl, nil
48
	}
49
	return "", "", 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
54
func getPackageNameVersionInfoPurl(anyPackage any) (string, string, error) {
55
	if pkg, ok := anyPackage.(map[string]any); ok {
56
		// read the "name" field
57
		anyName, ok := pkg["name"]
58
		if !ok {
59
			return "", "", fmt.Errorf("no name in package %v", anyPackage)
60
		}
61
		name, ok := anyName.(string)
62
		if !ok {
63
			return "", "", fmt.Errorf("name %v is not a string", anyName)
64
		}
65
		// read the optional "versionInfo" field
66
		var versionInfo string
67
		if anyVersionInfo, ok := pkg["versionInfo"]; ok {
68
			if versionInfo, ok = anyVersionInfo.(string); !ok {
69
				return "", "", fmt.Errorf("versionInfo %v is not a string", anyVersionInfo)
70
			}
71
		}
72
		// combine them
73
		nameWithVersionInfo := name
74
		if versionInfo != "" {
75
			nameWithVersionInfo += ("@" + versionInfo)
76
		}
77
		// now look for optional externalRefs[].purl if "referenceCategory"
78
		// is "PACKAGE-MANAGER" and "referenceType" is "purl"
79
		var purl string
80
		if anyExternalRefs, ok := pkg["externalRefs"]; ok {
81
			if externalRefs, ok := anyExternalRefs.([]any); ok {
82
				for _, anyExternalRef := range externalRefs {
83
					if externalRef, ok := anyExternalRef.(map[string]any); ok {
84
						anyReferenceCategory, ok := externalRef["referenceCategory"]
85
						if !ok {
86
							continue
87
						}
88
						if referenceCategory, ok := anyReferenceCategory.(string); !ok || referenceCategory != "PACKAGE-MANAGER" {
89
							continue
90
						}
91
						anyReferenceType, ok := externalRef["referenceType"]
92
						if !ok {
93
							continue
94
						}
95
						if referenceType, ok := anyReferenceType.(string); !ok || referenceType != "purl" {
96
							continue
97
						}
98
						if anyReferenceLocator, ok := externalRef["referenceLocator"]; ok {
99
							if purl, ok = anyReferenceLocator.(string); !ok {
100
								return "", "", fmt.Errorf("purl %v is not a string", anyReferenceLocator)
101
							}
102
						}
103
					}
104
				}
105
			}
106
		}
107
		return nameWithVersionInfo, purl, nil
108
	}
109
	return "", "", fmt.Errorf("package %v is not an object", anyPackage)
110
}
111

112
// getLicenseID extracts the "licenseId" field of an SPDX license record
113
func getLicenseID(anyLicense any) (string, error) {
114
	var licenseID string
115
	if lic, ok := anyLicense.(map[string]any); ok {
116
		anyID, ok := lic["licenseId"]
117
		if !ok {
118
			return "", fmt.Errorf("no licenseId in license %v", anyID)
119
		}
120
		id, ok := anyID.(string)
121
		if !ok {
122
			return "", fmt.Errorf("licenseId %v is not a string", anyID)
123
		}
124
		licenseID = id
125
	}
126
	return 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
132
func mergeSlicesWithoutDuplicates(base, merge map[string]any, sliceField string, getKey func(record any) (string, error)) error {
133
	uniqueKeys := make(map[string]struct{})
134
	// go through all of the values in the base slice, grab their
135
	// keys, and note them
136
	baseRecords := base[sliceField]
137
	baseRecordsSlice, ok := baseRecords.([]any)
138
	if !ok {
139
		baseRecordsSlice = []any{}
140
	}
141
	for _, anyRecord := range baseRecordsSlice {
142
		key, err := getKey(anyRecord)
143
		if err != nil {
144
			return err
145
		}
146
		uniqueKeys[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
151
	mergeRecords := merge[sliceField]
152
	mergeRecordsSlice, ok := mergeRecords.([]any)
153
	if !ok {
154
		mergeRecordsSlice = []any{}
155
	}
156
	for _, anyRecord := range mergeRecordsSlice {
157
		key, err := getKey(anyRecord)
158
		if err != nil {
159
			return err
160
		}
161
		if _, present := uniqueKeys[key]; !present {
162
			baseRecordsSlice = append(baseRecordsSlice, anyRecord)
163
			uniqueKeys[key] = struct{}{}
164
		}
165
	}
166
	if len(baseRecordsSlice) > 0 {
167
		base[sliceField] = baseRecordsSlice
168
	}
169
	return nil
170
}
171

172
// decodeJSON decodes a file into a map
173
func decodeJSON(inputFile string, document *map[string]any) error {
174
	src, err := os.Open(inputFile)
175
	if err != nil {
176
		return err
177
	}
178
	defer src.Close()
179
	if err = json.NewDecoder(src).Decode(document); err != nil {
180
		return fmt.Errorf("decoding JSON document from %q: %w", inputFile, err)
181
	}
182
	return nil
183
}
184

185
// encodeJSON encodes a map and saves it to a file
186
func encodeJSON(outputFile string, document any) error {
187
	dst, err := os.Create(outputFile)
188
	if err != nil {
189
		return err
190
	}
191
	defer dst.Close()
192
	if err = json.NewEncoder(dst).Encode(document); err != nil {
193
		return fmt.Errorf("writing JSON document to %q: %w", outputFile, err)
194
	}
195
	return nil
196
}
197

198
// Merge adds the contents of inputSBOM to inputOutputSBOM using one of a
199
// handful of named strategies.
200
func Merge(mergeStrategy define.SBOMMergeStrategy, inputOutputSBOM, inputSBOM, outputPURL string) (err error) {
201
	type purlImageContents struct {
202
		Dependencies []string `json:"dependencies,omitempty"`
203
	}
204
	type purlDocument struct {
205
		ImageContents purlImageContents `json:"image_contents,omitempty"`
206
	}
207
	purls := []string{}
208
	seenPurls := make(map[string]struct{})
209

210
	switch mergeStrategy {
211
	case define.SBOMMergeStrategyCycloneDXByComponentNameAndVersion:
212
		var base, merge map[string]any
213
		if err = decodeJSON(inputOutputSBOM, &base); err != nil {
214
			return fmt.Errorf("reading first SBOM to be merged from %q: %w", inputOutputSBOM, err)
215
		}
216
		if err = decodeJSON(inputSBOM, &merge); err != nil {
217
			return 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
223
		err = mergeSlicesWithoutDuplicates(base, merge, "components", func(anyPackage any) (string, error) {
224
			nameWithVersion, purl, err := getComponentNameVersionPurl(anyPackage)
225
			if purl != "" {
226
				if _, seen := seenPurls[purl]; !seen {
227
					purls = append(purls, purl)
228
					seenPurls[purl] = struct{}{}
229
				}
230
			}
231
			return nameWithVersion, err
232
		})
233
		if err != nil {
234
			return fmt.Errorf("merging the %q field of CycloneDX SBOMs: %w", "components", err)
235
		}
236

237
		// save the updated doc
238
		err = encodeJSON(inputOutputSBOM, base)
239

240
	case define.SBOMMergeStrategySPDXByPackageNameAndVersionInfo:
241
		var base, merge map[string]any
242
		if err = decodeJSON(inputOutputSBOM, &base); err != nil {
243
			return fmt.Errorf("reading first SBOM to be merged from %q: %w", inputOutputSBOM, err)
244
		}
245
		if err = decodeJSON(inputSBOM, &merge); err != nil {
246
			return 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
252
		err = mergeSlicesWithoutDuplicates(base, merge, "packages", func(anyPackage any) (string, error) {
253
			nameWithVersionInfo, purl, err := getPackageNameVersionInfoPurl(anyPackage)
254
			if purl != "" {
255
				if _, seen := seenPurls[purl]; !seen {
256
					purls = append(purls, purl)
257
					seenPurls[purl] = struct{}{}
258
				}
259
			}
260
			return nameWithVersionInfo, err
261
		})
262
		if err != nil {
263
			return 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
268
		err = mergeSlicesWithoutDuplicates(base, merge, "hasExtractedLicensingInfos", getLicenseID)
269
		if err != nil {
270
			return fmt.Errorf("merging the %q field of SPDX SBOMs: %w", "hasExtractedLicensingInfos", err)
271
		}
272

273
		// save the updated doc
274
		err = encodeJSON(inputOutputSBOM, base)
275

276
	case define.SBOMMergeStrategyCat:
277
		dst, err := os.OpenFile(inputOutputSBOM, os.O_RDWR|os.O_APPEND, 0o644)
278
		if err != nil {
279
			return err
280
		}
281
		defer dst.Close()
282
		src, err := os.Open(inputSBOM)
283
		if err != nil {
284
			return err
285
		}
286
		defer src.Close()
287
		if _, err = io.Copy(dst, src); err != nil {
288
			return err
289
		}
290
	}
291
	if err == nil {
292
		sort.Strings(purls)
293
		err = encodeJSON(outputPURL, &purlDocument{purlImageContents{Dependencies: purls}})
294
	}
295
	return err
296
}
297

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.