Skip to content

Commit 4072115

Browse files
afdeskknqyf263
andauthored
feat(cyclonedx): support dependency graph (#3177)
Co-authored-by: knqyf263 <[email protected]>
1 parent 7cad265 commit 4072115

File tree

6 files changed

+218
-66
lines changed

6 files changed

+218
-66
lines changed

integration/client_server_test.go

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -57,17 +57,23 @@ func TestClientServer(t *testing.T) {
5757
name: "alpine 3.9 with high and critical severity",
5858
args: csArgs{
5959
IgnoreUnfixed: true,
60-
Severity: []string{"HIGH", "CRITICAL"},
61-
Input: "testdata/fixtures/images/alpine-39.tar.gz",
60+
Severity: []string{
61+
"HIGH",
62+
"CRITICAL",
63+
},
64+
Input: "testdata/fixtures/images/alpine-39.tar.gz",
6265
},
6366
golden: "testdata/alpine-39-high-critical.json.golden",
6467
},
6568
{
6669
name: "alpine 3.9 with .trivyignore",
6770
args: csArgs{
6871
IgnoreUnfixed: false,
69-
IgnoreIDs: []string{"CVE-2019-1549", "CVE-2019-14697"},
70-
Input: "testdata/fixtures/images/alpine-39.tar.gz",
72+
IgnoreIDs: []string{
73+
"CVE-2019-1549",
74+
"CVE-2019-14697",
75+
},
76+
Input: "testdata/fixtures/images/alpine-39.tar.gz",
7177
},
7278
golden: "testdata/alpine-39-ignore-cveids.json.golden",
7379
},
@@ -401,7 +407,6 @@ func TestClientServerWithCycloneDX(t *testing.T) {
401407
args csArgs
402408
wantComponentsCount int
403409
wantDependenciesCount int
404-
wantDependsOnCount []int
405410
}{
406411
{
407412
name: "fluentd with RubyGems with CycloneDX format",
@@ -410,11 +415,7 @@ func TestClientServerWithCycloneDX(t *testing.T) {
410415
Input: "testdata/fixtures/images/fluentd-multiple-lockfiles.tar.gz",
411416
},
412417
wantComponentsCount: 161,
413-
wantDependenciesCount: 2,
414-
wantDependsOnCount: []int{
415-
105,
416-
56,
417-
},
418+
wantDependenciesCount: 80,
418419
},
419420
}
420421

@@ -437,9 +438,6 @@ func TestClientServerWithCycloneDX(t *testing.T) {
437438

438439
assert.EqualValues(t, tt.wantComponentsCount, len(lo.FromPtr(got.Components)))
439440
assert.EqualValues(t, tt.wantDependenciesCount, len(lo.FromPtr(got.Dependencies)))
440-
for i, dep := range *got.Dependencies {
441-
assert.EqualValues(t, tt.wantDependsOnCount[i], len(lo.FromPtr(dep.Dependencies)))
442-
}
443441
})
444442
}
445443
}
@@ -577,9 +575,21 @@ func setup(t *testing.T, options setupOptions) (string, string) {
577575
}
578576

579577
func setupServer(addr, token, tokenHeader, cacheDir, cacheBackend string) []string {
580-
osArgs := []string{"--cache-dir", cacheDir, "server", "--skip-update", "--listen", addr}
578+
osArgs := []string{
579+
"--cache-dir",
580+
cacheDir,
581+
"server",
582+
"--skip-update",
583+
"--listen",
584+
addr,
585+
}
581586
if token != "" {
582-
osArgs = append(osArgs, []string{"--token", token, "--token-header", tokenHeader}...)
587+
osArgs = append(osArgs, []string{
588+
"--token",
589+
token,
590+
"--token-header",
591+
tokenHeader,
592+
}...)
583593
}
584594
if cacheBackend != "" {
585595
osArgs = append(osArgs, "--cache-backend", cacheBackend)
@@ -595,7 +605,13 @@ func setupClient(t *testing.T, c csArgs, addr string, cacheDir string, golden st
595605
c.RemoteAddrOption = "--server"
596606
}
597607
t.Helper()
598-
osArgs := []string{"--cache-dir", cacheDir, c.Command, c.RemoteAddrOption, "http://" + addr}
608+
osArgs := []string{
609+
"--cache-dir",
610+
cacheDir,
611+
c.Command,
612+
c.RemoteAddrOption,
613+
"http://" + addr,
614+
}
599615

600616
if c.Format != "" {
601617
osArgs = append(osArgs, "--format", c.Format)

integration/testdata/conda-cyclonedx.json.golden

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,4 @@
8080
}
8181
],
8282
"vulnerabilities": []
83-
}
83+
}

