From 8b5c0adc84a208b6c9e85fd9c14d497d0377e0ac Mon Sep 17 00:00:00 2001 From: masahiro331 Date: Wed, 14 Sep 2022 01:50:54 +0900 Subject: [PATCH 01/12] feat(spdx): create spdx package --- pkg/report/spdx/spdx.go | 178 ++------------- pkg/report/spdx/spdx_test.go | 407 ----------------------------------- pkg/sbom/spdx/marshal.go | 376 ++++++++++++++++++++++++++++++++ 3 files changed, 396 insertions(+), 565 deletions(-) delete mode 100644 pkg/report/spdx/spdx_test.go create mode 100644 pkg/sbom/spdx/marshal.go diff --git a/pkg/report/spdx/spdx.go b/pkg/report/spdx/spdx.go index 498a671500d..f86885edf14 100644 --- a/pkg/report/spdx/spdx.go +++ b/pkg/report/spdx/spdx.go @@ -1,185 +1,47 @@ package spdx import ( - "fmt" "io" - "strings" - "time" - "github.com/google/uuid" - "github.com/mitchellh/hashstructure/v2" "github.com/spdx/tools-golang/jsonsaver" - "github.com/spdx/tools-golang/spdx" "github.com/spdx/tools-golang/tvsaver" "golang.org/x/xerrors" - "k8s.io/utils/clock" - ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/sbom/spdx" "github.com/aquasecurity/trivy/pkg/types" ) -const ( - SPDXVersion = "SPDX-2.2" - DataLicense = "CC0-1.0" - SPDXIdentifier = "DOCUMENT" - DocumentNamespace = "http://aquasecurity.github.io/trivy" - CreatorOrganization = "aquasecurity" - CreatorTool = "trivy" -) - -type Hash func(v interface{}, format hashstructure.Format, opts *hashstructure.HashOptions) (uint64, error) - type Writer struct { - output io.Writer - version string - *options -} - -type newUUID func() uuid.UUID - -type options struct { - format spdx.Document2_1 - clock clock.Clock - newUUID newUUID - hasher Hash - spdxFormat string -} - -type option func(*options) - -type spdxSaveFunction func(*spdx.Document2_2, io.Writer) error - -func WithClock(clock clock.Clock) option { - return func(opts *options) { - opts.clock = clock - } -} - -func WithNewUUID(newUUID newUUID) option { - return func(opts *options) { - opts.newUUID = newUUID - } -} - -func WithHasher(hasher Hash) option { - return func(opts *options) { - opts.hasher = hasher - } + output io.Writer + version string + format string + marshaler *spdx.Marshaler } -func NewWriter(output io.Writer, version string, spdxFormat string, opts ...option) Writer { - o := &options{ - format: spdx.Document2_1{}, - clock: clock.RealClock{}, - newUUID: uuid.New, - hasher: hashstructure.Hash, - spdxFormat: spdxFormat, - } - - for _, opt := range opts { - opt(o) - } - +func NewWriter(output io.Writer, version string, spdxFormat string) Writer { return Writer{ - output: output, - version: version, - options: o, + output: output, + version: version, + format: spdxFormat, + marshaler: spdx.NewMarshaler(), } } -func (cw Writer) Write(report types.Report) error { - spdxDoc, err := cw.convertToBom(report, cw.version) +func (w Writer) Write(report types.Report) error { + spdxDoc, err := w.marshaler.Marshal(report) if err != nil { - return xerrors.Errorf("failed to convert bom: %w", err) + return xerrors.Errorf("failed to marshal spdx: %w", err) } - var saveFunc spdxSaveFunction - if cw.spdxFormat != "spdx-json" { - saveFunc = tvsaver.Save2_2 + if w.format == "spdx-json" { + if err := jsonsaver.Save2_2(spdxDoc, w.output); err != nil { + return xerrors.Errorf("failed to save spdx json: %w", err) + } } else { - saveFunc = jsonsaver.Save2_2 - } - - if err = saveFunc(spdxDoc, cw.output); err != nil { - return xerrors.Errorf("failed to save bom: %w", err) - } - return nil -} - -func (cw *Writer) convertToBom(r types.Report, version string) (*spdx.Document2_2, error) { - packages := make(map[spdx.ElementID]*spdx.Package2_2) - - for _, result := range r.Results { - for _, pkg := range result.Packages { - spdxPackage, err := cw.pkgToSpdxPackage(pkg) - if err != nil { - return nil, xerrors.Errorf("failed to parse pkg: %w", err) - } - packages[spdxPackage.PackageSPDXIdentifier] = &spdxPackage + if err := tvsaver.Save2_2(spdxDoc, w.output); err != nil { + return xerrors.Errorf("failed to save spdx tag-value: %w", err) } } - return &spdx.Document2_2{ - CreationInfo: &spdx.CreationInfo2_2{ - SPDXVersion: SPDXVersion, - DataLicense: DataLicense, - SPDXIdentifier: SPDXIdentifier, - DocumentName: r.ArtifactName, - DocumentNamespace: getDocumentNamespace(r, cw), - CreatorOrganizations: []string{CreatorOrganization}, - CreatorTools: []string{CreatorTool}, - Created: cw.clock.Now().UTC().Format(time.RFC3339Nano), - }, - Packages: packages, - }, nil -} - -func (cw *Writer) pkgToSpdxPackage(pkg ftypes.Package) (spdx.Package2_2, error) { - var spdxPackage spdx.Package2_2 - license := getLicense(pkg) - - pkgID, err := getPackageID(cw.hasher, pkg) - if err != nil { - return spdx.Package2_2{}, xerrors.Errorf("failed to get %s package ID: %w", pkg.Name, err) - } - - spdxPackage.PackageSPDXIdentifier = spdx.ElementID(pkgID) - spdxPackage.PackageName = pkg.Name - spdxPackage.PackageVersion = pkg.Version - - // The Declared License is what the authors of a project believe govern the package - spdxPackage.PackageLicenseConcluded = license - - // The Concluded License field is the license the SPDX file creator believes governs the package - spdxPackage.PackageLicenseDeclared = license - - return spdxPackage, nil -} - -func getLicense(p ftypes.Package) string { - if len(p.Licenses) == 0 { - return "NONE" - } - - return strings.Join(p.Licenses, ", ") -} - -func getDocumentNamespace(r types.Report, cw *Writer) string { - return DocumentNamespace + "/" + string(r.ArtifactType) + "/" + r.ArtifactName + "-" + cw.newUUID().String() -} - -func getPackageID(h Hash, p ftypes.Package) (string, error) { - // Not use these values for the hash - p.Layer = ftypes.Layer{} - p.FilePath = "" - - f, err := h(p, hashstructure.FormatV2, &hashstructure.HashOptions{ - ZeroNil: true, - SlicesAsSets: true, - }) - if err != nil { - return "", xerrors.Errorf("could not build package ID for package=%+v: %+v", p, err) - } - - return fmt.Sprintf("%x", f), nil + return nil } diff --git a/pkg/report/spdx/spdx_test.go b/pkg/report/spdx/spdx_test.go deleted file mode 100644 index fef62bdee00..00000000000 --- a/pkg/report/spdx/spdx_test.go +++ /dev/null @@ -1,407 +0,0 @@ -package spdx_test - -import ( - "bytes" - "fmt" - "hash/fnv" - "testing" - "time" - - "github.com/mitchellh/hashstructure/v2" - - fos "github.com/aquasecurity/trivy/pkg/fanal/analyzer/os" - ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" - "github.com/aquasecurity/trivy/pkg/report" - reportSpdx "github.com/aquasecurity/trivy/pkg/report/spdx" - "github.com/aquasecurity/trivy/pkg/types" - v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/google/uuid" - "github.com/spdx/tools-golang/jsonloader" - "github.com/spdx/tools-golang/spdx" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - fake "k8s.io/utils/clock/testing" -) - -func TestWriter_Write(t *testing.T) { - testCases := []struct { - name string - inputReport types.Report - wantSBOM *spdx.Document2_2 - }{ - { - name: "happy path for container scan", - inputReport: types.Report{ - SchemaVersion: report.SchemaVersion, - ArtifactName: "rails:latest", - ArtifactType: ftypes.ArtifactContainerImage, - Metadata: types.Metadata{ - Size: 1024, - OS: &ftypes.OS{ - Family: fos.CentOS, - Name: "8.3.2011", - Eosl: true, - }, - ImageID: "sha256:5d0da3dc976460b72c77d94c8a1ad043720b0416bfc16c52c45d4847e53fadb6", - RepoTags: []string{"rails:latest"}, - DiffIDs: []string{"sha256:d871dadfb37b53ef1ca45be04fc527562b91989991a8f545345ae3be0b93f92a"}, - RepoDigests: []string{"rails@sha256:a27fd8080b517143cbbbab9dfb7c8571c40d67d534bbdee55bd6c473f432b177"}, - ImageConfig: v1.ConfigFile{ - Architecture: "arm64", - }, - }, - Results: types.Results{ - { - Target: "rails:latest (centos 8.3.2011)", - Class: types.ClassOSPkg, - Type: fos.CentOS, - Packages: []ftypes.Package{ - { - Name: "binutils", - Version: "2.30", - Release: "93.el8", - Epoch: 0, - Arch: "aarch64", - SrcName: "binutils", - SrcVersion: "2.30", - SrcRelease: "93.el8", - SrcEpoch: 0, - Modularitylabel: "", - Licenses: []string{"GPLv3+"}, - }, - }, - }, - { - Target: "app/subproject/Gemfile.lock", - Class: types.ClassLangPkg, - Type: ftypes.Bundler, - Packages: []ftypes.Package{ - { - Name: "actionpack", - Version: "7.0.1", - }, - { - Name: "actioncontroller", - Version: "7.0.1", - }, - }, - }, - { - Target: "app/Gemfile.lock", - Class: types.ClassLangPkg, - Type: ftypes.Bundler, - Packages: []ftypes.Package{ - { - Name: "actionpack", - Version: "7.0.1", - }, - }, - }, - }, - }, - wantSBOM: &spdx.Document2_2{ - CreationInfo: &spdx.CreationInfo2_2{ - SPDXVersion: "SPDX-2.2", - DataLicense: "CC0-1.0", - SPDXIdentifier: "DOCUMENT", - DocumentName: "rails:latest", - DocumentNamespace: "http://aquasecurity.github.io/trivy/container_image/rails:latest-3ff14136-e09f-4df9-80ea-000000000001", - CreatorOrganizations: []string{"aquasecurity"}, - CreatorTools: []string{"trivy"}, - Created: "2021-08-25T12:20:30.000000005Z", - ExternalDocumentReferences: map[string]spdx.ExternalDocumentRef2_2{}, - }, - Packages: map[spdx.ElementID]*spdx.Package2_2{ - spdx.ElementID("eb0263038c3b445b"): { - PackageSPDXIdentifier: spdx.ElementID("eb0263038c3b445b"), - PackageName: "actioncontroller", - PackageVersion: "7.0.1", - PackageLicenseConcluded: "NONE", - PackageLicenseDeclared: "NONE", - IsFilesAnalyzedTagPresent: true, - }, - spdx.ElementID("826226d056ff30c0"): { - PackageSPDXIdentifier: spdx.ElementID("826226d056ff30c0"), - PackageName: "actionpack", - PackageVersion: "7.0.1", - PackageLicenseConcluded: "NONE", - PackageLicenseDeclared: "NONE", - IsFilesAnalyzedTagPresent: true, - }, - spdx.ElementID("fd0dc3cf913d5bc3"): { - PackageSPDXIdentifier: spdx.ElementID("fd0dc3cf913d5bc3"), - PackageName: "binutils", - PackageVersion: "2.30", - PackageLicenseConcluded: "GPLv3+", - PackageLicenseDeclared: "GPLv3+", - IsFilesAnalyzedTagPresent: true, - }, - }, - UnpackagedFiles: nil, - OtherLicenses: nil, - Relationships: nil, - Annotations: nil, - Reviews: nil, - }, - }, - { - name: "happy path for local container scan", - inputReport: types.Report{ - SchemaVersion: report.SchemaVersion, - ArtifactName: "centos:latest", - ArtifactType: ftypes.ArtifactContainerImage, - Metadata: types.Metadata{ - Size: 1024, - OS: &ftypes.OS{ - Family: fos.CentOS, - Name: "8.3.2011", - Eosl: true, - }, - ImageID: "sha256:5d0da3dc976460b72c77d94c8a1ad043720b0416bfc16c52c45d4847e53fadb6", - RepoTags: []string{"centos:latest"}, - RepoDigests: []string{}, - ImageConfig: v1.ConfigFile{ - Architecture: "arm64", - }, - }, - Results: types.Results{ - { - Target: "centos:latest (centos 8.3.2011)", - Class: types.ClassOSPkg, - Type: fos.CentOS, - Packages: []ftypes.Package{ - { - Name: "acl", - Version: "2.2.53", - Release: "1.el8", - Epoch: 1, - Arch: "aarch64", - SrcName: "acl", - SrcVersion: "2.2.53", - SrcRelease: "1.el8", - SrcEpoch: 1, - Modularitylabel: "", - Licenses: []string{"GPLv2+"}, - }, - }, - }, - { - Target: "Ruby", - Class: types.ClassLangPkg, - Type: ftypes.GemSpec, - Packages: []ftypes.Package{ - { - Name: "actionpack", - Version: "7.0.1", - Layer: ftypes.Layer{ - DiffID: "sha256:ccb64cf0b7ba2e50741d0b64cae324eb5de3b1e2f580bbf177e721b67df38488", - }, - FilePath: "tools/project-john/specifications/actionpack.gemspec", - }, - { - Name: "actionpack", - Version: "7.0.1", - Layer: ftypes.Layer{ - DiffID: "sha256:ccb64cf0b7ba2e50741d0b64cae324eb5de3b1e2f580bbf177e721b67df38488", - }, - FilePath: "tools/project-doe/specifications/actionpack.gemspec", - }, - }, - }, - }, - }, - wantSBOM: &spdx.Document2_2{ - CreationInfo: &spdx.CreationInfo2_2{ - SPDXVersion: "SPDX-2.2", - DataLicense: "CC0-1.0", - SPDXIdentifier: "DOCUMENT", - DocumentName: "centos:latest", - DocumentNamespace: "http://aquasecurity.github.io/trivy/container_image/centos:latest-3ff14136-e09f-4df9-80ea-000000000001", - CreatorOrganizations: []string{"aquasecurity"}, - CreatorTools: []string{"trivy"}, - Created: "2021-08-25T12:20:30.000000005Z", - ExternalDocumentReferences: map[string]spdx.ExternalDocumentRef2_2{}, - }, - Packages: map[spdx.ElementID]*spdx.Package2_2{ - spdx.ElementID("d8dccb186bafaf37"): { - PackageSPDXIdentifier: spdx.ElementID("d8dccb186bafaf37"), - PackageName: "acl", - PackageVersion: "2.2.53", - PackageLicenseConcluded: "GPLv2+", - PackageLicenseDeclared: "GPLv2+", - IsFilesAnalyzedTagPresent: true, - }, - spdx.ElementID("826226d056ff30c0"): { - PackageSPDXIdentifier: spdx.ElementID("826226d056ff30c0"), - PackageName: "actionpack", - PackageVersion: "7.0.1", - PackageLicenseConcluded: "NONE", - PackageLicenseDeclared: "NONE", - IsFilesAnalyzedTagPresent: true, - }, - }, - UnpackagedFiles: nil, - OtherLicenses: nil, - Relationships: nil, - Annotations: nil, - Reviews: nil, - }, - }, - { - name: "happy path for fs scan", - inputReport: types.Report{ - SchemaVersion: report.SchemaVersion, - ArtifactName: "masahiro331/CVE-2021-41098", - ArtifactType: ftypes.ArtifactFilesystem, - Results: types.Results{ - { - Target: "Gemfile.lock", - Class: types.ClassLangPkg, - Type: ftypes.Bundler, - Packages: []ftypes.Package{ - { - Name: "actioncable", - Version: "6.1.4.1", - }, - }, - }, - }, - }, - wantSBOM: &spdx.Document2_2{ - CreationInfo: &spdx.CreationInfo2_2{ - SPDXVersion: "SPDX-2.2", - DataLicense: "CC0-1.0", - SPDXIdentifier: "DOCUMENT", - DocumentName: "masahiro331/CVE-2021-41098", - DocumentNamespace: "http://aquasecurity.github.io/trivy/filesystem/masahiro331/CVE-2021-41098-3ff14136-e09f-4df9-80ea-000000000001", - CreatorOrganizations: []string{"aquasecurity"}, - CreatorTools: []string{"trivy"}, - Created: "2021-08-25T12:20:30.000000005Z", - ExternalDocumentReferences: map[string]spdx.ExternalDocumentRef2_2{}, - }, - Packages: map[spdx.ElementID]*spdx.Package2_2{ - spdx.ElementID("3da61e86d0530402"): { - PackageSPDXIdentifier: spdx.ElementID("3da61e86d0530402"), - PackageName: "actioncable", - PackageVersion: "6.1.4.1", - PackageLicenseConcluded: "NONE", - PackageLicenseDeclared: "NONE", - IsFilesAnalyzedTagPresent: true, - }, - }, - }, - }, - { - name: "happy path aggregate results", - inputReport: types.Report{ - SchemaVersion: report.SchemaVersion, - ArtifactName: "test-aggregate", - ArtifactType: ftypes.ArtifactRemoteRepository, - Results: types.Results{ - { - Target: "Node.js", - Class: types.ClassLangPkg, - Type: ftypes.NodePkg, - Packages: []ftypes.Package{ - { - Name: "ruby-typeprof", - Version: "0.20.1", - Licenses: []string{"MIT"}, - Layer: ftypes.Layer{ - DiffID: "sha256:661c3fd3cc16b34c070f3620ca6b03b6adac150f9a7e5d0e3c707a159990f88e", - }, - FilePath: "usr/local/lib/ruby/gems/3.1.0/gems/typeprof-0.21.1/vscode/package.json", - }, - }, - }, - }, - }, - wantSBOM: &spdx.Document2_2{ - CreationInfo: &spdx.CreationInfo2_2{ - SPDXVersion: "SPDX-2.2", - DataLicense: "CC0-1.0", - SPDXIdentifier: "DOCUMENT", - DocumentName: "test-aggregate", - DocumentNamespace: "http://aquasecurity.github.io/trivy/repository/test-aggregate-3ff14136-e09f-4df9-80ea-000000000001", - CreatorOrganizations: []string{"aquasecurity"}, - CreatorTools: []string{"trivy"}, - Created: "2021-08-25T12:20:30.000000005Z", - ExternalDocumentReferences: map[string]spdx.ExternalDocumentRef2_2{}, - }, - Packages: map[spdx.ElementID]*spdx.Package2_2{ - spdx.ElementID("9f3d92e5ae2cadfb"): { - PackageSPDXIdentifier: spdx.ElementID("9f3d92e5ae2cadfb"), - PackageName: "ruby-typeprof", - PackageVersion: "0.20.1", - PackageLicenseConcluded: "MIT", - PackageLicenseDeclared: "MIT", - IsFilesAnalyzedTagPresent: true, - }, - }, - }, - }, - { - name: "happy path empty", - inputReport: types.Report{ - SchemaVersion: report.SchemaVersion, - ArtifactName: "empty/path", - ArtifactType: ftypes.ArtifactFilesystem, - Results: types.Results{}, - }, - wantSBOM: &spdx.Document2_2{ - CreationInfo: &spdx.CreationInfo2_2{ - SPDXVersion: "SPDX-2.2", - DataLicense: "CC0-1.0", - SPDXIdentifier: "DOCUMENT", - DocumentName: "empty/path", - DocumentNamespace: "http://aquasecurity.github.io/trivy/filesystem/empty/path-3ff14136-e09f-4df9-80ea-000000000001", - CreatorOrganizations: []string{"aquasecurity"}, - CreatorTools: []string{"trivy"}, - Created: "2021-08-25T12:20:30.000000005Z", - ExternalDocumentReferences: map[string]spdx.ExternalDocumentRef2_2{}, - }, - Packages: nil, - }, - }, - } - - clock := fake.NewFakeClock(time.Date(2021, 8, 25, 12, 20, 30, 5, time.UTC)) - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - var count int - newUUID := func() uuid.UUID { - count++ - return uuid.Must(uuid.Parse(fmt.Sprintf("3ff14136-e09f-4df9-80ea-%012d", count))) - } - - // Fake function calculating the hash value - h := fnv.New64() - hasher := func(v interface{}, format hashstructure.Format, opts *hashstructure.HashOptions) (uint64, error) { - pkg, ok := v.(ftypes.Package) - if !ok { - require.Failf(t, "unknown type", "%T", v) - } - - h.Reset() - if _, err := h.Write([]byte(pkg.Name)); err != nil { - return 0, err - } - - return h.Sum64(), nil - } - - output := bytes.NewBuffer(nil) - writer := reportSpdx.NewWriter(output, "dev", "spdx-json", - reportSpdx.WithClock(clock), reportSpdx.WithNewUUID(newUUID), reportSpdx.WithHasher(hasher)) - - err := writer.Write(tc.inputReport) - require.NoError(t, err) - - got, err := jsonloader.Load2_2(output) - require.NoError(t, err) - - assert.Equal(t, *tc.wantSBOM, *got) - }) - } -} diff --git a/pkg/sbom/spdx/marshal.go b/pkg/sbom/spdx/marshal.go new file mode 100644 index 00000000000..139227e7c2b --- /dev/null +++ b/pkg/sbom/spdx/marshal.go @@ -0,0 +1,376 @@ +package spdx + +import ( + "fmt" + "strconv" + "strings" + "time" + + "github.com/google/uuid" + "github.com/mitchellh/hashstructure/v2" + "github.com/spdx/tools-golang/spdx" + "golang.org/x/xerrors" + "k8s.io/utils/clock" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/purl" + "github.com/aquasecurity/trivy/pkg/scanner/utils" + "github.com/aquasecurity/trivy/pkg/types" +) + +const ( + SPDXVersion = "SPDX-2.2" + DataLicense = "CC0-1.0" + SPDXIdentifier = "DOCUMENT" + DocumentNamespace = "http://aquasecurity.github.io/trivy" + CreatorOrganization = "aquasecurity" + CreatorTool = "trivy" +) + +const ( + CategoryPackageManager = "PACKAGE-MANAGER" + RefTypePurl = "purl" + + PropertySchemaVersion = "SchemaVersion" + + // Image properties + PropertySize = "Size" + PropertyImageID = "ImageID" + PropertyRepoDigest = "RepoDigest" + PropertyDiffID = "DiffID" + PropertyRepoTag = "RepoTag" + + // Package properties + PropertyLayerDiffID = "LayerDiffID" + PropertyLayerDigest = "LayerDigest" + + RelationShipContains = "CONTAINS" + RelationShipDescribe = "DESCRIBE" + RelationShipDependsOn = "DEPENDS_ON" + + ElementOperatingSystem = "OperatingSystem" + ElementApplication = "Application" +) + +var ( + SourcePackagePrefix = "built package from" +) + +type Marshaler struct { + format spdx.Document2_1 + clock clock.Clock + newUUID newUUID + hasher Hash +} + +type Hash func(v interface{}, format hashstructure.Format, opts *hashstructure.HashOptions) (uint64, error) + +type newUUID func() uuid.UUID + +type marshalOption func(*Marshaler) + +func WithClock(clock clock.Clock) marshalOption { + return func(opts *Marshaler) { + opts.clock = clock + } +} + +func WithNewUUID(newUUID newUUID) marshalOption { + return func(opts *Marshaler) { + opts.newUUID = newUUID + } +} + +func WithHasher(hasher Hash) marshalOption { + return func(opts *Marshaler) { + opts.hasher = hasher + } +} + +func NewMarshaler(opts ...marshalOption) *Marshaler { + m := &Marshaler{ + format: spdx.Document2_1{}, + clock: clock.RealClock{}, + newUUID: uuid.New, + hasher: hashstructure.Hash, + } + + for _, opt := range opts { + opt(m) + } + + return m +} + +func relationShip(refA, refB spdx.ElementID, operator string) *spdx.Relationship2_2 { + ref := spdx.Relationship2_2{ + RefA: spdx.MakeDocElementID("", string(refA)), + RefB: spdx.MakeDocElementID("", string(refB)), + Relationship: operator, + } + return &ref +} + +func (m *Marshaler) Marshal(r types.Report) (*spdx.Document2_2, error) { + var relationShips []*spdx.Relationship2_2 + packages := make(map[spdx.ElementID]*spdx.Package2_2) + + reportPackage, err := m.reportToSpdxPackage(r) + if err != nil { + return nil, xerrors.Errorf("failed to parse report: %w", err) + } + packages[reportPackage.PackageSPDXIdentifier] = reportPackage + relationShips = append(relationShips, + relationShip(SPDXIdentifier, reportPackage.PackageSPDXIdentifier, RelationShipDescribe), + ) + + for _, result := range r.Results { + parentPackage, err := m.resultToSpdxPackage(result, r.Metadata.OS) + if err != nil { + return nil, xerrors.Errorf("failed to parse result: %w", err) + } + packages[parentPackage.PackageSPDXIdentifier] = &parentPackage + relationShips = append(relationShips, + relationShip(reportPackage.PackageSPDXIdentifier, parentPackage.PackageSPDXIdentifier, RelationShipContains), + ) + + for _, pkg := range result.Packages { + spdxPackage, err := m.pkgToSpdxPackage(result.Type, result.Class, r.Metadata, pkg) + if err != nil { + return nil, xerrors.Errorf("failed to parse package: %w", err) + } + packages[spdxPackage.PackageSPDXIdentifier] = &spdxPackage + relationShips = append(relationShips, + relationShip(parentPackage.PackageSPDXIdentifier, spdxPackage.PackageSPDXIdentifier, RelationShipDependsOn), + ) + } + } + + return &spdx.Document2_2{ + CreationInfo: &spdx.CreationInfo2_2{ + SPDXVersion: SPDXVersion, + DataLicense: DataLicense, + SPDXIdentifier: SPDXIdentifier, + DocumentName: r.ArtifactName, + DocumentNamespace: getDocumentNamespace(r, m), + CreatorOrganizations: []string{CreatorOrganization}, + CreatorTools: []string{CreatorTool}, + Created: m.clock.Now().UTC().Format(time.RFC3339Nano), + }, + Packages: packages, + Relationships: relationShips, + }, nil +} + +func (m *Marshaler) resultToSpdxPackage(result types.Result, os *ftypes.OS) (spdx.Package2_2, error) { + var pkg spdx.Package2_2 + var err error + switch result.Class { + case types.ClassOSPkg: + if os == nil { + } + pkg, err = m.operatingSystemPackage(os) + if err != nil { + return spdx.Package2_2{}, xerrors.Errorf("failed to parse operating system package: %w", err) + } + case types.ClassLangPkg: + pkg, err = m.applicationPackage(result.Target, result.Type) + if err != nil { + return spdx.Package2_2{}, xerrors.Errorf("failed to parse application package: %w", err) + } + } + return pkg, nil +} + +func (m *Marshaler) parseFile(filePath string) (spdx.File2_2, error) { + pkgID, err := getPackageID(m.hasher, filePath) + if err != nil { + return spdx.File2_2{}, xerrors.Errorf("failed to get %s package ID: %w", filePath, err) + } + file := spdx.File2_2{ + FileSPDXIdentifier: spdx.ElementID(fmt.Sprintf("File-%s", pkgID)), + FileName: filePath, + } + return file, nil +} + +func (m *Marshaler) operatingSystemPackage(osFound *ftypes.OS) (spdx.Package2_2, error) { + var spdxPackage spdx.Package2_2 + pkgID, err := getPackageID(m.hasher, osFound) + if err != nil { + return spdx.Package2_2{}, xerrors.Errorf("failed to get os metadata package ID: %w", err) + } + spdxPackage.PackageSPDXIdentifier = spdx.ElementID(fmt.Sprintf("%s-%s", ElementOperatingSystem, pkgID)) + spdxPackage.PackageName = osFound.Family + spdxPackage.PackageVersion = osFound.Name + return spdxPackage, nil +} + +func (m *Marshaler) reportToSpdxPackage(r types.Report) (*spdx.Package2_2, error) { + var spdxPackage spdx.Package2_2 + + attributionTexts := []string{attributionText(PropertySchemaVersion, strconv.Itoa(r.SchemaVersion))} + if r.Metadata.OS != nil { + p, err := purl.NewPackageURL(purl.TypeOCI, r.Metadata, ftypes.Package{}) + if err != nil { + return nil, xerrors.Errorf("failed to new package url for oci: %w", err) + } + if p.Type != "" { + spdxPackage.PackageExternalReferences = packageExternalReference(p.ToString()) + } + attributionTexts = appendAttributionText(attributionTexts, PropertyImageID, r.Metadata.ImageID) + } + + if r.Metadata.Size != 0 { + attributionTexts = appendAttributionText(attributionTexts, PropertySize, strconv.FormatInt(r.Metadata.Size, 10)) + } + + for _, d := range r.Metadata.RepoDigests { + attributionTexts = appendAttributionText(attributionTexts, PropertyRepoDigest, d) + } + for _, d := range r.Metadata.DiffIDs { + attributionTexts = appendAttributionText(attributionTexts, PropertyDiffID, d) + } + for _, t := range r.Metadata.RepoTags { + attributionTexts = appendAttributionText(attributionTexts, PropertyRepoTag, t) + } + + spdxPackage.PackageAttributionTexts = attributionTexts + pkgID, err := getPackageID(m.hasher, fmt.Sprintf("%s-%s", r.ArtifactName, r.ArtifactType)) + if err != nil { + return nil, xerrors.Errorf("failed to get %s package ID: %w", err) + } + spdxPackage.PackageSPDXIdentifier = spdx.ElementID(fmt.Sprintf("%s-%s", camelCase(string(r.ArtifactType)), pkgID)) + spdxPackage.PackageName = r.ArtifactName + + return &spdxPackage, nil +} + +func (m *Marshaler) applicationPackage(target, typ string) (spdx.Package2_2, error) { + var spdxPackage spdx.Package2_2 + + spdxPackage.PackageName = typ + spdxPackage.PackageSourceInfo = target + pkgID, err := getPackageID(m.hasher, fmt.Sprintf("%s-%s", target, typ)) + if err != nil { + return spdx.Package2_2{}, xerrors.Errorf("failed to get %s package ID: %w", target, err) + } + spdxPackage.PackageSPDXIdentifier = spdx.ElementID(fmt.Sprintf("%s-%s", ElementApplication, pkgID)) + + return spdxPackage, nil +} + +func (m *Marshaler) pkgToSpdxPackage(t string, class types.ResultClass, metadata types.Metadata, pkg ftypes.Package) (spdx.Package2_2, error) { + var spdxPackage spdx.Package2_2 + license := getLicense(pkg) + + pkgID, err := getPackageID(m.hasher, pkg) + if err != nil { + return spdx.Package2_2{}, xerrors.Errorf("failed to get %s package ID: %w", pkg.Name, err) + } + + spdxPackage.PackageSPDXIdentifier = spdx.ElementID(fmt.Sprintf("Package-%s", pkgID)) + spdxPackage.PackageName = pkg.Name + spdxPackage.PackageVersion = pkg.Version + + if class == types.ClassOSPkg { + spdxPackage.PackageSourceInfo = fmt.Sprintf("%s: %s %s", SourcePackagePrefix, pkg.SrcName, utils.FormatSrcVersion(pkg)) + } + + packageURL, err := purl.NewPackageURL(t, metadata, pkg) + if err != nil { + return spdx.Package2_2{}, xerrors.Errorf("failed to parse purl (%s): %w", pkg.Name, err) + } + spdxPackage.PackageExternalReferences = packageExternalReference(packageURL.String()) + + // The Declared License is what the authors of a project believe govern the package + spdxPackage.PackageLicenseConcluded = license + + // The Concluded License field is the license the SPDX file creator believes governs the package + spdxPackage.PackageLicenseDeclared = license + + spdxPackage.PackageAttributionTexts = appendAttributionText(spdxPackage.PackageAttributionTexts, PropertyLayerDigest, pkg.Layer.Digest) + spdxPackage.PackageAttributionTexts = appendAttributionText(spdxPackage.PackageAttributionTexts, PropertyLayerDiffID, pkg.Layer.DiffID) + + if pkg.FilePath != "" { + file, err := m.parseFile(pkg.FilePath) + if err != nil { + return spdx.Package2_2{}, xerrors.Errorf("failed to parse file: %w") + } + spdxPackage.Files = map[spdx.ElementID]*spdx.File2_2{ + file.FileSPDXIdentifier: &file, + } + } + + return spdxPackage, nil +} +func appendAttributionText(attributionTexts []string, key, value string) []string { + if value == "" { + return attributionTexts + } + return append(attributionTexts, attributionText(key, value)) +} + +func attributionText(key, value string) string { + return fmt.Sprintf("%s: %s", key, value) +} + +func packageExternalReference(packageURL string) []*spdx.PackageExternalReference2_2 { + return []*spdx.PackageExternalReference2_2{ + { + Category: CategoryPackageManager, + RefType: RefTypePurl, + Locator: packageURL, + }, + } +} + +func getLicense(p ftypes.Package) string { + if len(p.Licenses) == 0 { + return "NONE" + } + + return strings.Join(p.Licenses, ", ") +} + +func getDocumentNamespace(r types.Report, m *Marshaler) string { + return fmt.Sprintf("%s/%s/%s-%s", + DocumentNamespace, + string(r.ArtifactType), + r.ArtifactName, + m.newUUID().String(), + ) +} + +func getPackageID(h Hash, v interface{}) (string, error) { + f, err := h(v, hashstructure.FormatV2, &hashstructure.HashOptions{ + ZeroNil: true, + SlicesAsSets: true, + }) + if err != nil { + return "", xerrors.Errorf("could not build package ID for %+v: %w", v, err) + } + + return fmt.Sprintf("%x", f), nil +} + +func camelCase(inputUnderScoreStr string) (camelCase string) { + isToUpper := false + for k, v := range inputUnderScoreStr { + if k == 0 { + camelCase = strings.ToUpper(string(inputUnderScoreStr[0])) + } else { + if isToUpper { + camelCase += strings.ToUpper(string(v)) + isToUpper = false + } else { + if v == '_' { + isToUpper = true + } else { + camelCase += string(v) + } + } + } + } + return +} From 7a17ff753d418d15c3f60edd06db2906328f9bbb Mon Sep 17 00:00:00 2001 From: masahiro331 Date: Wed, 14 Sep 2022 01:51:09 +0900 Subject: [PATCH 02/12] feat(spdx): add test --- pkg/sbom/spdx/marshal_test.go | 688 ++++++++++++++++++++++++++++++++++ 1 file changed, 688 insertions(+) create mode 100644 pkg/sbom/spdx/marshal_test.go diff --git a/pkg/sbom/spdx/marshal_test.go b/pkg/sbom/spdx/marshal_test.go new file mode 100644 index 00000000000..6b5a28dd552 --- /dev/null +++ b/pkg/sbom/spdx/marshal_test.go @@ -0,0 +1,688 @@ +package spdx_test + +import ( + "fmt" + "hash/fnv" + "testing" + "time" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/uuid" + "github.com/mitchellh/hashstructure/v2" + "github.com/spdx/tools-golang/spdx" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + fake "k8s.io/utils/clock/testing" + + fos "github.com/aquasecurity/trivy/pkg/fanal/analyzer/os" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/report" + tspdx "github.com/aquasecurity/trivy/pkg/sbom/spdx" + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestMarshaler_Marshal(t *testing.T) { + testCases := []struct { + name string + inputReport types.Report + wantSBOM *spdx.Document2_2 + }{ + { + name: "happy path for container scan", + inputReport: types.Report{ + SchemaVersion: report.SchemaVersion, + ArtifactName: "rails:latest", + ArtifactType: ftypes.ArtifactContainerImage, + Metadata: types.Metadata{ + Size: 1024, + OS: &ftypes.OS{ + Family: fos.CentOS, + Name: "8.3.2011", + Eosl: true, + }, + ImageID: "sha256:5d0da3dc976460b72c77d94c8a1ad043720b0416bfc16c52c45d4847e53fadb6", + RepoTags: []string{"rails:latest"}, + DiffIDs: []string{"sha256:d871dadfb37b53ef1ca45be04fc527562b91989991a8f545345ae3be0b93f92a"}, + RepoDigests: []string{"rails@sha256:a27fd8080b517143cbbbab9dfb7c8571c40d67d534bbdee55bd6c473f432b177"}, + ImageConfig: v1.ConfigFile{ + Architecture: "arm64", + }, + }, + Results: types.Results{ + { + Target: "rails:latest (centos 8.3.2011)", + Class: types.ClassOSPkg, + Type: fos.CentOS, + Packages: []ftypes.Package{ + { + Name: "binutils", + Version: "2.30", + Release: "93.el8", + Epoch: 0, + Arch: "aarch64", + SrcName: "binutils", + SrcVersion: "2.30", + SrcRelease: "93.el8", + SrcEpoch: 0, + Modularitylabel: "", + Licenses: []string{"GPLv3+"}, + }, + }, + }, + { + Target: "app/subproject/Gemfile.lock", + Class: types.ClassLangPkg, + Type: ftypes.Bundler, + Packages: []ftypes.Package{ + { + Name: "actionpack", + Version: "7.0.1", + }, + { + Name: "actioncontroller", + Version: "7.0.1", + }, + }, + }, + { + Target: "app/Gemfile.lock", + Class: types.ClassLangPkg, + Type: ftypes.Bundler, + Packages: []ftypes.Package{ + { + Name: "actionpack", + Version: "7.0.1", + }, + }, + }, + }, + }, + wantSBOM: &spdx.Document2_2{ + CreationInfo: &spdx.CreationInfo2_2{ + SPDXVersion: "SPDX-2.2", + DataLicense: "CC0-1.0", + SPDXIdentifier: "DOCUMENT", + DocumentName: "rails:latest", + DocumentNamespace: "http://aquasecurity.github.io/trivy/container_image/rails:latest-3ff14136-e09f-4df9-80ea-000000000001", + CreatorOrganizations: []string{"aquasecurity"}, + CreatorTools: []string{"trivy"}, + Created: "2021-08-25T12:20:30.000000005Z", + }, + Packages: map[spdx.ElementID]*spdx.Package2_2{ + spdx.ElementID("ContainerImage-9396d894cd0cb6cb"): { + PackageSPDXIdentifier: spdx.ElementID("ContainerImage-9396d894cd0cb6cb"), + PackageName: "rails:latest", + PackageExternalReferences: []*spdx.PackageExternalReference2_2{ + { + Category: tspdx.CategoryPackageManager, + RefType: tspdx.RefTypePurl, + Locator: "pkg:oci/rails@sha256:a27fd8080b517143cbbbab9dfb7c8571c40d67d534bbdee55bd6c473f432b177?repository_url=index.docker.io%2Flibrary%2Frails&arch=arm64", + }, + }, + PackageAttributionTexts: []string{ + "SchemaVersion: 2", + "ImageID: sha256:5d0da3dc976460b72c77d94c8a1ad043720b0416bfc16c52c45d4847e53fadb6", + "Size: 1024", + "RepoDigest: rails@sha256:a27fd8080b517143cbbbab9dfb7c8571c40d67d534bbdee55bd6c473f432b177", + "DiffID: sha256:d871dadfb37b53ef1ca45be04fc527562b91989991a8f545345ae3be0b93f92a", + "RepoTag: rails:latest", + }, + }, + spdx.ElementID("Application-73c871d73f3c8248"): { + PackageSPDXIdentifier: spdx.ElementID("Application-73c871d73f3c8248"), + PackageName: "bundler", + PackageSourceInfo: "app/subproject/Gemfile.lock", + }, + spdx.ElementID("Application-c3fac92c1ac0a9fa"): { + PackageSPDXIdentifier: spdx.ElementID("Application-c3fac92c1ac0a9fa"), + PackageName: "bundler", + PackageSourceInfo: "app/Gemfile.lock", + }, + spdx.ElementID("OperatingSystem-197f9a00ebcb51f0"): { + PackageSPDXIdentifier: spdx.ElementID("OperatingSystem-197f9a00ebcb51f0"), + PackageName: "centos", + PackageVersion: "8.3.2011", + }, + + spdx.ElementID("Package-eb0263038c3b445b"): { + PackageSPDXIdentifier: spdx.ElementID("Package-eb0263038c3b445b"), + PackageName: "actioncontroller", + PackageVersion: "7.0.1", + PackageLicenseConcluded: "NONE", + PackageLicenseDeclared: "NONE", + PackageExternalReferences: []*spdx.PackageExternalReference2_2{ + { + Category: tspdx.CategoryPackageManager, + RefType: tspdx.RefTypePurl, + Locator: "pkg:gem/actioncontroller@7.0.1", + }, + }, + }, + spdx.ElementID("Package-826226d056ff30c0"): { + PackageSPDXIdentifier: spdx.ElementID("Package-826226d056ff30c0"), + PackageName: "actionpack", + PackageVersion: "7.0.1", + PackageLicenseConcluded: "NONE", + PackageLicenseDeclared: "NONE", + PackageExternalReferences: []*spdx.PackageExternalReference2_2{ + { + Category: tspdx.CategoryPackageManager, + RefType: tspdx.RefTypePurl, + Locator: "pkg:gem/actionpack@7.0.1", + }, + }, + }, + spdx.ElementID("Package-fd0dc3cf913d5bc3"): { + PackageSPDXIdentifier: spdx.ElementID("Package-fd0dc3cf913d5bc3"), + PackageName: "binutils", + PackageVersion: "2.30", + PackageLicenseConcluded: "GPLv3+", + PackageLicenseDeclared: "GPLv3+", + PackageExternalReferences: []*spdx.PackageExternalReference2_2{ + { + Category: tspdx.CategoryPackageManager, + RefType: tspdx.RefTypePurl, + Locator: "pkg:rpm/centos/binutils@2.30-93.el8?arch=aarch64&distro=centos-8.3.2011", + }, + }, + PackageSourceInfo: "built package from: binutils 2.30-93.el8", + }, + }, + Relationships: []*spdx.Relationship2_2{ + { + RefA: spdx.DocElementID{ElementRefID: "DOCUMENT"}, + RefB: spdx.DocElementID{ElementRefID: "ContainerImage-9396d894cd0cb6cb"}, + Relationship: "DESCRIBE", + }, + { + RefA: spdx.DocElementID{ElementRefID: "ContainerImage-9396d894cd0cb6cb"}, + RefB: spdx.DocElementID{ElementRefID: "OperatingSystem-197f9a00ebcb51f0"}, + Relationship: "CONTAINS", + }, + { + RefA: spdx.DocElementID{ElementRefID: "OperatingSystem-197f9a00ebcb51f0"}, + RefB: spdx.DocElementID{ElementRefID: "Package-fd0dc3cf913d5bc3"}, + Relationship: "DEPENDS_ON", + }, + { + RefA: spdx.DocElementID{ElementRefID: "ContainerImage-9396d894cd0cb6cb"}, + RefB: spdx.DocElementID{ElementRefID: "Application-73c871d73f3c8248"}, + Relationship: "CONTAINS", + }, + { + RefA: spdx.DocElementID{ElementRefID: "Application-73c871d73f3c8248"}, + RefB: spdx.DocElementID{ElementRefID: "Package-826226d056ff30c0"}, + Relationship: "DEPENDS_ON", + }, + { + RefA: spdx.DocElementID{ElementRefID: "Application-73c871d73f3c8248"}, + RefB: spdx.DocElementID{ElementRefID: "Package-eb0263038c3b445b"}, + Relationship: "DEPENDS_ON", + }, + { + RefA: spdx.DocElementID{ElementRefID: "ContainerImage-9396d894cd0cb6cb"}, + RefB: spdx.DocElementID{ElementRefID: "Application-c3fac92c1ac0a9fa"}, + Relationship: "CONTAINS", + }, + { + RefA: spdx.DocElementID{ElementRefID: "Application-c3fac92c1ac0a9fa"}, + RefB: spdx.DocElementID{ElementRefID: "Package-826226d056ff30c0"}, + Relationship: "DEPENDS_ON", + }, + }, + UnpackagedFiles: nil, + OtherLicenses: nil, + Annotations: nil, + Reviews: nil, + }, + }, + { + name: "happy path for local container scan", + inputReport: types.Report{ + SchemaVersion: report.SchemaVersion, + ArtifactName: "centos:latest", + ArtifactType: ftypes.ArtifactContainerImage, + Metadata: types.Metadata{ + Size: 1024, + OS: &ftypes.OS{ + Family: fos.CentOS, + Name: "8.3.2011", + Eosl: true, + }, + ImageID: "sha256:5d0da3dc976460b72c77d94c8a1ad043720b0416bfc16c52c45d4847e53fadb6", + RepoTags: []string{"centos:latest"}, + RepoDigests: []string{}, + ImageConfig: v1.ConfigFile{ + Architecture: "arm64", + }, + }, + Results: types.Results{ + { + Target: "centos:latest (centos 8.3.2011)", + Class: types.ClassOSPkg, + Type: fos.CentOS, + Packages: []ftypes.Package{ + { + Name: "acl", + Version: "2.2.53", + Release: "1.el8", + Epoch: 1, + Arch: "aarch64", + SrcName: "acl", + SrcVersion: "2.2.53", + SrcRelease: "1.el8", + SrcEpoch: 1, + Modularitylabel: "", + Licenses: []string{"GPLv2+"}, + }, + }, + }, + { + Target: "Ruby", + Class: types.ClassLangPkg, + Type: ftypes.GemSpec, + Packages: []ftypes.Package{ + { + Name: "actionpack", + Version: "7.0.1", + Layer: ftypes.Layer{ + DiffID: "sha256:ccb64cf0b7ba2e50741d0b64cae324eb5de3b1e2f580bbf177e721b67df38488", + }, + FilePath: "tools/project-john/specifications/actionpack.gemspec", + }, + { + Name: "actionpack", + Version: "7.0.1", + Layer: ftypes.Layer{ + DiffID: "sha256:ccb64cf0b7ba2e50741d0b64cae324eb5de3b1e2f580bbf177e721b67df38488", + }, + FilePath: "tools/project-doe/specifications/actionpack.gemspec", + }, + }, + }, + }, + }, + wantSBOM: &spdx.Document2_2{ + CreationInfo: &spdx.CreationInfo2_2{ + SPDXVersion: "SPDX-2.2", + DataLicense: "CC0-1.0", + SPDXIdentifier: "DOCUMENT", + DocumentName: "centos:latest", + DocumentNamespace: "http://aquasecurity.github.io/trivy/container_image/centos:latest-3ff14136-e09f-4df9-80ea-000000000001", + CreatorOrganizations: []string{"aquasecurity"}, + CreatorTools: []string{"trivy"}, + Created: "2021-08-25T12:20:30.000000005Z", + }, + Packages: map[spdx.ElementID]*spdx.Package2_2{ + spdx.ElementID("ContainerImage-413bfede37ad01fc"): { + PackageName: "centos:latest", + PackageSPDXIdentifier: "ContainerImage-413bfede37ad01fc", + PackageAttributionTexts: []string{ + "SchemaVersion: 2", + "ImageID: sha256:5d0da3dc976460b72c77d94c8a1ad043720b0416bfc16c52c45d4847e53fadb6", + "Size: 1024", + "RepoTag: centos:latest", + }, + }, + spdx.ElementID("Application-441a648f2aeeee72"): { + PackageSPDXIdentifier: spdx.ElementID("Application-441a648f2aeeee72"), + PackageName: "gemspec", + PackageSourceInfo: "Ruby", + }, + spdx.ElementID("OperatingSystem-197f9a00ebcb51f0"): { + PackageSPDXIdentifier: spdx.ElementID("OperatingSystem-197f9a00ebcb51f0"), + PackageName: "centos", + PackageVersion: "8.3.2011", + }, + spdx.ElementID("Package-d8dccb186bafaf37"): { + PackageSPDXIdentifier: spdx.ElementID("Package-d8dccb186bafaf37"), + PackageName: "acl", + PackageVersion: "2.2.53", + PackageLicenseConcluded: "GPLv2+", + PackageLicenseDeclared: "GPLv2+", + PackageExternalReferences: []*spdx.PackageExternalReference2_2{ + { + Category: tspdx.CategoryPackageManager, + RefType: tspdx.RefTypePurl, + Locator: "pkg:rpm/centos/acl@1:2.2.53-1.el8?arch=aarch64&distro=centos-8.3.2011", + }, + }, + PackageSourceInfo: "built package from: acl 1:2.2.53-1.el8", + }, + spdx.ElementID("Package-13fe667a0805e6b7"): { + PackageSPDXIdentifier: spdx.ElementID("Package-13fe667a0805e6b7"), + PackageName: "actionpack", + PackageVersion: "7.0.1", + PackageLicenseConcluded: "NONE", + PackageLicenseDeclared: "NONE", + PackageExternalReferences: []*spdx.PackageExternalReference2_2{ + { + Category: tspdx.CategoryPackageManager, + RefType: tspdx.RefTypePurl, + Locator: "pkg:gem/actionpack@7.0.1", + }, + }, + PackageAttributionTexts: []string{ + "LayerDiffID: sha256:ccb64cf0b7ba2e50741d0b64cae324eb5de3b1e2f580bbf177e721b67df38488", + }, + Files: map[spdx.ElementID]*spdx.File2_2{ + "File-fa42187221d0d0a8": { + FileSPDXIdentifier: "File-fa42187221d0d0a8", + FileName: "tools/project-doe/specifications/actionpack.gemspec", + }, + }, + }, + spdx.ElementID("Package-d5443dbcbba0dbd4"): { + PackageSPDXIdentifier: spdx.ElementID("Package-d5443dbcbba0dbd4"), + PackageName: "actionpack", + PackageVersion: "7.0.1", + PackageLicenseConcluded: "NONE", + PackageLicenseDeclared: "NONE", + PackageExternalReferences: []*spdx.PackageExternalReference2_2{ + { + Category: tspdx.CategoryPackageManager, + RefType: tspdx.RefTypePurl, + Locator: "pkg:gem/actionpack@7.0.1", + }, + }, + PackageAttributionTexts: []string{ + "LayerDiffID: sha256:ccb64cf0b7ba2e50741d0b64cae324eb5de3b1e2f580bbf177e721b67df38488", + }, + Files: map[spdx.ElementID]*spdx.File2_2{ + "File-6a540784b0dc6d55": { + FileSPDXIdentifier: "File-6a540784b0dc6d55", + FileName: "tools/project-john/specifications/actionpack.gemspec", + }, + }, + }, + }, + Relationships: []*spdx.Relationship2_2{ + { + RefA: spdx.DocElementID{ElementRefID: "DOCUMENT"}, + RefB: spdx.DocElementID{ElementRefID: "ContainerImage-413bfede37ad01fc"}, + Relationship: "DESCRIBE", + }, + { + RefA: spdx.DocElementID{ElementRefID: "ContainerImage-413bfede37ad01fc"}, + RefB: spdx.DocElementID{ElementRefID: "OperatingSystem-197f9a00ebcb51f0"}, + Relationship: "CONTAINS", + }, + { + RefA: spdx.DocElementID{ElementRefID: "OperatingSystem-197f9a00ebcb51f0"}, + RefB: spdx.DocElementID{ElementRefID: "Package-d8dccb186bafaf37"}, + Relationship: "DEPENDS_ON", + }, + { + RefA: spdx.DocElementID{ElementRefID: "ContainerImage-413bfede37ad01fc"}, + RefB: spdx.DocElementID{ElementRefID: "Application-441a648f2aeeee72"}, + Relationship: "CONTAINS", + }, + { + RefA: spdx.DocElementID{ElementRefID: "Application-441a648f2aeeee72"}, + RefB: spdx.DocElementID{ElementRefID: "Package-d5443dbcbba0dbd4"}, + Relationship: "DEPENDS_ON", + }, + { + RefA: spdx.DocElementID{ElementRefID: "Application-441a648f2aeeee72"}, + RefB: spdx.DocElementID{ElementRefID: "Package-13fe667a0805e6b7"}, + Relationship: "DEPENDS_ON", + }, + }, + + UnpackagedFiles: nil, + OtherLicenses: nil, + Annotations: nil, + Reviews: nil, + }, + }, + { + name: "happy path for fs scan", + inputReport: types.Report{ + SchemaVersion: report.SchemaVersion, + ArtifactName: "masahiro331/CVE-2021-41098", + ArtifactType: ftypes.ArtifactFilesystem, + Results: types.Results{ + { + Target: "Gemfile.lock", + Class: types.ClassLangPkg, + Type: ftypes.Bundler, + Packages: []ftypes.Package{ + { + Name: "actioncable", + Version: "6.1.4.1", + }, + }, + }, + }, + }, + wantSBOM: &spdx.Document2_2{ + CreationInfo: &spdx.CreationInfo2_2{ + SPDXVersion: "SPDX-2.2", + DataLicense: "CC0-1.0", + SPDXIdentifier: "DOCUMENT", + DocumentName: "masahiro331/CVE-2021-41098", + DocumentNamespace: "http://aquasecurity.github.io/trivy/filesystem/masahiro331/CVE-2021-41098-3ff14136-e09f-4df9-80ea-000000000001", + CreatorOrganizations: []string{"aquasecurity"}, + CreatorTools: []string{"trivy"}, + Created: "2021-08-25T12:20:30.000000005Z", + }, + Packages: map[spdx.ElementID]*spdx.Package2_2{ + spdx.ElementID("Filesystem-5af0f1f08c20909a"): { + PackageSPDXIdentifier: spdx.ElementID("Filesystem-5af0f1f08c20909a"), + PackageName: "masahiro331/CVE-2021-41098", + PackageAttributionTexts: []string{ + "SchemaVersion: 2", + }, + }, + spdx.ElementID("Application-9dd4a4ba7077cc5a"): { + PackageSPDXIdentifier: spdx.ElementID("Application-9dd4a4ba7077cc5a"), + PackageName: "bundler", + PackageSourceInfo: "Gemfile.lock", + }, + spdx.ElementID("Package-3da61e86d0530402"): { + PackageSPDXIdentifier: spdx.ElementID("Package-3da61e86d0530402"), + PackageName: "actioncable", + PackageVersion: "6.1.4.1", + PackageLicenseConcluded: "NONE", + PackageLicenseDeclared: "NONE", + PackageExternalReferences: []*spdx.PackageExternalReference2_2{ + { + Category: tspdx.CategoryPackageManager, + RefType: tspdx.RefTypePurl, + Locator: "pkg:gem/actioncable@6.1.4.1", + }, + }, + }, + }, + Relationships: []*spdx.Relationship2_2{ + { + RefA: spdx.DocElementID{ElementRefID: "DOCUMENT"}, + RefB: spdx.DocElementID{ElementRefID: "Filesystem-5af0f1f08c20909a"}, + Relationship: "DESCRIBE", + }, + { + RefA: spdx.DocElementID{ElementRefID: "Filesystem-5af0f1f08c20909a"}, + RefB: spdx.DocElementID{ElementRefID: "Application-9dd4a4ba7077cc5a"}, + Relationship: "CONTAINS", + }, + { + RefA: spdx.DocElementID{ElementRefID: "Application-9dd4a4ba7077cc5a"}, + RefB: spdx.DocElementID{ElementRefID: "Package-3da61e86d0530402"}, + Relationship: "DEPENDS_ON", + }, + }, + }, + }, + { + name: "happy path aggregate results", + inputReport: types.Report{ + SchemaVersion: report.SchemaVersion, + ArtifactName: "test-aggregate", + ArtifactType: ftypes.ArtifactRemoteRepository, + Results: types.Results{ + { + Target: "Node.js", + Class: types.ClassLangPkg, + Type: ftypes.NodePkg, + Packages: []ftypes.Package{ + { + Name: "ruby-typeprof", + Version: "0.20.1", + Licenses: []string{"MIT"}, + Layer: ftypes.Layer{ + DiffID: "sha256:661c3fd3cc16b34c070f3620ca6b03b6adac150f9a7e5d0e3c707a159990f88e", + }, + FilePath: "usr/local/lib/ruby/gems/3.1.0/gems/typeprof-0.21.1/vscode/package.json", + }, + }, + }, + }, + }, + wantSBOM: &spdx.Document2_2{ + CreationInfo: &spdx.CreationInfo2_2{ + SPDXVersion: "SPDX-2.2", + DataLicense: "CC0-1.0", + SPDXIdentifier: "DOCUMENT", + DocumentName: "test-aggregate", + DocumentNamespace: "http://aquasecurity.github.io/trivy/repository/test-aggregate-3ff14136-e09f-4df9-80ea-000000000001", + CreatorOrganizations: []string{"aquasecurity"}, + CreatorTools: []string{"trivy"}, + Created: "2021-08-25T12:20:30.000000005Z", + }, + Packages: map[spdx.ElementID]*spdx.Package2_2{ + spdx.ElementID("Repository-7cb7a269a391a798"): { + PackageName: "test-aggregate", + PackageSPDXIdentifier: "Repository-7cb7a269a391a798", + PackageAttributionTexts: []string{ + "SchemaVersion: 2", + }, + }, + spdx.ElementID("Application-24f8a80152e2c0fc"): { + PackageSPDXIdentifier: "Application-24f8a80152e2c0fc", + PackageName: "node-pkg", + PackageSourceInfo: "Node.js", + }, + spdx.ElementID("Package-daedb173cfd43058"): { + PackageSPDXIdentifier: spdx.ElementID("Package-daedb173cfd43058"), + PackageName: "ruby-typeprof", + PackageVersion: "0.20.1", + PackageLicenseConcluded: "MIT", + PackageLicenseDeclared: "MIT", + PackageExternalReferences: []*spdx.PackageExternalReference2_2{ + { + Category: tspdx.CategoryPackageManager, + RefType: tspdx.RefTypePurl, + Locator: "pkg:npm/ruby-typeprof@0.20.1", + }, + }, + PackageAttributionTexts: []string{ + "LayerDiffID: sha256:661c3fd3cc16b34c070f3620ca6b03b6adac150f9a7e5d0e3c707a159990f88e", + }, + Files: map[spdx.ElementID]*spdx.File2_2{ + "File-a52825a3e5bc6dfe": { + FileName: "usr/local/lib/ruby/gems/3.1.0/gems/typeprof-0.21.1/vscode/package.json", + FileSPDXIdentifier: "File-a52825a3e5bc6dfe", + }, + }, + }, + }, + Relationships: []*spdx.Relationship2_2{ + { + RefA: spdx.DocElementID{ElementRefID: "DOCUMENT"}, + RefB: spdx.DocElementID{ElementRefID: "Repository-7cb7a269a391a798"}, + Relationship: "DESCRIBE", + }, + { + RefA: spdx.DocElementID{ElementRefID: "Repository-7cb7a269a391a798"}, + RefB: spdx.DocElementID{ElementRefID: "Application-24f8a80152e2c0fc"}, + Relationship: "CONTAINS", + }, + { + RefA: spdx.DocElementID{ElementRefID: "Application-24f8a80152e2c0fc"}, + RefB: spdx.DocElementID{ElementRefID: "Package-daedb173cfd43058"}, + Relationship: "DEPENDS_ON", + }, + }, + }, + }, + { + name: "happy path empty", + inputReport: types.Report{ + SchemaVersion: report.SchemaVersion, + ArtifactName: "empty/path", + ArtifactType: ftypes.ArtifactFilesystem, + Results: types.Results{}, + }, + wantSBOM: &spdx.Document2_2{ + CreationInfo: &spdx.CreationInfo2_2{ + SPDXVersion: "SPDX-2.2", + DataLicense: "CC0-1.0", + SPDXIdentifier: "DOCUMENT", + DocumentName: "empty/path", + DocumentNamespace: "http://aquasecurity.github.io/trivy/filesystem/empty/path-3ff14136-e09f-4df9-80ea-000000000001", + CreatorOrganizations: []string{"aquasecurity"}, + CreatorTools: []string{"trivy"}, + Created: "2021-08-25T12:20:30.000000005Z", + }, + Packages: map[spdx.ElementID]*spdx.Package2_2{ + spdx.ElementID("Filesystem-70f34983067dba86"): { + PackageName: "empty/path", + PackageSPDXIdentifier: "Filesystem-70f34983067dba86", + PackageAttributionTexts: []string{ + "SchemaVersion: 2", + }, + }, + }, + Relationships: []*spdx.Relationship2_2{ + { + RefA: spdx.DocElementID{ElementRefID: "DOCUMENT"}, + RefB: spdx.DocElementID{ElementRefID: "Filesystem-70f34983067dba86"}, + Relationship: "DESCRIBE", + }, + }, + }, + }, + } + + clock := fake.NewFakeClock(time.Date(2021, 8, 25, 12, 20, 30, 5, time.UTC)) + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var count int + newUUID := func() uuid.UUID { + count++ + return uuid.Must(uuid.Parse(fmt.Sprintf("3ff14136-e09f-4df9-80ea-%012d", count))) + } + + // Fake function calculating the hash value + h := fnv.New64() + hasher := func(v interface{}, format hashstructure.Format, opts *hashstructure.HashOptions) (uint64, error) { + h.Reset() + + var str string + switch v.(type) { + case ftypes.Package: + str = v.(ftypes.Package).Name + v.(ftypes.Package).FilePath + case string: + str = v.(string) + case *ftypes.OS: + str = v.(*ftypes.OS).Name + default: + require.Failf(t, "unknown type", "%T", v) + } + + if _, err := h.Write([]byte(str)); err != nil { + return 0, err + } + + return h.Sum64(), nil + } + + marshaler := tspdx.NewMarshaler(tspdx.WithClock(clock), tspdx.WithNewUUID(newUUID), tspdx.WithHasher(hasher)) + spdxDoc, err := marshaler.Marshal(tc.inputReport) + require.NoError(t, err) + + assert.Equal(t, tc.wantSBOM, spdxDoc) + }) + } +} From 0406ca15c81faf287a311fb276188f0c5e1d9193 Mon Sep 17 00:00:00 2001 From: masahiro331 Date: Wed, 14 Sep 2022 02:05:11 +0900 Subject: [PATCH 03/12] feat(spdx): add unmarshal --- pkg/fanal/artifact/sbom/sbom.go | 11 ++ pkg/fanal/types/artifact.go | 1 + pkg/sbom/sbom.go | 43 +++++- pkg/sbom/spdx/unmarshal.go | 237 ++++++++++++++++++++++++++++++++ 4 files changed, 285 insertions(+), 7 deletions(-) create mode 100644 pkg/sbom/spdx/unmarshal.go diff --git a/pkg/fanal/artifact/sbom/sbom.go b/pkg/fanal/artifact/sbom/sbom.go index d20615d5eb9..530184f27e6 100644 --- a/pkg/fanal/artifact/sbom/sbom.go +++ b/pkg/fanal/artifact/sbom/sbom.go @@ -21,6 +21,7 @@ import ( "github.com/aquasecurity/trivy/pkg/log" "github.com/aquasecurity/trivy/pkg/sbom" "github.com/aquasecurity/trivy/pkg/sbom/cyclonedx" + "github.com/aquasecurity/trivy/pkg/sbom/spdx" ) type Artifact struct { @@ -85,6 +86,9 @@ func (a Artifact) Inspect(_ context.Context) (types.ArtifactReference, error) { switch format { case sbom.FormatCycloneDXJSON, sbom.FormatCycloneDXXML, sbom.FormatAttestCycloneDXJSON: artifactType = types.ArtifactCycloneDX + case sbom.FormatSPDXTV, sbom.FormatSPDXJSON: + artifactType = types.ArtifactSPDX + } return types.ArtifactReference{ @@ -119,6 +123,13 @@ func (a Artifact) Decode(f io.Reader, format sbom.Format) (sbom.SBOM, error) { }, } decoder = json.NewDecoder(f) + case sbom.FormatSPDXJSON: + v = &spdx.SPDX{SBOM: &bom} + decoder = json.NewDecoder(f) + case sbom.FormatSPDXTV: + v = &spdx.SPDX{SBOM: &bom} + decoder = spdx.NewTVDecoder(f) + default: return sbom.SBOM{}, xerrors.Errorf("%s scanning is not yet supported", format) diff --git a/pkg/fanal/types/artifact.go b/pkg/fanal/types/artifact.go index e1e6aea8962..636cf39aaaf 100644 --- a/pkg/fanal/types/artifact.go +++ b/pkg/fanal/types/artifact.go @@ -95,6 +95,7 @@ const ( ArtifactFilesystem ArtifactType = "filesystem" ArtifactRemoteRepository ArtifactType = "repository" ArtifactCycloneDX ArtifactType = "cyclonedx" + ArtifactSPDX ArtifactType = "spdx" ArtifactAWSAccount ArtifactType = "aws_account" ) diff --git a/pkg/sbom/sbom.go b/pkg/sbom/sbom.go index 04699c27ef0..a4bc70afea4 100644 --- a/pkg/sbom/sbom.go +++ b/pkg/sbom/sbom.go @@ -1,6 +1,7 @@ package sbom import ( + "bufio" "encoding/json" "encoding/xml" "io" @@ -27,19 +28,26 @@ const ( FormatCycloneDXJSON Format = "cyclonedx-json" FormatCycloneDXXML Format = "cyclonedx-xml" FormatSPDXJSON Format = "spdx-json" + FormatSPDXTV Format = "spdx-tv" FormatSPDXXML Format = "spdx-xml" FormatAttestCycloneDXJSON Format = "attest-cyclonedx-json" FormatUnknown Format = "unknown" ) func DetectFormat(r io.ReadSeeker) (Format, error) { - type cyclonedx struct { - // XML specific field - XMLNS string `json:"-" xml:"xmlns,attr"` + type ( + cyclonedx struct { + // XML specific field + XMLNS string `json:"-" xml:"xmlns,attr"` - // JSON specific field - BOMFormat string `json:"bomFormat" xml:"-"` - } + // JSON specific field + BOMFormat string `json:"bomFormat" xml:"-"` + } + + spdx struct { + SpdxID string `json:"SPDXID"` + } + ) // Try CycloneDX JSON var cdxBom cyclonedx @@ -64,7 +72,28 @@ func DetectFormat(r io.ReadSeeker) (Format, error) { return FormatUnknown, xerrors.Errorf("seek error: %w", err) } - // TODO: implement SPDX + // Try SPDX json + var spdxBom spdx + if err := json.NewDecoder(r).Decode(&spdxBom); err == nil { + if strings.HasPrefix(spdxBom.SpdxID, "SPDX") { + return FormatSPDXJSON, nil + } + } + + if _, err := r.Seek(0, io.SeekStart); err != nil { + return FormatUnknown, xerrors.Errorf("seek error: %w", err) + } + + // Try SPDX tag-value + if scanner := bufio.NewScanner(r); scanner.Scan() { + if strings.HasPrefix(scanner.Text(), "SPDX") { + return FormatSPDXTV, nil + } + } + + if _, err := r.Seek(0, io.SeekStart); err != nil { + return FormatUnknown, xerrors.Errorf("seek error: %w", err) + } // Try in-toto attestation var s attestation.Statement diff --git a/pkg/sbom/spdx/unmarshal.go b/pkg/sbom/spdx/unmarshal.go new file mode 100644 index 00000000000..0e2bc6afc5c --- /dev/null +++ b/pkg/sbom/spdx/unmarshal.go @@ -0,0 +1,237 @@ +package spdx + +import ( + "bytes" + "fmt" + "io" + "strings" + + version "github.com/knqyf263/go-rpm-version" + "github.com/package-url/packageurl-go" + "github.com/spdx/tools-golang/jsonloader" + "github.com/spdx/tools-golang/spdx" + "github.com/spdx/tools-golang/tvloader" + "golang.org/x/xerrors" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/purl" + "github.com/aquasecurity/trivy/pkg/sbom" +) + +var ( + errInvalidPackageFormat = xerrors.New("invalid package format") +) + +type SPDX struct { + *sbom.SBOM + + relationships map[spdx.ElementID][]spdx.ElementID + packages map[spdx.ElementID]*spdx.Package2_2 +} + +func NewTVDecoder(r io.Reader) *TVDecoder { + return &TVDecoder{r: r} +} + +type TVDecoder struct { + r io.Reader +} + +func (tv *TVDecoder) Decode(v interface{}) error { + spdxDocument, err := tvloader.Load2_2(tv.r) + if err != nil { + return xerrors.Errorf("failed to load tag-value spdx: %w", err) + } + + a, ok := v.(*SPDX) + if !ok { + return xerrors.Errorf("invalid struct type tag-value decoder needed SPDX struct") + } + err = a.unmarshal(spdxDocument) + if err != nil { + return xerrors.Errorf("failed to unmarshal spdx: %w", err) + } + + return nil +} + +func (s *SPDX) UnmarshalJSON(b []byte) error { + spdxDocument, err := jsonloader.Load2_2(bytes.NewReader(b)) + if err != nil { + return xerrors.Errorf("failed to load spdx json: %w", err) + } + err = s.unmarshal(spdxDocument) + if err != nil { + return xerrors.Errorf("failed to unmarshal spdx: %w", err) + } + return nil +} + +func (s *SPDX) unmarshal(spdxDocument *spdx.Document2_2) error { + s.relationships = relationshipMap(spdxDocument.Relationships) + s.packages = spdxDocument.Packages + + for pkgID := range s.relationships { + pkg := s.packages[pkgID] + switch { + case strings.HasPrefix(string(pkg.PackageSPDXIdentifier), ElementOperatingSystem): + s.SBOM.OS = parseOS(pkg) + pkgs, err := s.parsePkgs(pkg.PackageSPDXIdentifier) + if err != nil { + return xerrors.Errorf("failed to parse os packages: %w", err) + } + if len(pkgs) != 0 { + s.SBOM.Packages = []ftypes.PackageInfo{{Packages: pkgs}} + } + + case strings.HasPrefix(string(pkg.PackageSPDXIdentifier), ElementApplication): + app, err := s.parseApplication(pkg) + if err != nil { + return xerrors.Errorf("failed to parse application: %w", err) + } + s.SBOM.Applications = append(s.SBOM.Applications, *app) + } + } + + return nil +} + +func (s *SPDX) parseApplication(pkg *spdx.Package2_2) (*ftypes.Application, error) { + pkgs, err := s.parsePkgs(pkg.PackageSPDXIdentifier) + if err != nil { + return nil, xerrors.Errorf("failed to parse language packages: %w", err) + } + app := &ftypes.Application{ + Type: pkg.PackageName, + FilePath: pkg.PackageSourceInfo, + Libraries: pkgs, + } + if pkg.PackageName == ftypes.NodePkg || pkg.PackageName == ftypes.PythonPkg || + pkg.PackageName == ftypes.GemSpec || pkg.PackageName == ftypes.Jar { + app.FilePath = "" + } + return app, nil + +} + +func (s *SPDX) parsePkgs(id spdx.ElementID) ([]ftypes.Package, error) { + pkgIDs := s.relationships[id] + + var pkgs []ftypes.Package + for _, id := range pkgIDs { + spdxPkg := s.packages[id] + pkg, err := parsePkg(spdxPkg) + if err != nil { + return nil, xerrors.Errorf("failed to parse package: %w", err) + } + + pkgs = append(pkgs, *pkg) + } + return pkgs, nil +} + +func parseOS(pkg *spdx.Package2_2) *ftypes.OS { + return &ftypes.OS{ + Family: pkg.PackageName, + Name: pkg.PackageVersion, + } +} + +func parsePkg(package2_2 *spdx.Package2_2) (*ftypes.Package, error) { + var ( + pkg *ftypes.Package + typ string + ) + for _, ref := range package2_2.PackageExternalReferences { + if ref.RefType == RefTypePurl && ref.Category == CategoryPackageManager { + packageURL, err := purl.FromString(ref.Locator) + if err != nil { + return nil, xerrors.Errorf("failed to parse purl from string: %w", err) + } + pkg = packageURL.Package() + pkg.Ref = ref.Locator + typ = packageURL.Type + break + } + } + if pkg == nil { + return nil, errInvalidPackageFormat + } + + if package2_2.PackageLicenseDeclared != "NONE" { + pkg.Licenses = strings.Split(package2_2.PackageLicenseDeclared, ",") + } + pkg.Name = package2_2.PackageName + pkg.Version = package2_2.PackageVersion + + if strings.HasPrefix(package2_2.PackageSourceInfo, SourcePackagePrefix) { + var err error + srcPkgName := strings.TrimPrefix(package2_2.PackageSourceInfo, fmt.Sprintf("%s: ", SourcePackagePrefix)) + pkg.SrcEpoch, pkg.SrcName, pkg.SrcVersion, pkg.SrcRelease, err = parseSourceInfo(typ, srcPkgName) + if err != nil { + return nil, xerrors.Errorf("failed to parse source info: %w", err) + } + } + for _, f := range package2_2.Files { + pkg.FilePath = f.FileName + } + + pkg.Layer.Digest = lookupAttributionTexts(package2_2.PackageAttributionTexts, PropertyLayerDigest) + pkg.Layer.DiffID = lookupAttributionTexts(package2_2.PackageAttributionTexts, PropertyLayerDiffID) + + return pkg, nil +} + +func lookupAttributionTexts(attributionTexts []string, key string) (value string) { + for _, text := range attributionTexts { + if strings.HasPrefix(text, key) { + return strings.TrimPrefix(text, fmt.Sprintf("%s: ", key)) + } + } + + return "" +} + +func parseSourceInfo(typ, sourceInfo string) (epoch int, name, ver, rel string, err error) { + srcNameVersion := strings.TrimPrefix(sourceInfo, fmt.Sprintf("%s: ", SourcePackagePrefix)) + ss := strings.Split(srcNameVersion, " ") + if len(ss) != 2 { + return 0, "", "", "", xerrors.Errorf("invalid source info (%s)", sourceInfo) + } + name = ss[0] + if typ == packageurl.TypeRPM { + v := version.NewVersion(ss[1]) + epoch = v.Epoch() + ver = v.Version() + rel = v.Release() + } else { + ver = ss[1] + } + return epoch, name, ver, rel, nil +} + +func relationshipMap(relationships []*spdx.Relationship2_2) map[spdx.ElementID][]spdx.ElementID { + relationshipMap := make(map[spdx.ElementID][]spdx.ElementID) + var rootElement spdx.ElementID + for _, relationship := range relationships { + if relationship.Relationship == RelationShipDescribe { + rootElement = relationship.RefB.ElementRefID + } + } + for _, relationship := range relationships { + if relationship.Relationship == RelationShipContains { + if relationship.RefA.ElementRefID == rootElement { + relationshipMap[relationship.RefB.ElementRefID] = []spdx.ElementID{} + } + } + } + for _, relationship := range relationships { + if relationship.Relationship == RelationShipDependsOn { + if array, ok := relationshipMap[relationship.RefA.ElementRefID]; ok { + relationshipMap[relationship.RefA.ElementRefID] = append(array, relationship.RefB.ElementRefID) + } + } + } + + return relationshipMap +} From 2a83723c77cd7dec9f3d75b84b473f37e775c2d0 Mon Sep 17 00:00:00 2001 From: masahiro331 Date: Wed, 14 Sep 2022 02:05:35 +0900 Subject: [PATCH 04/12] test(spdx): add unmarshal test --- pkg/sbom/spdx/testdata/happy/bom.json | 230 ++++++++++++++++++ pkg/sbom/spdx/testdata/happy/empty-bom.json | 34 +++ pkg/sbom/spdx/testdata/happy/os-only-bom.json | 42 ++++ .../spdx/testdata/happy/unrelated-bom.json | 82 +++++++ .../testdata/sad/invalid-source-info.json | 58 +++++ pkg/sbom/spdx/unmarshal_test.go | 184 ++++++++++++++ 6 files changed, 630 insertions(+) create mode 100644 pkg/sbom/spdx/testdata/happy/bom.json create mode 100644 pkg/sbom/spdx/testdata/happy/empty-bom.json create mode 100644 pkg/sbom/spdx/testdata/happy/os-only-bom.json create mode 100644 pkg/sbom/spdx/testdata/happy/unrelated-bom.json create mode 100644 pkg/sbom/spdx/testdata/sad/invalid-source-info.json create mode 100644 pkg/sbom/spdx/unmarshal_test.go diff --git a/pkg/sbom/spdx/testdata/happy/bom.json b/pkg/sbom/spdx/testdata/happy/bom.json new file mode 100644 index 00000000000..f0405a9ded2 --- /dev/null +++ b/pkg/sbom/spdx/testdata/happy/bom.json @@ -0,0 +1,230 @@ +{ + "SPDXID": "SPDXRef-DOCUMENT", + "creationInfo": { + "created": "2022-09-12T17:02:46.826609Z", + "creators": [ + "Tool: trivy", + "Organization: aquasecurity" + ] + }, + "dataLicense": "CC0-1.0", + "documentNamespace": "http://aquasecurity.github.io/trivy/container/meven-test-project-eb7a0384-b04a-4fc6-8afb-1662fe59ca79", + "name": "maven-test-projecct", + "packages": [ + { + "SPDXID": "SPDXRef-Application-150e605f5f17224d", + "filesAnalyzed": false, + "name": "jar", + "sourceInfo": "Java" + }, + { + "SPDXID": "SPDXRef-Application-24f8a80152e2c0fc", + "filesAnalyzed": false, + "name": "node-pkg", + "sourceInfo": "Node.js" + }, + { + "SPDXID": "SPDXRef-Application-36324ee492e03f0a", + "filesAnalyzed": false, + "name": "gobinary", + "sourceInfo": "app/gobinary/gobinary" + }, + { + "SPDXID": "SPDXRef-Application-4af197c15114fb0e", + "filesAnalyzed": false, + "name": "composer", + "sourceInfo": "app/composer/composer.lock" + }, + { + "SPDXID": "SPDXRef-ContainerImage-b5d81cde5f95c8fc", + "attributionTexts": [ + "SchemaVersion: 2", + "ImageID: sha256:49193a2310dbad4c02382da87ac624a80a92387a4f7536235f9ba590e5bcd7b5", + "DiffID: sha256:dd565ff850e7003356e2b252758f9bdc1ff2803f61e995e24c7844f6297f8fc3", + "DiffID: sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1", + "RepoTag: maven-test-project:latest", + "RepoTag: tmp-test:latest" + ], + "filesAnalyzed": false, + "name": "meven-test-project" + }, + { + "SPDXID": "SPDXRef-OperatingSystem-bd17bf9010aa612c", + "filesAnalyzed": false, + "name": "alpine", + "versionInfo": "3.16.0" + }, + { + "SPDXID": "SPDXRef-Package-2906575950df652b", + "attributionTexts": [ + "LayerDiffID: sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1" + ], + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceLocator": "pkg:composer/pear/log@1.13.1", + "referenceType": "purl" + } + ], + "filesAnalyzed": false, + "licenseConcluded": "NONE", + "licenseDeclared": "NONE", + "name": "pear/log", + "versionInfo": "1.13.1" + }, + { + "SPDXID": "SPDXRef-Package-2a53baa495b9ddaf", + "attributionTexts": [ + "LayerDiffID: sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1" + ], + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceLocator": "pkg:maven/org.codehaus.mojo/child-project@1.0", + "referenceType": "purl" + } + ], + "filesAnalyzed": false, + "licenseConcluded": "NONE", + "licenseDeclared": "NONE", + "name": "org.codehaus.mojo:child-project", + "versionInfo": "1.0" + }, + { + "SPDXID": "SPDXRef-Package-5e2e255ac76747ef", + "attributionTexts": [ + "LayerDiffID: sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1" + ], + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceLocator": "pkg:composer/pear/pear_exception@v1.0.0", + "referenceType": "purl" + } + ], + "filesAnalyzed": false, + "licenseConcluded": "NONE", + "licenseDeclared": "NONE", + "name": "pear/pear_exception", + "versionInfo": "v1.0.0" + }, + { + "SPDXID": "SPDXRef-Package-5f1dbaff8de5eb06", + "attributionTexts": [ + "LayerDiffID: sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1" + ], + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceLocator": "pkg:npm/bootstrap@5.0.2", + "referenceType": "purl" + } + ], + "filesAnalyzed": false, + "licenseConcluded": "MIT", + "licenseDeclared": "MIT", + "name": "bootstrap", + "versionInfo": "5.0.2" + }, + { + "SPDXID": "SPDXRef-Package-84ebffe38343d949", + "attributionTexts": [ + "LayerDiffID: sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1" + ], + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceLocator": "pkg:golang/github.com/package-url/packageurl-go@v0.1.1-0.20220203205134-d70459300c8a", + "referenceType": "purl" + } + ], + "filesAnalyzed": false, + "licenseConcluded": "NONE", + "licenseDeclared": "NONE", + "name": "github.com/package-url/packageurl-go", + "versionInfo": "v0.1.1-0.20220203205134-d70459300c8a" + }, + { + "SPDXID": "SPDXRef-Package-b7ebaf0233f1ef7b", + "attributionTexts": [ + "LayerDiffID: sha256:dd565ff850e7003356e2b252758f9bdc1ff2803f61e995e24c7844f6297f8fc3" + ], + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceLocator": "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.0", + "referenceType": "purl" + } + ], + "filesAnalyzed": false, + "licenseConcluded": "MIT", + "licenseDeclared": "MIT", + "name": "musl", + "sourceInfo": "built package from: musl 1.2.3-r0", + "versionInfo": "1.2.3-r0" + } + ], + "relationships": [ + { + "relatedSpdxElement": "SPDXRef-ContainerImage-b5d81cde5f95c8fc", + "relationshipType": "DESCRIBE", + "spdxElementId": "SPDXRef-DOCUMENT" + }, + { + "relatedSpdxElement": "SPDXRef-OperatingSystem-bd17bf9010aa612c", + "relationshipType": "CONTAINS", + "spdxElementId": "SPDXRef-ContainerImage-b5d81cde5f95c8fc" + }, + { + "relatedSpdxElement": "SPDXRef-Package-b7ebaf0233f1ef7b", + "relationshipType": "DEPENDS_ON", + "spdxElementId": "SPDXRef-OperatingSystem-bd17bf9010aa612c" + }, + { + "relatedSpdxElement": "SPDXRef-Application-150e605f5f17224d", + "relationshipType": "CONTAINS", + "spdxElementId": "SPDXRef-ContainerImage-b5d81cde5f95c8fc" + }, + { + "relatedSpdxElement": "SPDXRef-Package-2a53baa495b9ddaf", + "relationshipType": "DEPENDS_ON", + "spdxElementId": "SPDXRef-Application-150e605f5f17224d" + }, + { + "relatedSpdxElement": "SPDXRef-Application-24f8a80152e2c0fc", + "relationshipType": "CONTAINS", + "spdxElementId": "SPDXRef-ContainerImage-b5d81cde5f95c8fc" + }, + { + "relatedSpdxElement": "SPDXRef-Package-5f1dbaff8de5eb06", + "relationshipType": "DEPENDS_ON", + "spdxElementId": "SPDXRef-Application-24f8a80152e2c0fc" + }, + { + "relatedSpdxElement": "SPDXRef-Application-4af197c15114fb0e", + "relationshipType": "CONTAINS", + "spdxElementId": "SPDXRef-ContainerImage-b5d81cde5f95c8fc" + }, + { + "relatedSpdxElement": "SPDXRef-Package-2906575950df652b", + "relationshipType": "DEPENDS_ON", + "spdxElementId": "SPDXRef-Application-4af197c15114fb0e" + }, + { + "relatedSpdxElement": "SPDXRef-Package-5e2e255ac76747ef", + "relationshipType": "DEPENDS_ON", + "spdxElementId": "SPDXRef-Application-4af197c15114fb0e" + }, + { + "relatedSpdxElement": "SPDXRef-Application-36324ee492e03f0a", + "relationshipType": "CONTAINS", + "spdxElementId": "SPDXRef-ContainerImage-b5d81cde5f95c8fc" + }, + { + "relatedSpdxElement": "SPDXRef-Package-84ebffe38343d949", + "relationshipType": "DEPENDS_ON", + "spdxElementId": "SPDXRef-Application-36324ee492e03f0a" + } + ], + "spdxVersion": "SPDX-2.2" +} \ No newline at end of file diff --git a/pkg/sbom/spdx/testdata/happy/empty-bom.json b/pkg/sbom/spdx/testdata/happy/empty-bom.json new file mode 100644 index 00000000000..e32e2f4e692 --- /dev/null +++ b/pkg/sbom/spdx/testdata/happy/empty-bom.json @@ -0,0 +1,34 @@ +{ + "SPDXID": "SPDXRef-DOCUMENT", + "creationInfo": { + "created": "2022-09-12T17:03:35.840861Z", + "creators": [ + "Tool: trivy", + "Organization: aquasecurity" + ] + }, + "dataLicense": "CC0-1.0", + "documentDescribes": [ + "SPDXRef-ContainerImage-7f428bd075b13fe8" + ], + "documentNamespace": "http://aquasecurity.github.io/trivy/container/maven-test-project-3e87878b-eac3-4baa-af11-bdf2c5eab8ea", + "name": "maven-test-project", + "packages": [ + { + "SPDXID": "SPDXRef-ContainerImage-7f428bd075b13fe8", + "attributionTexts": [ + "SchemaVersion: 2" + ], + "filesAnalyzed": false, + "name": "maven-test-project" + } + ], + "relationships": [ + { + "relatedSpdxElement": "SPDXRef-ContainerImage-7f428bd075b13fe8", + "relationshipType": "DESCRIBE", + "spdxElementId": "SPDXRef-DOCUMENT" + } + ], + "spdxVersion": "SPDX-2.2" +} \ No newline at end of file diff --git a/pkg/sbom/spdx/testdata/happy/os-only-bom.json b/pkg/sbom/spdx/testdata/happy/os-only-bom.json new file mode 100644 index 00000000000..c6faec2d014 --- /dev/null +++ b/pkg/sbom/spdx/testdata/happy/os-only-bom.json @@ -0,0 +1,42 @@ +{ + "SPDXID": "SPDXRef-DOCUMENT", + "creationInfo": { + "created": "2022-09-12T17:04:09.262672Z", + "creators": [ + "Tool: trivy", + "Organization: aquasecurity" + ] + }, + "dataLicense": "CC0-1.0", + "documentNamespace": "http://aquasecurity.github.io/trivy/container/maven-test-project-1a255568-e896-498f-93de-7975a5cb0212", + "name": "maven-test-project", + "packages": [ + { + "SPDXID": "SPDXRef-ContainerImage-fbbee9b988766322", + "attributionTexts": [ + "SchemaVersion: 2" + ], + "filesAnalyzed": false, + "name": "maven-test-project" + }, + { + "SPDXID": "SPDXRef-OperatingSystem-bd17bf9010aa612c", + "filesAnalyzed": false, + "name": "alpine", + "versionInfo": "3.16.0" + } + ], + "relationships": [ + { + "relatedSpdxElement": "SPDXRef-ContainerImage-fbbee9b988766322", + "relationshipType": "DESCRIBE", + "spdxElementId": "SPDXRef-DOCUMENT" + }, + { + "relatedSpdxElement": "SPDXRef-OperatingSystem-bd17bf9010aa612c", + "relationshipType": "CONTAINS", + "spdxElementId": "SPDXRef-ContainerImage-fbbee9b988766322" + } + ], + "spdxVersion": "SPDX-2.2" +} \ No newline at end of file diff --git a/pkg/sbom/spdx/testdata/happy/unrelated-bom.json b/pkg/sbom/spdx/testdata/happy/unrelated-bom.json new file mode 100644 index 00000000000..1eaac40af7a --- /dev/null +++ b/pkg/sbom/spdx/testdata/happy/unrelated-bom.json @@ -0,0 +1,82 @@ +{ + "SPDXID": "SPDXRef-DOCUMENT", + "creationInfo": { + "created": "2022-09-12T17:04:28.43059Z", + "creators": [ + "Tool: trivy", + "Organization: aquasecurity" + ] + }, + "dataLicense": "CC0-1.0", + "documentNamespace": "http://aquasecurity.github.io/trivy/application/maven-test-project-8aa3b6db-bae7-4755-b43e-00b4bcf46410", + "name": "maven-test-project", + "packages": [ + { + "SPDXID": "SPDXRef-Application-193be12e25033404", + "filesAnalyzed": false, + "name": "composer", + "sourceInfo": "app/composer/composer.lock" + }, + { + "SPDXID": "SPDXRef-Filesystem-12d960c003a8275b", + "attributionTexts": [ + "SchemaVersion: 2" + ], + "filesAnalyzed": false, + "name": "maven-test-project" + }, + { + "SPDXID": "SPDXRef-Package-2906575950df652b", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceLocator": "pkg:composer/pear/log@1.13.1", + "referenceType": "purl" + } + ], + "filesAnalyzed": false, + "licenseConcluded": "NONE", + "licenseDeclared": "NONE", + "name": "pear/log", + "versionInfo": "1.13.1" + }, + { + "SPDXID": "SPDXRef-Package-5e2e255ac76747ef", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceLocator": "pkg:composer/pear/pear_exception@v1.0.0", + "referenceType": "purl" + } + ], + "filesAnalyzed": false, + "licenseConcluded": "NONE", + "licenseDeclared": "NONE", + "name": "pear/pear_exception", + "versionInfo": "v1.0.0" + } + ], + "relationships": [ + { + "relatedSpdxElement": "SPDXRef-Filesystem-12d960c003a8275b", + "relationshipType": "DESCRIBE", + "spdxElementId": "SPDXRef-DOCUMENT" + }, + { + "relatedSpdxElement": "SPDXRef-Application-193be12e25033404", + "relationshipType": "CONTAINS", + "spdxElementId": "SPDXRef-Filesystem-12d960c003a8275b" + }, + { + "relatedSpdxElement": "SPDXRef-Package-2906575950df652b", + "relationshipType": "DEPENDS_ON", + "spdxElementId": "SPDXRef-Application-193be12e25033404" + }, + { + "relatedSpdxElement": "SPDXRef-Package-5e2e255ac76747ef", + "relationshipType": "DEPENDS_ON", + "spdxElementId": "SPDXRef-Application-193be12e25033404" + } + ], + "spdxVersion": "SPDX-2.2" +} \ No newline at end of file diff --git a/pkg/sbom/spdx/testdata/sad/invalid-source-info.json b/pkg/sbom/spdx/testdata/sad/invalid-source-info.json new file mode 100644 index 00000000000..949d7e11701 --- /dev/null +++ b/pkg/sbom/spdx/testdata/sad/invalid-source-info.json @@ -0,0 +1,58 @@ +{ + "SPDXID": "SPDXRef-DOCUMENT", + "creationInfo": { + "created": "2022-09-12T17:02:46.826609Z", + "creators": [ + "Tool: trivy", + "Organization: aquasecurity" + ] + }, + "dataLicense": "CC0-1.0", + "documentNamespace": "http://aquasecurity.github.io/trivy/container/meven-test-project-eb7a0384-b04a-4fc6-8afb-1662fe59ca79", + "name": "maven-test-projecct", + "packages": [ + { + "SPDXID": "SPDXRef-ContainerImage-b5d81cde5f95c8fc", + "filesAnalyzed": false, + "name": "meven-test-project" + }, + { + "SPDXID": "SPDXRef-OperatingSystem-bd17bf9010aa612c", + "filesAnalyzed": false, + "name": "alpine", + "versionInfo": "3.16.0" + }, + { + "SPDXID": "SPDXRef-Package-b7ebaf0233f1ef7b", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceLocator": "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.0", + "referenceType": "purl" + } + ], + "filesAnalyzed": false, + "name": "musl", + "sourceInfo": "built package from: invalid", + "versionInfo": "1.2.3-r0" + } + ], + "relationships": [ + { + "relatedSpdxElement": "SPDXRef-ContainerImage-b5d81cde5f95c8fc", + "relationshipType": "DESCRIBE", + "spdxElementId": "SPDXRef-DOCUMENT" + }, + { + "relatedSpdxElement": "SPDXRef-OperatingSystem-bd17bf9010aa612c", + "relationshipType": "CONTAINS", + "spdxElementId": "SPDXRef-ContainerImage-b5d81cde5f95c8fc" + }, + { + "relatedSpdxElement": "SPDXRef-Package-b7ebaf0233f1ef7b", + "relationshipType": "DEPENDS_ON", + "spdxElementId": "SPDXRef-OperatingSystem-bd17bf9010aa612c" + } + ], + "spdxVersion": "SPDX-2.2" +} \ No newline at end of file diff --git a/pkg/sbom/spdx/unmarshal_test.go b/pkg/sbom/spdx/unmarshal_test.go new file mode 100644 index 00000000000..c5089ac802d --- /dev/null +++ b/pkg/sbom/spdx/unmarshal_test.go @@ -0,0 +1,184 @@ +package spdx_test + +import ( + "encoding/json" + "os" + "sort" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/sbom" + "github.com/aquasecurity/trivy/pkg/sbom/spdx" +) + +func TestUnmarshaler_Unmarshal(t *testing.T) { + tests := []struct { + name string + inputFile string + want sbom.SBOM + wantErr string + }{ + { + name: "happy path", + inputFile: "testdata/happy/bom.json", + want: sbom.SBOM{ + OS: &ftypes.OS{ + Family: "alpine", + Name: "3.16.0", + }, + Packages: []ftypes.PackageInfo{ + { + Packages: []ftypes.Package{ + { + Name: "musl", Version: "1.2.3-r0", SrcName: "musl", SrcVersion: "1.2.3-r0", Licenses: []string{"MIT"}, + Ref: "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.0", + Layer: ftypes.Layer{ + DiffID: "sha256:dd565ff850e7003356e2b252758f9bdc1ff2803f61e995e24c7844f6297f8fc3", + }, + }, + }, + }, + }, + Applications: []ftypes.Application{ + { + Type: "composer", + FilePath: "app/composer/composer.lock", + Libraries: []ftypes.Package{ + { + Name: "pear/log", + Version: "1.13.1", + Ref: "pkg:composer/pear/log@1.13.1", + Layer: ftypes.Layer{ + DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1", + }, + }, + { + + Name: "pear/pear_exception", + Version: "v1.0.0", + Ref: "pkg:composer/pear/pear_exception@v1.0.0", + Layer: ftypes.Layer{ + DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1", + }, + }, + }, + }, + { + Type: "gobinary", + FilePath: "app/gobinary/gobinary", + Libraries: []ftypes.Package{ + { + Name: "github.com/package-url/packageurl-go", + Version: "v0.1.1-0.20220203205134-d70459300c8a", + Ref: "pkg:golang/github.com/package-url/packageurl-go@v0.1.1-0.20220203205134-d70459300c8a", + Layer: ftypes.Layer{ + DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1", + }, + }, + }, + }, + { + Type: "jar", + Libraries: []ftypes.Package{ + { + Name: "org.codehaus.mojo:child-project", + Ref: "pkg:maven/org.codehaus.mojo/child-project@1.0", + Version: "1.0", + Layer: ftypes.Layer{ + DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1", + }, + }, + }, + }, + { + Type: "node-pkg", + Libraries: []ftypes.Package{ + { + Name: "bootstrap", + Version: "5.0.2", + Ref: "pkg:npm/bootstrap@5.0.2", + Licenses: []string{"MIT"}, + Layer: ftypes.Layer{ + DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1", + }, + }, + }, + }, + }, + }, + }, + { + name: "happy path for unrelated bom", + inputFile: "testdata/happy/unrelated-bom.json", + want: sbom.SBOM{ + Applications: []ftypes.Application{ + { + Type: "composer", + FilePath: "app/composer/composer.lock", + Libraries: []ftypes.Package{ + { + Name: "pear/log", + Version: "1.13.1", + Ref: "pkg:composer/pear/log@1.13.1", + }, + { + + Name: "pear/pear_exception", + Version: "v1.0.0", + Ref: "pkg:composer/pear/pear_exception@v1.0.0", + }, + }, + }, + }, + }, + }, + { + name: "happy path only os component", + inputFile: "testdata/happy/os-only-bom.json", + want: sbom.SBOM{ + OS: &ftypes.OS{ + Family: "alpine", + Name: "3.16.0", + }, + }, + }, + { + name: "happy path empty component", + inputFile: "testdata/happy/empty-bom.json", + want: sbom.SBOM{}, + }, + { + name: "sad path invalid purl", + inputFile: "testdata/sad/invalid-source-info.json", + wantErr: "failed to parse source info:", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + defer f.Close() + + v := &spdx.SPDX{SBOM: &sbom.SBOM{}} + err = json.NewDecoder(f).Decode(v) + if tt.wantErr != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantErr) + return + } + + // Not compare the CycloneDX field + v.SPDX = nil + + sort.Slice(v.Applications, func(i, j int) bool { + return v.Applications[i].Type < v.Applications[j].Type + }) + require.NoError(t, err) + assert.Equal(t, tt.want, *v.SBOM) + }) + } +} From ba9c271cd764a42b4f1dd474c99bf4f0edad9c8f Mon Sep 17 00:00:00 2001 From: masahiro331 Date: Wed, 14 Sep 2022 02:10:21 +0900 Subject: [PATCH 05/12] feat(spdx): keep original spdx bom --- pkg/fanal/types/artifact.go | 2 ++ pkg/sbom/sbom.go | 2 ++ pkg/sbom/spdx/unmarshal.go | 1 + pkg/scanner/scan.go | 1 + pkg/types/report.go | 3 +++ 5 files changed, 9 insertions(+) diff --git a/pkg/fanal/types/artifact.go b/pkg/fanal/types/artifact.go index 636cf39aaaf..ffd1e3e2677 100644 --- a/pkg/fanal/types/artifact.go +++ b/pkg/fanal/types/artifact.go @@ -4,6 +4,7 @@ import ( "time" v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/spdx/tools-golang/spdx" ) type OS struct { @@ -109,6 +110,7 @@ type ArtifactReference struct { // SBOM CycloneDX *CycloneDX + SPDX *spdx.Document2_2 } type ImageMetadata struct { diff --git a/pkg/sbom/sbom.go b/pkg/sbom/sbom.go index a4bc70afea4..e789758340f 100644 --- a/pkg/sbom/sbom.go +++ b/pkg/sbom/sbom.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/in-toto/in-toto-golang/in_toto" + stypes "github.com/spdx/tools-golang/spdx" "golang.org/x/xerrors" "github.com/aquasecurity/trivy/pkg/attestation" @@ -20,6 +21,7 @@ type SBOM struct { Applications []types.Application CycloneDX *types.CycloneDX + SPDX *stypes.Document2_2 } type Format string diff --git a/pkg/sbom/spdx/unmarshal.go b/pkg/sbom/spdx/unmarshal.go index 0e2bc6afc5c..033144f37bb 100644 --- a/pkg/sbom/spdx/unmarshal.go +++ b/pkg/sbom/spdx/unmarshal.go @@ -93,6 +93,7 @@ func (s *SPDX) unmarshal(spdxDocument *spdx.Document2_2) error { } } + s.SPDX = spdxDocument return nil } diff --git a/pkg/scanner/scan.go b/pkg/scanner/scan.go index 9afe5dd0aa2..9e9a9620b26 100644 --- a/pkg/scanner/scan.go +++ b/pkg/scanner/scan.go @@ -162,6 +162,7 @@ func (s Scanner) ScanArtifact(ctx context.Context, options types.ScanOptions) (t ImageConfig: artifactInfo.ImageMetadata.ConfigFile, }, CycloneDX: artifactInfo.CycloneDX, + SPDX: artifactInfo.SPDX, Results: results, }, nil } diff --git a/pkg/types/report.go b/pkg/types/report.go index 003112da2a4..16890844af7 100644 --- a/pkg/types/report.go +++ b/pkg/types/report.go @@ -4,6 +4,7 @@ import ( "encoding/json" v1 "github.com/google/go-containerregistry/pkg/v1" // nolint: goimports + "github.com/spdx/tools-golang/spdx" ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" ) @@ -18,6 +19,8 @@ type Report struct { // SBOM CycloneDX *ftypes.CycloneDX `json:"-"` // Just for internal usage, not exported in JSON + SPDX *spdx.Document2_2 `json:"-"` // Just for internal usage, not exported in JSON + } // Metadata represents a metadata of artifact From cd82fb31192b5993d0200396c35dfb793d5b5f30 Mon Sep 17 00:00:00 2001 From: masahiro331 Date: Wed, 14 Sep 2022 02:13:32 +0900 Subject: [PATCH 06/12] test(integration): add spdx integration test --- integration/sbom_test.go | 58 ++++- .../testdata/centos-7-spdx.json.golden | 220 ++++++++++++++++++ .../testdata/fixtures/sbom/centos-7-spdx.json | 94 ++++++++ .../testdata/fixtures/sbom/centos-7-spdx.txt | 57 +++++ 4 files changed, 425 insertions(+), 4 deletions(-) create mode 100644 integration/testdata/centos-7-spdx.json.golden create mode 100644 integration/testdata/fixtures/sbom/centos-7-spdx.json create mode 100644 integration/testdata/fixtures/sbom/centos-7-spdx.txt diff --git a/integration/sbom_test.go b/integration/sbom_test.go index 1f44310ca22..4a91f65e5de 100644 --- a/integration/sbom_test.go +++ b/integration/sbom_test.go @@ -3,16 +3,20 @@ package integration import ( + "fmt" "os" "path/filepath" "testing" cdx "github.com/CycloneDX/cyclonedx-go" + "github.com/spdx/tools-golang/jsonloader" + "github.com/spdx/tools-golang/spdx" + "github.com/spdx/tools-golang/tvloader" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestCycloneDX(t *testing.T) { +func TestSBOM(t *testing.T) { type args struct { input string format string @@ -50,6 +54,24 @@ func TestCycloneDX(t *testing.T) { }, golden: "testdata/centos-7-cyclonedx.json.golden", }, + { + name: "centos7 spdx type tag-value by trivy", + args: args{ + input: "testdata/fixtures/sbom/centos-7-spdx.txt", + format: "json", + artifactType: "spdx", + }, + golden: "testdata/centos-7-spdx.json.golden", + }, + { + name: "centos7 spdx type json by trivy", + args: args{ + input: "testdata/fixtures/sbom/centos-7-spdx.json", + format: "json", + artifactType: "spdx-json", + }, + golden: "testdata/centos-7-spdx.json.golden", + }, } // Set up testing DB @@ -75,13 +97,41 @@ func TestCycloneDX(t *testing.T) { assert.NoError(t, err) // Compare want and got - want := decodeCycloneDX(t, tt.golden) - got := decodeCycloneDX(t, outputFile) - assert.Equal(t, want, got) + switch tt.args.artifactType { + case "cyclonedx": + want := decodeCycloneDX(t, tt.golden) + got := decodeCycloneDX(t, outputFile) + assert.Equal(t, want, got) + case "spdx", "spdx-json": + want := decodeSPDX(t, tt.args.format, tt.golden) + got := decodeSPDX(t, tt.args.format, outputFile) + assert.Equal(t, want, got) + default: + t.Fatalf("invalid arguments format: %q", tt.args.format) + } }) } } +func decodeSPDX(t *testing.T, format string, filePath string) *spdx.Document2_2 { + f, err := os.Open(filePath) + require.NoError(t, err) + defer f.Close() + + var spdxDocument *spdx.Document2_2 + switch format { + case "spdx-json": + fmt.Println(filePath) + spdxDocument, err = jsonloader.Load2_2(f) + require.NoError(t, err) + case "spdx": + fmt.Println(filePath) + spdxDocument, err = tvloader.Load2_2(f) + require.NoError(t, err) + } + return spdxDocument +} + func decodeCycloneDX(t *testing.T, filePath string) *cdx.BOM { f, err := os.Open(filePath) require.NoError(t, err) diff --git a/integration/testdata/centos-7-spdx.json.golden b/integration/testdata/centos-7-spdx.json.golden new file mode 100644 index 00000000000..d6bf4cbdf87 --- /dev/null +++ b/integration/testdata/centos-7-spdx.json.golden @@ -0,0 +1,220 @@ +{ + "SchemaVersion": 2, + "ArtifactName": "testdata/fixtures/sbom/centos-7-spdx.json", + "ArtifactType": "spdx", + "Metadata": { + "OS": { + "Family": "centos", + "Name": "7.6.1810" + }, + "ImageConfig": { + "architecture": "", + "created": "0001-01-01T00:00:00Z", + "os": "", + "rootfs": { + "type": "", + "diff_ids": null + }, + "config": {} + } + }, + "Results": [ + { + "Target": "testdata/fixtures/sbom/centos-7-spdx.json (centos 7.6.1810)", + "Class": "os-pkgs", + "Type": "centos", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2019-18276", + "PkgName": "bash", + "InstalledVersion": "4.2.46-31.el7", + "Layer": {}, + "SeveritySource": "redhat", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-18276", + "Ref": "pkg:rpm/centos/bash@4.2.46-31.el7?arch=x86_64\u0026distro=centos-7.6.1810", + "Title": "bash: when effective UID is not equal to its real UID the saved UID is not dropped", + "Description": "An issue was discovered in disable_priv_mode in shell.c in GNU Bash through 5.0 patch 11. By default, if Bash is run with its effective UID not equal to its real UID, it will drop privileges by setting its effective UID to its real UID. However, it does so incorrectly. On Linux and other systems that support \"saved UID\" functionality, the saved UID is not dropped. An attacker with command execution in the shell can use \"enable -f\" for runtime loading of a new builtin, which can be a shared object that calls setuid() and therefore regains privileges. However, binaries running with an effective UID of 0 are unaffected.", + "Severity": "LOW", + "CweIDs": [ + "CWE-273" + ], + "CVSS": { + "nvd": { + "V2Vector": "AV:L/AC:L/Au:N/C:C/I:C/A:C", + "V3Vector": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 7.2, + "V3Score": 7.8 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", + "V3Score": 7.8 + } + }, + "References": [ + "http://packetstormsecurity.com/files/155498/Bash-5.0-Patch-11-Privilege-Escalation.html", + "https://access.redhat.com/security/cve/CVE-2019-18276", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-18276", + "https://github.com/bminor/bash/commit/951bdaad7a18cc0dc1036bba86b18b90874d39ff", + "https://linux.oracle.com/cve/CVE-2019-18276.html", + "https://linux.oracle.com/errata/ELSA-2021-1679.html", + "https://lists.apache.org/thread.html/rf9fa47ab66495c78bb4120b0754dd9531ca2ff0430f6685ac9b07772@%3Cdev.mina.apache.org%3E", + "https://nvd.nist.gov/vuln/detail/CVE-2019-18276", + "https://security.gentoo.org/glsa/202105-34", + "https://security.netapp.com/advisory/ntap-20200430-0003/", + "https://www.youtube.com/watch?v=-wGtxJ8opa8" + ], + "PublishedDate": "2019-11-28T01:15:00Z", + "LastModifiedDate": "2021-05-26T12:15:00Z" + }, + { + "VulnerabilityID": "CVE-2019-1559", + "VendorIDs": [ + "RHSA-2019:2304" + ], + "PkgName": "openssl-libs", + "InstalledVersion": "1:1.0.2k-16.el7", + "FixedVersion": "1:1.0.2k-19.el7", + "Layer": {}, + "SeveritySource": "redhat", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-1559", + "Ref": "pkg:rpm/centos/openssl-libs@1:1.0.2k-16.el7?arch=x86_64\u0026distro=centos-7.6.1810", + "Title": "openssl: 0-byte record padding oracle", + "Description": "If an application encounters a fatal protocol error and then calls SSL_shutdown() twice (once to send a close_notify, and once to receive one) then OpenSSL can respond differently to the calling application if a 0 byte record is received with invalid padding compared to if a 0 byte record is received with an invalid MAC. If the application then behaves differently based on that in a way that is detectable to the remote peer, then this amounts to a padding oracle that could be used to decrypt data. In order for this to be exploitable \"non-stitched\" ciphersuites must be in use. Stitched ciphersuites are optimised implementations of certain commonly used ciphersuites. Also the application must call SSL_shutdown() twice even if a protocol error has occurred (applications should not do this but some do anyway). Fixed in OpenSSL 1.0.2r (Affected 1.0.2-1.0.2q).", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-203" + ], + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:M/Au:N/C:P/I:N/A:N", + "V3Vector": "CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", + "V2Score": 4.3, + "V3Score": 5.9 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", + "V3Score": 5.9 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2019-03/msg00041.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-04/msg00019.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-04/msg00046.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-04/msg00047.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-05/msg00049.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-06/msg00080.html", + "http://www.securityfocus.com/bid/107174", + "https://access.redhat.com/errata/RHSA-2019:2304", + "https://access.redhat.com/errata/RHSA-2019:2437", + "https://access.redhat.com/errata/RHSA-2019:2439", + "https://access.redhat.com/errata/RHSA-2019:2471", + "https://access.redhat.com/errata/RHSA-2019:3929", + "https://access.redhat.com/errata/RHSA-2019:3931", + "https://access.redhat.com/security/cve/CVE-2019-1559", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1559", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=e9bbefbf0f24c57645e7ad6a5a71ae649d18ac8e", + "https://github.com/RUB-NDS/TLS-Padding-Oracles", + "https://kc.mcafee.com/corporate/index?page=content\u0026id=SB10282", + "https://linux.oracle.com/cve/CVE-2019-1559.html", + "https://linux.oracle.com/errata/ELSA-2019-2471.html", + "https://lists.debian.org/debian-lts-announce/2019/03/msg00003.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/EWC42UXL5GHTU5G77VKBF6JYUUNGSHOM/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/Y3IVFGSERAZLNJCK35TEM2R4726XIH3Z/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/ZBEV5QGDRFUZDMNECFXUSN5FMYOZDE4V/", + "https://security.gentoo.org/glsa/201903-10", + "https://security.netapp.com/advisory/ntap-20190301-0001/", + "https://security.netapp.com/advisory/ntap-20190301-0002/", + "https://security.netapp.com/advisory/ntap-20190423-0002/", + "https://support.f5.com/csp/article/K18549143", + "https://support.f5.com/csp/article/K18549143?utm_source=f5support\u0026amp;utm_medium=RSS", + "https://ubuntu.com/security/notices/USN-3899-1", + "https://ubuntu.com/security/notices/USN-4376-2", + "https://usn.ubuntu.com/3899-1/", + "https://usn.ubuntu.com/4376-2/", + "https://www.debian.org/security/2019/dsa-4400", + "https://www.openssl.org/news/secadv/20190226.txt", + "https://www.oracle.com/security-alerts/cpujan2020.html", + "https://www.oracle.com/security-alerts/cpujan2021.html", + "https://www.oracle.com/technetwork/security-advisory/cpuapr2019-5072813.html", + "https://www.oracle.com/technetwork/security-advisory/cpujul2019-5072835.html", + "https://www.oracle.com/technetwork/security-advisory/cpuoct2019-5072832.html", + "https://www.tenable.com/security/tns-2019-02", + "https://www.tenable.com/security/tns-2019-03" + ], + "PublishedDate": "2019-02-27T23:29:00Z", + "LastModifiedDate": "2021-01-20T15:15:00Z" + }, + { + "VulnerabilityID": "CVE-2018-0734", + "VendorIDs": [ + "RHSA-2019:2304" + ], + "PkgName": "openssl-libs", + "InstalledVersion": "1:1.0.2k-16.el7", + "FixedVersion": "1:1.0.2k-19.el7", + "Layer": {}, + "SeveritySource": "redhat", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2018-0734", + "Ref": "pkg:rpm/centos/openssl-libs@1:1.0.2k-16.el7?arch=x86_64\u0026distro=centos-7.6.1810", + "Title": "openssl: timing side channel attack in the DSA signature algorithm", + "Description": "The OpenSSL DSA signature algorithm has been shown to be vulnerable to a timing side channel attack. An attacker could use variations in the signing algorithm to recover the private key. Fixed in OpenSSL 1.1.1a (Affected 1.1.1). Fixed in OpenSSL 1.1.0j (Affected 1.1.0-1.1.0i). Fixed in OpenSSL 1.0.2q (Affected 1.0.2-1.0.2p).", + "Severity": "LOW", + "CweIDs": [ + "CWE-327" + ], + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:M/Au:N/C:P/I:N/A:N", + "V3Vector": "CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", + "V2Score": 4.3, + "V3Score": 5.9 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:L/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", + "V3Score": 5.1 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2019-06/msg00030.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-07/msg00056.html", + "http://www.securityfocus.com/bid/105758", + "https://access.redhat.com/errata/RHSA-2019:2304", + "https://access.redhat.com/errata/RHSA-2019:3700", + "https://access.redhat.com/errata/RHSA-2019:3932", + "https://access.redhat.com/errata/RHSA-2019:3933", + "https://access.redhat.com/errata/RHSA-2019:3935", + "https://access.redhat.com/security/cve/CVE-2018-0734", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-0734", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=43e6a58d4991a451daf4891ff05a48735df871ac", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=8abfe72e8c1de1b95f50aa0d9134803b4d00070f", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=ef11e19d1365eea2b1851e6f540a0bf365d303e7", + "https://linux.oracle.com/cve/CVE-2018-0734.html", + "https://linux.oracle.com/errata/ELSA-2019-3700.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/EWC42UXL5GHTU5G77VKBF6JYUUNGSHOM/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/Y3IVFGSERAZLNJCK35TEM2R4726XIH3Z/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/ZBEV5QGDRFUZDMNECFXUSN5FMYOZDE4V/", + "https://nodejs.org/en/blog/vulnerability/november-2018-security-releases/", + "https://nvd.nist.gov/vuln/detail/CVE-2018-0734", + "https://security.netapp.com/advisory/ntap-20181105-0002/", + "https://security.netapp.com/advisory/ntap-20190118-0002/", + "https://security.netapp.com/advisory/ntap-20190423-0002/", + "https://ubuntu.com/security/notices/USN-3840-1", + "https://usn.ubuntu.com/3840-1/", + "https://www.debian.org/security/2018/dsa-4348", + "https://www.debian.org/security/2018/dsa-4355", + "https://www.openssl.org/news/secadv/20181030.txt", + "https://www.oracle.com/security-alerts/cpuapr2020.html", + "https://www.oracle.com/security-alerts/cpujan2020.html", + "https://www.oracle.com/technetwork/security-advisory/cpuapr2019-5072813.html", + "https://www.oracle.com/technetwork/security-advisory/cpujan2019-5072801.html", + "https://www.oracle.com/technetwork/security-advisory/cpujul2019-5072835.html", + "https://www.tenable.com/security/tns-2018-16", + "https://www.tenable.com/security/tns-2018-17" + ], + "PublishedDate": "2018-10-30T12:29:00Z", + "LastModifiedDate": "2020-08-24T17:37:00Z" + } + ] + } + ] +} diff --git a/integration/testdata/fixtures/sbom/centos-7-spdx.json b/integration/testdata/fixtures/sbom/centos-7-spdx.json new file mode 100644 index 00000000000..ca635244bee --- /dev/null +++ b/integration/testdata/fixtures/sbom/centos-7-spdx.json @@ -0,0 +1,94 @@ +{ + "SPDXID": "SPDXRef-DOCUMENT", + "creationInfo": { + "created": "2022-09-13T13:27:55.874784Z", + "creators": [ + "Tool: trivy", + "Organization: aquasecurity" + ] + }, + "dataLicense": "CC0-1.0", + "documentNamespace": "http://aquasecurity.github.io/trivy/container_image/integration/testdata/fixtures/images/centos-7.tar.gz-2906855d-5098-4a22-9a72-4f7099ea3d66", + "name": "integration/testdata/fixtures/images/centos-7.tar.gz", + "packages": [ + { + "SPDXID": "SPDXRef-ContainerImage-dd5cad897c6263", + "attributionTexts": [ + "SchemaVersion: 2", + "ImageID: sha256:f1cb7c7d58b73eac859c395882eec49d50651244e342cd6c68a5c7809785f427", + "DiffID: sha256:89169d87dbe2b72ba42bfbb3579c957322baca28e03a1e558076542a1c1b2b4a" + ], + "filesAnalyzed": false, + "name": "integration/testdata/fixtures/images/centos-7.tar.gz" + }, + { + "SPDXID": "SPDXRef-OperatingSystem-2e91c856c499a371", + "filesAnalyzed": false, + "name": "centos", + "versionInfo": "7.6.1810" + }, + { + "SPDXID": "SPDXRef-Package-5a18334f22149877", + "attributionTexts": [ + "LayerDigest: sha256:ac9208207adaac3a48e54a4dc6b49c69e78c3072d2b3add7efdabf814db2133b", + "LayerDiffID: sha256:89169d87dbe2b72ba42bfbb3579c957322baca28e03a1e558076542a1c1b2b4a" + ], + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceLocator": "pkg:rpm/centos/bash@4.2.46-31.el7?arch=x86_64\u0026distro=centos-7.6.1810", + "referenceType": "purl" + } + ], + "filesAnalyzed": false, + "licenseConcluded": "GPLv3+", + "licenseDeclared": "GPLv3+", + "name": "bash", + "sourceInfo": "built package from: bash 4.2.46-31.el7", + "versionInfo": "4.2.46" + }, + { + "SPDXID": "SPDXRef-Package-e16b1cbaa5186199", + "attributionTexts": [ + "LayerDigest: sha256:ac9208207adaac3a48e54a4dc6b49c69e78c3072d2b3add7efdabf814db2133b", + "LayerDiffID: sha256:89169d87dbe2b72ba42bfbb3579c957322baca28e03a1e558076542a1c1b2b4a" + ], + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceLocator": "pkg:rpm/centos/openssl-libs@1:1.0.2k-16.el7?arch=x86_64\u0026distro=centos-7.6.1810", + "referenceType": "purl" + } + ], + "filesAnalyzed": false, + "licenseConcluded": "OpenSSL", + "licenseDeclared": "OpenSSL", + "name": "openssl-libs", + "sourceInfo": "built package from: openssl-libs 1:1.0.2k-16.el7", + "versionInfo": "1.0.2k" + } + ], + "relationships": [ + { + "relatedSpdxElement": "SPDXRef-ContainerImage-dd5cad897c6263", + "relationshipType": "DESCRIBE", + "spdxElementId": "SPDXRef-DOCUMENT" + }, + { + "relatedSpdxElement": "SPDXRef-OperatingSystem-2e91c856c499a371", + "relationshipType": "CONTAINS", + "spdxElementId": "SPDXRef-ContainerImage-dd5cad897c6263" + }, + { + "relatedSpdxElement": "SPDXRef-Package-5a18334f22149877", + "relationshipType": "DEPENDS_ON", + "spdxElementId": "SPDXRef-OperatingSystem-2e91c856c499a371" + }, + { + "relatedSpdxElement": "SPDXRef-Package-e16b1cbaa5186199", + "relationshipType": "DEPENDS_ON", + "spdxElementId": "SPDXRef-OperatingSystem-2e91c856c499a371" + } + ], + "spdxVersion": "SPDX-2.2" +} \ No newline at end of file diff --git a/integration/testdata/fixtures/sbom/centos-7-spdx.txt b/integration/testdata/fixtures/sbom/centos-7-spdx.txt new file mode 100644 index 00000000000..7032af92b1b --- /dev/null +++ b/integration/testdata/fixtures/sbom/centos-7-spdx.txt @@ -0,0 +1,57 @@ +SPDXVersion: SPDX-2.2 +DataLicense: CC0-1.0 +SPDXID: SPDXRef-DOCUMENT +DocumentName: integration/testdata/fixtures/images/centos-7.tar.gz +DocumentNamespace: http://aquasecurity.github.io/trivy/container_image/integration/testdata/fixtures/images/centos-7.tar.gz-6a2c050f-bc12-46dc-b2df-1f4e3e0b5e1d +Creator: Organization: aquasecurity +Creator: Tool: trivy +Created: 2022-09-13T13:24:58.796907Z + +##### Package: integration/testdata/fixtures/images/centos-7.tar.gz + +PackageName: integration/testdata/fixtures/images/centos-7.tar.gz +SPDXID: SPDXRef-ContainerImage-dd5cad897c6263 +FilesAnalyzed: false +PackageAttributionText: SchemaVersion: 2 +PackageAttributionText: ImageID: sha256:f1cb7c7d58b73eac859c395882eec49d50651244e342cd6c68a5c7809785f427 +PackageAttributionText: DiffID: sha256:89169d87dbe2b72ba42bfbb3579c957322baca28e03a1e558076542a1c1b2b4a + +##### Package: centos + +PackageName: centos +SPDXID: SPDXRef-OperatingSystem-2e91c856c499a371 +PackageVersion: 7.6.1810 +FilesAnalyzed: false + +##### Package: bash + +PackageName: bash +SPDXID: SPDXRef-Package-5a18334f22149877 +PackageVersion: 4.2.46 +FilesAnalyzed: false +PackageSourceInfo: built package from: bash 4.2.46-31.el7 +PackageLicenseConcluded: GPLv3+ +PackageLicenseDeclared: GPLv3+ +ExternalRef: PACKAGE-MANAGER purl pkg:rpm/centos/bash@4.2.46-31.el7?arch=x86_64&distro=centos-7.6.1810 +PackageAttributionText: LayerDigest: sha256:ac9208207adaac3a48e54a4dc6b49c69e78c3072d2b3add7efdabf814db2133b +PackageAttributionText: LayerDiffID: sha256:89169d87dbe2b72ba42bfbb3579c957322baca28e03a1e558076542a1c1b2b4a + +##### Package: openssl-libs + +PackageName: openssl-libs +SPDXID: SPDXRef-Package-e16b1cbaa5186199 +PackageVersion: 1.0.2k +FilesAnalyzed: false +PackageSourceInfo: built package from: openssl-libs 1:1.0.2k-16.el7 +PackageLicenseConcluded: OpenSSL +PackageLicenseDeclared: OpenSSL +ExternalRef: PACKAGE-MANAGER purl pkg:rpm/centos/openssl-libs@1:1.0.2k-16.el7?arch=x86_64&distro=centos-7.6.1810 +PackageAttributionText: LayerDigest: sha256:ac9208207adaac3a48e54a4dc6b49c69e78c3072d2b3add7efdabf814db2133b +PackageAttributionText: LayerDiffID: sha256:89169d87dbe2b72ba42bfbb3579c957322baca28e03a1e558076542a1c1b2b4a + +##### Relationships + +Relationship: SPDXRef-DOCUMENT DESCRIBE SPDXRef-ContainerImage-dd5cad897c6263 +Relationship: SPDXRef-ContainerImage-dd5cad897c6263 CONTAINS SPDXRef-OperatingSystem-2e91c856c499a371 +Relationship: SPDXRef-OperatingSystem-2e91c856c499a371 DEPENDS_ON SPDXRef-Package-5a18334f22149877 +Relationship: SPDXRef-OperatingSystem-2e91c856c499a371 DEPENDS_ON SPDXRef-Package-e16b1cbaa5186199 \ No newline at end of file From a7d5cc2585445b52ba1655717507c2a08615c46d Mon Sep 17 00:00:00 2001 From: masahiro331 Date: Wed, 14 Sep 2022 02:15:42 +0900 Subject: [PATCH 07/12] fix(spdx): keep original spdx --- pkg/fanal/artifact/sbom/sbom.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/fanal/artifact/sbom/sbom.go b/pkg/fanal/artifact/sbom/sbom.go index 530184f27e6..b74d5a9f251 100644 --- a/pkg/fanal/artifact/sbom/sbom.go +++ b/pkg/fanal/artifact/sbom/sbom.go @@ -99,6 +99,7 @@ func (a Artifact) Inspect(_ context.Context) (types.ArtifactReference, error) { // Keep an original report CycloneDX: bom.CycloneDX, + SPDX: bom.SPDX, }, nil } From e11576a13e5adbf99fc5fd7d7fd18b456ea28605 Mon Sep 17 00:00:00 2001 From: knqyf263 Date: Wed, 14 Sep 2022 15:31:25 +0300 Subject: [PATCH 08/12] refactor --- pkg/sbom/spdx/unmarshal.go | 200 ++++++++++++++++++------------------- 1 file changed, 95 insertions(+), 105 deletions(-) diff --git a/pkg/sbom/spdx/unmarshal.go b/pkg/sbom/spdx/unmarshal.go index 033144f37bb..e610cf1cacc 100644 --- a/pkg/sbom/spdx/unmarshal.go +++ b/pkg/sbom/spdx/unmarshal.go @@ -8,6 +8,7 @@ import ( version "github.com/knqyf263/go-rpm-version" "github.com/package-url/packageurl-go" + "github.com/samber/lo" "github.com/spdx/tools-golang/jsonloader" "github.com/spdx/tools-golang/spdx" "github.com/spdx/tools-golang/tvloader" @@ -19,14 +20,11 @@ import ( ) var ( - errInvalidPackageFormat = xerrors.New("invalid package format") + errUnknownPackageFormat = xerrors.New("unknown package format") ) type SPDX struct { *sbom.SBOM - - relationships map[spdx.ElementID][]spdx.ElementID - packages map[spdx.ElementID]*spdx.Package2_2 } func NewTVDecoder(r io.Reader) *TVDecoder { @@ -68,139 +66,157 @@ func (s *SPDX) UnmarshalJSON(b []byte) error { } func (s *SPDX) unmarshal(spdxDocument *spdx.Document2_2) error { - s.relationships = relationshipMap(spdxDocument.Relationships) - s.packages = spdxDocument.Packages + var osPkgs []ftypes.Package + apps := map[spdx.ElementID]*ftypes.Application{} + + // Package relationships would be as belows: + // - Root (container image, filesystem, etc.) + // - Operating System (debian 10) + // - OS package A + // - OS package B + // - Application 1 (package-lock.json) + // - Node.js package A + // - Node.js package B + // - Application 2 (Pipfile.lock) + // - Python package A + // - Python package B + for _, rel := range spdxDocument.Relationships { + pkgA := lo.FromPtr(spdxDocument.Packages[rel.RefA.ElementRefID]) + pkgB := lo.FromPtr(spdxDocument.Packages[rel.RefB.ElementRefID]) - for pkgID := range s.relationships { - pkg := s.packages[pkgID] switch { - case strings.HasPrefix(string(pkg.PackageSPDXIdentifier), ElementOperatingSystem): - s.SBOM.OS = parseOS(pkg) - pkgs, err := s.parsePkgs(pkg.PackageSPDXIdentifier) + // Relationship: root package => OS + case isOperatingSystem(pkgB.PackageSPDXIdentifier): + s.SBOM.OS = parseOS(pkgB) + // Relationship: OS => OS package + case isOperatingSystem(pkgA.PackageSPDXIdentifier): + pkg, err := parsePkg(pkgB) if err != nil { - return xerrors.Errorf("failed to parse os packages: %w", err) + return xerrors.Errorf("failed to parse os package: %w", err) } - if len(pkgs) != 0 { - s.SBOM.Packages = []ftypes.PackageInfo{{Packages: pkgs}} + osPkgs = append(osPkgs, *pkg) + // Relationship: root package => application + case isApplication(pkgB.PackageSPDXIdentifier): + // pass + // Relationship: application => language-specific package + case isApplication(pkgA.PackageSPDXIdentifier): + app, ok := apps[pkgA.PackageSPDXIdentifier] + if !ok { + app = initApplication(pkgA) + apps[pkgA.PackageSPDXIdentifier] = app } - case strings.HasPrefix(string(pkg.PackageSPDXIdentifier), ElementApplication): - app, err := s.parseApplication(pkg) + lib, err := parsePkg(pkgB) if err != nil { - return xerrors.Errorf("failed to parse application: %w", err) + return xerrors.Errorf("failed to parse language-specific package: %w", err) } - s.SBOM.Applications = append(s.SBOM.Applications, *app) + app.Libraries = append(app.Libraries, *lib) } } + // Fill OS packages + if len(osPkgs) > 0 { + s.Packages = []ftypes.PackageInfo{{Packages: osPkgs}} + } + + // Fill applications + for _, app := range apps { + s.SBOM.Applications = append(s.SBOM.Applications, *app) + } + + // Keep the original document s.SPDX = spdxDocument return nil } -func (s *SPDX) parseApplication(pkg *spdx.Package2_2) (*ftypes.Application, error) { - pkgs, err := s.parsePkgs(pkg.PackageSPDXIdentifier) - if err != nil { - return nil, xerrors.Errorf("failed to parse language packages: %w", err) - } +func isOperatingSystem(elementID spdx.ElementID) bool { + return strings.HasPrefix(string(elementID), ElementOperatingSystem) +} + +func isApplication(elementID spdx.ElementID) bool { + return strings.HasPrefix(string(elementID), ElementApplication) +} + +func initApplication(pkg spdx.Package2_2) *ftypes.Application { app := &ftypes.Application{ - Type: pkg.PackageName, - FilePath: pkg.PackageSourceInfo, - Libraries: pkgs, + Type: pkg.PackageName, + FilePath: pkg.PackageSourceInfo, } if pkg.PackageName == ftypes.NodePkg || pkg.PackageName == ftypes.PythonPkg || pkg.PackageName == ftypes.GemSpec || pkg.PackageName == ftypes.Jar { app.FilePath = "" } - return app, nil - -} - -func (s *SPDX) parsePkgs(id spdx.ElementID) ([]ftypes.Package, error) { - pkgIDs := s.relationships[id] - - var pkgs []ftypes.Package - for _, id := range pkgIDs { - spdxPkg := s.packages[id] - pkg, err := parsePkg(spdxPkg) - if err != nil { - return nil, xerrors.Errorf("failed to parse package: %w", err) - } - - pkgs = append(pkgs, *pkg) - } - return pkgs, nil + return app } -func parseOS(pkg *spdx.Package2_2) *ftypes.OS { +func parseOS(pkg spdx.Package2_2) *ftypes.OS { return &ftypes.OS{ Family: pkg.PackageName, Name: pkg.PackageVersion, } } -func parsePkg(package2_2 *spdx.Package2_2) (*ftypes.Package, error) { - var ( - pkg *ftypes.Package - typ string - ) - for _, ref := range package2_2.PackageExternalReferences { - if ref.RefType == RefTypePurl && ref.Category == CategoryPackageManager { - packageURL, err := purl.FromString(ref.Locator) - if err != nil { - return nil, xerrors.Errorf("failed to parse purl from string: %w", err) - } - pkg = packageURL.Package() - pkg.Ref = ref.Locator - typ = packageURL.Type - break - } - } - if pkg == nil { - return nil, errInvalidPackageFormat +func parsePkg(spdxPkg spdx.Package2_2) (*ftypes.Package, error) { + pkg, pkgType, err := parseExternalReferences(spdxPkg.PackageExternalReferences) + if err != nil { + return nil, xerrors.Errorf("external references error: %w", err) } - if package2_2.PackageLicenseDeclared != "NONE" { - pkg.Licenses = strings.Split(package2_2.PackageLicenseDeclared, ",") + if spdxPkg.PackageLicenseDeclared != "NONE" { + pkg.Licenses = strings.Split(spdxPkg.PackageLicenseDeclared, ",") } - pkg.Name = package2_2.PackageName - pkg.Version = package2_2.PackageVersion - if strings.HasPrefix(package2_2.PackageSourceInfo, SourcePackagePrefix) { - var err error - srcPkgName := strings.TrimPrefix(package2_2.PackageSourceInfo, fmt.Sprintf("%s: ", SourcePackagePrefix)) - pkg.SrcEpoch, pkg.SrcName, pkg.SrcVersion, pkg.SrcRelease, err = parseSourceInfo(typ, srcPkgName) + if strings.HasPrefix(spdxPkg.PackageSourceInfo, SourcePackagePrefix) { + srcPkgName := strings.TrimPrefix(spdxPkg.PackageSourceInfo, fmt.Sprintf("%s: ", SourcePackagePrefix)) + pkg.SrcEpoch, pkg.SrcName, pkg.SrcVersion, pkg.SrcRelease, err = parseSourceInfo(pkgType, srcPkgName) if err != nil { return nil, xerrors.Errorf("failed to parse source info: %w", err) } } - for _, f := range package2_2.Files { + for _, f := range spdxPkg.Files { pkg.FilePath = f.FileName + break // Take the first file name } - pkg.Layer.Digest = lookupAttributionTexts(package2_2.PackageAttributionTexts, PropertyLayerDigest) - pkg.Layer.DiffID = lookupAttributionTexts(package2_2.PackageAttributionTexts, PropertyLayerDiffID) + pkg.Layer.Digest = lookupAttributionTexts(spdxPkg.PackageAttributionTexts, PropertyLayerDigest) + pkg.Layer.DiffID = lookupAttributionTexts(spdxPkg.PackageAttributionTexts, PropertyLayerDiffID) return pkg, nil } -func lookupAttributionTexts(attributionTexts []string, key string) (value string) { +func parseExternalReferences(refs []*spdx.PackageExternalReference2_2) (*ftypes.Package, string, error) { + for _, ref := range refs { + // Extract the package information from PURL + if ref.RefType == RefTypePurl && ref.Category == CategoryPackageManager { + packageURL, err := purl.FromString(ref.Locator) + if err != nil { + return nil, "", xerrors.Errorf("failed to parse purl from string: %w", err) + } + pkg := packageURL.Package() + pkg.Ref = ref.Locator + return pkg, packageURL.Type, nil + } + } + return nil, "", errUnknownPackageFormat +} + +func lookupAttributionTexts(attributionTexts []string, key string) string { for _, text := range attributionTexts { if strings.HasPrefix(text, key) { return strings.TrimPrefix(text, fmt.Sprintf("%s: ", key)) } } - return "" } -func parseSourceInfo(typ, sourceInfo string) (epoch int, name, ver, rel string, err error) { +func parseSourceInfo(pkgType, sourceInfo string) (epoch int, name, ver, rel string, err error) { srcNameVersion := strings.TrimPrefix(sourceInfo, fmt.Sprintf("%s: ", SourcePackagePrefix)) ss := strings.Split(srcNameVersion, " ") if len(ss) != 2 { return 0, "", "", "", xerrors.Errorf("invalid source info (%s)", sourceInfo) } name = ss[0] - if typ == packageurl.TypeRPM { + if pkgType == packageurl.TypeRPM { v := version.NewVersion(ss[1]) epoch = v.Epoch() ver = v.Version() @@ -210,29 +226,3 @@ func parseSourceInfo(typ, sourceInfo string) (epoch int, name, ver, rel string, } return epoch, name, ver, rel, nil } - -func relationshipMap(relationships []*spdx.Relationship2_2) map[spdx.ElementID][]spdx.ElementID { - relationshipMap := make(map[spdx.ElementID][]spdx.ElementID) - var rootElement spdx.ElementID - for _, relationship := range relationships { - if relationship.Relationship == RelationShipDescribe { - rootElement = relationship.RefB.ElementRefID - } - } - for _, relationship := range relationships { - if relationship.Relationship == RelationShipContains { - if relationship.RefA.ElementRefID == rootElement { - relationshipMap[relationship.RefB.ElementRefID] = []spdx.ElementID{} - } - } - } - for _, relationship := range relationships { - if relationship.Relationship == RelationShipDependsOn { - if array, ok := relationshipMap[relationship.RefA.ElementRefID]; ok { - relationshipMap[relationship.RefA.ElementRefID] = append(array, relationship.RefB.ElementRefID) - } - } - } - - return relationshipMap -} From 9eead27f140ba82a4b2c7a1fb067165b82451d2d Mon Sep 17 00:00:00 2001 From: knqyf263 Date: Wed, 14 Sep 2022 15:59:13 +0300 Subject: [PATCH 09/12] test: remove cruft --- integration/sbom_test.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/integration/sbom_test.go b/integration/sbom_test.go index 4a91f65e5de..6216af8511c 100644 --- a/integration/sbom_test.go +++ b/integration/sbom_test.go @@ -3,7 +3,6 @@ package integration import ( - "fmt" "os" "path/filepath" "testing" @@ -28,7 +27,7 @@ func TestSBOM(t *testing.T) { golden string }{ { - name: "centos7-bom by trivy", + name: "centos7 cyclonedx by trivy", args: args{ input: "testdata/fixtures/sbom/centos-7-cyclonedx.json", format: "cyclonedx", @@ -55,7 +54,7 @@ func TestSBOM(t *testing.T) { golden: "testdata/centos-7-cyclonedx.json.golden", }, { - name: "centos7 spdx type tag-value by trivy", + name: "centos7 spdx tag-value by trivy", args: args{ input: "testdata/fixtures/sbom/centos-7-spdx.txt", format: "json", @@ -64,7 +63,7 @@ func TestSBOM(t *testing.T) { golden: "testdata/centos-7-spdx.json.golden", }, { - name: "centos7 spdx type json by trivy", + name: "centos7 spdx json by trivy", args: args{ input: "testdata/fixtures/sbom/centos-7-spdx.json", format: "json", @@ -121,11 +120,9 @@ func decodeSPDX(t *testing.T, format string, filePath string) *spdx.Document2_2 var spdxDocument *spdx.Document2_2 switch format { case "spdx-json": - fmt.Println(filePath) spdxDocument, err = jsonloader.Load2_2(f) require.NoError(t, err) case "spdx": - fmt.Println(filePath) spdxDocument, err = tvloader.Load2_2(f) require.NoError(t, err) } From fcc2651b14d51e8b85dc079140193e331abe449e Mon Sep 17 00:00:00 2001 From: knqyf263 Date: Wed, 14 Sep 2022 15:59:24 +0300 Subject: [PATCH 10/12] remove an unused field --- pkg/fanal/artifact/sbom/sbom.go | 1 - pkg/fanal/types/artifact.go | 2 -- pkg/scanner/scan.go | 1 - pkg/types/report.go | 3 --- 4 files changed, 7 deletions(-) diff --git a/pkg/fanal/artifact/sbom/sbom.go b/pkg/fanal/artifact/sbom/sbom.go index b74d5a9f251..530184f27e6 100644 --- a/pkg/fanal/artifact/sbom/sbom.go +++ b/pkg/fanal/artifact/sbom/sbom.go @@ -99,7 +99,6 @@ func (a Artifact) Inspect(_ context.Context) (types.ArtifactReference, error) { // Keep an original report CycloneDX: bom.CycloneDX, - SPDX: bom.SPDX, }, nil } diff --git a/pkg/fanal/types/artifact.go b/pkg/fanal/types/artifact.go index ffd1e3e2677..636cf39aaaf 100644 --- a/pkg/fanal/types/artifact.go +++ b/pkg/fanal/types/artifact.go @@ -4,7 +4,6 @@ import ( "time" v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/spdx/tools-golang/spdx" ) type OS struct { @@ -110,7 +109,6 @@ type ArtifactReference struct { // SBOM CycloneDX *CycloneDX - SPDX *spdx.Document2_2 } type ImageMetadata struct { diff --git a/pkg/scanner/scan.go b/pkg/scanner/scan.go index 9e9a9620b26..9afe5dd0aa2 100644 --- a/pkg/scanner/scan.go +++ b/pkg/scanner/scan.go @@ -162,7 +162,6 @@ func (s Scanner) ScanArtifact(ctx context.Context, options types.ScanOptions) (t ImageConfig: artifactInfo.ImageMetadata.ConfigFile, }, CycloneDX: artifactInfo.CycloneDX, - SPDX: artifactInfo.SPDX, Results: results, }, nil } diff --git a/pkg/types/report.go b/pkg/types/report.go index 16890844af7..003112da2a4 100644 --- a/pkg/types/report.go +++ b/pkg/types/report.go @@ -4,7 +4,6 @@ import ( "encoding/json" v1 "github.com/google/go-containerregistry/pkg/v1" // nolint: goimports - "github.com/spdx/tools-golang/spdx" ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" ) @@ -19,8 +18,6 @@ type Report struct { // SBOM CycloneDX *ftypes.CycloneDX `json:"-"` // Just for internal usage, not exported in JSON - SPDX *spdx.Document2_2 `json:"-"` // Just for internal usage, not exported in JSON - } // Metadata represents a metadata of artifact From bd6ffe3ec2213e9e020a031ebcbd4bb7ba71e060 Mon Sep 17 00:00:00 2001 From: knqyf263 Date: Wed, 14 Sep 2022 17:12:12 +0300 Subject: [PATCH 11/12] test(integration): fix comparison Signed-off-by: knqyf263 --- integration/sbom_test.go | 42 +++++-------------- .../testdata/fixtures/sbom/centos-7-spdx.json | 4 +- 2 files changed, 12 insertions(+), 34 deletions(-) diff --git a/integration/sbom_test.go b/integration/sbom_test.go index 6216af8511c..d0e2cc87100 100644 --- a/integration/sbom_test.go +++ b/integration/sbom_test.go @@ -8,9 +8,6 @@ import ( "testing" cdx "github.com/CycloneDX/cyclonedx-go" - "github.com/spdx/tools-golang/jsonloader" - "github.com/spdx/tools-golang/spdx" - "github.com/spdx/tools-golang/tvloader" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -27,7 +24,7 @@ func TestSBOM(t *testing.T) { golden string }{ { - name: "centos7 cyclonedx by trivy", + name: "centos7 cyclonedx", args: args{ input: "testdata/fixtures/sbom/centos-7-cyclonedx.json", format: "cyclonedx", @@ -36,7 +33,7 @@ func TestSBOM(t *testing.T) { golden: "testdata/centos-7-cyclonedx.json.golden", }, { - name: "fluentd-multiple-lockfiles-bom by trivy", + name: "fluentd-multiple-lockfiles cyclonedx", args: args{ input: "testdata/fixtures/sbom/fluentd-multiple-lockfiles-cyclonedx.json", format: "cyclonedx", @@ -45,7 +42,7 @@ func TestSBOM(t *testing.T) { golden: "testdata/fluentd-multiple-lockfiles-cyclonedx.json.golden", }, { - name: "centos7-bom in in-toto attestation", + name: "centos7 in in-toto attestation", args: args{ input: "testdata/fixtures/sbom/centos-7-cyclonedx.intoto.jsonl", format: "cyclonedx", @@ -54,7 +51,7 @@ func TestSBOM(t *testing.T) { golden: "testdata/centos-7-cyclonedx.json.golden", }, { - name: "centos7 spdx tag-value by trivy", + name: "centos7 spdx tag-value", args: args{ input: "testdata/fixtures/sbom/centos-7-spdx.txt", format: "json", @@ -63,7 +60,7 @@ func TestSBOM(t *testing.T) { golden: "testdata/centos-7-spdx.json.golden", }, { - name: "centos7 spdx json by trivy", + name: "centos7 spdx json", args: args{ input: "testdata/fixtures/sbom/centos-7-spdx.json", format: "json", @@ -82,7 +79,7 @@ func TestSBOM(t *testing.T) { "--cache-dir", cacheDir, "sbom", "-q", "--skip-db-update", "--format", tt.args.format, } - // Setup the output file + // Set up the output file outputFile := filepath.Join(t.TempDir(), "output.json") if *update { outputFile = tt.golden @@ -96,39 +93,20 @@ func TestSBOM(t *testing.T) { assert.NoError(t, err) // Compare want and got - switch tt.args.artifactType { + switch tt.args.format { case "cyclonedx": want := decodeCycloneDX(t, tt.golden) got := decodeCycloneDX(t, outputFile) assert.Equal(t, want, got) - case "spdx", "spdx-json": - want := decodeSPDX(t, tt.args.format, tt.golden) - got := decodeSPDX(t, tt.args.format, outputFile) - assert.Equal(t, want, got) + case "json": + compareReports(t, tt.golden, outputFile) default: - t.Fatalf("invalid arguments format: %q", tt.args.format) + require.Fail(t, "invalid format", "format: %s", tt.args.format) } }) } } -func decodeSPDX(t *testing.T, format string, filePath string) *spdx.Document2_2 { - f, err := os.Open(filePath) - require.NoError(t, err) - defer f.Close() - - var spdxDocument *spdx.Document2_2 - switch format { - case "spdx-json": - spdxDocument, err = jsonloader.Load2_2(f) - require.NoError(t, err) - case "spdx": - spdxDocument, err = tvloader.Load2_2(f) - require.NoError(t, err) - } - return spdxDocument -} - func decodeCycloneDX(t *testing.T, filePath string) *cdx.BOM { f, err := os.Open(filePath) require.NoError(t, err) diff --git a/integration/testdata/fixtures/sbom/centos-7-spdx.json b/integration/testdata/fixtures/sbom/centos-7-spdx.json index ca635244bee..2b8e2011c90 100644 --- a/integration/testdata/fixtures/sbom/centos-7-spdx.json +++ b/integration/testdata/fixtures/sbom/centos-7-spdx.json @@ -81,12 +81,12 @@ }, { "relatedSpdxElement": "SPDXRef-Package-5a18334f22149877", - "relationshipType": "DEPENDS_ON", + "relationshipType": "CONTAINS", "spdxElementId": "SPDXRef-OperatingSystem-2e91c856c499a371" }, { "relatedSpdxElement": "SPDXRef-Package-e16b1cbaa5186199", - "relationshipType": "DEPENDS_ON", + "relationshipType": "CONTAINS", "spdxElementId": "SPDXRef-OperatingSystem-2e91c856c499a371" } ], From 1c2a7deac286e68d3ee34dc2e12e63e6dbcdcb41 Mon Sep 17 00:00:00 2001 From: knqyf263 Date: Thu, 15 Sep 2022 00:33:56 +0300 Subject: [PATCH 12/12] test(integration): use the same golden --- integration/client_server_test.go | 3 +- integration/docker_engine_test.go | 8 +- integration/sbom_test.go | 73 +++++- .../testdata/centos-7-spdx.json.golden | 220 ------------------ 4 files changed, 71 insertions(+), 233 deletions(-) delete mode 100644 integration/testdata/centos-7-spdx.json.golden diff --git a/integration/client_server_test.go b/integration/client_server_test.go index e8c8151c6a2..d858a7aa66a 100644 --- a/integration/client_server_test.go +++ b/integration/client_server_test.go @@ -12,10 +12,9 @@ import ( "testing" "time" - "github.com/samber/lo" - cdx "github.com/CycloneDX/cyclonedx-go" "github.com/docker/go-connections/nat" + "github.com/samber/lo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" testcontainers "github.com/testcontainers/testcontainers-go" diff --git a/integration/docker_engine_test.go b/integration/docker_engine_test.go index ea1fb92722a..3dc30830914 100644 --- a/integration/docker_engine_test.go +++ b/integration/docker_engine_test.go @@ -11,7 +11,7 @@ import ( "strings" "testing" - "github.com/docker/docker/api/types" + api "github.com/docker/docker/api/types" "github.com/docker/docker/client" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -213,7 +213,7 @@ func TestDockerEngine(t *testing.T) { require.NoError(t, err, tt.name) // ensure image doesnt already exists - _, _ = cli.ImageRemove(ctx, tt.input, types.ImageRemoveOptions{ + _, _ = cli.ImageRemove(ctx, tt.input, api.ImageRemoveOptions{ Force: true, PruneChildren: true, }) @@ -264,11 +264,11 @@ func TestDockerEngine(t *testing.T) { compareReports(t, tt.golden, output) // cleanup - _, err = cli.ImageRemove(ctx, tt.input, types.ImageRemoveOptions{ + _, err = cli.ImageRemove(ctx, tt.input, api.ImageRemoveOptions{ Force: true, PruneChildren: true, }) - _, err = cli.ImageRemove(ctx, tt.imageTag, types.ImageRemoveOptions{ + _, err = cli.ImageRemove(ctx, tt.imageTag, api.ImageRemoveOptions{ Force: true, PruneChildren: true, }) diff --git a/integration/sbom_test.go b/integration/sbom_test.go index d0e2cc87100..f9e2c5f4c11 100644 --- a/integration/sbom_test.go +++ b/integration/sbom_test.go @@ -8,8 +8,12 @@ import ( "testing" cdx "github.com/CycloneDX/cyclonedx-go" + v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/types" ) func TestSBOM(t *testing.T) { @@ -19,9 +23,10 @@ func TestSBOM(t *testing.T) { artifactType string } tests := []struct { - name string - args args - golden string + name string + args args + golden string + override types.Report }{ { name: "centos7 cyclonedx", @@ -57,16 +62,44 @@ func TestSBOM(t *testing.T) { format: "json", artifactType: "spdx", }, - golden: "testdata/centos-7-spdx.json.golden", + golden: "testdata/centos-7.json.golden", + override: types.Report{ + ArtifactName: "testdata/fixtures/sbom/centos-7-spdx.txt", + ArtifactType: ftypes.ArtifactType("spdx"), + Results: types.Results{ + { + Target: "testdata/fixtures/sbom/centos-7-spdx.txt (centos 7.6.1810)", + Vulnerabilities: []types.DetectedVulnerability{ + {Ref: "pkg:rpm/centos/bash@4.2.46-31.el7?arch=x86_64&distro=centos-7.6.1810"}, + {Ref: "pkg:rpm/centos/openssl-libs@1:1.0.2k-16.el7?arch=x86_64&distro=centos-7.6.1810"}, + {Ref: "pkg:rpm/centos/openssl-libs@1:1.0.2k-16.el7?arch=x86_64&distro=centos-7.6.1810"}, + }, + }, + }, + }, }, { name: "centos7 spdx json", args: args{ input: "testdata/fixtures/sbom/centos-7-spdx.json", format: "json", - artifactType: "spdx-json", + artifactType: "spdx", + }, + golden: "testdata/centos-7.json.golden", + override: types.Report{ + ArtifactName: "testdata/fixtures/sbom/centos-7-spdx.json", + ArtifactType: ftypes.ArtifactType("spdx"), + Results: types.Results{ + { + Target: "testdata/fixtures/sbom/centos-7-spdx.json (centos 7.6.1810)", + Vulnerabilities: []types.DetectedVulnerability{ + {Ref: "pkg:rpm/centos/bash@4.2.46-31.el7?arch=x86_64&distro=centos-7.6.1810"}, + {Ref: "pkg:rpm/centos/openssl-libs@1:1.0.2k-16.el7?arch=x86_64&distro=centos-7.6.1810"}, + {Ref: "pkg:rpm/centos/openssl-libs@1:1.0.2k-16.el7?arch=x86_64&distro=centos-7.6.1810"}, + }, + }, + }, }, - golden: "testdata/centos-7-spdx.json.golden", }, } @@ -99,7 +132,7 @@ func TestSBOM(t *testing.T) { got := decodeCycloneDX(t, outputFile) assert.Equal(t, want, got) case "json": - compareReports(t, tt.golden, outputFile) + compareSBOMReports(t, tt.golden, outputFile, tt.override) default: require.Fail(t, "invalid format", "format: %s", tt.args.format) } @@ -107,6 +140,32 @@ func TestSBOM(t *testing.T) { } } +// TODO(teppei): merge into compareReports +func compareSBOMReports(t *testing.T, wantFile, gotFile string, overrideWant types.Report) { + want := readReport(t, wantFile) + + want.ArtifactName = overrideWant.ArtifactName + want.ArtifactType = overrideWant.ArtifactType + want.Metadata.ImageID = "" + want.Metadata.ImageConfig = v1.ConfigFile{} + want.Metadata.DiffIDs = nil + for i, result := range want.Results { + for j := range result.Vulnerabilities { + want.Results[i].Vulnerabilities[j].Layer.DiffID = "" + } + } + + for i, result := range overrideWant.Results { + want.Results[i].Target = result.Target + for j, vuln := range result.Vulnerabilities { + want.Results[i].Vulnerabilities[j].Ref = vuln.Ref + } + } + + got := readReport(t, gotFile) + assert.Equal(t, want, got) +} + func decodeCycloneDX(t *testing.T, filePath string) *cdx.BOM { f, err := os.Open(filePath) require.NoError(t, err) diff --git a/integration/testdata/centos-7-spdx.json.golden b/integration/testdata/centos-7-spdx.json.golden deleted file mode 100644 index d6bf4cbdf87..00000000000 --- a/integration/testdata/centos-7-spdx.json.golden +++ /dev/null @@ -1,220 +0,0 @@ -{ - "SchemaVersion": 2, - "ArtifactName": "testdata/fixtures/sbom/centos-7-spdx.json", - "ArtifactType": "spdx", - "Metadata": { - "OS": { - "Family": "centos", - "Name": "7.6.1810" - }, - "ImageConfig": { - "architecture": "", - "created": "0001-01-01T00:00:00Z", - "os": "", - "rootfs": { - "type": "", - "diff_ids": null - }, - "config": {} - } - }, - "Results": [ - { - "Target": "testdata/fixtures/sbom/centos-7-spdx.json (centos 7.6.1810)", - "Class": "os-pkgs", - "Type": "centos", - "Vulnerabilities": [ - { - "VulnerabilityID": "CVE-2019-18276", - "PkgName": "bash", - "InstalledVersion": "4.2.46-31.el7", - "Layer": {}, - "SeveritySource": "redhat", - "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-18276", - "Ref": "pkg:rpm/centos/bash@4.2.46-31.el7?arch=x86_64\u0026distro=centos-7.6.1810", - "Title": "bash: when effective UID is not equal to its real UID the saved UID is not dropped", - "Description": "An issue was discovered in disable_priv_mode in shell.c in GNU Bash through 5.0 patch 11. By default, if Bash is run with its effective UID not equal to its real UID, it will drop privileges by setting its effective UID to its real UID. However, it does so incorrectly. On Linux and other systems that support \"saved UID\" functionality, the saved UID is not dropped. An attacker with command execution in the shell can use \"enable -f\" for runtime loading of a new builtin, which can be a shared object that calls setuid() and therefore regains privileges. However, binaries running with an effective UID of 0 are unaffected.", - "Severity": "LOW", - "CweIDs": [ - "CWE-273" - ], - "CVSS": { - "nvd": { - "V2Vector": "AV:L/AC:L/Au:N/C:C/I:C/A:C", - "V3Vector": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", - "V2Score": 7.2, - "V3Score": 7.8 - }, - "redhat": { - "V3Vector": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", - "V3Score": 7.8 - } - }, - "References": [ - "http://packetstormsecurity.com/files/155498/Bash-5.0-Patch-11-Privilege-Escalation.html", - "https://access.redhat.com/security/cve/CVE-2019-18276", - "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-18276", - "https://github.com/bminor/bash/commit/951bdaad7a18cc0dc1036bba86b18b90874d39ff", - "https://linux.oracle.com/cve/CVE-2019-18276.html", - "https://linux.oracle.com/errata/ELSA-2021-1679.html", - "https://lists.apache.org/thread.html/rf9fa47ab66495c78bb4120b0754dd9531ca2ff0430f6685ac9b07772@%3Cdev.mina.apache.org%3E", - "https://nvd.nist.gov/vuln/detail/CVE-2019-18276", - "https://security.gentoo.org/glsa/202105-34", - "https://security.netapp.com/advisory/ntap-20200430-0003/", - "https://www.youtube.com/watch?v=-wGtxJ8opa8" - ], - "PublishedDate": "2019-11-28T01:15:00Z", - "LastModifiedDate": "2021-05-26T12:15:00Z" - }, - { - "VulnerabilityID": "CVE-2019-1559", - "VendorIDs": [ - "RHSA-2019:2304" - ], - "PkgName": "openssl-libs", - "InstalledVersion": "1:1.0.2k-16.el7", - "FixedVersion": "1:1.0.2k-19.el7", - "Layer": {}, - "SeveritySource": "redhat", - "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-1559", - "Ref": "pkg:rpm/centos/openssl-libs@1:1.0.2k-16.el7?arch=x86_64\u0026distro=centos-7.6.1810", - "Title": "openssl: 0-byte record padding oracle", - "Description": "If an application encounters a fatal protocol error and then calls SSL_shutdown() twice (once to send a close_notify, and once to receive one) then OpenSSL can respond differently to the calling application if a 0 byte record is received with invalid padding compared to if a 0 byte record is received with an invalid MAC. If the application then behaves differently based on that in a way that is detectable to the remote peer, then this amounts to a padding oracle that could be used to decrypt data. In order for this to be exploitable \"non-stitched\" ciphersuites must be in use. Stitched ciphersuites are optimised implementations of certain commonly used ciphersuites. Also the application must call SSL_shutdown() twice even if a protocol error has occurred (applications should not do this but some do anyway). Fixed in OpenSSL 1.0.2r (Affected 1.0.2-1.0.2q).", - "Severity": "MEDIUM", - "CweIDs": [ - "CWE-203" - ], - "CVSS": { - "nvd": { - "V2Vector": "AV:N/AC:M/Au:N/C:P/I:N/A:N", - "V3Vector": "CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", - "V2Score": 4.3, - "V3Score": 5.9 - }, - "redhat": { - "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", - "V3Score": 5.9 - } - }, - "References": [ - "http://lists.opensuse.org/opensuse-security-announce/2019-03/msg00041.html", - "http://lists.opensuse.org/opensuse-security-announce/2019-04/msg00019.html", - "http://lists.opensuse.org/opensuse-security-announce/2019-04/msg00046.html", - "http://lists.opensuse.org/opensuse-security-announce/2019-04/msg00047.html", - "http://lists.opensuse.org/opensuse-security-announce/2019-05/msg00049.html", - "http://lists.opensuse.org/opensuse-security-announce/2019-06/msg00080.html", - "http://www.securityfocus.com/bid/107174", - "https://access.redhat.com/errata/RHSA-2019:2304", - "https://access.redhat.com/errata/RHSA-2019:2437", - "https://access.redhat.com/errata/RHSA-2019:2439", - "https://access.redhat.com/errata/RHSA-2019:2471", - "https://access.redhat.com/errata/RHSA-2019:3929", - "https://access.redhat.com/errata/RHSA-2019:3931", - "https://access.redhat.com/security/cve/CVE-2019-1559", - "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1559", - "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=e9bbefbf0f24c57645e7ad6a5a71ae649d18ac8e", - "https://github.com/RUB-NDS/TLS-Padding-Oracles", - "https://kc.mcafee.com/corporate/index?page=content\u0026id=SB10282", - "https://linux.oracle.com/cve/CVE-2019-1559.html", - "https://linux.oracle.com/errata/ELSA-2019-2471.html", - "https://lists.debian.org/debian-lts-announce/2019/03/msg00003.html", - "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/EWC42UXL5GHTU5G77VKBF6JYUUNGSHOM/", - "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/Y3IVFGSERAZLNJCK35TEM2R4726XIH3Z/", - "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/ZBEV5QGDRFUZDMNECFXUSN5FMYOZDE4V/", - "https://security.gentoo.org/glsa/201903-10", - "https://security.netapp.com/advisory/ntap-20190301-0001/", - "https://security.netapp.com/advisory/ntap-20190301-0002/", - "https://security.netapp.com/advisory/ntap-20190423-0002/", - "https://support.f5.com/csp/article/K18549143", - "https://support.f5.com/csp/article/K18549143?utm_source=f5support\u0026amp;utm_medium=RSS", - "https://ubuntu.com/security/notices/USN-3899-1", - "https://ubuntu.com/security/notices/USN-4376-2", - "https://usn.ubuntu.com/3899-1/", - "https://usn.ubuntu.com/4376-2/", - "https://www.debian.org/security/2019/dsa-4400", - "https://www.openssl.org/news/secadv/20190226.txt", - "https://www.oracle.com/security-alerts/cpujan2020.html", - "https://www.oracle.com/security-alerts/cpujan2021.html", - "https://www.oracle.com/technetwork/security-advisory/cpuapr2019-5072813.html", - "https://www.oracle.com/technetwork/security-advisory/cpujul2019-5072835.html", - "https://www.oracle.com/technetwork/security-advisory/cpuoct2019-5072832.html", - "https://www.tenable.com/security/tns-2019-02", - "https://www.tenable.com/security/tns-2019-03" - ], - "PublishedDate": "2019-02-27T23:29:00Z", - "LastModifiedDate": "2021-01-20T15:15:00Z" - }, - { - "VulnerabilityID": "CVE-2018-0734", - "VendorIDs": [ - "RHSA-2019:2304" - ], - "PkgName": "openssl-libs", - "InstalledVersion": "1:1.0.2k-16.el7", - "FixedVersion": "1:1.0.2k-19.el7", - "Layer": {}, - "SeveritySource": "redhat", - "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2018-0734", - "Ref": "pkg:rpm/centos/openssl-libs@1:1.0.2k-16.el7?arch=x86_64\u0026distro=centos-7.6.1810", - "Title": "openssl: timing side channel attack in the DSA signature algorithm", - "Description": "The OpenSSL DSA signature algorithm has been shown to be vulnerable to a timing side channel attack. An attacker could use variations in the signing algorithm to recover the private key. Fixed in OpenSSL 1.1.1a (Affected 1.1.1). Fixed in OpenSSL 1.1.0j (Affected 1.1.0-1.1.0i). Fixed in OpenSSL 1.0.2q (Affected 1.0.2-1.0.2p).", - "Severity": "LOW", - "CweIDs": [ - "CWE-327" - ], - "CVSS": { - "nvd": { - "V2Vector": "AV:N/AC:M/Au:N/C:P/I:N/A:N", - "V3Vector": "CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", - "V2Score": 4.3, - "V3Score": 5.9 - }, - "redhat": { - "V3Vector": "CVSS:3.0/AV:L/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", - "V3Score": 5.1 - } - }, - "References": [ - "http://lists.opensuse.org/opensuse-security-announce/2019-06/msg00030.html", - "http://lists.opensuse.org/opensuse-security-announce/2019-07/msg00056.html", - "http://www.securityfocus.com/bid/105758", - "https://access.redhat.com/errata/RHSA-2019:2304", - "https://access.redhat.com/errata/RHSA-2019:3700", - "https://access.redhat.com/errata/RHSA-2019:3932", - "https://access.redhat.com/errata/RHSA-2019:3933", - "https://access.redhat.com/errata/RHSA-2019:3935", - "https://access.redhat.com/security/cve/CVE-2018-0734", - "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-0734", - "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=43e6a58d4991a451daf4891ff05a48735df871ac", - "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=8abfe72e8c1de1b95f50aa0d9134803b4d00070f", - "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=ef11e19d1365eea2b1851e6f540a0bf365d303e7", - "https://linux.oracle.com/cve/CVE-2018-0734.html", - "https://linux.oracle.com/errata/ELSA-2019-3700.html", - "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/EWC42UXL5GHTU5G77VKBF6JYUUNGSHOM/", - "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/Y3IVFGSERAZLNJCK35TEM2R4726XIH3Z/", - "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/ZBEV5QGDRFUZDMNECFXUSN5FMYOZDE4V/", - "https://nodejs.org/en/blog/vulnerability/november-2018-security-releases/", - "https://nvd.nist.gov/vuln/detail/CVE-2018-0734", - "https://security.netapp.com/advisory/ntap-20181105-0002/", - "https://security.netapp.com/advisory/ntap-20190118-0002/", - "https://security.netapp.com/advisory/ntap-20190423-0002/", - "https://ubuntu.com/security/notices/USN-3840-1", - "https://usn.ubuntu.com/3840-1/", - "https://www.debian.org/security/2018/dsa-4348", - "https://www.debian.org/security/2018/dsa-4355", - "https://www.openssl.org/news/secadv/20181030.txt", - "https://www.oracle.com/security-alerts/cpuapr2020.html", - "https://www.oracle.com/security-alerts/cpujan2020.html", - "https://www.oracle.com/technetwork/security-advisory/cpuapr2019-5072813.html", - "https://www.oracle.com/technetwork/security-advisory/cpujan2019-5072801.html", - "https://www.oracle.com/technetwork/security-advisory/cpujul2019-5072835.html", - "https://www.tenable.com/security/tns-2018-16", - "https://www.tenable.com/security/tns-2018-17" - ], - "PublishedDate": "2018-10-30T12:29:00Z", - "LastModifiedDate": "2020-08-24T17:37:00Z" - } - ] - } - ] -}