Skip to content

Commit 4ace979

Browse files
committed
spdx: Add converter for index reports
Adding a function to be able to convert index reports into SPDX documents and SPDX documents into index reports. Signed-off-by: crozzy <[email protected]>
1 parent 395b041 commit 4ace979

File tree

4 files changed

+460
-1
lines changed

4 files changed

+460
-1
lines changed

go.mod

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ require (
2222
github.com/quay/zlog v1.1.8
2323
github.com/remind101/migrate v0.0.0-20170729031349-52c1edff7319
2424
github.com/rs/zerolog v1.30.0
25+
github.com/spdx/tools-golang v0.5.3
2526
github.com/ulikunitz/xz v0.5.11
2627
go.opentelemetry.io/otel v1.24.0
2728
go.opentelemetry.io/otel/trace v1.24.0
@@ -35,6 +36,7 @@ require (
3536
)
3637

3738
require (
39+
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 // indirect
3840
github.com/beorn7/perks v1.0.1 // indirect
3941
github.com/cespare/xxhash/v2 v2.2.0 // indirect
4042
github.com/davecgh/go-spew v1.1.1 // indirect

go.sum

+12-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3Q
55
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
66
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
77
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
8+
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 h1:aM1rlcoLz8y5B2r4tTLMiVTrMtpfY0O8EScKJxaSaEc=
9+
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA=
810
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
911
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
1012
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
@@ -170,15 +172,22 @@ github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXY
170172
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
171173
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
172174
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
175+
github.com/spdx/gordf v0.0.0-20201111095634-7098f93598fb/go.mod h1:uKWaldnbMnjsSAXRurWqqrdyZen1R7kxl8TkmWk2OyM=
176+
github.com/spdx/tools-golang v0.5.3 h1:ialnHeEYUC4+hkm5vJm4qz2x+oEJbS0mAMFrNXdQraY=
177+
github.com/spdx/tools-golang v0.5.3/go.mod h1:/ETOahiAo96Ob0/RAIBmFZw6XN0yTnyr/uFZm2NTMhI=
173178
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
174179
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
175-
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
176180
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
181+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
182+
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
183+
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
177184
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
178185
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
179186
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
180187
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
181188
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
189+
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
190+
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
182191
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
183192
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
184193
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
@@ -290,6 +299,7 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
290299
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
291300
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
292301
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
302+
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
293303
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
294304
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
295305
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@@ -310,3 +320,4 @@ modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
310320
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
311321
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
312322
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
323+
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=

pkg/sbom/spdx/spdx.go

+314
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
package spdx
2+
3+
import (
4+
"fmt"
5+
"runtime/debug"
6+
"time"
7+
8+
"github.com/google/uuid"
9+
"github.com/spdx/tools-golang/spdx/v2/common"
10+
spdxtools "github.com/spdx/tools-golang/spdx/v2/v2_3"
11+
12+
"github.com/quay/claircore"
13+
"github.com/quay/claircore/pkg/cpe"
14+
)
15+
16+
func ParseSPDXDocument(sd *spdxtools.Document) (*claircore.IndexReport, error) {
17+
pkgMap := map[string]*spdxtools.Package{}
18+
for _, p := range sd.Packages {
19+
pkgMap[string(p.PackageSPDXIdentifier)] = p
20+
}
21+
digest, err := claircore.ParseDigest(sd.DocumentName)
22+
if err != nil {
23+
return nil, fmt.Errorf("cannot parse document name as a digest: %w", err)
24+
}
25+
out := &claircore.IndexReport{
26+
Hash: digest,
27+
Repositories: map[string]*claircore.Repository{},
28+
Packages: map[string]*claircore.Package{},
29+
Distributions: map[string]*claircore.Distribution{},
30+
Environments: map[string][]*claircore.Environment{},
31+
Success: true,
32+
}
33+
for _, r := range sd.Relationships {
34+
aPkg := pkgMap[string(r.RefA.ElementRefID)]
35+
bPkg := pkgMap[string(r.RefB.ElementRefID)]
36+
37+
if r.Relationship == "CONTAINED_BY" {
38+
if bPkg.PackageSummary == "repository" {
39+
// Create repository
40+
repo := &claircore.Repository{
41+
ID: string(bPkg.PackageSPDXIdentifier),
42+
Name: bPkg.PackageName,
43+
}
44+
for _, er := range bPkg.PackageExternalReferences {
45+
switch er.RefType {
46+
case "cpe23Type":
47+
if er.Locator == "" {
48+
continue
49+
}
50+
repo.CPE, err = cpe.Unbind(er.Locator)
51+
if err != nil {
52+
return nil, fmt.Errorf("error unbinding repository CPE: %w", err)
53+
}
54+
case "url":
55+
repo.URI = er.Locator
56+
case "key":
57+
repo.Key = er.Locator
58+
}
59+
}
60+
out.Repositories[string(bPkg.PackageSPDXIdentifier)] = repo
61+
if _, ok := out.Packages[string(aPkg.PackageSPDXIdentifier)]; !ok {
62+
out.Packages[string(aPkg.PackageSPDXIdentifier)] = &claircore.Package{
63+
ID: string(aPkg.PackageSPDXIdentifier),
64+
Name: aPkg.PackageName,
65+
Version: aPkg.PackageVersion,
66+
Kind: claircore.BINARY,
67+
}
68+
}
69+
}
70+
if bPkg.PackageSummary == "distribution" {
71+
if _, ok := out.Distributions[string(bPkg.PackageSPDXIdentifier)]; !ok {
72+
dist := &claircore.Distribution{
73+
ID: string(bPkg.PackageSPDXIdentifier),
74+
Name: bPkg.PackageName,
75+
Version: bPkg.PackageVersion,
76+
}
77+
for _, er := range bPkg.PackageExternalReferences {
78+
switch er.RefType {
79+
case "cpe23Type":
80+
if er.Locator == "" {
81+
continue
82+
}
83+
dist.CPE, err = cpe.Unbind(er.Locator)
84+
if err != nil {
85+
return nil, fmt.Errorf("error unbinding distribution CPE: %w", err)
86+
}
87+
case "did":
88+
dist.DID = er.Locator
89+
case "version_id":
90+
dist.VersionID = er.Locator
91+
case "pretty_name":
92+
dist.PrettyName = er.Locator
93+
}
94+
}
95+
out.Distributions[string(bPkg.PackageSPDXIdentifier)] = dist
96+
}
97+
}
98+
}
99+
// Make or get environment for package
100+
envs, ok := out.Environments[string(aPkg.PackageSPDXIdentifier)]
101+
if !ok {
102+
envs = append(envs, &claircore.Environment{
103+
PackageDB: aPkg.PackageFileName,
104+
})
105+
}
106+
if r.Relationship == "CONTAINED_BY" {
107+
switch bPkg.PackageSummary {
108+
case "layer":
109+
envs[0].IntroducedIn = claircore.MustParseDigest(bPkg.PackageName)
110+
case "repository":
111+
envs[0].RepositoryIDs = append(envs[0].RepositoryIDs, string(bPkg.PackageSPDXIdentifier))
112+
case "distribution":
113+
envs[0].DistributionID = string(bPkg.PackageSPDXIdentifier)
114+
}
115+
}
116+
out.Environments[string(aPkg.PackageSPDXIdentifier)] = envs
117+
}
118+
// Go through and add the source packages
119+
for _, r := range sd.Relationships {
120+
aPkg := pkgMap[string(r.RefA.ElementRefID)]
121+
bPkg := pkgMap[string(r.RefB.ElementRefID)]
122+
if r.Relationship == "GENERATED_FROM" {
123+
out.Packages[string(aPkg.PackageSPDXIdentifier)].Source = &claircore.Package{
124+
ID: string(bPkg.PackageSPDXIdentifier),
125+
Name: bPkg.PackageName,
126+
Version: bPkg.PackageVersion,
127+
Kind: claircore.SOURCE,
128+
}
129+
}
130+
}
131+
return out, nil
132+
}
133+
134+
func ParseIndexReport(ir *claircore.IndexReport) (*spdxtools.Document, error) {
135+
// Initial metadata
136+
out := &spdxtools.Document{
137+
SPDXVersion: spdxtools.Version,
138+
DataLicense: spdxtools.DataLicense,
139+
SPDXIdentifier: "DOCUMENT",
140+
DocumentName: ir.Hash.String(),
141+
// This would be nice to have but don't know how we'd get context w/o
142+
// having to accept it as an argument.
143+
// DocumentNamespace: "https://clairproject.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301",
144+
CreationInfo: &spdxtools.CreationInfo{
145+
Creators: []common.Creator{
146+
{CreatorType: "Tool", Creator: "Claircore"},
147+
{CreatorType: "Organization", Creator: "Clair"},
148+
},
149+
Created: time.Now().Format("2006-01-02T15:04:05Z"),
150+
},
151+
DocumentComment: fmt.Sprintf("This document was created using claircore (%s).", getVersion()),
152+
}
153+
154+
rels := []*spdxtools.Relationship{}
155+
repoMap := map[string]*spdxtools.Package{}
156+
distMap := map[string]*spdxtools.Package{}
157+
for _, r := range ir.IndexRecords() {
158+
pkgDB := ""
159+
for _, e := range ir.Environments[r.Package.ID] {
160+
if e.PackageDB != "" {
161+
pkgDB = e.PackageDB
162+
}
163+
}
164+
pkg := &spdxtools.Package{
165+
PackageName: r.Package.Name,
166+
PackageSPDXIdentifier: common.ElementID(r.Package.ID),
167+
PackageVersion: r.Package.Version,
168+
PackageFileName: pkgDB,
169+
PackageDownloadLocation: "NOASSERTION",
170+
FilesAnalyzed: true,
171+
}
172+
out.Packages = append(out.Packages, pkg)
173+
if r.Package.Source != nil {
174+
srcPkg := &spdxtools.Package{
175+
PackageName: r.Package.Source.Name,
176+
PackageSPDXIdentifier: common.ElementID(r.Package.Source.ID),
177+
PackageVersion: r.Package.Source.Version,
178+
}
179+
out.Packages = append(out.Packages, srcPkg)
180+
rels = append(rels, &spdxtools.Relationship{
181+
RefA: common.MakeDocElementID("", string(pkg.PackageSPDXIdentifier)),
182+
RefB: common.MakeDocElementID("", string(srcPkg.PackageSPDXIdentifier)),
183+
Relationship: "GENERATED_FROM",
184+
})
185+
}
186+
if r.Repository != nil {
187+
repo, ok := repoMap[r.Repository.ID]
188+
if !ok {
189+
repo = &spdxtools.Package{
190+
PackageName: r.Repository.Name,
191+
PackageSPDXIdentifier: common.ElementID(r.Repository.ID),
192+
FilesAnalyzed: true,
193+
PackageSummary: "repository",
194+
PackageExternalReferences: []*spdxtools.PackageExternalReference{
195+
{
196+
Category: "SECURITY",
197+
// TODO: always cpe:2.3?
198+
RefType: "cpe23Type",
199+
Locator: r.Repository.CPE.String(),
200+
},
201+
{
202+
Category: "OTHER",
203+
RefType: "url",
204+
Locator: r.Repository.URI,
205+
},
206+
{
207+
Category: "OTHER",
208+
RefType: "key",
209+
Locator: r.Repository.Key,
210+
},
211+
},
212+
}
213+
repoMap[r.Repository.ID] = repo
214+
}
215+
out.Packages = append(out.Packages, repo)
216+
rel := &spdxtools.Relationship{
217+
RefA: common.MakeDocElementID("", string(pkg.PackageSPDXIdentifier)),
218+
RefB: common.MakeDocElementID("", string(repo.PackageSPDXIdentifier)),
219+
Relationship: "CONTAINED_BY",
220+
}
221+
rels = append(rels, rel)
222+
}
223+
if r.Distribution != nil {
224+
dist, ok := distMap[r.Distribution.ID]
225+
if !ok {
226+
dist = &spdxtools.Package{
227+
PackageName: r.Distribution.Name,
228+
PackageSPDXIdentifier: common.ElementID(r.Distribution.ID),
229+
PackageVersion: r.Distribution.Version,
230+
FilesAnalyzed: true,
231+
PackageSummary: "distribution",
232+
PackageExternalReferences: []*spdxtools.PackageExternalReference{
233+
{
234+
Category: "SECURITY",
235+
// TODO: always cpe:2.3?
236+
RefType: "cpe23Type",
237+
Locator: r.Distribution.CPE.String(),
238+
},
239+
{
240+
Category: "OTHER",
241+
RefType: "did",
242+
Locator: r.Distribution.DID,
243+
},
244+
{
245+
Category: "OTHER",
246+
RefType: "version_id",
247+
Locator: r.Distribution.VersionID,
248+
},
249+
{
250+
Category: "OTHER",
251+
RefType: "pretty_name",
252+
Locator: r.Distribution.PrettyName,
253+
},
254+
},
255+
}
256+
distMap[r.Distribution.ID] = dist
257+
}
258+
out.Packages = append(out.Packages, dist)
259+
rel := &spdxtools.Relationship{
260+
RefA: common.MakeDocElementID("", string(pkg.PackageSPDXIdentifier)),
261+
RefB: common.MakeDocElementID("", string(dist.PackageSPDXIdentifier)),
262+
Relationship: "CONTAINED_BY",
263+
}
264+
rels = append(rels, rel)
265+
}
266+
}
267+
268+
layerMap := map[string]*spdxtools.Package{}
269+
for pkgID, envs := range ir.Environments {
270+
for _, e := range envs {
271+
pkg, ok := layerMap[e.IntroducedIn.String()]
272+
if !ok {
273+
pkg = &spdxtools.Package{
274+
PackageName: e.IntroducedIn.String(),
275+
PackageSPDXIdentifier: common.ElementID(uuid.New().String()),
276+
FilesAnalyzed: true,
277+
PackageSummary: "layer",
278+
}
279+
out.Packages = append(out.Packages, pkg)
280+
layerMap[e.IntroducedIn.String()] = pkg
281+
}
282+
rel := &spdxtools.Relationship{
283+
RefA: common.MakeDocElementID("", pkgID),
284+
RefB: common.MakeDocElementID("", string(pkg.PackageSPDXIdentifier)),
285+
Relationship: "CONTAINED_BY",
286+
}
287+
rels = append(rels, rel)
288+
}
289+
}
290+
out.Relationships = rels
291+
return out, nil
292+
}
293+
294+
// GetVersion is copied from Clair and can hopefully give some
295+
// context as to which revision of claircore was used.
296+
func getVersion() string {
297+
info, infoOK := debug.ReadBuildInfo()
298+
var core string
299+
if infoOK {
300+
for _, m := range info.Deps {
301+
if m.Path != "github.com/quay/claircore" {
302+
continue
303+
}
304+
core = m.Version
305+
if m.Replace != nil && m.Replace.Version != m.Version {
306+
core = m.Replace.Version
307+
}
308+
}
309+
}
310+
if core == "" {
311+
core = "unknown revision"
312+
}
313+
return core
314+
}

0 commit comments

Comments
 (0)