pkg/fanal/types/artifact.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,24 @@ func (pkgs Packages) Less(i, j int) bool {
134134
return pkgs[i].FilePath < pkgs[j].FilePath
135135
}
136136

137+
// ParentDeps returns a map where the keys are package IDs and the values are the packages
138+
// that depend on the respective package ID (parent dependencies).
139+
func (pkgs Packages) ParentDeps() map[string]Packages {
140+
parents := make(map[string]Packages)
141+
for _, pkg := range pkgs {
142+
for _, dependOn := range pkg.DependsOn {
143+
parents[dependOn] = append(parents[dependOn], pkg)
144+
}
145+
}
146+
147+
for k, v := range parents {
148+
parents[k] = lo.UniqBy(v, func(pkg Package) string {
149+
return pkg.ID
150+
})
151+
}
152+
return parents
153+
}
154+
137155
type SrcPackage struct {
138156
Name string `json:"name"`
139157
Version string `json:"version"`

pkg/report/table/vulnerability.go

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ func (r *vulnerabilityRenderer) countSeverities(vulns []types.DetectedVulnerabil
146146

147147
func (r *vulnerabilityRenderer) renderDependencyTree() {
148148
// Get parents of each dependency
149-
parents := reverseDeps(r.result.Packages)
149+
parents := ftypes.Packages(r.result.Packages).ParentDeps()
150150
if len(parents) == 0 {
151151
return
152152
}
@@ -232,22 +232,6 @@ func addParents(topItem treeprint.Tree, pkg ftypes.Package, parentMap map[string
232232
}
233233
}
234234

235-
func reverseDeps(pkgs []ftypes.Package) map[string]ftypes.Packages {
236-
reversed := make(map[string]ftypes.Packages)
237-
for _, pkg := range pkgs {
238-
for _, dependOn := range pkg.DependsOn {
239-
reversed[dependOn] = append(reversed[dependOn], pkg)
240-
}
241-
}
242-
243-
for k, v := range reversed {
244-
reversed[k] = lo.UniqBy(v, func(pkg ftypes.Package) string {
245-
return pkg.ID
246-
})
247-
}
248-
return reversed
249-
}
250-
251235
func traverseAncestors(pkgs []ftypes.Package, parentMap map[string]ftypes.Packages) map[string][]string {
252236
ancestors := map[string][]string{}
253237
for _, pkg := range pkgs {

pkg/sbom/cyclonedx/marshal.go

Lines changed: 50 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -192,22 +192,32 @@ func externalRef(bomLink string, bomRef string) (string, error) {
192192

193193
func (e *Marshaler) marshalComponents(r types.Report, bomRef string) (*[]cdx.Component, *[]cdx.Dependency, *[]cdx.Vulnerability, error) {
194194
components := make([]cdx.Component, 0) // To export an empty array in JSON
195-
var dependencies []cdx.Dependency
195+
// we use map to avoid duplicate components
196+
dependencies := map[string]cdx.Dependency{}
196197
metadataDependencies := make([]string, 0) // To export an empty array in JSON
197198
libraryUniqMap := map[string]struct{}{}
198199
vulnMap := map[string]cdx.Vulnerability{}
199200
for _, result := range r.Results {
200201
bomRefMap := map[string]string{}
201-
var componentDependencies []string
202+
pkgIDToRef := map[string]string{}
203+
var directDepRefs []string
204+
205+
// Get dependency parents first
206+
parents := ftypes.Packages(result.Packages).ParentDeps()
207+
202208
for _, pkg := range result.Packages {
203209
pkgComponent, err := pkgToCdxComponent(result.Type, r.Metadata, pkg)
204210
if err != nil {
205211
return nil, nil, nil, xerrors.Errorf("failed to parse pkg: %w", err)
206212
}
207213
pkgID := packageID(result.Target, pkg.Name, utils.FormatVersion(pkg), pkg.FilePath)
208-
if _, ok := bomRefMap[pkgID]; !ok {
209-
bomRefMap[pkgID] = pkgComponent.BOMRef
210-
componentDependencies = append(componentDependencies, pkgComponent.BOMRef)
214+
bomRefMap[pkgID] = pkgComponent.BOMRef
215+
if pkg.ID != "" {
216+
pkgIDToRef[pkg.ID] = pkgComponent.BOMRef
217+
}
218+
// This package is a direct dependency
219+
if !pkg.Indirect || len(parents[pkg.ID]) == 0 {
220+
directDepRefs = append(directDepRefs, pkgComponent.BOMRef)
211221
}
212222

213223
// When multiple lock files have the same dependency with the same name and version,
@@ -226,12 +236,30 @@ func (e *Marshaler) marshalComponents(r types.Report, bomRef string) (*[]cdx.Com
226236

227237
// For components
228238
// ref. https://cyclonedx.org/use-cases/#inventory
229-
//
230-
// TODO: All packages are flattened at the moment. We should construct dependency tree.
231239
components = append(components, pkgComponent)
232240
}
233241
}
234242

243+
// Iterate packages again to build dependency graph
244+
for _, pkg := range result.Packages {
245+
deps := lo.FilterMap(pkg.DependsOn, func(dep string, _ int) (string, bool) {
246+
if ref, ok := pkgIDToRef[dep]; ok {
247+
return ref, true
248+
}
249+
return "", false
250+
})
251+
if len(deps) == 0 {
252+
continue
253+
}
254+
sort.Strings(deps)
255+
ref := pkgIDToRef[pkg.ID]
256+
dependencies[ref] = cdx.Dependency{
257+
Ref: ref,
258+
Dependencies: &deps,
259+
}
260+
}
261+
sort.Strings(directDepRefs)
262+
235263
for _, vuln := range result.Vulnerabilities {
236264
// Take a bom-ref
237265
pkgID := packageID(result.Target, vuln.PkgName, vuln.InstalledVersion, vuln.PkgPath)
@@ -260,7 +288,7 @@ func (e *Marshaler) marshalComponents(r types.Report, bomRef string) (*[]cdx.Com
260288
// ref. https://cyclonedx.org/use-cases/#inventory
261289

262290
// Dependency graph from #1 to #2
263-
metadataDependencies = append(metadataDependencies, componentDependencies...)
291+
metadataDependencies = append(metadataDependencies, directDepRefs...)
264292
} else if result.Class == types.ClassOSPkg || result.Class == types.ClassLangPkg {
265293
// If a package is OS package, it will be a dependency of "Operating System" component.
266294
// e.g.
@@ -283,29 +311,29 @@ func (e *Marshaler) marshalComponents(r types.Report, bomRef string) (*[]cdx.Com
283311
components = append(components, resultComponent)
284312

285313
// Dependency graph from #2 to #3
286-
dependencies = append(dependencies,
287-
cdx.Dependency{
288-
Ref: resultComponent.BOMRef,
289-
Dependencies: &componentDependencies,
290-
},
291-
)
292-
314+
dependencies[resultComponent.BOMRef] = cdx.Dependency{
315+
Ref: resultComponent.BOMRef,
316+
Dependencies: &directDepRefs,
317+
}
293318
// Dependency graph from #1 to #2
294319
metadataDependencies = append(metadataDependencies, resultComponent.BOMRef)
295320
}
296321
}
322+
297323
vulns := maps.Values(vulnMap)
298324
sort.Slice(vulns, func(i, j int) bool {
299325
return vulns[i].ID > vulns[j].ID
300326
})
301327

302-
dependencies = append(dependencies,
303-
cdx.Dependency{
304-
Ref: bomRef,
305-
Dependencies: &metadataDependencies,
306-
},
307-
)
308-
return &components, &dependencies, &vulns, nil
328+
dependencies[bomRef] = cdx.Dependency{
329+
Ref: bomRef,
330+
Dependencies: &metadataDependencies,
331+
}
332+
dependencyList := maps.Values(dependencies)
333+
sort.Slice(dependencyList, func(i, j int) bool {
334+
return dependencyList[i].Ref < dependencyList[j].Ref
335+
})
336+
return &components, &dependencyList, &vulns, nil
309337
}
310338

311339
func packageID(target, pkgName, pkgVersion, pkgFilePath string) string {

0 commit comments

Comments
 (0)