feat(cyclonedx): support dependency graph (#3177)

Co-authored-by: knqyf263 <knqyf263@gmail.com>
This commit is contained in:
afdesk
2023-04-01 03:46:30 +06:00
committed by GitHub
parent 7cad265b7a
commit 4072115e5a
6 changed files with 222 additions and 70 deletions

View File

@@ -57,8 +57,11 @@ func TestClientServer(t *testing.T) {
name: "alpine 3.9 with high and critical severity",
args: csArgs{
IgnoreUnfixed: true,
Severity: []string{"HIGH", "CRITICAL"},
Input: "testdata/fixtures/images/alpine-39.tar.gz",
Severity: []string{
"HIGH",
"CRITICAL",
},
Input: "testdata/fixtures/images/alpine-39.tar.gz",
},
golden: "testdata/alpine-39-high-critical.json.golden",
},
@@ -66,8 +69,11 @@ func TestClientServer(t *testing.T) {
name: "alpine 3.9 with .trivyignore",
args: csArgs{
IgnoreUnfixed: false,
IgnoreIDs: []string{"CVE-2019-1549", "CVE-2019-14697"},
Input: "testdata/fixtures/images/alpine-39.tar.gz",
IgnoreIDs: []string{
"CVE-2019-1549",
"CVE-2019-14697",
},
Input: "testdata/fixtures/images/alpine-39.tar.gz",
},
golden: "testdata/alpine-39-ignore-cveids.json.golden",
},
@@ -401,7 +407,6 @@ func TestClientServerWithCycloneDX(t *testing.T) {
args csArgs
wantComponentsCount int
wantDependenciesCount int
wantDependsOnCount []int
}{
{
name: "fluentd with RubyGems with CycloneDX format",
@@ -410,11 +415,7 @@ func TestClientServerWithCycloneDX(t *testing.T) {
Input: "testdata/fixtures/images/fluentd-multiple-lockfiles.tar.gz",
},
wantComponentsCount: 161,
wantDependenciesCount: 2,
wantDependsOnCount: []int{
105,
56,
},
wantDependenciesCount: 80,
},
}
@@ -437,9 +438,6 @@ func TestClientServerWithCycloneDX(t *testing.T) {
assert.EqualValues(t, tt.wantComponentsCount, len(lo.FromPtr(got.Components)))
assert.EqualValues(t, tt.wantDependenciesCount, len(lo.FromPtr(got.Dependencies)))
for i, dep := range *got.Dependencies {
assert.EqualValues(t, tt.wantDependsOnCount[i], len(lo.FromPtr(dep.Dependencies)))
}
})
}
}
@@ -577,9 +575,21 @@ func setup(t *testing.T, options setupOptions) (string, string) {
}
func setupServer(addr, token, tokenHeader, cacheDir, cacheBackend string) []string {
osArgs := []string{"--cache-dir", cacheDir, "server", "--skip-update", "--listen", addr}
osArgs := []string{
"--cache-dir",
cacheDir,
"server",
"--skip-update",
"--listen",
addr,
}
if token != "" {
osArgs = append(osArgs, []string{"--token", token, "--token-header", tokenHeader}...)
osArgs = append(osArgs, []string{
"--token",
token,
"--token-header",
tokenHeader,
}...)
}
if cacheBackend != "" {
osArgs = append(osArgs, "--cache-backend", cacheBackend)
@@ -595,7 +605,13 @@ func setupClient(t *testing.T, c csArgs, addr string, cacheDir string, golden st
c.RemoteAddrOption = "--server"
}
t.Helper()
osArgs := []string{"--cache-dir", cacheDir, c.Command, c.RemoteAddrOption, "http://" + addr}
osArgs := []string{
"--cache-dir",
cacheDir,
c.Command,
c.RemoteAddrOption,
"http://" + addr,
}
if c.Format != "" {
osArgs = append(osArgs, "--format", c.Format)

View File

@@ -80,4 +80,4 @@
}
],
"vulnerabilities": []
}
}

View File

