diff --git a/.github/workflows/reusable-release.yaml b/.github/workflows/reusable-release.yaml index 0db71c1ccad..554d8820f23 100644 --- a/.github/workflows/reusable-release.yaml +++ b/.github/workflows/reusable-release.yaml @@ -13,7 +13,6 @@ on: type: string env: - GO_VERSION: "1.18" GH_USER: "aqua-bot" jobs: @@ -60,16 +59,16 @@ jobs: username: ${{ secrets.ECR_ACCESS_KEY_ID }} password: ${{ secrets.ECR_SECRET_ACCESS_KEY }} - - name: Setup Go - uses: actions/setup-go@v3 - with: - go-version: ${{ env.GO_VERSION }} - - name: Checkout code uses: actions/checkout@v3 with: fetch-depth: 0 + - name: Setup Go + uses: actions/setup-go@v3 + with: + go-version-file: go.mod + - name: Generate SBOM uses: CycloneDX/gh-gomod-generate-sbom@v1 with: @@ -106,4 +105,4 @@ jobs: # use 'github.sha' to create a unique cache folder for each run. # use 'github.workflow' to create a unique cache folder if some runs have same commit sha. # e.g. build and release runs - key: ${{ runner.os }}-bins-${{github.workflow}}-${{github.sha}} \ No newline at end of file + key: ${{ runner.os }}-bins-${{github.workflow}}-${{github.sha}} diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index bd0f3353b45..7da575afb19 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -10,8 +10,7 @@ on: - 'LICENSE' pull_request: env: - GO_VERSION: "1.18" - TINYGO_VERSION: "0.24.0" + TINYGO_VERSION: "0.25.0" jobs: test: name: Test @@ -22,7 +21,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v3 with: - go-version: ${{ env.GO_VERSION }} + go-version-file: go.mod - name: go mod tidy run: | @@ -35,7 +34,7 @@ jobs: - name: Lint uses: golangci/golangci-lint-action@v3.2.0 with: - version: v1.45 + version: v1.49 args: --deadline=30m skip-cache: true # https://github.com/golangci/golangci-lint-action/issues/244#issuecomment-1052197778 @@ -51,36 +50,34 @@ jobs: name: Integration Test runs-on: ubuntu-latest steps: - - name: Set up Go - uses: actions/setup-go@v3 - with: - go-version: ${{ env.GO_VERSION }} - id: go + - name: Check out code into the Go module directory + uses: actions/checkout@v3 - - name: Check out code into the Go module directory - uses: actions/checkout@v3 + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version-file: go.mod - - name: Run integration tests - run: make test-integration + - name: Run integration tests + run: make test-integration module-test: name: Module Integration Test runs-on: ubuntu-latest steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up Go uses: actions/setup-go@v3 with: - go-version: ${{ env.GO_VERSION }} - id: go + go-version-file: go.mod - name: Install TinyGo run: | wget https://github.com/tinygo-org/tinygo/releases/download/v${TINYGO_VERSION}/tinygo_${TINYGO_VERSION}_amd64.deb sudo dpkg -i tinygo_${TINYGO_VERSION}_amd64.deb - - name: Checkout - uses: actions/checkout@v3 - - name: Run module integration tests run: | make test-module-integration @@ -107,7 +104,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v3 with: - go-version: ${{ env.GO_VERSION }} + go-version-file: go.mod - name: Run GoReleaser uses: goreleaser/goreleaser-action@v3 diff --git a/.golangci.yaml b/.golangci.yaml index 844a86c0269..bcdaafec3a3 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -21,18 +21,17 @@ linters-settings: local-prefixes: github.com/aquasecurity gosec: excludes: + - G114 - G204 - G402 linters: disable-all: true enable: - - structcheck + - unused - ineffassign - typecheck - govet - - varcheck - - deadcode - revive - gosec - unconvert @@ -43,7 +42,7 @@ linters: - misspell run: - go: 1.18 + go: 1.19 skip-files: - ".*._mock.go$" - ".*._test.go$" diff --git a/Dockerfile.protoc b/Dockerfile.protoc index 791d6185d2c..88671e805ff 100644 --- a/Dockerfile.protoc +++ b/Dockerfile.protoc @@ -1,4 +1,4 @@ -FROM golang:1.18.4 +FROM golang:1.19.0 # Install protoc (cf. http://google.github.io/proto-lens/installing-protoc.html) ENV PROTOC_ZIP=protoc-3.19.4-linux-x86_64.zip diff --git a/Makefile b/Makefile index 8a81662f199..6f8c498b710 100644 --- a/Makefile +++ b/Makefile @@ -26,7 +26,7 @@ $(GOBIN)/crane: go install github.com/google/go-containerregistry/cmd/crane@v0.9.0 $(GOBIN)/golangci-lint: - curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $(GOBIN) v1.45.2 + curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $(GOBIN) v1.49.0 $(GOBIN)/labeler: go install github.com/knqyf263/labeler@latest diff --git a/docs/docs/integrations/azure-devops.md b/docs/docs/integrations/azure-devops.md index 17535ccee84..1fbac0e848d 100644 --- a/docs/docs/integrations/azure-devops.md +++ b/docs/docs/integrations/azure-devops.md @@ -4,6 +4,11 @@ ![trivy-azure](https://github.com/aquasecurity/trivy-azure-pipelines-task/blob/main/screenshot.png?raw=true) +### [Use ImageCleaner to clean up stale images on your Azure Kubernetes Service cluster][azure2] + +It's common to use pipelines to build and deploy images on Azure Kubernetes Service (AKS) clusters. While great for image creation, this process often doesn't account for the stale images left behind and can lead to image bloat on cluster nodes. These images can present security issues as they may contain vulnerabilities. By cleaning these unreferenced images, you can remove an area of risk in your clusters. When done manually, this process can be time intensive, which ImageCleaner can mitigate via automatic image identification and removal. + + Vulnerability is determined based on a trivy scan, after which images with a LOW, MEDIUM, HIGH, or CRITICAL classification are flagged. An updated ImageList will be automatically generated by ImageCleaner based on a set time interval, and can also be supplied manually. ### [Microsoft Defender for container registries and Trivy][azure] This blog explains how to scan your Azure Container Registry-based container images with the integrated vulnerability scanner when they're built as part of your GitHub workflows. @@ -14,4 +19,4 @@ The findings of the CI/CD scans are an enrichment to the existing registry scan [action]: https://github.com/aquasecurity/trivy-azure-pipelines-task [azure]: https://docs.microsoft.com/en-us/azure/defender-for-cloud/defender-for-containers-cicd - +[azure2]: https://docs.microsoft.com/en-us/azure/aks/image-cleaner?tabs=azure-cli diff --git a/go.mod b/go.mod index beeaa91a270..15dec9f3c00 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/aquasecurity/trivy -go 1.18 +go 1.19 require ( github.com/CycloneDX/cyclonedx-go v0.6.0 diff --git a/pkg/fanal/analyzer/pkg/rpm/rpm.go b/pkg/fanal/analyzer/pkg/rpm/rpm.go index f35b430e64d..c4431a918a9 100644 --- a/pkg/fanal/analyzer/pkg/rpm/rpm.go +++ b/pkg/fanal/analyzer/pkg/rpm/rpm.go @@ -160,10 +160,12 @@ func (a rpmPkgAnalyzer) parsePkgInfo(rc io.Reader) ([]types.Package, []string, e return pkgs, installedFiles, nil } -// splitFileName returns a name, version, release, epoch, arch -// e.g. -// foo-1.0-1.i386.rpm returns foo, 1.0, 1, i386 -// 1:bar-9-123a.ia64.rpm returns bar, 9, 123a, 1, ia64 +// splitFileName returns a name, version, release, epoch, arch: +// +// e.g. +// foo-1.0-1.i386.rpm => foo, 1.0, 1, i386 +// 1:bar-9-123a.ia64.rpm => bar, 9, 123a, 1, ia64 +// // https://github.com/rpm-software-management/yum/blob/043e869b08126c1b24e392f809c9f6871344c60d/rpmUtils/miscutils.py#L301 func splitFileName(filename string) (name, ver, rel string, err error) { if strings.HasSuffix(filename, ".rpm") { diff --git a/pkg/fanal/artifact/sbom/sbom.go b/pkg/fanal/artifact/sbom/sbom.go index 530184f27e6..c6599668843 100644 --- a/pkg/fanal/artifact/sbom/sbom.go +++ b/pkg/fanal/artifact/sbom/sbom.go @@ -13,7 +13,6 @@ import ( "github.com/aquasecurity/trivy/pkg/attestation" "github.com/aquasecurity/trivy/pkg/fanal/analyzer" - "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config" "github.com/aquasecurity/trivy/pkg/fanal/artifact" "github.com/aquasecurity/trivy/pkg/fanal/cache" "github.com/aquasecurity/trivy/pkg/fanal/handler" @@ -30,8 +29,7 @@ type Artifact struct { analyzer analyzer.AnalyzerGroup handlerManager handler.Manager - artifactOption artifact.Option - configScannerOption config.ScannerOption + artifactOption artifact.Option } func NewArtifact(filePath string, c cache.ArtifactCache, opt artifact.Option) (artifact.Artifact, error) { diff --git a/pkg/fanal/test/integration/library_test.go b/pkg/fanal/test/integration/library_test.go index df496704de1..7c1eec215a5 100644 --- a/pkg/fanal/test/integration/library_test.go +++ b/pkg/fanal/test/integration/library_test.go @@ -241,6 +241,14 @@ func commonChecks(t *testing.T, detail types.ArtifactDetail, tc testCase) { } func checkOSPackages(t *testing.T, detail types.ArtifactDetail, tc testCase) { + // Sort OS packages for consistency + sort.Slice(detail.Packages, func(i, j int) bool { + if detail.Packages[i].Name != detail.Packages[j].Name { + return detail.Packages[i].Name < detail.Packages[j].Name + } + return detail.Packages[i].Version < detail.Packages[j].Version + }) + splitted := strings.Split(tc.remoteImageName, ":") goldenFile := fmt.Sprintf("testdata/goldens/packages/%s.json.golden", splitted[len(splitted)-1]) diff --git a/pkg/fanal/test/integration/testdata/goldens/packages/opensuse-leap-151.json.golden b/pkg/fanal/test/integration/testdata/goldens/packages/opensuse-leap-151.json.golden index 9209f6f2040..f86b539200c 100644 --- a/pkg/fanal/test/integration/testdata/goldens/packages/opensuse-leap-151.json.golden +++ b/pkg/fanal/test/integration/testdata/goldens/packages/opensuse-leap-151.json.golden @@ -209,8 +209,8 @@ }, { "Name": "gpg-pubkey", - "Version": "3dbdc284", - "Release": "53674dd4", + "Version": "307e3d54", + "Release": "5aaa90a5", "Arch": "None", "License": "pubkey", "Layer": { @@ -219,8 +219,8 @@ }, { "Name": "gpg-pubkey", - "Version": "307e3d54", - "Release": "5aaa90a5", + "Version": "39db7c82", + "Release": "5847eb1f", "Arch": "None", "License": "pubkey", "Layer": { @@ -229,8 +229,8 @@ }, { "Name": "gpg-pubkey", - "Version": "39db7c82", - "Release": "5847eb1f", + "Version": "3dbdc284", + "Release": "53674dd4", "Arch": "None", "License": "pubkey", "Layer": { diff --git a/pkg/fanal/test/integration/testdata/goldens/packages/suse-15.3_ndb.json.golden b/pkg/fanal/test/integration/testdata/goldens/packages/suse-15.3_ndb.json.golden index f697d356845..0b3f4f1acd2 100644 --- a/pkg/fanal/test/integration/testdata/goldens/packages/suse-15.3_ndb.json.golden +++ b/pkg/fanal/test/integration/testdata/goldens/packages/suse-15.3_ndb.json.golden @@ -209,8 +209,8 @@ }, { "Name": "gpg-pubkey", - "Version": "39db7c82", - "Release": "5f68629b", + "Version": "307e3d54", + "Release": "5aaa90a5", "Arch": "None", "License": "pubkey", "Layer": { @@ -219,8 +219,8 @@ }, { "Name": "gpg-pubkey", - "Version": "307e3d54", - "Release": "5aaa90a5", + "Version": "39db7c82", + "Release": "5f68629b", "Arch": "None", "License": "pubkey", "Layer": { diff --git a/pkg/flag/cache_flags.go b/pkg/flag/cache_flags.go index d4aad96bd5a..e37f040f778 100644 --- a/pkg/flag/cache_flags.go +++ b/pkg/flag/cache_flags.go @@ -9,14 +9,15 @@ import ( "golang.org/x/xerrors" ) -// e.g. config yaml -// cache: -// clear: true -// backend: "redis://localhost:6379" -// redis: -// ca: ca-cert.pem -// cert: cert.pem -// key: key.pem +// e.g. config yaml: +// +// cache: +// clear: true +// backend: "redis://localhost:6379" +// redis: +// ca: ca-cert.pem +// cert: cert.pem +// key: key.pem var ( ClearCacheFlag = Flag{ Name: "clear-cache", diff --git a/pkg/flag/misconf_flags.go b/pkg/flag/misconf_flags.go index 69356d18e82..2281de2c69e 100644 --- a/pkg/flag/misconf_flags.go +++ b/pkg/flag/misconf_flags.go @@ -4,11 +4,12 @@ import ( "github.com/aquasecurity/trivy/pkg/log" ) -// e.g. config yaml -// misconfiguration: -// trace: true -// config-policy: "custom-policy/policy" -// policy-namespaces: "user" +// e.g. config yaml: +// +// misconfiguration: +// trace: true +// config-policy: "custom-policy/policy" +// policy-namespaces: "user" var ( IncludeNonFailuresFlag = Flag{ Name: "include-non-failures", diff --git a/pkg/flag/options.go b/pkg/flag/options.go index 35caeb2de46..bf6852b1570 100644 --- a/pkg/flag/options.go +++ b/pkg/flag/options.go @@ -289,7 +289,7 @@ func (f *Flags) Bind(cmd *cobra.Command) error { return nil } -//nolint: gocyclo +// nolint: gocyclo func (f *Flags) ToOptions(appVersion string, args []string, globalFlags *GlobalFlagGroup, output io.Writer) (Options, error) { var err error opts := Options{ diff --git a/pkg/flag/report_flags.go b/pkg/flag/report_flags.go index e450b993b1a..13d7b647caf 100644 --- a/pkg/flag/report_flags.go +++ b/pkg/flag/report_flags.go @@ -14,12 +14,11 @@ import ( "github.com/aquasecurity/trivy/pkg/result" ) -// e.g. config yaml -// report: -// format: table -// dependency-tree: true -// exit-code: 1 -// severity: HIGH,CRITICAL +// e.g. config yaml: +// +// format: table +// dependency-tree: true +// severity: HIGH,CRITICAL var ( FormatFlag = Flag{ Name: "format", diff --git a/pkg/k8s/commands/run.go b/pkg/k8s/commands/run.go index 62ca5164f0e..a424c47b047 100644 --- a/pkg/k8s/commands/run.go +++ b/pkg/k8s/commands/run.go @@ -93,8 +93,9 @@ func run(ctx context.Context, opts flag.Options, cluster string, artifacts []*ar // To show all the results, user needs to specify "--report all" explicitly // even though the default value of "--report" is "all". // -// e.g. $ trivy k8s --report all cluster -// $ trivy k8s --report all all +// e.g. +// $ trivy k8s --report all cluster +// $ trivy k8s --report all all // // Or they can use "--format json" with implicit "--report all". // diff --git a/pkg/sbom/cyclonedx/unmarshal.go b/pkg/sbom/cyclonedx/unmarshal.go index 6f7a2f69d40..1744fb1b559 100644 --- a/pkg/sbom/cyclonedx/unmarshal.go +++ b/pkg/sbom/cyclonedx/unmarshal.go @@ -155,17 +155,16 @@ func parsePkgs(components []cdx.Component, seen map[string]struct{}) ([]ftypes.P } // walkDependencies takes all nested dependencies of the root component. -// -// e.g. Library A, B, C, D and E will be returned as dependencies of Application 1. -// type: Application 1 -// - type: Library A -// - type: Library B -// - type: Application 2 -// - type: Library C -// - type: Application 3 -// - type: Library D -// - type: Library E func (c *CycloneDX) walkDependencies(rootRef string) []cdx.Component { + // e.g. Library A, B, C, D and E will be returned as dependencies of Application 1. + // type: Application 1 + // - type: Library A + // - type: Library B + // - type: Application 2 + // - type: Library C + // - type: Application 3 + // - type: Library D + // - type: Library E var components []cdx.Component for _, dep := range c.dependencies[rootRef] { component, ok := c.components[dep] diff --git a/pkg/sbom/spdx/marshal.go b/pkg/sbom/spdx/marshal.go index 139227e7c2b..ff07312341e 100644 --- a/pkg/sbom/spdx/marshal.go +++ b/pkg/sbom/spdx/marshal.go @@ -50,6 +50,8 @@ const ( ElementOperatingSystem = "OperatingSystem" ElementApplication = "Application" + ElementPackage = "Package" + ElementFile = "File" ) var ( @@ -102,26 +104,18 @@ func NewMarshaler(opts ...marshalOption) *Marshaler { 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) + // Root package contains OS, OS packages, language-specific packages and so on. + rootPkg, err := m.rootPackage(r) if err != nil { - return nil, xerrors.Errorf("failed to parse report: %w", err) + return nil, xerrors.Errorf("failed to generate a root package: %w", err) } - packages[reportPackage.PackageSPDXIdentifier] = reportPackage + packages[rootPkg.PackageSPDXIdentifier] = rootPkg relationShips = append(relationShips, - relationShip(SPDXIdentifier, reportPackage.PackageSPDXIdentifier, RelationShipDescribe), + relationShip(SPDXIdentifier, rootPkg.PackageSPDXIdentifier, RelationShipDescribe), ) for _, result := range r.Results { @@ -131,7 +125,7 @@ func (m *Marshaler) Marshal(r types.Report) (*spdx.Document2_2, error) { } packages[parentPackage.PackageSPDXIdentifier] = &parentPackage relationShips = append(relationShips, - relationShip(reportPackage.PackageSPDXIdentifier, parentPackage.PackageSPDXIdentifier, RelationShipContains), + relationShip(rootPkg.PackageSPDXIdentifier, parentPackage.PackageSPDXIdentifier, RelationShipContains), ) for _, pkg := range result.Packages { @@ -141,7 +135,7 @@ func (m *Marshaler) Marshal(r types.Report) (*spdx.Document2_2, error) { } packages[spdxPackage.PackageSPDXIdentifier] = &spdxPackage relationShips = append(relationShips, - relationShip(parentPackage.PackageSPDXIdentifier, spdxPackage.PackageSPDXIdentifier, RelationShipDependsOn), + relationShip(parentPackage.PackageSPDXIdentifier, spdxPackage.PackageSPDXIdentifier, RelationShipContains), ) } } @@ -163,27 +157,27 @@ func (m *Marshaler) Marshal(r types.Report) (*spdx.Document2_2, error) { } 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) + osPkg, err := m.osPackage(os) if err != nil { return spdx.Package2_2{}, xerrors.Errorf("failed to parse operating system package: %w", err) } + return osPkg, nil case types.ClassLangPkg: - pkg, err = m.applicationPackage(result.Target, result.Type) + langPkg, err := m.langPackage(result.Target, result.Type) if err != nil { return spdx.Package2_2{}, xerrors.Errorf("failed to parse application package: %w", err) } + return langPkg, nil + default: + // unsupported packages + return spdx.Package2_2{}, nil } - return pkg, nil } func (m *Marshaler) parseFile(filePath string) (spdx.File2_2, error) { - pkgID, err := getPackageID(m.hasher, filePath) + pkgID, err := calcPkgID(m.hasher, filePath) if err != nil { return spdx.File2_2{}, xerrors.Errorf("failed to get %s package ID: %w", filePath, err) } @@ -194,33 +188,20 @@ func (m *Marshaler) parseFile(filePath string) (spdx.File2_2, error) { 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) rootPackage(r types.Report) (*spdx.Package2_2, error) { + var externalReferences []*spdx.PackageExternalReference2_2 + attributionTexts := []string{attributionText(PropertySchemaVersion, strconv.Itoa(r.SchemaVersion))} -func (m *Marshaler) reportToSpdxPackage(r types.Report) (*spdx.Package2_2, error) { - var spdxPackage spdx.Package2_2 + // When the target is a container image, add PURL to the external references of the root package. + if p, err := purl.NewPackageURL(purl.TypeOCI, r.Metadata, ftypes.Package{}); err != nil { + return nil, xerrors.Errorf("failed to new package url for oci: %w", err) + } else if p.Type != "" { + externalReferences = append(externalReferences, purlExternalReference(p.ToString())) + } - 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()) - } + if r.Metadata.ImageID != "" { attributionTexts = appendAttributionText(attributionTexts, PropertyImageID, r.Metadata.ImageID) } - if r.Metadata.Size != 0 { attributionTexts = appendAttributionText(attributionTexts, PropertySize, strconv.FormatInt(r.Metadata.Size, 10)) } @@ -235,75 +216,122 @@ func (m *Marshaler) reportToSpdxPackage(r types.Report) (*spdx.Package2_2, error attributionTexts = appendAttributionText(attributionTexts, PropertyRepoTag, t) } - spdxPackage.PackageAttributionTexts = attributionTexts - pkgID, err := getPackageID(m.hasher, fmt.Sprintf("%s-%s", r.ArtifactName, r.ArtifactType)) + pkgID, err := calcPkgID(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 + return &spdx.Package2_2{ + PackageName: r.ArtifactName, + PackageSPDXIdentifier: elementID(camelCase(string(r.ArtifactType)), pkgID), + PackageAttributionTexts: attributionTexts, + PackageExternalReferences: externalReferences, + }, nil } -func (m *Marshaler) applicationPackage(target, typ string) (spdx.Package2_2, error) { - var spdxPackage spdx.Package2_2 +func (m *Marshaler) osPackage(osFound *ftypes.OS) (spdx.Package2_2, error) { + if osFound == nil { + return spdx.Package2_2{}, nil + } + + pkgID, err := calcPkgID(m.hasher, osFound) + if err != nil { + return spdx.Package2_2{}, xerrors.Errorf("failed to get os metadata package ID: %w", err) + } + + return spdx.Package2_2{ + PackageName: osFound.Family, + PackageVersion: osFound.Name, + PackageSPDXIdentifier: elementID(ElementOperatingSystem, pkgID), + }, nil +} - spdxPackage.PackageName = typ - spdxPackage.PackageSourceInfo = target - pkgID, err := getPackageID(m.hasher, fmt.Sprintf("%s-%s", target, typ)) +func (m *Marshaler) langPackage(target, appType string) (spdx.Package2_2, error) { + pkgID, err := calcPkgID(m.hasher, fmt.Sprintf("%s-%s", target, appType)) 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 + return spdx.Package2_2{ + PackageName: appType, + PackageSourceInfo: target, // TODO: Files seems better + PackageSPDXIdentifier: elementID(ElementApplication, pkgID), + }, 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) + pkgID, err := calcPkgID(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 - + var pkgSrcInfo string if class == types.ClassOSPkg { - spdxPackage.PackageSourceInfo = fmt.Sprintf("%s: %s %s", SourcePackagePrefix, pkg.SrcName, utils.FormatSrcVersion(pkg)) + pkgSrcInfo = 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()) + pkgExtRefs := []*spdx.PackageExternalReference2_2{purlExternalReference(packageURL.String())} - // The Declared License is what the authors of a project believe govern the package - spdxPackage.PackageLicenseConcluded = license + var attrTexts []string + attrTexts = appendAttributionText(attrTexts, PropertyLayerDigest, pkg.Layer.Digest) + attrTexts = appendAttributionText(attrTexts, PropertyLayerDiffID, pkg.Layer.DiffID) - // The Concluded License field is the license the SPDX file creator believes governs the package - spdxPackage.PackageLicenseDeclared = license + files, err := m.pkgFiles(pkg) + if err != nil { + return spdx.Package2_2{}, xerrors.Errorf("package file error: %w", err) + } - spdxPackage.PackageAttributionTexts = appendAttributionText(spdxPackage.PackageAttributionTexts, PropertyLayerDigest, pkg.Layer.Digest) - spdxPackage.PackageAttributionTexts = appendAttributionText(spdxPackage.PackageAttributionTexts, PropertyLayerDiffID, pkg.Layer.DiffID) + return spdx.Package2_2{ + PackageName: pkg.Name, + PackageVersion: pkg.Version, + PackageSPDXIdentifier: elementID(ElementPackage, pkgID), + PackageSourceInfo: pkgSrcInfo, - 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, - } + // The Declared License is what the authors of a project believe govern the package + PackageLicenseConcluded: license, + + // The Concluded License field is the license the SPDX file creator believes governs the package + PackageLicenseDeclared: license, + + PackageExternalReferences: pkgExtRefs, + PackageAttributionTexts: attrTexts, + Files: files, + }, nil +} + +func (m *Marshaler) pkgFiles(pkg ftypes.Package) (map[spdx.ElementID]*spdx.File2_2, error) { + if pkg.FilePath == "" { + return nil, nil } - return spdxPackage, nil + file, err := m.parseFile(pkg.FilePath) + if err != nil { + return nil, xerrors.Errorf("failed to parse file: %w") + } + return map[spdx.ElementID]*spdx.File2_2{ + file.FileSPDXIdentifier: &file, + }, nil +} + +func elementID(elementType, pkgID string) spdx.ElementID { + return spdx.ElementID(fmt.Sprintf("%s-%s", elementType, pkgID)) +} + +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 appendAttributionText(attributionTexts []string, key, value string) []string { if value == "" { return attributionTexts @@ -315,13 +343,11 @@ 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 purlExternalReference(packageURL string) *spdx.PackageExternalReference2_2 { + return &spdx.PackageExternalReference2_2{ + Category: CategoryPackageManager, + RefType: RefTypePurl, + Locator: packageURL, } } @@ -342,7 +368,7 @@ func getDocumentNamespace(r types.Report, m *Marshaler) string { ) } -func getPackageID(h Hash, v interface{}) (string, error) { +func calcPkgID(h Hash, v interface{}) (string, error) { f, err := h(v, hashstructure.FormatV2, &hashstructure.HashOptions{ ZeroNil: true, SlicesAsSets: true, diff --git a/pkg/sbom/spdx/marshal_test.go b/pkg/sbom/spdx/marshal_test.go index 6b5a28dd552..e01bc249432 100644 --- a/pkg/sbom/spdx/marshal_test.go +++ b/pkg/sbom/spdx/marshal_test.go @@ -202,7 +202,7 @@ func TestMarshaler_Marshal(t *testing.T) { { RefA: spdx.DocElementID{ElementRefID: "OperatingSystem-197f9a00ebcb51f0"}, RefB: spdx.DocElementID{ElementRefID: "Package-fd0dc3cf913d5bc3"}, - Relationship: "DEPENDS_ON", + Relationship: "CONTAINS", }, { RefA: spdx.DocElementID{ElementRefID: "ContainerImage-9396d894cd0cb6cb"}, @@ -212,12 +212,12 @@ func TestMarshaler_Marshal(t *testing.T) { { RefA: spdx.DocElementID{ElementRefID: "Application-73c871d73f3c8248"}, RefB: spdx.DocElementID{ElementRefID: "Package-826226d056ff30c0"}, - Relationship: "DEPENDS_ON", + Relationship: "CONTAINS", }, { RefA: spdx.DocElementID{ElementRefID: "Application-73c871d73f3c8248"}, RefB: spdx.DocElementID{ElementRefID: "Package-eb0263038c3b445b"}, - Relationship: "DEPENDS_ON", + Relationship: "CONTAINS", }, { RefA: spdx.DocElementID{ElementRefID: "ContainerImage-9396d894cd0cb6cb"}, @@ -227,7 +227,7 @@ func TestMarshaler_Marshal(t *testing.T) { { RefA: spdx.DocElementID{ElementRefID: "Application-c3fac92c1ac0a9fa"}, RefB: spdx.DocElementID{ElementRefID: "Package-826226d056ff30c0"}, - Relationship: "DEPENDS_ON", + Relationship: "CONTAINS", }, }, UnpackagedFiles: nil, @@ -410,7 +410,7 @@ func TestMarshaler_Marshal(t *testing.T) { { RefA: spdx.DocElementID{ElementRefID: "OperatingSystem-197f9a00ebcb51f0"}, RefB: spdx.DocElementID{ElementRefID: "Package-d8dccb186bafaf37"}, - Relationship: "DEPENDS_ON", + Relationship: "CONTAINS", }, { RefA: spdx.DocElementID{ElementRefID: "ContainerImage-413bfede37ad01fc"}, @@ -420,12 +420,12 @@ func TestMarshaler_Marshal(t *testing.T) { { RefA: spdx.DocElementID{ElementRefID: "Application-441a648f2aeeee72"}, RefB: spdx.DocElementID{ElementRefID: "Package-d5443dbcbba0dbd4"}, - Relationship: "DEPENDS_ON", + Relationship: "CONTAINS", }, { RefA: spdx.DocElementID{ElementRefID: "Application-441a648f2aeeee72"}, RefB: spdx.DocElementID{ElementRefID: "Package-13fe667a0805e6b7"}, - Relationship: "DEPENDS_ON", + Relationship: "CONTAINS", }, }, @@ -508,7 +508,7 @@ func TestMarshaler_Marshal(t *testing.T) { { RefA: spdx.DocElementID{ElementRefID: "Application-9dd4a4ba7077cc5a"}, RefB: spdx.DocElementID{ElementRefID: "Package-3da61e86d0530402"}, - Relationship: "DEPENDS_ON", + Relationship: "CONTAINS", }, }, }, @@ -600,7 +600,7 @@ func TestMarshaler_Marshal(t *testing.T) { { RefA: spdx.DocElementID{ElementRefID: "Application-24f8a80152e2c0fc"}, RefB: spdx.DocElementID{ElementRefID: "Package-daedb173cfd43058"}, - Relationship: "DEPENDS_ON", + Relationship: "CONTAINS", }, }, },