@@ -134,6 +134,24 @@ func (pkgs Packages) Less(i, j int) bool {
return pkgs[i].FilePath < pkgs[j].FilePath
}
// ParentDeps returns a map where the keys are package IDs and the values are the packages
// that depend on the respective package ID (parent dependencies).
func (pkgs Packages) ParentDeps() map[string]Packages {
parents := make(map[string]Packages)
for _, pkg := range pkgs {
for _, dependOn := range pkg.DependsOn {
parents[dependOn] = append(parents[dependOn], pkg)
}
}
for k, v := range parents {
parents[k] = lo.UniqBy(v, func(pkg Package) string {
return pkg.ID
})
}
return parents
}
type SrcPackage struct {
Name string `json:"name"`
Version string `json:"version"`

View File

@@ -146,7 +146,7 @@ func (r *vulnerabilityRenderer) countSeverities(vulns []types.DetectedVulnerabil
func (r *vulnerabilityRenderer) renderDependencyTree() {
// Get parents of each dependency
parents := reverseDeps(r.result.Packages)
parents := ftypes.Packages(r.result.Packages).ParentDeps()
if len(parents) == 0 {
return
}
@@ -232,22 +232,6 @@ func addParents(topItem treeprint.Tree, pkg ftypes.Package, parentMap map[string
}
}
func reverseDeps(pkgs []ftypes.Package) map[string]ftypes.Packages {
reversed := make(map[string]ftypes.Packages)
for _, pkg := range pkgs {
for _, dependOn := range pkg.DependsOn {
reversed[dependOn] = append(reversed[dependOn], pkg)
}
}
for k, v := range reversed {
reversed[k] = lo.UniqBy(v, func(pkg ftypes.Package) string {
return pkg.ID
})
}
return reversed
}
func traverseAncestors(pkgs []ftypes.Package, parentMap map[string]ftypes.Packages) map[string][]string {
ancestors := map[string][]string{}
for _, pkg := range pkgs {

View File

@@ -192,22 +192,32 @@ func externalRef(bomLink string, bomRef string) (string, error) {
func (e *Marshaler) marshalComponents(r types.Report, bomRef string) (*[]cdx.Component, *[]cdx.Dependency, *[]cdx.Vulnerability, error) {
components := make([]cdx.Component, 0) // To export an empty array in JSON
var dependencies []cdx.Dependency
// we use map to avoid duplicate components
dependencies := map[string]cdx.Dependency{}
metadataDependencies := make([]string, 0) // To export an empty array in JSON
libraryUniqMap := map[string]struct{}{}
vulnMap := map[string]cdx.Vulnerability{}
for _, result := range r.Results {
bomRefMap := map[string]string{}
var componentDependencies []string
pkgIDToRef := map[string]string{}
var directDepRefs []string
// Get dependency parents first
parents := ftypes.Packages(result.Packages).ParentDeps()
for _, pkg := range result.Packages {
pkgComponent, err := pkgToCdxComponent(result.Type, r.Metadata, pkg)
if err != nil {
return nil, nil, nil, xerrors.Errorf("failed to parse pkg: %w", err)
}
pkgID := packageID(result.Target, pkg.Name, utils.FormatVersion(pkg), pkg.FilePath)
if _, ok := bomRefMap[pkgID]; !ok {
bomRefMap[pkgID] = pkgComponent.BOMRef
componentDependencies = append(componentDependencies, pkgComponent.BOMRef)
bomRefMap[pkgID] = pkgComponent.BOMRef
if pkg.ID != "" {
pkgIDToRef[pkg.ID] = pkgComponent.BOMRef
}
// This package is a direct dependency
if !pkg.Indirect || len(parents[pkg.ID]) == 0 {
directDepRefs = append(directDepRefs, pkgComponent.BOMRef)
}
// 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
// For components
// ref. https://cyclonedx.org/use-cases/#inventory
//
// TODO: All packages are flattened at the moment. We should construct dependency tree.
components = append(components, pkgComponent)
}
}
// Iterate packages again to build dependency graph
for _, pkg := range result.Packages {
deps := lo.FilterMap(pkg.DependsOn, func(dep string, _ int) (string, bool) {
if ref, ok := pkgIDToRef[dep]; ok {
return ref, true
}
return "", false
})
if len(deps) == 0 {
continue
}
sort.Strings(deps)
ref := pkgIDToRef[pkg.ID]
dependencies[ref] = cdx.Dependency{
Ref: ref,
Dependencies: &deps,
}
}
sort.Strings(directDepRefs)
for _, vuln := range result.Vulnerabilities {
// Take a bom-ref
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
// ref. https://cyclonedx.org/use-cases/#inventory
// Dependency graph from #1 to #2
metadataDependencies = append(metadataDependencies, componentDependencies...)
metadataDependencies = append(metadataDependencies, directDepRefs...)
} else if result.Class == types.ClassOSPkg || result.Class == types.ClassLangPkg {
// If a package is OS package, it will be a dependency of "Operating System" component.
// e.g.
@@ -283,29 +311,29 @@ func (e *Marshaler) marshalComponents(r types.Report, bomRef string) (*[]cdx.Com
components = append(components, resultComponent)
// Dependency graph from #2 to #3
dependencies = append(dependencies,
cdx.Dependency{
Ref: resultComponent.BOMRef,
Dependencies: &componentDependencies,
},
)
dependencies[resultComponent.BOMRef] = cdx.Dependency{
Ref: resultComponent.BOMRef,
Dependencies: &directDepRefs,
}
// Dependency graph from #1 to #2
metadataDependencies = append(metadataDependencies, resultComponent.BOMRef)
}
}
vulns := maps.Values(vulnMap)
sort.Slice(vulns, func(i, j int) bool {
return vulns[i].ID > vulns[j].ID
})
dependencies = append(dependencies,
cdx.Dependency{
Ref: bomRef,
Dependencies: &metadataDependencies,
},
)
return &components, &dependencies, &vulns, nil
dependencies[bomRef] = cdx.Dependency{
Ref: bomRef,
Dependencies: &metadataDependencies,
}
dependencyList := maps.Values(dependencies)
sort.Slice(dependencyList, func(i, j int) bool {
return dependencyList[i].Ref < dependencyList[j].Ref
})
return &components, &dependencyList, &vulns, nil
}
func packageID(target, pkgName, pkgVersion, pkgFilePath string) string {

View File

@@ -57,6 +57,7 @@ func TestMarshaler_Marshal(t *testing.T) {
Type: fos.CentOS,
Packages: []ftypes.Package{
{
ID: "binutils@2.30-93.el8",
Name: "binutils",
Version: "2.30",
Release: "93.el8",
@@ -122,12 +123,17 @@ func TestMarshaler_Marshal(t *testing.T) {
Type: ftypes.Bundler,
Packages: []ftypes.Package{
{
ID: "actionpack@7.0.0",
Name: "actionpack",
Version: "7.0.0",
},
{
ID: "actioncontroller@7.0.0",
Name: "actioncontroller",
Version: "7.0.0",
DependsOn: []string{
"actionpack@7.0.0",
},
},
},
},
@@ -137,6 +143,7 @@ func TestMarshaler_Marshal(t *testing.T) {
Type: ftypes.Bundler,
Packages: []ftypes.Package{
{
ID: "actionpack@7.0.0",
Name: "actionpack",
Version: "7.0.0",
},
@@ -148,6 +155,7 @@ func TestMarshaler_Marshal(t *testing.T) {
Type: ftypes.DotNetCore,
Packages: []ftypes.Package{
{
ID: "Newtonsoft.Json@9.0.1",
Name: "Newtonsoft.Json",
Version: "9.0.1",
},
@@ -225,6 +233,10 @@ func TestMarshaler_Marshal(t *testing.T) {
},
PackageURL: "pkg:rpm/centos/binutils@2.30-93.el8?arch=aarch64&distro=centos-8.3.2011",
Properties: &[]cdx.Property{
{
Name: "aquasecurity:trivy:PkgID",
Value: "binutils@2.30-93.el8",
},
{
Name: "aquasecurity:trivy:PkgType",
Value: "centos",
@@ -266,6 +278,10 @@ func TestMarshaler_Marshal(t *testing.T) {
Version: "7.0.0",
PackageURL: "pkg:gem/actionpack@7.0.0",
Properties: &[]cdx.Property{
{
Name: "aquasecurity:trivy:PkgID",
Value: "actionpack@7.0.0",
},
{
Name: "aquasecurity:trivy:PkgType",
Value: "bundler",
@@ -279,6 +295,10 @@ func TestMarshaler_Marshal(t *testing.T) {
Version: "7.0.0",
PackageURL: "pkg:gem/actioncontroller@7.0.0",
Properties: &[]cdx.Property{
{
Name: "aquasecurity:trivy:PkgID",
Value: "actioncontroller@7.0.0",
},
{
Name: "aquasecurity:trivy:PkgType",
Value: "bundler",
@@ -324,6 +344,10 @@ func TestMarshaler_Marshal(t *testing.T) {
Version: "9.0.1",
PackageURL: "pkg:nuget/Newtonsoft.Json@9.0.1",
Properties: &[]cdx.Property{
{
Name: "aquasecurity:trivy:PkgID",
Value: "Newtonsoft.Json@9.0.1",
},
{
Name: "aquasecurity:trivy:PkgType",
Value: "dotnet-core",
@@ -386,8 +410,8 @@ func TestMarshaler_Marshal(t *testing.T) {
{
Ref: "3ff14136-e09f-4df9-80ea-000000000003",
Dependencies: &[]string{
"pkg:gem/actionpack@7.0.0",
"pkg:gem/actioncontroller@7.0.0",
"pkg:gem/actionpack@7.0.0",
},
},
{
@@ -408,6 +432,12 @@ func TestMarshaler_Marshal(t *testing.T) {
"pkg:golang/golang.org/x/crypto@v0.0.0-20210421170649-83a5a9bb288b",
},
},
{
Ref: "pkg:gem/actioncontroller@7.0.0",
Dependencies: &[]string{
"pkg:gem/actionpack@7.0.0",
},
},
{
Ref: "pkg:oci/rails@sha256:a27fd8080b517143cbbbab9dfb7c8571c40d67d534bbdee55bd6c473f432b177?repository_url=index.docker.io%2Flibrary%2Frails&arch=arm64",
Dependencies: &[]string{
@@ -514,6 +544,7 @@ func TestMarshaler_Marshal(t *testing.T) {
Type: fos.CentOS,
Packages: []ftypes.Package{
{
ID: "acl@2.2.53-1.el8",
Name: "acl",
Version: "2.2.53",
Release: "1.el8",
@@ -525,6 +556,23 @@ func TestMarshaler_Marshal(t *testing.T) {
SrcEpoch: 1,
Modularitylabel: "",
Licenses: []string{"GPLv2+"},
DependsOn: []string{
"glibc@2.28-151.el8",
},
},
{
ID: "glibc@2.28-151.el8",
Name: "glibc",
Version: "2.28",
Release: "151.el8",
Epoch: 0,
Arch: "aarch64",
SrcName: "glibc",
SrcVersion: "2.28",
SrcRelease: "151.el8",
SrcEpoch: 0,
Modularitylabel: "",
Licenses: []string{"GPLv2+"},
},
},
},
@@ -534,6 +582,7 @@ func TestMarshaler_Marshal(t *testing.T) {
Type: ftypes.GemSpec,
Packages: []ftypes.Package{
{
ID: "actionpack@7.0.0",
Name: "actionpack",
Version: "7.0.0",
Layer: ftypes.Layer{
@@ -542,6 +591,7 @@ func TestMarshaler_Marshal(t *testing.T) {
FilePath: "tools/project-john/specifications/actionpack.gemspec",
},
{
ID: "actionpack@7.0.1",
Name: "actionpack",
Version: "7.0.1",
Layer: ftypes.Layer{
@@ -690,6 +740,10 @@ func TestMarshaler_Marshal(t *testing.T) {
},
PackageURL: "pkg:rpm/centos/acl@2.2.53-1.el8?arch=aarch64&epoch=1&distro=centos-8.3.2011",
Properties: &[]cdx.Property{
{
Name: "aquasecurity:trivy:PkgID",
Value: "acl@2.2.53-1.el8",
},
{
Name: "aquasecurity:trivy:PkgType",
Value: "centos",
@@ -712,6 +766,38 @@ func TestMarshaler_Marshal(t *testing.T) {
},
},
},
{
BOMRef: "pkg:rpm/centos/glibc@2.28-151.el8?arch=aarch64&distro=centos-8.3.2011",
Type: cdx.ComponentTypeLibrary,
Name: "glibc",
Version: "2.28-151.el8",
Licenses: &cdx.Licenses{
cdx.LicenseChoice{Expression: "GPLv2+"},
},
PackageURL: "pkg:rpm/centos/glibc@2.28-151.el8?arch=aarch64&distro=centos-8.3.2011",
Properties: &[]cdx.Property{
{
Name: "aquasecurity:trivy:PkgID",
Value: "glibc@2.28-151.el8",
},
{
Name: "aquasecurity:trivy:PkgType",
Value: "centos",
},
{
Name: "aquasecurity:trivy:SrcName",
Value: "glibc",
},
{
Name: "aquasecurity:trivy:SrcVersion",
Value: "2.28",
},
{
Name: "aquasecurity:trivy:SrcRelease",
Value: "151.el8",
},
},
},
{
BOMRef: "3ff14136-e09f-4df9-80ea-000000000003",
Type: cdx.ComponentTypeOS,
@@ -735,6 +821,10 @@ func TestMarshaler_Marshal(t *testing.T) {
Version: "7.0.0",
PackageURL: "pkg:gem/actionpack@7.0.0",
Properties: &[]cdx.Property{
{
Name: "aquasecurity:trivy:PkgID",
Value: "actionpack@7.0.0",
},
{
Name: "aquasecurity:trivy:PkgType",
Value: "gemspec",
@@ -756,6 +846,10 @@ func TestMarshaler_Marshal(t *testing.T) {
Version: "7.0.1",
PackageURL: "pkg:gem/actionpack@7.0.1",
Properties: &[]cdx.Property{
{
Name: "aquasecurity:trivy:PkgID",
Value: "actionpack@7.0.1",
},
{
Name: "aquasecurity:trivy:PkgType",
Value: "gemspec",
@@ -772,12 +866,6 @@ func TestMarshaler_Marshal(t *testing.T) {
},
},
Dependencies: &[]cdx.Dependency{
{
Ref: "3ff14136-e09f-4df9-80ea-000000000003",
Dependencies: &[]string{
"pkg:rpm/centos/acl@2.2.53-1.el8?arch=aarch64&epoch=1&distro=centos-8.3.2011",
},
},
{
Ref: "3ff14136-e09f-4df9-80ea-000000000002",
Dependencies: &[]string{
@@ -786,6 +874,20 @@ func TestMarshaler_Marshal(t *testing.T) {
"pkg:gem/actionpack@7.0.1?file_path=tools%2Fproject-doe%2Fspecifications%2Factionpack.gemspec",
},
},
{
Ref: "3ff14136-e09f-4df9-80ea-000000000003",
Dependencies: &[]string{
"pkg:rpm/centos/acl@2.2.53-1.el8?arch=aarch64&epoch=1&distro=centos-8.3.2011",
// Trivy is unable to identify the direct OS packages as of today.
"pkg:rpm/centos/glibc@2.28-151.el8?arch=aarch64&distro=centos-8.3.2011",
},
},
{
Ref: "pkg:rpm/centos/acl@2.2.53-1.el8?arch=aarch64&epoch=1&distro=centos-8.3.2011",
Dependencies: &[]string{
"pkg:rpm/centos/glibc@2.28-151.el8?arch=aarch64&distro=centos-8.3.2011",
},
},
},
Vulnerabilities: &[]cdx.Vulnerability{
{
@@ -944,18 +1046,18 @@ func TestMarshaler_Marshal(t *testing.T) {
},
Vulnerabilities: &[]cdx.Vulnerability{},
Dependencies: &[]cdx.Dependency{
{
Ref: "3ff14136-e09f-4df9-80ea-000000000003",
Dependencies: &[]string{
"pkg:gem/actioncable@6.1.4.1",
},
},
{
Ref: "3ff14136-e09f-4df9-80ea-000000000002",
Dependencies: &[]string{
"3ff14136-e09f-4df9-80ea-000000000003",
},
},
{
Ref: "3ff14136-e09f-4df9-80ea-000000000003",
Dependencies: &[]string{
"pkg:gem/actioncable@6.1.4.1",
},
},
},
},
},
@@ -972,6 +1074,7 @@ func TestMarshaler_Marshal(t *testing.T) {
Type: ftypes.NodePkg,
Packages: []ftypes.Package{
{
ID: "ruby-typeprof@0.20.1",
Name: "ruby-typeprof",
Version: "0.20.1",
Licenses: []string{"MIT"},
@@ -1022,6 +1125,10 @@ func TestMarshaler_Marshal(t *testing.T) {
cdx.LicenseChoice{Expression: "MIT"},
},
Properties: &[]cdx.Property{
{
Name: "aquasecurity:trivy:PkgID",
Value: "ruby-typeprof@0.20.1",
},
{
Name: "aquasecurity:trivy:PkgType",
Value: "node-pkg",
@@ -1108,7 +1215,6 @@ func TestMarshaler_Marshal(t *testing.T) {
marshaler := cyclonedx.NewMarshaler("dev", cyclonedx.WithClock(clock), cyclonedx.WithNewUUID(newUUID))
got, err := marshaler.Marshal(tt.inputReport)
require.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}