Merge remote-tracking branch 'upstream/main' into add-nested-archive-cataloging

This commit is contained in:
Keith Zantow
2026-09-10 13:32:07 -04:00
451 changed files with 45797 additions and 2113 deletions
+1 -1
View File
@@ -24,7 +24,7 @@ tools:
# used to parse JSON/YAML annotations on the fixture cache image
- name: yq
version:
want: v4.53.3
want: v4.53.6
method: github-release
with:
repo: mikefarah/yq
+3 -3
View File
@@ -30,7 +30,7 @@ runs:
using: "composite"
steps:
- name: Setup go + go-make tooling
uses: anchore/go-make/.github/actions/setup@9de27be11ed73e2f9d5406a836a492b7d8aa1225 # v0.5.0
uses: anchore/go-make/.github/actions/setup@430e2175bb166bfd138ef75466adbcc8ddab875f # v0.8.0
with:
go-version: ${{ inputs.go-version }}
cache-key-prefix: ${{ inputs.cache-key-prefix }}
@@ -52,7 +52,7 @@ runs:
# ORAS cache: restore-only on non-default branches / forks
- name: Restore ORAS cache from github actions
if: ${{ inputs.download-test-fixture-cache == 'true' && (github.ref != format('refs/heads/{0}', github.event.repository.default_branch) || github.event.repository.fork == true) }}
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ github.workspace }}/.tmp/oras-cache
key: ${{ inputs.cache-key-prefix }}-oras-cache
@@ -60,7 +60,7 @@ runs:
# ORAS cache: restore + save on the default branch of the canonical repo only.
- name: Restore and save ORAS cache from github actions
if: ${{ inputs.download-test-fixture-cache == 'true' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && github.event.repository.fork == false }}
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ github.workspace }}/.tmp/oras-cache
key: ${{ inputs.cache-key-prefix }}-oras-cache
-25
View File
@@ -1,13 +1,4 @@
# Dependabot configuration
#
# Grouping behavior (see inline comments for details):
# - Minor + patch updates: grouped into a single PR per ecosystem
# - Major version bumps: individual PR per dependency
# - Security updates: individual PR per dependency
#
# Note: "patch" refers to semver version bumps (1.2.3 -> 1.2.4), not security fixes.
# Security updates are identified separately via GitHub's Advisory Database and
# can be any version bump (patch, minor, or major) that fixes a known CVE.
version: 2
@@ -30,14 +21,6 @@ updates:
- dependency-name: "github.com/aquasecurity/go-version"
- dependency-name: "github.com/knqyf263/go-apk-version"
- dependency-name: "github.com/knqyf263/go-deb-version"
groups:
go-minor-patch:
applies-to: version-updates # security updates get individual PRs
patterns:
- "*"
update-types: # major omitted, gets individual PRs
- "minor"
- "patch"
- package-ecosystem: "github-actions"
directories:
@@ -51,11 +34,3 @@ updates:
open-pull-requests-limit: 10
labels:
- "dependencies"
groups:
actions-minor-patch:
applies-to: version-updates # security updates get individual PRs
patterns:
- "*"
update-types: # major omitted, gets individual PRs
- "minor"
- "patch"
+1 -1
View File
@@ -13,7 +13,7 @@ permissions: {}
jobs:
analyze:
name: Analyze
uses: anchore/workflows/.github/workflows/codeql.yaml@b3e328b5ae31ba96297e2ed9a6124e5e6352a4c5 # v0.7.0
uses: anchore/workflows/.github/workflows/codeql.yaml@7212994dc8fc3a53fe9c8e766ab5b4ddd16ea3d4 # v0.8.0
permissions:
security-events: write
packages: read
+2 -2
View File
@@ -86,7 +86,7 @@ jobs:
# dockers_v2 builds multi-platform manifests in a single buildx invocation, which requires a
# docker-container driver builder (the default builder on the runner cannot build multi-platform).
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
- name: Login to Docker Hub
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f #v4.6.0
@@ -118,7 +118,7 @@ jobs:
# for updating brew formula in anchore/homebrew-syft
GITHUB_BREW_TOKEN: ${{ secrets.ANCHOREOPS_GITHUB_OSS_WRITE_TOKEN }}
- uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 #v0.24.0
- uses: anchore/sbom-action@3ad7283483fc7af8ff2b4ea19663c2d5ca935e26 #v0.24.2
continue-on-error: true
with:
file: go.mod
@@ -20,12 +20,12 @@ jobs:
contents: read
security-events: write # for uploading SARIF results
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: "Run zizmor"
uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6
uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2
with:
# there is a pass/fail gate as a repo ruleset (if there is no ruleset configured then the action will pass by default)
advanced-security: true
+15 -9
View File
@@ -28,7 +28,7 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
with:
persist-credentials: false
@@ -50,7 +50,7 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
with:
persist-credentials: false
@@ -72,7 +72,7 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
with:
persist-credentials: false
@@ -92,14 +92,20 @@ jobs:
# runs-on.com: compute instances for parallel builds
# spot disabled: reliability for build workflows (used for releases too)
# goreleaser uses parallelism of 12, so we need more CPUs
# tmpfs: faster io-intensive workflows
# tmpfs intentionally omitted -- a RAM-backed dist/ competes with the goreleaser
# build's memory peak (parallel binaries + multi-arch image assembly) and OOMs the
# instance; keep dist/ on disk.
# note: s3-cache intentionally omitted -- PR runs are untrusted and must not write to the
# shared cache backend that the trusted release workflow reads from (cache poisoning).
runs-on: "runs-on=${{ github.run_id }}/cpu=16+32/ram=32+128/family=c5+c6+c7+c8/spot=false/extras=tmpfs"
# volume: enlarge dist/ disk -- the default 40GB EBS root fills up during the multi-arch
# image + binary build and OOMs (runs out of disk); size it well above peak usage.
# provisioned throughput/iops above the gp3 free baseline (125mbs/3000iops) approximate
# the tmpfs IO speed on disk, for ~1c/run.
runs-on: "runs-on=${{ github.run_id }}/cpu=16+32/ram=32+128/family=c5+c6+c7+c8/spot=false/volume=120gb:gp3:500mbs:4000iops"
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
with:
persist-credentials: false
@@ -129,7 +135,7 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
with:
persist-credentials: false
@@ -173,7 +179,7 @@ jobs:
- name: Install Cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
with:
persist-credentials: false
@@ -207,7 +213,7 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
with:
persist-credentials: false
-5
View File
@@ -1,9 +1,4 @@
rules:
unpinned-uses:
config:
policies:
# anchore/workflows is an internal repository; using @main is acceptable
anchore/*: any
dangerous-triggers:
ignore:
# Safe use of pull_request_target - only runs trusted scripts from base repo,
+8 -2
View File
@@ -70,7 +70,6 @@ linters:
# - godot
# - godox
# - goerr113
# - goimports # we're using gosimports now instead to account for extra whitespaces (see https://github.com/golang/go/issues/20818)
# - golint # deprecated
# - gomnd # this is too aggressive
# - interfacer # this is a good idea, but is no longer supported and is prone to false positives
@@ -96,11 +95,18 @@ issues:
formatters:
enable:
- gci
- gofmt
- goimports
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
settings:
gci:
# See https://golangci-lint.run/docs/formatters/configuration/#gci
sections:
- standard # Standard section: captures all standard packages.
- default # Default section: contains all imports that could not be matched to another section type.
- prefix(github.com/anchore)
+76 -294
View File
@@ -73,306 +73,88 @@ brews:
description: *description
license: "Apache License 2.0"
dockers:
# production images...
- image_templates:
- anchore/syft:{{.Tag}}-amd64
- ghcr.io/anchore/syft:{{.Tag}}-amd64
goarch: amd64
dockers_v2:
# production images (scratch base, root)
- id: production
dockerfile: Dockerfile
use: buildx
build_flag_templates:
- "--platform=linux/amd64"
- "--build-arg=BUILD_DATE={{.Date}}"
- "--build-arg=BUILD_VERSION={{.Version}}"
- "--build-arg=VCS_REF={{.FullCommit}}"
- "--build-arg=VCS_URL={{.GitURL}}"
ids: &docker-ids
- linux-build
images: &docker-images
- anchore/syft
- ghcr.io/anchore/syft
platforms: &docker-platforms
- linux/amd64
- linux/arm64
- linux/ppc64le
- linux/riscv64
- linux/s390x
labels: &docker-labels
"org.opencontainers.image.created": "{{.Date}}"
"org.opencontainers.image.title": "syft"
"org.opencontainers.image.description": "CLI tool and library for generating a Software Bill of Materials from container images and filesystems"
"org.opencontainers.image.source": "{{.GitURL}}"
"org.opencontainers.image.revision": "{{.FullCommit}}"
"org.opencontainers.image.vendor": "Anchore, Inc."
"org.opencontainers.image.version": "{{.Version}}"
"org.opencontainers.image.licenses": "Apache-2.0"
"io.artifacthub.package.readme-url": "https://raw.githubusercontent.com/anchore/syft/main/README.md"
"io.artifacthub.package.logo-url": "https://user-images.githubusercontent.com/5199289/136844524-1527b09f-c5cb-4aa9-be54-5aa92a6086c1.png"
"io.artifacthub.package.license": "Apache-2.0"
tags:
- latest
- "{{.Tag}}"
# DEBIAN_VERSION 13 (trixie) is the first distroless release with a riscv64 base image
# and is a superset of debian 12 for the other targeted arches; docker_v2 builds all
# platforms in a single buildx invocation, so build_args cannot vary per platform.
build_args: &docker-build-args
DEBIAN_VERSION: "13"
# disable provenance attestations to keep the manifest free of unknown/unknown entries
flags: &docker-flags
- "--provenance=false"
# SBOMs are produced separately for the archives (see the sboms section)
sbom: &docker-sbom "false"
- image_templates:
- anchore/syft:{{.Tag}}-arm64v8
- ghcr.io/anchore/syft:{{.Tag}}-arm64v8
goarch: arm64
dockerfile: Dockerfile
use: buildx
build_flag_templates:
- "--platform=linux/arm64/v8"
- "--build-arg=BUILD_DATE={{.Date}}"
- "--build-arg=BUILD_VERSION={{.Version}}"
- "--build-arg=VCS_REF={{.FullCommit}}"
- "--build-arg=VCS_URL={{.GitURL}}"
- image_templates:
- anchore/syft:{{.Tag}}-ppc64le
- ghcr.io/anchore/syft:{{.Tag}}-ppc64le
goarch: ppc64le
dockerfile: Dockerfile
use: buildx
build_flag_templates:
- "--platform=linux/ppc64le"
- "--build-arg=BUILD_DATE={{.Date}}"
- "--build-arg=BUILD_VERSION={{.Version}}"
- "--build-arg=VCS_REF={{.FullCommit}}"
- "--build-arg=VCS_URL={{.GitURL}}"
- image_templates:
- anchore/syft:{{.Tag}}-riscv64
- ghcr.io/anchore/syft:{{.Tag}}-riscv64
goarch: riscv64
dockerfile: Dockerfile
use: buildx
build_flag_templates:
- "--platform=linux/riscv64"
- "--build-arg=DEBIAN_VERSION=13"
- "--build-arg=BUILD_DATE={{.Date}}"
- "--build-arg=BUILD_VERSION={{.Version}}"
- "--build-arg=VCS_REF={{.FullCommit}}"
- "--build-arg=VCS_URL={{.GitURL}}"
- image_templates:
- anchore/syft:{{.Tag}}-s390x
- ghcr.io/anchore/syft:{{.Tag}}-s390x
goarch: s390x
dockerfile: Dockerfile
use: buildx
build_flag_templates:
- "--platform=linux/s390x"
- "--build-arg=BUILD_DATE={{.Date}}"
- "--build-arg=BUILD_VERSION={{.Version}}"
- "--build-arg=VCS_REF={{.FullCommit}}"
- "--build-arg=VCS_URL={{.GitURL}}"
# nonroot images...
- image_templates:
- anchore/syft:{{.Tag}}-nonroot-amd64
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-amd64
goarch: amd64
# nonroot images
- id: nonroot
dockerfile: Dockerfile.nonroot
use: buildx
build_flag_templates:
- "--platform=linux/amd64"
- "--build-arg=BUILD_DATE={{.Date}}"
- "--build-arg=BUILD_VERSION={{.Version}}"
- "--build-arg=VCS_REF={{.FullCommit}}"
- "--build-arg=VCS_URL={{.GitURL}}"
ids: *docker-ids
images: *docker-images
platforms: *docker-platforms
labels: *docker-labels
tags:
- nonroot
- "{{.Tag}}-nonroot"
build_args: *docker-build-args
flags: *docker-flags
sbom: *docker-sbom
- image_templates:
- anchore/syft:{{.Tag}}-nonroot-arm64v8
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-arm64v8
goarch: arm64
dockerfile: Dockerfile.nonroot
use: buildx
build_flag_templates:
- "--platform=linux/arm64/v8"
- "--build-arg=BUILD_DATE={{.Date}}"
- "--build-arg=BUILD_VERSION={{.Version}}"
- "--build-arg=VCS_REF={{.FullCommit}}"
- "--build-arg=VCS_URL={{.GitURL}}"
- image_templates:
- anchore/syft:{{.Tag}}-nonroot-ppc64le
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-ppc64le
goarch: ppc64le
dockerfile: Dockerfile.nonroot
use: buildx
build_flag_templates:
- "--platform=linux/ppc64le"
- "--build-arg=BUILD_DATE={{.Date}}"
- "--build-arg=BUILD_VERSION={{.Version}}"
- "--build-arg=VCS_REF={{.FullCommit}}"
- "--build-arg=VCS_URL={{.GitURL}}"
- image_templates:
- anchore/syft:{{.Tag}}-nonroot-riscv64
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-riscv64
goarch: riscv64
dockerfile: Dockerfile.nonroot
use: buildx
build_flag_templates:
- "--platform=linux/riscv64"
- "--build-arg=DEBIAN_VERSION=13"
- "--build-arg=BUILD_DATE={{.Date}}"
- "--build-arg=BUILD_VERSION={{.Version}}"
- "--build-arg=VCS_REF={{.FullCommit}}"
- "--build-arg=VCS_URL={{.GitURL}}"
- image_templates:
- anchore/syft:{{.Tag}}-nonroot-s390x
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-s390x
goarch: s390x
dockerfile: Dockerfile.nonroot
use: buildx
build_flag_templates:
- "--platform=linux/s390x"
- "--build-arg=BUILD_DATE={{.Date}}"
- "--build-arg=BUILD_VERSION={{.Version}}"
- "--build-arg=VCS_REF={{.FullCommit}}"
- "--build-arg=VCS_URL={{.GitURL}}"
# debug images...
- image_templates:
- anchore/syft:{{.Tag}}-debug-amd64
- ghcr.io/anchore/syft:{{.Tag}}-debug-amd64
goarch: amd64
# debug images (root)
- id: debug
dockerfile: Dockerfile.debug
use: buildx
build_flag_templates:
- "--platform=linux/amd64"
- "--build-arg=BUILD_DATE={{.Date}}"
- "--build-arg=BUILD_VERSION={{.Version}}"
- "--build-arg=VCS_REF={{.FullCommit}}"
- "--build-arg=VCS_URL={{.GitURL}}"
ids: *docker-ids
images: *docker-images
platforms: *docker-platforms
labels: *docker-labels
tags:
- debug
- "{{.Tag}}-debug"
build_args: *docker-build-args
flags: *docker-flags
sbom: *docker-sbom
- image_templates:
- anchore/syft:{{.Tag}}-debug-arm64v8
- ghcr.io/anchore/syft:{{.Tag}}-debug-arm64v8
goarch: arm64
dockerfile: Dockerfile.debug
use: buildx
build_flag_templates:
- "--platform=linux/arm64/v8"
- "--build-arg=BUILD_DATE={{.Date}}"
- "--build-arg=BUILD_VERSION={{.Version}}"
- "--build-arg=VCS_REF={{.FullCommit}}"
- "--build-arg=VCS_URL={{.GitURL}}"
- image_templates:
- anchore/syft:{{.Tag}}-debug-ppc64le
- ghcr.io/anchore/syft:{{.Tag}}-debug-ppc64le
goarch: ppc64le
dockerfile: Dockerfile.debug
use: buildx
build_flag_templates:
- "--platform=linux/ppc64le"
- "--build-arg=BUILD_DATE={{.Date}}"
- "--build-arg=BUILD_VERSION={{.Version}}"
- "--build-arg=VCS_REF={{.FullCommit}}"
- "--build-arg=VCS_URL={{.GitURL}}"
- image_templates:
- anchore/syft:{{.Tag}}-debug-riscv64
- ghcr.io/anchore/syft:{{.Tag}}-debug-riscv64
goarch: riscv64
dockerfile: Dockerfile.debug
use: buildx
build_flag_templates:
- "--platform=linux/riscv64"
- "--build-arg=DEBIAN_VERSION=13"
- "--build-arg=BUILD_DATE={{.Date}}"
- "--build-arg=BUILD_VERSION={{.Version}}"
- "--build-arg=VCS_REF={{.FullCommit}}"
- "--build-arg=VCS_URL={{.GitURL}}"
- image_templates:
- anchore/syft:{{.Tag}}-debug-s390x
- ghcr.io/anchore/syft:{{.Tag}}-debug-s390x
goarch: s390x
dockerfile: Dockerfile.debug
use: buildx
build_flag_templates:
- "--platform=linux/s390x"
- "--build-arg=BUILD_DATE={{.Date}}"
- "--build-arg=BUILD_VERSION={{.Version}}"
- "--build-arg=VCS_REF={{.FullCommit}}"
- "--build-arg=VCS_URL={{.GitURL}}"
docker_manifests:
- name_template: anchore/syft:latest
image_templates:
- anchore/syft:{{.Tag}}-amd64
- anchore/syft:{{.Tag}}-arm64v8
- anchore/syft:{{.Tag}}-ppc64le
- anchore/syft:{{.Tag}}-riscv64
- anchore/syft:{{.Tag}}-s390x
- name_template: ghcr.io/anchore/syft:latest
image_templates:
- ghcr.io/anchore/syft:{{.Tag}}-amd64
- ghcr.io/anchore/syft:{{.Tag}}-arm64v8
- ghcr.io/anchore/syft:{{.Tag}}-ppc64le
- ghcr.io/anchore/syft:{{.Tag}}-riscv64
- ghcr.io/anchore/syft:{{.Tag}}-s390x
- name_template: anchore/syft:{{.Tag}}
image_templates:
- anchore/syft:{{.Tag}}-amd64
- anchore/syft:{{.Tag}}-arm64v8
- anchore/syft:{{.Tag}}-ppc64le
- anchore/syft:{{.Tag}}-riscv64
- anchore/syft:{{.Tag}}-s390x
- name_template: ghcr.io/anchore/syft:{{.Tag}}
image_templates:
- ghcr.io/anchore/syft:{{.Tag}}-amd64
- ghcr.io/anchore/syft:{{.Tag}}-arm64v8
- ghcr.io/anchore/syft:{{.Tag}}-ppc64le
- ghcr.io/anchore/syft:{{.Tag}}-riscv64
- ghcr.io/anchore/syft:{{.Tag}}-s390x
# nonroot images...
- name_template: anchore/syft:nonroot
image_templates:
- anchore/syft:{{.Tag}}-nonroot-amd64
- anchore/syft:{{.Tag}}-nonroot-arm64v8
- anchore/syft:{{.Tag}}-nonroot-ppc64le
- anchore/syft:{{.Tag}}-nonroot-riscv64
- anchore/syft:{{.Tag}}-nonroot-s390x
- name_template: ghcr.io/anchore/syft:nonroot
image_templates:
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-amd64
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-arm64v8
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-ppc64le
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-riscv64
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-s390x
- name_template: anchore/syft:{{.Tag}}-nonroot
image_templates:
- anchore/syft:{{.Tag}}-nonroot-amd64
- anchore/syft:{{.Tag}}-nonroot-arm64v8
- anchore/syft:{{.Tag}}-nonroot-ppc64le
- anchore/syft:{{.Tag}}-nonroot-riscv64
- anchore/syft:{{.Tag}}-nonroot-s390x
- name_template: ghcr.io/anchore/syft:{{.Tag}}-nonroot
image_templates:
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-amd64
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-arm64v8
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-ppc64le
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-riscv64
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-s390x
# debug images...
- name_template: anchore/syft:debug
image_templates:
- anchore/syft:{{.Tag}}-debug-amd64
- anchore/syft:{{.Tag}}-debug-arm64v8
- anchore/syft:{{.Tag}}-debug-ppc64le
- anchore/syft:{{.Tag}}-debug-riscv64
- anchore/syft:{{.Tag}}-debug-s390x
- name_template: ghcr.io/anchore/syft:debug
image_templates:
- ghcr.io/anchore/syft:{{.Tag}}-debug-amd64
- ghcr.io/anchore/syft:{{.Tag}}-debug-arm64v8
- ghcr.io/anchore/syft:{{.Tag}}-debug-ppc64le
- ghcr.io/anchore/syft:{{.Tag}}-debug-riscv64
- ghcr.io/anchore/syft:{{.Tag}}-debug-s390x
- name_template: anchore/syft:{{.Tag}}-debug
image_templates:
- anchore/syft:{{.Tag}}-debug-amd64
- anchore/syft:{{.Tag}}-debug-arm64v8
- anchore/syft:{{.Tag}}-debug-ppc64le
- anchore/syft:{{.Tag}}-debug-riscv64
- anchore/syft:{{.Tag}}-debug-s390x
- name_template: ghcr.io/anchore/syft:{{.Tag}}-debug
image_templates:
- ghcr.io/anchore/syft:{{.Tag}}-debug-amd64
- ghcr.io/anchore/syft:{{.Tag}}-debug-arm64v8
- ghcr.io/anchore/syft:{{.Tag}}-debug-ppc64le
- ghcr.io/anchore/syft:{{.Tag}}-debug-riscv64
- ghcr.io/anchore/syft:{{.Tag}}-debug-s390x
# debug-nonroot images
- id: debug-nonroot
dockerfile: Dockerfile.debug-nonroot
ids: *docker-ids
images: *docker-images
platforms: *docker-platforms
labels: *docker-labels
tags:
- debug-nonroot
- "{{.Tag}}-debug-nonroot"
build_args: *docker-build-args
flags: *docker-flags
sbom: *docker-sbom
sboms:
- artifacts: archive
+4 -6
View File
@@ -2,13 +2,11 @@ module github.com/anchore/syft/.make
go 1.25.8
require (
github.com/anchore/go-make v0.5.0
github.com/goccy/go-yaml v1.19.2
)
require github.com/anchore/go-make v0.8.0
require (
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect
golang.org/x/mod v0.36.0 // indirect
golang.org/x/sys v0.44.0 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
golang.org/x/mod v0.37.0 // indirect
golang.org/x/sys v0.46.0 // indirect
)
+6 -6
View File
@@ -1,10 +1,10 @@
github.com/anchore/go-make v0.5.0 h1:VGlwqVhzowFb+9w/gaWUIid/YXvQZReBWKcj4LaZ3dM=
github.com/anchore/go-make v0.5.0/go.mod h1:Nc/tkwQHW1d1Vi8+0rtS/vSrH6pxieaUQXLdrctn+8g=
github.com/anchore/go-make v0.8.0 h1:3XAD4WNHIbqY4kwFyCQJ7kKiYLzSzjB0UwzDVwRqlUY=
github.com/anchore/go-make v0.8.0/go.mod h1:4M6TnArb5w693VyWsgr5dCWrk2BLNu/ed4JUcsrzS34=
github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+104 -118
View File
@@ -1,28 +1,21 @@
package main
import (
"fmt"
"os"
"path/filepath"
"runtime"
"github.com/goccy/go-yaml"
"strconv"
. "github.com/anchore/go-make"
"github.com/anchore/go-make/config"
"github.com/anchore/go-make/file"
"github.com/anchore/go-make/git"
"github.com/anchore/go-make/lang"
"github.com/anchore/go-make/run"
"github.com/anchore/go-make/tasks/golint"
"github.com/anchore/go-make/tasks/goreleaser"
"github.com/anchore/go-make/tasks/gotask"
"github.com/anchore/go-make/tasks/gotest"
)
// taskfileDescriptions maps Taskfile.yaml task names to their `desc:` field.
// Loaded at package init so wrap() can use Taskfile.yaml as the single source
// of truth for wrapped-task descriptions.
var taskfileDescriptions = mustReadTaskfileDescriptions()
func main() {
Makefile(
// shared anchore tasks
@@ -36,31 +29,55 @@ func main() {
gotest.Name("unit"),
gotest.ExcludeGlob("**/test/**"),
gotest.CoverageThreshold(62),
race(),
),
// integration tests: native go-make Task. The race-detector smoke against a
// real image stays bundled here (RunsOn integration) so `make integration`
// behaves like the Taskfile version did.
gotest.Tasks(
gotest.Name("integration"),
gotest.IncludeGlob("./cmd/syft/internal/test/integration/..."),
gotest.Verbose(),
gotest.NoCoverage(),
),
// integration tests: run `go test` directly instead of via gotest.Tasks(), which
// has no way to set a timeout. The suite is a single package of ~36 sequential
// tests over 18 docker fixture images, so with a cold fixture cache (every image
// built + saved inline, under -race) it runs well past `go test`'s default 10m
// timeout -- which is exactly the case when regenerating the fixture cache from
// scratch. -count=1 keeps the go test cache from short-circuiting a run whose
// side effect (the built fixtures) is the thing we're after.
//
// The race-detector smoke against a real image stays bundled here (RunsOn
// integration) so `make integration` behaves like the Taskfile version did.
Task{
Name: "integration",
Description: "run integration tests",
RunsOn: lang.List("test"),
Run: func() {
raceFlag := ""
if raceEnabled() {
raceFlag = " -race"
}
Run(
"go test -count=1 -timeout=30m -v"+raceFlag+" ./cmd/syft/internal/test/integration/...",
run.Env("GODEBUG", "dontfreezetheworld=1"),
)
},
},
Task{
Name: "integration:race-smoke",
Description: "exercise the CLI with the race detector",
RunsOn: lang.List("integration"),
Run: func() {
if !raceEnabled() {
Log("race detector disabled (RACE=false); skipping race smoke")
return
}
Run("go run -race cmd/syft/main.go anchore/test_images:grype-quality-dotnet-69f15d2")
},
},
// cli tests: native go-make Task. Requires SYFT_BINARY_LOCATION pointing at
// an *absolute* path to the snapshot binary. Intentionally does NOT depend
// on snapshot: in CI we download a pre-built snapshot artifact and re-running
// goreleaser here would both burn ~10m and clobber the downloaded binary.
// Locally, the failure message tells you to run `make snapshot` first.
// cli tests: native go-make Task. Runs SYFT_BINARY_LOCATION at an *absolute*
// path to the snapshot binary. Builds the snapshot only when the binary is
// missing rather than depending on the snapshot task unconditionally: in
// validations.yaml CI we download a pre-built snapshot artifact, so the binary
// already exists and rebuilding would both burn ~10m and clobber it. When
// `make test` runs cold (e.g. the release pipeline) or locally with no
// snapshot, we build a single-target snapshot (current OS/arch only) since
// that's all the CLI tests need.
Task{
Name: "cli",
Description: "Run CLI tests",
@@ -68,7 +85,8 @@ func main() {
Run: func() {
bin := snapshotBinPath()
if !file.Exists(bin) {
panic(fmt.Sprintf("snapshot binary not found at %s; run `make snapshot` first", bin))
Log("snapshot binary not found at %s; building single-target snapshot", bin)
Run("make snapshot:single-target")
}
Log("testing binary: %s", bin)
Run(
@@ -85,108 +103,76 @@ func main() {
Dependencies: Deps("static-analysis", "test", "install-test"),
},
// --- everything below is implemented in Taskfile.yaml and surfaced here
// via wrap(). Descriptions come from Taskfile.yaml (single source of truth).
// --- everything else is implemented in Taskfile.yaml. gotask.Tasks()
// discovers every (non-internal) Taskfile task — including the namespaced
// `generate:cpe-index:*` tasks from the included task.d file — and surfaces
// them as first-class go-make tasks (with their canonical names and `desc:`)
// that forward to `task <name>`. Descriptions live in Taskfile.yaml as the
// single source of truth; no per-task wrapping needed here.
gotask.Tasks(),
// static analysis extras
wrap("check-json-schema-drift").RunOn("static-analysis"),
wrap("check-capability-drift"),
wrap("check-binary-fixture-size").RunOn("static-analysis"),
// gotask.Tasks() discovers canonical task names only, not Taskfile aliases,
// so re-expose `refresh-fixtures`'s `fixtures` alias for manual use.
Task{
Name: "fixtures",
Description: "Clear and fetch all test fixture cache (alias of refresh-fixtures)",
Dependencies: Deps("refresh-fixtures"),
},
// test extras
wrap("validate-cyclonedx-schema").RunOn("test"),
wrap("test-utils").RunOn("test"),
wrap("check-docker-cache").RunOn("test"),
wrap("snapshot-smoke-test"),
// update commands
wrap("update-format-golden-files"),
// fixture cache plumbing (heavy ORAS logic, lives in Taskfile).
// refresh-fixtures hooks into "unit" so `make unit` triggers the
// stale-cache detection + download just like `task unit` did on main
// (its `deps: [tmpdir, fixtures]` is what kept the fixture cache fresh).
wrap("fingerprints"),
wrap("refresh-fixtures").RunOn("unit"),
wrap("fixtures"),
wrap("build-fixtures"),
wrap("download-test-fixture-cache"),
wrap("upload-test-fixture-cache"),
wrap("show-test-image-cache"),
// install-script tests (delegates to test/install/Makefile)
wrap("install-test"),
wrap("install-test-cache-save"),
wrap("install-test-cache-load"),
wrap("install-test-ci-mac"),
// compare tests
wrap("generate-compare-file"),
wrap("compare-mac"),
wrap("compare-linux"),
wrap("compare-test-deb-package-install"),
wrap("compare-test-rpm-package-install"),
// code/data generation (umbrella + per-target; each lives in Taskfile)
wrap("generate"),
wrap("generate-json-schema"),
wrap("generate-license-list"),
wrap("generate-cpe-dictionary-index"),
wrap("generate-capabilities"),
// cleanup (each hooks into go-make's built-in `clean` label)
wrap("clean-snapshot").RunOn("clean"),
wrap("clean-docker-cache").RunOn("clean"),
wrap("clean-oras-cache").RunOn("clean"),
wrap("clean-cache").RunOn("clean"),
wrap("clean-test-observations").RunOn("clean"),
// gotask.Tasks() can't attach RunsOn labels, so wire the syft-specific
// Taskfile tasks into go-make's native phases here. These thin hooks have
// no body and no description (hidden from `make help`); they only pull the
// discovered tasks in when the labeled phase runs.
Task{
Name: "static-analysis:syft",
RunsOn: lang.List("static-analysis"),
Dependencies: Deps("check-json-schema-drift", "check-binary-fixture-size"),
},
Task{
Name: "test:syft",
RunsOn: lang.List("test"),
Dependencies: Deps("validate-cyclonedx-schema", "test-utils", "check-docker-cache"),
},
// refresh-fixtures hooks into "unit" so `make unit` triggers the stale-cache
// detection + download just like `task unit` did on main (its
// `deps: [tmpdir, fixtures]` is what kept the fixture cache fresh).
Task{
Name: "unit:syft",
RunsOn: lang.List("unit"),
Dependencies: Deps("refresh-fixtures"),
},
Task{
Name: "clean:syft",
RunsOn: lang.List("clean"),
Dependencies: Deps(
"clean-snapshot",
"clean-docker-cache",
"clean-oras-cache",
"clean-cache",
"clean-test-observations",
),
},
)
}
// wrap creates a go-make Task that delegates execution to `task <name>`. The
// task's description is pulled from Taskfile.yaml's `desc:` field — descriptions
// for wrapped tasks must always live in Taskfile.yaml, never here.
func wrap(name string) Task {
desc, ok := taskfileDescriptions[name]
if !ok || desc == "" {
// loud-fail at startup so missing descs can't sneak through review.
panic(fmt.Sprintf("Taskfile.yaml task %q is missing a `desc:` field; please add one", name))
}
return Task{
Name: name,
Description: desc,
Run: func() { Run("task " + name) },
// raceEnabled is the single switch for the race detector across every test suite.
// Unset it and we keep go-make's behavior (on in CI, off locally and on windows);
// set RACE=false to turn it off everywhere -- worth doing when rebuilding the test
// fixture cache from scratch, where every suite is dominated by building docker
// fixtures and the race detector only adds wall clock. RACE=true forces it on.
func raceEnabled() bool {
if enabled, err := strconv.ParseBool(config.Env("RACE", "")); err == nil {
return enabled
}
return config.CI && !config.Windows
}
// mustReadTaskfileDescriptions parses Taskfile.yaml at the repo root and returns
// a map of task name -> desc. Runs at package init time so wrap() can use it.
func mustReadTaskfileDescriptions() map[string]string {
root := git.Root()
if root == "" {
return nil
// race applies raceEnabled() to a gotest suite. gotest exposes no functional option
// for the race detector, but gotest.Config.Race is exported.
func race() gotest.Option {
return func(c *gotest.Config) {
c.Race = raceEnabled()
}
path := filepath.Join(root, "Taskfile.yaml")
data, err := os.ReadFile(path) //nolint:gosec // G304: path resolved from git.Root()
if err != nil {
return nil
}
var tf struct {
Tasks map[string]struct {
Desc string `yaml:"desc"`
Aliases []string `yaml:"aliases"`
} `yaml:"tasks"`
}
lang.Throw(yaml.Unmarshal(data, &tf))
out := make(map[string]string, len(tf.Tasks))
for name, t := range tf.Tasks {
out[name] = t.Desc
// aliases inherit the canonical task's description so wrap() can find them.
for _, alias := range t.Aliases {
out[alias] = t.Desc
}
}
return out
}
// snapshotBinPath replicates the SNAPSHOT_BIN computation from the prior Taskfile:
+49
View File
@@ -0,0 +1,49 @@
package main
import (
"os"
"testing"
"github.com/anchore/go-make/config"
"github.com/anchore/go-make/tasks/gotest"
)
func TestRaceEnabled(t *testing.T) {
tests := []struct {
name string
env string // "" means RACE is unset
ci bool
want bool
}{
{name: "default in CI", ci: true, want: true},
{name: "default locally", ci: false, want: false},
{name: "explicitly off in CI", env: "false", ci: true, want: false},
{name: "explicitly on locally", env: "true", ci: false, want: true},
{name: "unparseable falls back to default", env: "yes-please", ci: true, want: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// always go through t.Setenv (it registers the restore) so an ambient
// RACE from the caller's environment can't leak into the unset cases
t.Setenv("RACE", tt.env)
if tt.env == "" {
os.Unsetenv("RACE")
}
orig := config.CI
config.CI = tt.ci
t.Cleanup(func() { config.CI = orig })
if got := raceEnabled(); got != tt.want {
t.Errorf("raceEnabled() = %v, want %v", got, tt.want)
}
// the gotest suites get the same answer through the functional option
var cfg gotest.Config
race()(&cfg)
if cfg.Race != tt.want {
t.Errorf("race() set Race = %v, want %v", cfg.Race, tt.want)
}
})
}
}
+3 -19
View File
@@ -1,4 +1,4 @@
ARG DEBIAN_VERSION=12
ARG DEBIAN_VERSION=13
FROM gcr.io/distroless/static-debian${DEBIAN_VERSION}:latest AS build
FROM scratch
@@ -8,23 +8,7 @@ COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certifica
# create the /tmp dir, which is needed for image content cache
WORKDIR /tmp
COPY syft /
ARG BUILD_DATE
ARG BUILD_VERSION
ARG VCS_REF
ARG VCS_URL
LABEL org.opencontainers.image.created=$BUILD_DATE
LABEL org.opencontainers.image.title="syft"
LABEL org.opencontainers.image.description="CLI tool and library for generating a Software Bill of Materials from container images and filesystems"
LABEL org.opencontainers.image.source=$VCS_URL
LABEL org.opencontainers.image.revision=$VCS_REF
LABEL org.opencontainers.image.vendor="Anchore, Inc."
LABEL org.opencontainers.image.version=$BUILD_VERSION
LABEL org.opencontainers.image.licenses="Apache-2.0"
LABEL io.artifacthub.package.readme-url="https://raw.githubusercontent.com/anchore/syft/main/README.md"
LABEL io.artifacthub.package.logo-url="https://user-images.githubusercontent.com/5199289/136844524-1527b09f-c5cb-4aa9-be54-5aa92a6086c1.png"
LABEL io.artifacthub.package.license="Apache-2.0"
ARG TARGETPLATFORM
COPY ${TARGETPLATFORM}/syft /
ENTRYPOINT ["/syft"]
+4 -22
View File
@@ -1,28 +1,10 @@
ARG DEBIAN_VERSION=12
FROM gcr.io/distroless/static-debian${DEBIAN_VERSION}:debug-nonroot
ARG DEBIAN_VERSION=13
FROM gcr.io/distroless/static-debian${DEBIAN_VERSION}:debug
# create the /tmp dir, which is needed for image content cache
WORKDIR /tmp
COPY syft /
USER nonroot
ARG BUILD_DATE
ARG BUILD_VERSION
ARG VCS_REF
ARG VCS_URL
LABEL org.opencontainers.image.created=$BUILD_DATE
LABEL org.opencontainers.image.title="syft"
LABEL org.opencontainers.image.description="CLI tool and library for generating a Software Bill of Materials from container images and filesystems"
LABEL org.opencontainers.image.source=$VCS_URL
LABEL org.opencontainers.image.revision=$VCS_REF
LABEL org.opencontainers.image.vendor="Anchore, Inc."
LABEL org.opencontainers.image.version=$BUILD_VERSION
LABEL org.opencontainers.image.licenses="Apache-2.0"
LABEL io.artifacthub.package.readme-url="https://raw.githubusercontent.com/anchore/syft/main/README.md"
LABEL io.artifacthub.package.logo-url="https://user-images.githubusercontent.com/5199289/136844524-1527b09f-c5cb-4aa9-be54-5aa92a6086c1.png"
LABEL io.artifacthub.package.license="Apache-2.0"
ARG TARGETPLATFORM
COPY ${TARGETPLATFORM}/syft /
ENTRYPOINT ["/syft"]
+12
View File
@@ -0,0 +1,12 @@
ARG DEBIAN_VERSION=13
FROM gcr.io/distroless/static-debian${DEBIAN_VERSION}:debug-nonroot
# create the /tmp dir, which is needed for image content cache
WORKDIR /tmp
ARG TARGETPLATFORM
COPY ${TARGETPLATFORM}/syft /
USER nonroot
ENTRYPOINT ["/syft"]
+3 -19
View File
@@ -1,28 +1,12 @@
ARG DEBIAN_VERSION=12
ARG DEBIAN_VERSION=13
FROM gcr.io/distroless/static-debian${DEBIAN_VERSION}:nonroot
# create the /tmp dir, which is needed for image content cache
WORKDIR /tmp
COPY syft /
ARG TARGETPLATFORM
COPY ${TARGETPLATFORM}/syft /
USER nonroot
ARG BUILD_DATE
ARG BUILD_VERSION
ARG VCS_REF
ARG VCS_URL
LABEL org.opencontainers.image.created=$BUILD_DATE
LABEL org.opencontainers.image.title="syft"
LABEL org.opencontainers.image.description="CLI tool and library for generating a Software Bill of Materials from container images and filesystems"
LABEL org.opencontainers.image.source=$VCS_URL
LABEL org.opencontainers.image.revision=$VCS_REF
LABEL org.opencontainers.image.vendor="Anchore, Inc."
LABEL org.opencontainers.image.version=$BUILD_VERSION
LABEL org.opencontainers.image.licenses="Apache-2.0"
LABEL io.artifacthub.package.readme-url="https://raw.githubusercontent.com/anchore/syft/main/README.md"
LABEL io.artifacthub.package.logo-url="https://user-images.githubusercontent.com/5199289/136844524-1527b09f-c5cb-4aa9-be54-5aa92a6086c1.png"
LABEL io.artifacthub.package.license="Apache-2.0"
ENTRYPOINT ["/syft"]
+1 -2
View File
@@ -8,10 +8,9 @@
<p align="center">
&nbsp;<a href="https://github.com/anchore/syft/actions/workflows/validations.yaml" target="_blank"><img alt="Validations" src="https://github.com/anchore/syft/actions/workflows/validations.yaml/badge.svg"></a>&nbsp;
&nbsp;<a href="https://goreportcard.com/report/github.com/anchore/syft" target="_blank"><img alt="Go Report Card" src="https://goreportcard.com/badge/github.com/anchore/syft"></a>&nbsp;
&nbsp;<a href="https://github.com/anchore/syft/releases/latest" target="_blank"><img alt="GitHub release" src="https://img.shields.io/github/release/anchore/syft.svg"></a>&nbsp;
&nbsp;<a href="https://github.com/anchore/syft" target="_blank"><img alt="GitHub go.mod Go version" src="https://img.shields.io/github/go-mod/go-version/anchore/syft.svg"></a>&nbsp;
&nbsp;<a href="" target="_blank"><img alt="License: Apache-2.0" src="https://img.shields.io/badge/License-Apache%202.0-blue.svg"></a>&nbsp;
&nbsp;<a href="https://github.com/anchore/syft/blob/main/LICENSE" target="_blank"><img alt="License: Apache-2.0" src="https://img.shields.io/badge/License-Apache%202.0-blue.svg"></a>&nbsp;
&nbsp;<a href="https://anchore.com/discourse" target="_blank"><img alt="Join our Discourse" src="https://img.shields.io/badge/Discourse-Join-blue?logo=discourse"/></a>&nbsp;
&nbsp;<a rel="me" href="https://fosstodon.org/@syft"><img alt="Follow on Mastodon" src="https://img.shields.io/badge/Mastodon-Follow-blue?logoColor=white&logo=mastodon"/></a>&nbsp;
</p>
+13 -5
View File
@@ -2,9 +2,9 @@ version: "3"
# NOTE: most generic tasks (static-analysis, format, lint, unit, snapshot, release,
# changelog, ci-release, etc.) are now provided natively by anchore/go-make and
# defined in .make/main.go. This file holds the syft-specific tasks that go-make
# wraps via `wrap("<name>")` calls — keep descriptions (`desc:`) populated so they
# show up in `make help`.
# defined in .make/main.go. This file holds the syft-specific tasks, which go-make
# auto-discovers via gotask.Tasks() and surfaces as `make <name>` — keep
# descriptions (`desc:`) populated so they show up in `make help`.
includes:
generate:cpe-index: ./task.d/generate/cpe-index.yaml
@@ -39,6 +39,12 @@ vars:
PROJECT_ROOT:
sh: echo $PWD
# docker platform suffix goreleaser appends to snapshot image tags (e.g. latest-amd64).
# snapshot builds load per-arch images rather than a multi-arch manifest, so the smoke
# test must run the host-native tag to avoid emulation (and red-local/green-CI splits).
DOCKER_ARCH:
sh: go env GOARCH
# note: the snapshot dir must be a relative path starting with ./
# e.g. when installing snapshot debs from a local path, ./ forces the deb to be installed in the current working directory instead of referencing a package name
SNAPSHOT_DIR: ./snapshot
@@ -119,6 +125,7 @@ tasks:
desc: Run tests for pipeline utils
cmds:
- cmd: .github/scripts/labeler_test.py
- cmd: go test -C .make ./...
snapshot-smoke-test:
desc: Run a smoke test on the snapshot builds + docker images
@@ -129,14 +136,15 @@ tasks:
silent: true
- "{{ .SNAPSHOT_BIN }} version"
- "{{ .SNAPSHOT_BIN }} scan alpine:latest"
- docker run --rm anchore/syft:latest version
- docker run --rm anchore/syft:latest scan alpine:latest
- docker run --pull=never --rm anchore/syft:latest-{{ .DOCKER_ARCH }} version
- docker run --pull=never --rm anchore/syft:latest-{{ .DOCKER_ARCH }} scan alpine:latest
## Test-fixture-related targets ###########################################
fingerprints:
desc: Generate fingerprints for all non-docker test fixtures
silent: true
deps: [tmpdir]
# this will look for `testdata/Makefile` and invoke the `fingerprint` target to calculate all cache input fingerprint files
generates:
- '**/testdata/**/*.fingerprint'
@@ -1,6 +1,6 @@
[TestHandler_handleAttestationStarted/attesting_in_progress/task_line - 1]
⠋ Creating a thing running a thing
⠋ Creating a thing running a thing
---
[TestHandler_handleAttestationStarted/attesting_in_progress/log - 1]
@@ -11,7 +11,7 @@
---
[TestHandler_handleAttestationStarted/attesting_complete/task_line - 1]
✔ Created a thing running a thing
✔ Created a thing running a thing
---
[TestHandler_handleAttestationStarted/attesting_complete/log - 1]
@@ -1,8 +1,8 @@
[TestHandler_handleFetchImage/fetch_image_in_progress - 1]
⠋ Loading image ━━━━━━━━━━━━━━━━━━━━ [current] the-image
⠋ Loading image ━━━━━━━━━━━━━━━━━━━━ [current] the-image
---
[TestHandler_handleFetchImage/fetch_image_complete - 1]
✔ Loaded image the-image
✔ Loaded image the-image
---
@@ -1,8 +1,8 @@
[TestHandler_handleFileIndexingStarted/cataloging_in_progress - 1]
⠋ Indexing file system ━━━━━━━━━━━━━━━━━━━━ [current] /some/path
⠋ Indexing file system ━━━━━━━━━━━━━━━━━━━━ [current] /some/path
---
[TestHandler_handleFileIndexingStarted/cataloging_complete - 1]
✔ Indexed file system /some/path
✔ Indexed file system /some/path
---
@@ -1,8 +1,8 @@
[TestHandler_handlePullSourceStarted/snap_download_in_progress - 1]
⠋ Downloading snap file... ━━━━━━━━━━━━━━━━━━━━ example-app_1.0_amd64.snap
⠋ Downloading snap file... ━━━━━━━━━━━━━━━━━━━━ example-app_1.0_amd64.snap
---
[TestHandler_handlePullSourceStarted/snap_download_complete - 1]
✔ Snap downloaded successfully example-app_1.0_amd64.snap
✔ Snap downloaded successfully example-app_1.0_amd64.snap
---
@@ -1,8 +1,8 @@
[TestHandler_handleReadImage/read_image_in_progress - 1]
⠋ Parsing image ━━━━━━━━━━━━━━━━━━━━ id
⠋ Parsing image ━━━━━━━━━━━━━━━━━━━━ id
---
[TestHandler_handleReadImage/read_image_complete - 1]
✔ Parsed image id
✔ Parsed image id
---
+18 -6
View File
@@ -60,6 +60,7 @@ type (
Packages []detectorPackageInfo `json:"packages,omitempty"`
Comment string `json:"comment,omitempty"`
PackageTypes []string `json:"package_types,omitempty"`
PURLTypes []string `json:"purl_types,omitempty"`
JSONSchemaTypes []string `json:"json_schema_types,omitempty"`
Capabilities capabilities.CapabilitySet `json:"capabilities,omitempty"`
}
@@ -235,7 +236,7 @@ func renderCatalogerInfoJSON(doc *capabilities.Document, catalogers []capabiliti
// if no parsers, use detectors instead
if len(info.Patterns) == 0 {
info.Capabilities = cat.Capabilities
info.Patterns = convertDetectorsToPatterns(cat.Detectors, cat.PackageTypes, cat.JSONSchemaTypes)
info.Patterns = convertDetectorsToPatterns(cat.Detectors, cat.PackageTypes, cat.PURLTypes, cat.JSONSchemaTypes)
}
info.Config = getConfigInfoFromDocument(doc, cat.Config)
@@ -278,6 +279,7 @@ func convertParsersToPatterns(parsers []capabilities.Parser) []patternInfo {
Packages: convertDetectorPackages(parser.Detector.Packages),
Comment: parser.Detector.Comment,
PackageTypes: parser.PackageTypes,
PURLTypes: parser.PURLTypes,
JSONSchemaTypes: parser.JSONSchemaTypes,
Capabilities: parser.Capabilities,
})
@@ -286,7 +288,7 @@ func convertParsersToPatterns(parsers []capabilities.Parser) []patternInfo {
}
// convertDetectorsToPatterns converts detector entries to pattern info for JSON output (for non-parser catalogers)
func convertDetectorsToPatterns(detectors []capabilities.Detector, packageTypes, jsonSchemaTypes []string) []patternInfo {
func convertDetectorsToPatterns(detectors []capabilities.Detector, packageTypes, purlTypes, jsonSchemaTypes []string) []patternInfo {
var patterns []patternInfo
for _, det := range detectors {
patterns = append(patterns, patternInfo{
@@ -296,6 +298,7 @@ func convertDetectorsToPatterns(detectors []capabilities.Detector, packageTypes,
Packages: convertDetectorPackages(det.Packages),
Comment: det.Comment,
PackageTypes: packageTypes,
PURLTypes: purlTypes,
JSONSchemaTypes: jsonSchemaTypes,
})
}
@@ -353,7 +356,7 @@ func renderCatalogerInfoTable(_ *capabilities.Document, catalogers []capabilitie
)
// set headers
table.Header("ECOSYSTEM", "CATALOGER", "CRITERIA", "LICENSE", "NODES", "EDGES", "KINDS", "LISTING", "DIGESTS", "HASH")
table.Header("ECOSYSTEM", "CATALOGER", "CRITERIA", "PURL", "LICENSE", "NODES", "EDGES", "KINDS", "LISTING", "DIGESTS", "HASH")
// build rows for each cataloger
var data [][]string
@@ -367,13 +370,13 @@ func renderCatalogerInfoTable(_ *capabilities.Document, catalogers []capabilitie
// generic catalogers: one row per parser
for _, parser := range cat.Parsers {
criteria := formatCriteria([]capabilities.Detector{parser.Detector})
row := buildTableRowFromCapabilities(ecosystem, cat.Name, criteria, parser.Capabilities)
row := buildTableRowFromCapabilities(ecosystem, cat.Name, criteria, parser.PURLTypes, parser.Capabilities)
data = append(data, row)
}
} else {
// custom catalogers: one row with all detectors
criteria := formatCriteria(cat.Detectors)
row := buildTableRowFromCapabilities(ecosystem, cat.Name, criteria, cat.Capabilities)
row := buildTableRowFromCapabilities(ecosystem, cat.Name, criteria, cat.PURLTypes, cat.Capabilities)
data = append(data, row)
}
}
@@ -385,7 +388,7 @@ func renderCatalogerInfoTable(_ *capabilities.Document, catalogers []capabilitie
}
// buildTableRowFromCapabilities builds a table row from capability values
func buildTableRowFromCapabilities(ecosystem, name, criteria string, caps capabilities.CapabilitySet) []string {
func buildTableRowFromCapabilities(ecosystem, name, criteria string, purlTypes []string, caps capabilities.CapabilitySet) []string {
// extract capability default values
license := extractBoolCapability(caps, "license")
nodes := extractNodesCapability(caps)
@@ -399,6 +402,7 @@ func buildTableRowFromCapabilities(ecosystem, name, criteria string, caps capabi
ecosystem,
name,
criteria,
formatPURLTypes(purlTypes),
license,
nodes,
edges,
@@ -409,6 +413,14 @@ func buildTableRowFromCapabilities(ecosystem, name, criteria string, caps capabi
}
}
// formatPURLTypes renders the PURL types as a comma-separated list, or a placeholder when empty
func formatPURLTypes(purlTypes []string) string {
if len(purlTypes) == 0 {
return noStyle.Render("·")
}
return strings.Join(purlTypes, ", ")
}
// extractBoolCapability extracts a boolean capability value and formats it
func extractBoolCapability(caps capabilities.CapabilitySet, name string) string {
for _, cap := range caps {
@@ -280,12 +280,15 @@ func Test_catalogerInfoReport(t *testing.T) {
Detectors: []capabilities.Detector{
{Method: capabilities.GlobDetection, Criteria: []string{"**/*.test"}},
},
PURLTypes: []string{"npm"},
},
},
assertions: func(t *testing.T, got string) {
assert.Contains(t, got, "test-cataloger")
assert.Contains(t, got, "ECOSYSTEM")
assert.Contains(t, got, "CATALOGER")
assert.Contains(t, got, "PURL")
assert.Contains(t, got, "npm")
},
},
{
@@ -298,12 +301,17 @@ func Test_catalogerInfoReport(t *testing.T) {
Ecosystem: "test",
Type: "custom",
Selectors: []string{"test", "custom"},
Detectors: []capabilities.Detector{
{Method: capabilities.GlobDetection, Criteria: []string{"**/*.test"}},
},
PURLTypes: []string{"npm"},
},
},
assertions: func(t *testing.T, got string) {
assert.Contains(t, got, `"name":"test-cataloger"`)
assert.Contains(t, got, `"ecosystem":"test"`)
assert.Contains(t, got, `"type":"custom"`)
assert.Contains(t, got, `"purl_types":["npm"]`)
},
},
{
+6
View File
@@ -7,6 +7,7 @@ import (
"os"
"reflect"
"strings"
"time"
"github.com/hashicorp/go-multierror"
"github.com/spf13/cobra"
@@ -171,6 +172,11 @@ func validateArgs(cmd *cobra.Command, args []string, err string) error {
}
func runScan(ctx context.Context, id clio.Identification, opts *scanOptions, userInput string) error {
scanStart := time.Now()
defer func() {
log.Infof("scan completed in %s", time.Since(scanStart).Round(time.Millisecond))
}()
writer, err := opts.SBOMWriter()
if err != nil {
return err
+12 -2
View File
@@ -20,6 +20,7 @@ import (
"github.com/anchore/syft/syft/file/cataloger/executable"
"github.com/anchore/syft/syft/file/cataloger/filecontent"
"github.com/anchore/syft/syft/pkg/cataloger/binary"
"github.com/anchore/syft/syft/pkg/cataloger/cpp"
"github.com/anchore/syft/syft/pkg/cataloger/dotnet"
"github.com/anchore/syft/syft/pkg/cataloger/golang"
"github.com/anchore/syft/syft/pkg/cataloger/java"
@@ -44,6 +45,7 @@ type Catalog struct {
Enrich []string `yaml:"enrich" json:"enrich" mapstructure:"enrich"`
// ecosystem-specific cataloger configuration
Cpp cppConfig `yaml:"cpp" json:"cpp" mapstructure:"cpp"`
Dotnet dotnetConfig `yaml:"dotnet" json:"dotnet" mapstructure:"dotnet"`
Golang golangConfig `yaml:"golang" json:"golang" mapstructure:"golang"`
Java javaConfig `yaml:"java" json:"java" mapstructure:"java"`
@@ -80,6 +82,7 @@ func DefaultCatalog() Catalog {
JavaScript: defaultJavaScriptConfig(),
Python: defaultPythonConfig(),
Nix: defaultNixConfig(),
Cpp: defaultCppConfig(),
Dotnet: defaultDotnetConfig(),
Golang: defaultGolangConfig(),
Java: defaultJavaConfig(),
@@ -186,11 +189,15 @@ func (cfg Catalog) ToPackagesConfig() pkgcataloging.Config {
}
return pkgcataloging.Config{
Binary: binary.DefaultClassifierCatalogerConfig(),
Cpp: cpp.DefaultCatalogerConfig().
WithVcpkgAllowGitClone(*multiLevelOption(false, enrichmentEnabled(cfg.Enrich, task.Cpp, task.Vcpkg), cfg.Cpp.VcpkgAllowGitClone)),
Dotnet: dotnet.DefaultCatalogerConfig().
WithDepPackagesMustHaveDLL(cfg.Dotnet.DepPackagesMustHaveDLL).
WithDepPackagesMustClaimDLL(cfg.Dotnet.DepPackagesMustClaimDLL).
WithPropagateDLLClaimsToParents(cfg.Dotnet.PropagateDLLClaimsToParents).
WithRelaxDLLClaimsWhenBundlingDetected(cfg.Dotnet.RelaxDLLClaimsWhenBundlingDetected),
WithRelaxDLLClaimsWhenBundlingDetected(cfg.Dotnet.RelaxDLLClaimsWhenBundlingDetected).
WithExcludeProjectReferences(cfg.Dotnet.ExcludeProjectReferences),
Golang: golang.DefaultCatalogerConfig().
WithSearchLocalModCacheLicenses(*multiLevelOption(false, enrichmentEnabled(cfg.Enrich, task.Go, task.Golang), cfg.Golang.SearchLocalModCacheLicenses)).
WithLocalModCacheDir(cfg.Golang.LocalModCacheDir).
@@ -205,7 +212,9 @@ func (cfg Catalog) ToPackagesConfig() pkgcataloging.Config {
WithFromBuildSettings(cfg.Golang.MainModuleVersion.FromBuildSettings).
WithFromLDFlags(cfg.Golang.MainModuleVersion.FromLDFlags),
).
WithUsePackagesLib(*multiLevelOption(true, enrichmentEnabled(cfg.Enrich, task.Go, task.Golang), cfg.Golang.UsePackagesLib)),
WithUsePackagesLib(*multiLevelOption(true, enrichmentEnabled(cfg.Enrich, task.Go, task.Golang), cfg.Golang.UsePackagesLib)).
WithCaptureSymbols(cfg.Golang.CaptureSymbols).
WithCaptureSymbolsModules(cfg.Golang.CaptureSymbolsModules),
JavaScript: javascript.DefaultCatalogerConfig().
WithIncludeDevDependencies(*multiLevelOption(false, cfg.JavaScript.IncludeDevDependencies)).
WithSearchRemoteLicenses(*multiLevelOption(false, enrichmentEnabled(cfg.Enrich, task.JavaScript, task.Node, task.NPM), cfg.JavaScript.SearchRemoteLicenses)).
@@ -313,6 +322,7 @@ var publicisedEnrichmentOptions = []string{
task.Java,
task.JavaScript,
task.Python,
task.Vcpkg,
}
func enrichmentEnabled(enrichDirectives []string, features ...string) *bool {
+30
View File
@@ -0,0 +1,30 @@
package options
import (
"github.com/anchore/clio"
"github.com/anchore/syft/syft/pkg/cataloger/cpp"
)
type cppConfig struct {
VcpkgAllowGitClone *bool `yaml:"vcpkg-allow-git-clone" json:"vcpkg-allow-git-clone" mapstructure:"vcpkg-allow-git-clone"`
}
func defaultCppConfig() cppConfig {
// reference the cataloger default so capability generation can associate this config with the cpp
// ecosystem (it discovers ecosystem configs by their cataloger import). the value itself stays nil:
// nil defaults to false (no network), and leaving it unset lets --enrich opt in. cloning requires a
// network connection, which must be opt-in.
_ = cpp.DefaultCatalogerConfig()
return cppConfig{
VcpkgAllowGitClone: nil,
}
}
var _ interface {
clio.FieldDescriber
} = (*cppConfig)(nil)
func (o *cppConfig) DescribeFields(descriptions clio.FieldDescriptionSet) {
descriptions.Add(&o.VcpkgAllowGitClone, `enables Syft to use clone remote repositories for vcpkg custom git registries.
(also useful if the builtin vcpkg registry is not cloned locally)`)
}
@@ -13,12 +13,10 @@ func DefaultFormatSPDXJSON() FormatSPDXJSON {
}
func (o FormatSPDXJSON) config(v string) spdxjson.EncoderConfig {
var pretty bool
c := spdxjson.DefaultEncoderConfig()
c.Version = v
if o.Pretty != nil {
pretty = *o.Pretty
}
return spdxjson.EncoderConfig{
Version: v,
Pretty: pretty,
c.Pretty = *o.Pretty
}
return c
}
@@ -2,6 +2,8 @@ package options
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestFormatSPDXJSON_buildConfig(t *testing.T) {
@@ -11,5 +13,7 @@ func TestFormatSPDXJSON_buildConfig(t *testing.T) {
ft = setAllToNonZero(t, ft).(*FormatSPDXJSON)
subject := ft.config("Version")
assertExpectedValue(t, subject)
require.Equal(t, "Version", subject.Version)
require.Equal(t, "2.3", subject.DefaultVersion)
require.True(t, subject.Pretty)
}
+41 -2
View File
@@ -4,6 +4,8 @@ import (
"strings"
"github.com/anchore/clio"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/cataloging"
"github.com/anchore/syft/syft/pkg/cataloger/golang"
)
@@ -17,10 +19,13 @@ type golangConfig struct {
NoProxy string `json:"no-proxy" yaml:"no-proxy" mapstructure:"no-proxy"`
MainModuleVersion golangMainModuleVersionConfig `json:"main-module-version" yaml:"main-module-version" mapstructure:"main-module-version"`
UsePackagesLib *bool `json:"use-packages-lib" yaml:"use-packages-lib" mapstructure:"use-packages-lib"`
CaptureSymbols cataloging.SymbolScope `json:"capture-symbols" yaml:"capture-symbols" mapstructure:"capture-symbols"`
CaptureSymbolsModules []string `json:"capture-symbols-modules" yaml:"capture-symbols-modules" mapstructure:"capture-symbols-modules"`
}
var _ interface {
clio.FieldDescriber
clio.PostLoader
} = (*golangConfig)(nil)
func (o *golangConfig) DescribeFields(descriptions clio.FieldDescriptionSet) {
@@ -39,12 +44,44 @@ if unset this defaults to $GONOPROXY`)
always show (devel) as the version. Use these options to control heuristics to guess
a more accurate version from the binary.`)
descriptions.Add(&o.UsePackagesLib, `use the golang.org/x/tools/go/packages library, which executes golang tooling found on the path in addition to potential network access to get the most accurate results`)
// note: descriptions must be static string literals; the app config discovery that generates the
// capability docs reads them straight out of the AST
descriptions.Add(&o.CaptureSymbols, `capture function symbols from the binary symbol table (pclntab). valid values are:
"none" (disabled), "stdlib" (only the synthetic stdlib package), "extended-stdlib" (stdlib plus every
module under golang.org/x/), and "all" (all module packages plus stdlib)`)
descriptions.Add(&o.CaptureSymbolsModules, `glob patterns matched against go module paths (e.g. github.com/klauspost/**) that should have symbols
captured in addition to whatever capture-symbols selects. ** crosses path separators, * does not.
a trailing major version suffix is ignored when matching, so github.com/foo/* covers github.com/foo/bar/v2;
spelling a suffix out in the pattern selects only that major version.
this can only widen the selection, never narrow it, and is inert when capture-symbols is none`)
descriptions.Add(&o.MainModuleVersion.FromLDFlags, `look for LD flags that appear to be setting a version (e.g. -X main.version=1.0.0)`)
descriptions.Add(&o.MainModuleVersion.FromBuildSettings, `use the build settings (e.g. vcs.version & vcs.time) to craft a v0 pseudo version
descriptions.Add(&o.MainModuleVersion.FromBuildSettings, `use the build settings (e.g. vcs.version & vcs.time) to craft a v0 pseudo version
(e.g. v0.0.0-20220308212642-53e6d0aaf6fb) when a more accurate version cannot be found otherwise`)
descriptions.Add(&o.MainModuleVersion.FromContents, `search for semver-like strings in the binary contents`)
}
func (o *golangConfig) PostLoad() error {
raw := strings.TrimSpace(string(o.CaptureSymbols))
o.CaptureSymbols = o.CaptureSymbols.Parse()
// an unrecognized value still resolves to "none", but say so rather than silently capturing nothing.
// stay quiet for unset and an explicit "none", which is the default and would otherwise warn on nearly
// every scan.
if o.CaptureSymbols == cataloging.SymbolScopeNone && raw != "" && !strings.EqualFold(raw, string(cataloging.SymbolScopeNone)) {
log.Warnf("unknown golang.capture-symbols value %q, defaulting to %q (valid values: none, stdlib, extended-stdlib, all)", raw, cataloging.SymbolScopeNone)
}
// trim only, deliberately not Flatten: viper already splits a comma-separated scalar (env var or a bare
// yaml string) into a slice before this point, and there is no CLI flag feeding this key. The one thing
// Flatten would add is splitting commas *inside* a list entry, which silently breaks doublestar brace
// alternation like github.com/{foo,bar}/**. Viper's split does not trim, so that part is still needed.
for i, pattern := range o.CaptureSymbolsModules {
o.CaptureSymbolsModules[i] = strings.TrimSpace(pattern)
}
return nil
}
type golangMainModuleVersionConfig struct {
FromLDFlags bool `json:"from-ld-flags" yaml:"from-ld-flags" mapstructure:"from-ld-flags"`
FromContents bool `json:"from-contents" yaml:"from-contents" mapstructure:"from-contents"`
@@ -66,6 +103,8 @@ func defaultGolangConfig() golangConfig {
FromContents: def.MainModuleVersion.FromContents,
FromBuildSettings: def.MainModuleVersion.FromBuildSettings,
},
UsePackagesLib: nil, // this defaults to true, which is the API default
UsePackagesLib: nil, // this defaults to true, which is the API default
CaptureSymbols: def.CaptureSymbols,
CaptureSymbolsModules: def.CaptureSymbolsModules,
}
}
+95
View File
@@ -0,0 +1,95 @@
package options
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/anchore/syft/syft/cataloging"
)
func Test_golangConfig_PostLoad(t *testing.T) {
tests := []struct {
name string
cfg golangConfig
expected cataloging.SymbolScope
expectedModules []string
wantErr assert.ErrorAssertionFunc
}{
{
name: "normalize all",
cfg: golangConfig{CaptureSymbols: "all"},
expected: cataloging.SymbolScopeAll,
},
{
name: "normalize stdlib",
cfg: golangConfig{CaptureSymbols: "stdlib"},
expected: cataloging.SymbolScopeStdlib,
},
{
name: "normalize extended-stdlib",
cfg: golangConfig{CaptureSymbols: " Extended-Stdlib "},
expected: cataloging.SymbolScopeExtendedStdlib,
},
{
name: "module patterns keep embedded commas",
cfg: golangConfig{
CaptureSymbols: "stdlib",
// brace alternation contains a comma; splitting on it would corrupt the pattern
CaptureSymbolsModules: []string{"github.com/{foo,bar}/**", "golang.org/x/**"},
},
expected: cataloging.SymbolScopeStdlib,
expectedModules: []string{"github.com/{foo,bar}/**", "golang.org/x/**"},
},
{
// viper splits a comma-separated scalar (env var or bare yaml string) but does not trim,
// so a leading space would otherwise survive into a pattern that silently matches nothing
name: "module patterns are trimmed",
cfg: golangConfig{
CaptureSymbols: "stdlib",
CaptureSymbolsModules: []string{"golang.org/x/**", " github.com/foo/** "},
},
expected: cataloging.SymbolScopeStdlib,
expectedModules: []string{"golang.org/x/**", "github.com/foo/**"},
},
{
name: "empty defaults to none",
cfg: golangConfig{CaptureSymbols: ""},
expected: cataloging.SymbolScopeNone,
},
{
name: "invalid value defaults to none",
cfg: golangConfig{CaptureSymbols: "stdlbi"},
expected: cataloging.SymbolScopeNone,
},
{
name: "boolean spellings default to none",
cfg: golangConfig{CaptureSymbols: "true"},
expected: cataloging.SymbolScopeNone,
},
{
name: "explicit none resolves to none",
cfg: golangConfig{CaptureSymbols: "none"},
expected: cataloging.SymbolScopeNone,
},
{
name: "explicit none is not case sensitive",
cfg: golangConfig{CaptureSymbols: " NONE "},
expected: cataloging.SymbolScopeNone,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.wantErr == nil {
tt.wantErr = assert.NoError
}
err := tt.cfg.PostLoad()
tt.wantErr(t, err)
if err != nil {
return
}
assert.Equal(t, tt.expected, tt.cfg.CaptureSymbols)
assert.Equal(t, tt.expectedModules, tt.cfg.CaptureSymbolsModules)
})
}
}
@@ -89,6 +89,8 @@ func TestPkgCoverageImage(t *testing.T) {
definedPkgs.Remove(string(pkg.PhpPeclPkg)) // we have coverage for pear instead
definedPkgs.Remove(string(pkg.CondaPkg))
definedPkgs.Remove(string(pkg.ModelPkg))
definedPkgs.Remove(string(pkg.VcpkgPkg))
definedPkgs.Remove(string(pkg.AppleAppBundlePkg))
var cases []testCase
cases = append(cases, commonTestCases...)
@@ -164,6 +166,8 @@ func TestPkgCoverageDirectory(t *testing.T) {
definedPkgs.Remove(string(pkg.CondaPkg))
definedPkgs.Remove(string(pkg.PhpPeclPkg)) // this is covered as pear packages
definedPkgs.Remove(string(pkg.ModelPkg))
definedPkgs.Remove(string(pkg.VcpkgPkg))
definedPkgs.Remove(string(pkg.AppleAppBundlePkg))
// for directory scans we should not expect to see any of the following package types
definedPkgs.Remove(string(pkg.KbPkg))
@@ -0,0 +1,145 @@
package integration
import (
"context"
"encoding/json"
"fmt"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/source"
)
// TestMultiPlatformOCIImageSelection verifies that syft can load a single platform out of a
// multi-platform OCI image on disk (both --from oci-dir and --from oci-archive) by selecting the
// image that matches the requested platform.
func TestMultiPlatformOCIImageSelection(t *testing.T) {
remoteImage := "docker.io/library/busybox:1.38.0"
// Per-platform image config digests within the multi-platform index, obtained from the OCI layout
// (see anchore/stereoscope integration tests: TestPlatformSelectionWithOciLocalSources).
expectedDigest := map[string]string{
"arm64": "sha256:e0e8b3cbfed68a90084781e2962f9c0deead51c5a3f11a488eef0283a4284bc2",
"s390x": "sha256:0cf160e720a8e4f20883b72276a6eaded83b79539f3f1d39e35a3336154b9960",
"amd64": "sha256:c6348fa86ba0fb2108c9334f5fe913ddc6d853313e655891f133a0127c30099f",
"ppc64le": "sha256:b144fc0e06537d07956cde340b878a81a717e394edb736d3110858a12a6635cb",
}
// syft --from source tag -> stereoscope OCI source used to prepare the local fixture
sources := map[string]image.Source{
"oci-dir": image.OciDirectorySource,
"oci-archive": image.OciTarballSource,
}
for from, imageSource := range sources {
t.Run(from, func(t *testing.T) {
localPath := imagetest.PrepareMultiplatformFixtureImage(t, imageSource, remoteImage)
for _, arch := range []string{"amd64", "arm64", "s390x", "ppc64le"} {
t.Run(fmt.Sprintf("linux/%s", arch), func(t *testing.T) {
platform, err := image.NewPlatform("linux/" + arch)
require.NoError(t, err)
src, err := syft.GetSource(
context.Background(),
localPath,
syft.DefaultGetSourceConfig().WithSources(from).WithPlatform(platform),
)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, src.Close())
})
meta, ok := src.Describe().Metadata.(source.ImageMetadata)
require.True(t, ok, "expected image metadata, got %T", src.Describe().Metadata)
// The raw config of the selected image must match the requested platform...
assertConfigPlatform(t, meta.RawConfig, "linux", arch)
// ...and it must be the exact per-platform image from the multi-platform index.
assert.Equal(t, expectedDigest[arch], meta.ID)
})
}
})
}
}
// TestMultiPlatformOCIImageSelection_UnavailablePlatform verifies that requesting a platform not present
// in the multi-platform OCI image results in an error rather than silently selecting the wrong image.
func TestMultiPlatformOCIImageSelection_UnavailablePlatform(t *testing.T) {
remoteImage := "docker.io/library/busybox:1.38.0"
// windows/amd64 is not present in this linux-only multi-platform image
platform, err := image.NewPlatform("windows/amd64")
require.NoError(t, err)
sources := map[string]image.Source{
"oci-dir": image.OciDirectorySource,
"oci-archive": image.OciTarballSource,
}
for from, imageSource := range sources {
t.Run(from, func(t *testing.T) {
localPath := imagetest.PrepareMultiplatformFixtureImage(t, imageSource, remoteImage)
_, err := syft.GetSource(
context.Background(),
localPath,
syft.DefaultGetSourceConfig().WithSources(from).WithPlatform(platform),
)
require.ErrorContains(t, err, "windows/amd64")
})
}
}
// TestMultiPlatformOCIImageSelection_DefaultPlatform verifies that not specifying a platform results
// in the current platform being selected.
func TestMultiPlatformOCIImageSelection_DefaultPlatform(t *testing.T) {
remoteImage := "docker.io/library/busybox:1.38.0"
sources := map[string]image.Source{
"oci-dir": image.OciDirectorySource,
"oci-archive": image.OciTarballSource,
}
for from, imageSource := range sources {
t.Run(from, func(t *testing.T) {
localPath := imagetest.PrepareMultiplatformFixtureImage(t, imageSource, remoteImage)
src, err := syft.GetSource(
context.Background(),
localPath,
syft.DefaultGetSourceConfig().WithSources(from),
)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, src.Close())
})
meta, ok := src.Describe().Metadata.(source.ImageMetadata)
require.True(t, ok, "expected image metadata, got %T", src.Describe().Metadata)
// The raw config of the selected image must match the current platform
assertConfigPlatform(t, meta.RawConfig, "linux", runtime.GOARCH)
})
}
}
// assertConfigPlatform asserts the os/architecture recorded in a raw OCI image config document.
func assertConfigPlatform(t *testing.T, rawConfig []byte, os, architecture string) {
t.Helper()
require.NotEmpty(t, rawConfig)
var cfg struct {
OS string `json:"os"`
Architecture string `json:"architecture"`
}
require.NoError(t, json.Unmarshal(rawConfig, &cfg))
assert.Equal(t, os, cfg.OS)
assert.Equal(t, architecture, cfg.Architecture)
}
@@ -7,4 +7,6 @@
!image-pkg-coverage/pkgs/java/*.hpi
**/go.sum
!image-go-bin-arch-coverage/go.sum
!image-go-bin-arch-coverage/go.sum
cache/
+1 -1
View File
@@ -101,7 +101,7 @@ func (m *UI) Teardown(force bool) error {
})
// it may be tempting to use Kill() however it has been found that this can cause the terminal to be left in
// a bad state (where Ctrl+C and other control characters no longer works for future processes in that terminal).
// a bad state (where Ctrl+C and other control characters no longer work for future processes in that terminal).
m.program.Quit()
_ = runWithTimeout(250*time.Millisecond, func() error {
+3 -1
View File
@@ -5,8 +5,10 @@ This directory contains examples of how to use the Syft API.
- `create_simple_sbom`: Create a simple SBOM from scratch
- `create_custom_sbom`: Create an SBOM using as much custom configuration as possible, including a custom cataloger implementation
- `decode_sbom`: Take an existing SBOM file (of arbitrary format) and decode it into a Syft SBOM object
- `select_catalogers`: Example demonstrating how to select specific catalogers to run
- `source_detection`: Shows how to detect what to catalog automatically from a user string (e.g. container image vs directory)
- `source_from_image`: Construct a source from a only a container image
- `source_from_image`: Construct a source from only a container image
- `source_from_registry`: Construct a source directly from a container registry
You can run any of these examples from this directory with:
+10 -9
View File
@@ -4,7 +4,7 @@ go 1.26.3
require (
github.com/BurntSushi/toml v1.6.0
github.com/CycloneDX/cyclonedx-go v0.11.0
github.com/CycloneDX/cyclonedx-go v0.12.0
github.com/Masterminds/semver/v3 v3.5.0
github.com/Masterminds/sprig/v3 v3.3.0
github.com/OneOfOne/xxhash v1.2.8
@@ -22,7 +22,7 @@ require (
github.com/anchore/go-sync v0.1.1
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
github.com/anchore/packageurl-go v0.2.0
github.com/anchore/stereoscope v0.3.1-0.20260820190420-3cd14ecb9fbf
github.com/anchore/stereoscope v0.3.1
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
github.com/aquasecurity/go-pep440-version v0.0.1
github.com/bitnami/go-version v0.0.0-20250131085805-b1f57a8634ef
@@ -48,11 +48,11 @@ require (
github.com/goccy/go-yaml v1.19.2
github.com/gohugoio/hashstructure v0.6.0
github.com/google/go-cmp v0.7.0
github.com/google/go-containerregistry v0.21.9
github.com/google/go-containerregistry v0.22.0
github.com/google/licensecheck v0.3.1
github.com/google/uuid v1.6.0
github.com/gookit/color v1.6.1
github.com/gpustack/gguf-parser-go v0.25.0
github.com/gpustack/gguf-parser-go v0.26.0
github.com/hashicorp/go-cleanhttp v0.5.2
github.com/hashicorp/go-getter v1.8.8
github.com/hashicorp/go-multierror v1.1.1
@@ -82,7 +82,7 @@ require (
github.com/spdx/tools-golang v0.6.0-rc4
github.com/spf13/afero v1.15.0
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.12.0
github.com/stretchr/testify v1.12.1
github.com/ulikunitz/xz v0.5.16
github.com/vbatts/go-mtree v0.7.0
github.com/vifraa/gopom v1.0.0
@@ -98,12 +98,13 @@ require (
golang.org/x/time v0.15.0
golang.org/x/tools v0.49.0
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.56.0
modernc.org/sqlite v1.57.0
)
require (
github.com/pb33f/ordered-map/v2 v2.3.1
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd
howett.net/plist v1.0.1
)
require (
@@ -169,13 +170,13 @@ require (
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
github.com/containerd/cgroups/v3 v3.1.3 // indirect
github.com/containerd/containerd/api v1.11.1 // indirect
github.com/containerd/containerd/v2 v2.3.3 // indirect
github.com/containerd/containerd/v2 v2.3.4 // indirect
github.com/containerd/continuity v0.5.0 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/fifo v1.1.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v1.0.0-rc.4 // indirect
github.com/containerd/platforms v1.0.0-rc.5 // indirect
github.com/containerd/plugin v1.1.0 // indirect
github.com/containerd/ttrpc v1.2.8 // indirect
github.com/containerd/typeurl/v2 v2.2.3 // indirect
@@ -317,7 +318,7 @@ require (
google.golang.org/genproto v0.0.0-20260519071638-aa98bba5eb94 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d // indirect
google.golang.org/grpc v1.83.0 // indirect
google.golang.org/grpc v1.83.1 // indirect
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
modernc.org/libc v1.74.4 // indirect
+24 -20
View File
@@ -77,8 +77,8 @@ github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbi
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/CycloneDX/cyclonedx-go v0.11.0 h1:GokP8FiRC+foiuwWhSSLpSD5H4hSWtGnR3wo7apkBFI=
github.com/CycloneDX/cyclonedx-go v0.11.0/go.mod h1:vUvbCXQsEm48OI6oOlanxstwNByXjCZ2wuleUlwGEO8=
github.com/CycloneDX/cyclonedx-go v0.12.0 h1:/7Jum36UA6V043tQZ/fE3jf+Nf9gn/qxUFfd7QReMy8=
github.com/CycloneDX/cyclonedx-go v0.12.0/go.mod h1:V2577HhxDDCDLYfkm55WJrz16nHTfyQZwcWUBSG7Z28=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ=
github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
@@ -146,8 +146,8 @@ github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZV
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E=
github.com/anchore/packageurl-go v0.2.0 h1:CkrM4RMUwrEGAiE1OVlxaZNzWj0TuHRey7o4T/EAErk=
github.com/anchore/packageurl-go v0.2.0/go.mod h1:2JCgOQMIsqZ7TmliXG4PnUthPJAKE3mWQbsW2XHjAOE=
github.com/anchore/stereoscope v0.3.1-0.20260820190420-3cd14ecb9fbf h1:eTQTL06tadOz+V4ubhXOlc5oKQ11cXbyo/clnxlcsnU=
github.com/anchore/stereoscope v0.3.1-0.20260820190420-3cd14ecb9fbf/go.mod h1:cWQ8fLFblnkJ0c3BSFY+BwXe4DcJDdysP/gnoWHVb7s=
github.com/anchore/stereoscope v0.3.1 h1:SFLeRyi+3L0fVDTcd4sjT6Xalvi9pju7DegeUMmxna8=
github.com/anchore/stereoscope v0.3.1/go.mod h1:KHLeUz03/lpsxM8jvs7gFSWRHCXB9MLnxpusCIPe3ME=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
@@ -294,8 +294,8 @@ github.com/containerd/cgroups/v3 v3.1.3 h1:eUNflyMddm18+yrDmZPn3jI7C5hJ9ahABE5q6
github.com/containerd/cgroups/v3 v3.1.3/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw=
github.com/containerd/containerd/api v1.11.1 h1:h8nfoDW9+fNsC/9TwiAHj8B1GzXKtR4eFtkhi/X5RLU=
github.com/containerd/containerd/api v1.11.1/go.mod h1:CaQFRu+N1MtbgL6JDOJLUB1hCKESU1lD6MuTJhgtdlw=
github.com/containerd/containerd/v2 v2.3.3 h1:MUNBVVBTBpPll7KPh5GTvkC3cfG03PQLAHVdsUoue9k=
github.com/containerd/containerd/v2 v2.3.3/go.mod h1:rHKGm3VW6wNrINb3x8mNT+w7qYXFVElTt/8HTuxVhD4=
github.com/containerd/containerd/v2 v2.3.4 h1:c2PJo/9UGVdiiw8SwrxuLxWGY+9b3jQ6Xp9zntneIvI=
github.com/containerd/containerd/v2 v2.3.4/go.mod h1:a30D8fWZJ1Uzx/2WpjLbLsxBkq9He41pe8ENW+QZ3LY=
github.com/containerd/continuity v0.5.0 h1:7a85HZpCSs+1Zps0Ee3DPSuAWY+0SJM1JNM51nlEVDg=
github.com/containerd/continuity v0.5.0/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
@@ -306,8 +306,8 @@ github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY
github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v1.0.0-rc.4 h1:M42JrUT4zfZTqtkUwkr0GzmUWbfyO5VO0Q5b3op97T4=
github.com/containerd/platforms v1.0.0-rc.4/go.mod h1:lKlMXyLybmBedS/JJm11uDofzI8L2v0J2ZbYvNsbq1A=
github.com/containerd/platforms v1.0.0-rc.5 h1:vXd569rDrz8LeMXzAnBsy6LADV5YtsD8oyaRarxdmSU=
github.com/containerd/platforms v1.0.0-rc.5/go.mod h1:lKlMXyLybmBedS/JJm11uDofzI8L2v0J2ZbYvNsbq1A=
github.com/containerd/plugin v1.1.0 h1:O+7lczNJVMy8rz0YNx3xGB8tTf5qY4i5abF041Ew19U=
github.com/containerd/plugin v1.1.0/go.mod h1:qBTum+A8lJ6lO44A19Eo7y1OlcLj4OWFH1DA/vnHmcc=
github.com/containerd/ttrpc v1.2.8 h1:xbVu6D4qF2jihdh9rDVOKqUMiFBQk6YctTdo1zk087Y=
@@ -507,8 +507,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-containerregistry v0.21.9 h1:F+D4uZ3iA3DLMJLfhaqMdHJbzeqm/216WGQq2dokuLs=
github.com/google/go-containerregistry v0.21.9/go.mod h1:dP5XNKcL7kMFF/TB3LfvWmVhAcv7iqkHb3oDK8aauTo=
github.com/google/go-containerregistry v0.22.0 h1:eGbCiPeYxAH/7WLLq6zTBALP0tUIFsoyRauhxXDJ53I=
github.com/google/go-containerregistry v0.22.0/go.mod h1:bJR35SK8XgisYmhg/FMQ/5RK0S/XrOAqLBV5/LR2XE0=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/licensecheck v0.3.1 h1:QoxgoDkaeC4nFrtGN1jV7IPmDCHFNIVh54e5hSt6sPs=
github.com/google/licensecheck v0.3.1/go.mod h1:ORkR35t/JjW+emNKtfJDII0zlciG9JgbT7SmsohlHmY=
@@ -556,8 +556,8 @@ github.com/gookit/assert v0.1.1/go.mod h1:jS5bmIVQZTIwk42uXl4lyj4iaaxx32tqH16CFj
github.com/gookit/color v1.2.5/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg=
github.com/gookit/color v1.6.1 h1:KoTnDxJPRgrL0SoX0f8rCFg2zI0t4E3GZZBMo2nN8LU=
github.com/gookit/color v1.6.1/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs=
github.com/gpustack/gguf-parser-go v0.25.0 h1:1AMBhMKtI24nTtn588Bq53FqNiOvEw1x9Nb4HbRrThs=
github.com/gpustack/gguf-parser-go v0.25.0/go.mod h1:y4TwTtDqFWTK+xvprOjRUh+dowgU2TKCX37vRKvGiZ0=
github.com/gpustack/gguf-parser-go v0.26.0 h1:klBqNj3w5oyY2GWC6/aQJ6MtPUYtHyYRlEyzEf00cxA=
github.com/gpustack/gguf-parser-go v0.26.0/go.mod h1:y4TwTtDqFWTK+xvprOjRUh+dowgU2TKCX37vRKvGiZ0=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.74 h1:mymLUKThnV9wFvogOK8NnsMP9/vlhnjXY98gr2QIGW8=
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.74/go.mod h1:Bh9qYL8ehmDxSg14Tk8oxFCP90XHfs6NxV1D84884xA=
@@ -624,6 +624,7 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOl
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jedib0t/go-pretty/v6 v6.8.3 h1:yVSk5aemoYHCvcrtqyXklwqcgHQIQzmy/oUzFlmffSQ=
github.com/jedib0t/go-pretty/v6 v6.8.3/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
@@ -917,8 +918,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.12.0 h1:K6Mr6jO9JICuend/5xzTM03ydSV3vdNRYAdPSukj8uI=
github.com/stretchr/testify v1.12.0/go.mod h1:bOYBZb5qJ00vPzWfIqBUZPaxK8jWiXc6d3ErP4Ca9Gw=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
@@ -928,8 +929,8 @@ github.com/sylabs/squashfs v1.0.6 h1:PvJcDzxr+vIm2kH56mEMbaOzvGu79gK7P7IX+R7BDZI
github.com/sylabs/squashfs v1.0.6/go.mod h1:DlDeUawVXLWAsSRa085Eo0ZenGzAB32JdAUFaB0LZfE=
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd h1:Rf9uhF1+VJ7ZHqxrG8pJ6YacmHvVCmByDmGbAWCc/gA=
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd/go.mod h1:EbW0wDK/qEUYI0A5bqq0C2kF8JTQwWONmGDBbzsxxHo=
github.com/terminalstatic/go-xsd-validate v0.1.6 h1:TenYeQ3eY631qNi1/cTmLH/s2slHPRKTTHT+XSHkepo=
github.com/terminalstatic/go-xsd-validate v0.1.6/go.mod h1:18lsvYFofBflqCrvo1umpABZ99+GneNTw2kEEc8UPJw=
github.com/terminalstatic/go-xsd-validate v0.1.8 h1:UVrTCy1j3DhwaYTTUF+QYO/Nan13S0tf+Jwi+p45Bf0=
github.com/terminalstatic/go-xsd-validate v0.1.8/go.mod h1:1kb47fi2c6onlf+B7UrrQ9VYraOhcYwFm3iG+J6F4Zo=
github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw=
github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
@@ -1462,8 +1463,8 @@ google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnD
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
google.golang.org/grpc v1.83.0 h1:JeNZEKJFbQxArAMl+hiytHauacDNqJUllNfmIMmpqnQ=
google.golang.org/grpc v1.83.0/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ=
google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y=
google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
@@ -1490,6 +1491,7 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@@ -1510,6 +1512,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
modernc.org/cc/v4 v4.29.1 h1:MKgdCV3WykTSPqpVrnxdEDS0HEd2FHpKZDzxzU5LyeI=
modernc.org/cc/v4 v4.29.1/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU=
@@ -1532,8 +1536,8 @@ modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.56.0 h1:/D8e2RfFqoy/Zc6PuC76U28zFwmI/sYx1Kjm4yEn9e0=
modernc.org/sqlite v1.56.0/go.mod h1:yCJ2cmAaIkHQ25oXWrF8H4O1lIfPYPR26yCEDj2P3pQ=
modernc.org/sqlite v1.57.0 h1:qNQP6xnx5M0ISNtlnxoOX0+cD5bJ0/gr9aMmndFczzg=
modernc.org/sqlite v1.57.0/go.mod h1:yCJ2cmAaIkHQ25oXWrF8H4O1lIfPYPR26yCEDj2P3pQ=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+8 -7
View File
@@ -35,7 +35,8 @@ is_command() (
)
echo_stderr() (
echo "$@" 1>&2
IFS=" "
printf "%b\n" "$*" 1>&2
)
_logp=2
@@ -75,12 +76,12 @@ init_colors
log_tag() (
case $1 in
0) echo "${RED}${BOLD}[error]${RESET}" ;;
1) echo "${RED}[warn]${RESET}" ;;
2) echo "[info]${RESET}" ;;
3) echo "${BLUE}[debug]${RESET}" ;;
4) echo "${PURPLE}[trace]${RESET}" ;;
*) echo "[$1]" ;;
0) printf "%b" "${RED}${BOLD}[error]${RESET}" ;;
1) printf "%b" "${RED}[warn]${RESET}" ;;
2) printf "%b" "[info]${RESET}" ;;
3) printf "%b" "${BLUE}[debug]${RESET}" ;;
4) printf "%b" "${PURPLE}[trace]${RESET}" ;;
*) printf "[%s]" "$1" ;;
esac
)
+6
View File
@@ -2,6 +2,8 @@
# This file is partially auto-generated. Run 'go generate ./internal/capabilities' to regenerate.
application: # AUTO-GENERATED - application-level config keys
- key: cpp.vcpkg-allow-git-clone
description: enables Syft to use clone remote repositories for vcpkg custom git registries. (also useful if the builtin vcpkg registry is not cloned locally)
- key: dotnet.dep-packages-must-claim-dll
description: only keep dep.json packages which have a runtime/resource DLL claimed in the deps.json targets section (but not necessarily found on disk). The package is also included if any child package claims a DLL, even if the package itself does not claim a DLL.
- key: dotnet.dep-packages-must-have-dll
@@ -12,6 +14,10 @@ application: # AUTO-GENERATED - application-level config keys
description: treat DLL claims or on-disk evidence for child packages as DLL claims or on-disk evidence for any parent package
- key: dotnet.relax-dll-claims-when-bundling-detected
description: show all packages from the deps.json if bundling tooling is present as a dependency (e.g. ILRepack)
- key: golang.capture-symbols
description: 'capture function symbols from the binary symbol table (pclntab). valid values are: "none" (disabled), "stdlib" (only the synthetic stdlib package), "extended-stdlib" (stdlib plus every module under golang.org/x/), and "all" (all module packages plus stdlib)'
- key: golang.capture-symbols-modules
description: glob patterns matched against go module paths (e.g. github.com/klauspost/**) that should have symbols captured in addition to whatever capture-symbols selects. ** crosses path separators, * does not. a trailing major version suffix is ignored when matching, so github.com/foo/* covers github.com/foo/bar/v2; spelling a suffix out in the pattern selects only that major version. this can only widen the selection, never narrow it, and is inert when capture-symbols is none
- key: golang.local-mod-cache-dir
description: specify an explicit go mod cache directory, if unset this defaults to $GOPATH/pkg/mod or $HOME/go/pkg/mod
- key: golang.local-vendor-dir
+6 -1
View File
@@ -297,6 +297,11 @@ func addCatalogerFieldComment(keyNode, valueNode *yaml.Node, catalogerName strin
if keyNode.LineComment == "" {
keyNode.LineComment = autoGeneratedComment
}
case "purl_types":
// cataloger-level purl_types (for custom catalogers) are AUTO-GENERATED
if keyNode.LineComment == "" {
keyNode.LineComment = autoGeneratedComment
}
case "json_schema_types":
// json_schema_types are AUTO-GENERATED
if keyNode.LineComment == "" {
@@ -367,7 +372,7 @@ func addParserComments(parsersNode *yaml.Node) {
valueNode := parserNode.Content[i+1]
switch keyNode.Value {
case "parser_function", "metadata_types", "package_types", "json_schema_types":
case "parser_function", "metadata_types", "package_types", "purl_types", "json_schema_types":
// add AUTO-GENERATED comment to these fields
if keyNode.LineComment == "" {
keyNode.LineComment = autoGeneratedComment
+38
View File
@@ -3,6 +3,7 @@ package main
import (
"fmt"
"sort"
"strings"
"github.com/scylladb/go-set/strset"
@@ -10,6 +11,7 @@ import (
"github.com/anchore/syft/internal/capabilities"
"github.com/anchore/syft/internal/capabilities/internal"
"github.com/anchore/syft/internal/packagemetadata"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/binary"
)
@@ -192,6 +194,9 @@ func RegenerateCapabilities(catalogerDir string, repoRoot string) (*Statistics,
formatOrphans(orphans))
}
// 5a. Derive PURL types from the discovered package types (purely a function of pkg.Type)
derivePURLTypes(updated)
// 6. Write back to YAML files with comments, preserving existing node trees
fmt.Print(" → Writing updated capabilities files...")
if err := saveCapabilities(catalogerDir, repoRoot, updated, existingNodes); err != nil {
@@ -427,6 +432,39 @@ func (e *EnrichmentData) GetMetadataTypes(catalogerName string) ([]string, bool)
return types, ok
}
// derivePURLTypes populates the PURLTypes field on every cataloger and parser by mapping each
// observed package type to its canonical PURL type via pkg.Type.PackageURLType. this is a pure
// function of the already-discovered package_types, so it needs no additional test observations.
func derivePURLTypes(doc *capabilities.Document) {
for i := range doc.Catalogers {
cat := &doc.Catalogers[i]
cat.PURLTypes = purlTypesFromPackageTypes(cat.PackageTypes)
for j := range cat.Parsers {
cat.Parsers[j].PURLTypes = purlTypesFromPackageTypes(cat.Parsers[j].PackageTypes)
}
}
}
// purlTypesFromPackageTypes maps syft package type strings (e.g. "go-module") to their PURL types
// (e.g. "golang"), returning a deduplicated, sorted slice. package types with no PURL mapping are skipped.
func purlTypesFromPackageTypes(packageTypes []string) []string {
if len(packageTypes) == 0 {
return nil
}
set := strset.New()
for _, pt := range packageTypes {
if purlType := pkg.Type(pt).PackageURLType(); purlType != "" {
set.Add(purlType)
}
}
if set.IsEmpty() {
return nil
}
result := set.List()
sort.Strings(result)
return result
}
// GetPackageTypes returns the package types for the given cataloger name
func (e *EnrichmentData) GetPackageTypes(catalogerName string) ([]string, bool) {
types, ok := e.packageTypes[catalogerName]
@@ -552,3 +552,48 @@ func TestConvertToJSONSchemaTypesFromMetadata(t *testing.T) {
})
}
}
func TestPURLTypesFromPackageTypes(t *testing.T) {
tests := []struct {
name string
packageTypes []string
want []string
}{
{
name: "nil slice returns nil",
packageTypes: nil,
want: nil,
},
{
name: "single package type maps to purl type",
packageTypes: []string{"go-module"},
want: []string{"golang"},
},
{
name: "multiple package types are sorted",
packageTypes: []string{"npm", "go-module"},
want: []string{"golang", "npm"},
},
{
// java and jenkins-plugin both map to maven, so the result is deduplicated
name: "distinct package types collapsing to one purl type are deduplicated",
packageTypes: []string{"java-archive", "jenkins-plugin"},
want: []string{"maven"},
},
{
// unknown package types have no purl mapping and are skipped
name: "package type without a purl mapping is skipped",
packageTypes: []string{"UnknownPackage"},
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := purlTypesFromPackageTypes(tt.packageTypes)
if diff := cmp.Diff(tt.want, got); diff != "" {
t.Errorf("purlTypesFromPackageTypes() mismatch (-want +got):\n%s", diff)
}
})
}
}
+2
View File
@@ -82,6 +82,7 @@ type CatalogerEntry struct {
Detectors []Detector `yaml:"detectors,omitempty" json:"detectors,omitempty"` // AUTO-GENERATED - detection methods (only for type=custom)
MetadataTypes []string `yaml:"metadata_types,omitempty" json:"metadata_types,omitempty"` // AUTO-GENERATED - pkg metadata types emitted (only for type=custom)
PackageTypes []string `yaml:"package_types,omitempty" json:"package_types,omitempty"` // AUTO-GENERATED - package types emitted (only for type=custom)
PURLTypes []string `yaml:"purl_types,omitempty" json:"purl_types,omitempty"` // AUTO-GENERATED - PURL types derived from package_types (only for type=custom)
JSONSchemaTypes []string `yaml:"json_schema_types,omitempty" json:"json_schema_types,omitempty"` // AUTO-GENERATED - JSON schema type names (UpperCamelCase)
Capabilities CapabilitySet `yaml:"capabilities,omitempty" json:"capabilities,omitempty"` // MANUAL - config-driven capability definitions (only for type=custom)
}
@@ -92,6 +93,7 @@ type Parser struct {
Detector Detector `yaml:"detector" json:"detector"` // AUTO-GENERATED - how artifacts are detected
MetadataTypes []string `yaml:"metadata_types,omitempty" json:"metadata_types,omitempty"` // AUTO-GENERATED - pkg metadata types emitted by this parser
PackageTypes []string `yaml:"package_types,omitempty" json:"package_types,omitempty"` // AUTO-GENERATED - package types emitted by this parser
PURLTypes []string `yaml:"purl_types,omitempty" json:"purl_types,omitempty"` // AUTO-GENERATED - PURL types derived from package_types
JSONSchemaTypes []string `yaml:"json_schema_types,omitempty" json:"json_schema_types,omitempty"` // AUTO-GENERATED - JSON schema type names (UpperCamelCase)
Capabilities CapabilitySet `yaml:"capabilities,omitempty" json:"capabilities,omitempty"` // MANUAL - config-driven capability definitions
}
+159 -11
View File
@@ -2,6 +2,8 @@ package os
import (
"context"
"encoding/json"
"errors"
"fmt"
"regexp"
@@ -10,6 +12,7 @@ import (
"github.com/anchore/syft/internal/sbomsync"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
)
@@ -19,8 +22,11 @@ func DetectFeatures(_ context.Context, resolver file.Resolver, builder sbomsync.
return
}
err := findRhelFeatures(resolver, s.Artifacts.LinuxDistribution)
if err != nil {
if err := findRhelFeatures(resolver, s.Artifacts.LinuxDistribution); err != nil {
log.WithFields("error", err, "release", s.Artifacts.LinuxDistribution).Trace("error searching for extended support")
}
if err := findUbuntuFeatures(resolver, s.Artifacts.LinuxDistribution, s.Artifacts.Packages); err != nil {
log.WithFields("error", err, "release", s.Artifacts.LinuxDistribution).Trace("error searching for extended support")
}
})
@@ -28,6 +34,9 @@ func DetectFeatures(_ context.Context, resolver file.Resolver, builder sbomsync.
return nil
}
// rhelEUSPatterns match a RHEL content manifest referencing an extended-update-support (EUS) repo.
var rhelEUSPatterns = []*regexp.Regexp{regexp.MustCompile(`baseos-eus`), regexp.MustCompile(`baseos-eus|appstream-eus`)}
func findRhelFeatures(resolver file.Resolver, release *linux.Release) error {
if release == nil || release.ID != "rhel" {
return nil
@@ -37,9 +46,9 @@ func findRhelFeatures(resolver file.Resolver, release *linux.Release) error {
return fmt.Errorf("unable to find content manifests: %w", err)
}
for _, contentManifestFile := range contentManifestFiles {
found, err := hasRhelExtendedSupportInContentManifest(resolver, contentManifestFile)
found, err := fileMatchesAny(resolver, contentManifestFile, rhelEUSPatterns...)
if err != nil {
return fmt.Errorf("unable to read content manifest from %s: %w", contentManifestFile.String(), err)
return fmt.Errorf("unable to read content manifest from %s: %w", contentManifestFile.RealPath, err)
}
if found {
release.ExtendedSupport = true
@@ -49,13 +58,152 @@ func findRhelFeatures(resolver file.Resolver, release *linux.Release) error {
return nil
}
func hasRhelExtendedSupportInContentManifest(resolver file.Resolver, contentManifestFile file.Location) (bool, error) {
contents, err := resolver.FileContentsByLocation(contentManifestFile)
if err != nil {
return false, fmt.Errorf("unable to read content manifest from %s: %w", contentManifestFile.String(), err)
}
defer internal.CloseAndLogError(contents, "content-manifest")
// Ubuntu Pro (formerly Ubuntu Advantage) is Canonical's subscription that unlocks Expanded Security
// Maintenance (ESM): security fixes past a package's standard support window. we detect the two base ESM
// streams, esm-infra (the `main` repo) and esm-apps (`universe`), and collapse them into one ExtendedSupport
// signal; the infra-vs-apps split is disambiguated by the downstream consumer, not by this boolean.
//
// the other Pro streams on the same host (esm.ubuntu.com/fips, /fips-updates, /realtime) are deliberately
// excluded: they are separate compliance products with their own downstream channels, and a host can run
// them with base ESM disabled, so folding them in would be unsound.
//
// detection is deliberately forgiving: apt/status evidence disappears when ESM is disabled, but an installed
// +esmN package is durable proof ESM content is on disk. so ExtendedSupport means "ESM was or is in effect
// for this host or its content", not "currently entitled" (mere eligibility, without evidence, is not enough).
patterns := []*regexp.Regexp{regexp.MustCompile(`baseos-eus`), regexp.MustCompile(`baseos-eus|appstream-eus`)}
// esmAptSourcePattern matches an uncommented apt source (classic or DEB822) or auth entry pointing at the
// plain ESM streams esm.ubuntu.com/infra or /apps. it deliberately scopes to /(infra|apps) so the other
// Pro streams served from the same host (/fips, /fips-updates, /realtime) are never folded into the base
// esm channel. the [^#\n]* before the host ensures a leading `#` (comment) can never satisfy the match.
var esmAptSourcePattern = regexp.MustCompile(`(?m)^[^#\n]*esm\.ubuntu\.com/(infra|apps)`)
// esmVersionPattern matches a dpkg version carrying a numbered ESM pocket suffix, e.g. `...+esm1` or
// `...~esm2`. the trailing \d scopes this to the ESM pocket and excludes other Pro pockets (e.g. +fips).
var esmVersionPattern = regexp.MustCompile(`[~+]esm\d`)
func findUbuntuFeatures(resolver file.Resolver, release *linux.Release, packages *pkg.Collection) error {
if release == nil || release.ID != "ubuntu" {
return nil
}
// an uncommented esm.ubuntu.com apt source or an active pro esm service both prove Pro/ESM was enabled.
// these are the durable, file-based signals present on attached hosts. a read error in one signal must
// not suppress the others, so errors are collected and detection continues to the next signal.
var errs error
for _, check := range []func() (bool, error){
func() (bool, error) { return hasUbuntuESMAptEvidence(resolver) },
func() (bool, error) { return hasActiveUbuntuESMService(resolver) },
} {
found, err := check()
if err != nil {
errs = errors.Join(errs, err)
continue
}
if found {
release.ExtendedSupport = true
return errs
}
}
// fallback for images (typically not pro-attached): an installed package whose version carries an
// +esm/~esm suffix was pulled from an ESM pocket, so ESM was in effect when it was installed.
if hasInstalledESMPackage(packages) {
release.ExtendedSupport = true
}
return errs
}
// hasUbuntuESMAptEvidence reports whether any apt source (classic .list or DEB822 .sources) or auth entry
// references esm.ubuntu.com via an uncommented line.
func hasUbuntuESMAptEvidence(resolver file.Resolver) (bool, error) {
// apt source files live in a directory (classic .list or DEB822 .sources), the credentials file is a fixed path.
sourceLocations, err := resolver.FilesByGlob("/etc/apt/sources.list.d/*")
if err != nil {
return false, fmt.Errorf("unable to find apt esm sources: %w", err)
}
authLocations, err := resolver.FilesByPath("/etc/apt/auth.conf.d/90ubuntu-advantage")
if err != nil {
return false, fmt.Errorf("unable to find apt esm auth: %w", err)
}
// ponytail: a DEB822 source with `Enabled: no` would still match; the pro client removes/comments the
// file on disable rather than toggling that key, so this is an accepted false-positive ceiling. the
// auth.conf.d entry is deleted by the pro client on disable, so it is a durable positive-only signal.
for _, location := range append(sourceLocations, authLocations...) {
match, err := fileMatchesAny(resolver, location, esmAptSourcePattern)
if err != nil {
return false, err
}
if match {
return true, nil
}
}
return false, nil
}
// hasActiveUbuntuESMService reports whether the ubuntu-advantage/pro status cache shows an enabled esm service.
func hasActiveUbuntuESMService(resolver file.Resolver) (bool, error) {
locations, err := resolver.FilesByPath("/var/lib/ubuntu-advantage/status.json")
if err != nil {
return false, fmt.Errorf("unable to find ubuntu-advantage status: %w", err)
}
for _, location := range locations {
enabled, err := hasEnabledESMServiceInStatus(resolver, location)
if err != nil {
return false, err
}
if enabled {
return true, nil
}
}
return false, nil
}
func hasEnabledESMServiceInStatus(resolver file.Resolver, location file.Location) (bool, error) {
contents, err := resolver.FileContentsByLocation(location)
if err != nil {
return false, fmt.Errorf("unable to read ubuntu-advantage status from %s: %w", location.RealPath, err)
}
defer internal.CloseAndLogError(contents, location.RealPath)
var status struct {
Services []struct {
Name string `json:"name"`
Status string `json:"status"`
} `json:"services"`
}
if err := json.NewDecoder(contents).Decode(&status); err != nil {
return false, fmt.Errorf("unable to parse ubuntu-advantage status from %s: %w", location.RealPath, err)
}
// pro status values are enabled/disabled/n-a/warning/—; only "enabled" proves the stream is active.
// warning/expired states are intentionally excluded here and left to the apt-source and installed-package signals.
for _, svc := range status.Services {
if (svc.Name == "esm-infra" || svc.Name == "esm-apps") && svc.Status == "enabled" {
return true, nil
}
}
return false, nil
}
// hasInstalledESMPackage reports whether any installed dpkg package version carries an ESM pocket suffix.
func hasInstalledESMPackage(packages *pkg.Collection) bool {
if packages == nil {
return false
}
for p := range packages.Enumerate(pkg.DebPkg) {
if esmVersionPattern.MatchString(p.Version) {
return true
}
}
return false
}
func fileMatchesAny(resolver file.Resolver, location file.Location, patterns ...*regexp.Regexp) (bool, error) {
contents, err := resolver.FileContentsByLocation(location)
if err != nil {
return false, fmt.Errorf("unable to read %s: %w", location.RealPath, err)
}
defer internal.CloseAndLogError(contents, location.RealPath)
return internal.MatchAnyFromReader(contents, patterns...)
}
+214
View File
@@ -2,6 +2,11 @@ package os_test
import (
"context"
"io"
stdos "os"
"path"
"path/filepath"
"slices"
"testing"
"github.com/stretchr/testify/require"
@@ -11,7 +16,9 @@ import (
"github.com/anchore/syft/internal/sbomsync"
"github.com/anchore/syft/internal/task"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
@@ -99,3 +106,210 @@ func Test_EnvironmentTask(t *testing.T) {
})
}
}
func Test_findUbuntuFeatures(t *testing.T) {
tests := []struct {
name string
dir string
releaseID string
packages []pkg.Package
expected bool
}{
{
name: "esm apt source present",
dir: "testdata/ubuntu_esm_apt",
releaseID: "ubuntu",
expected: true,
},
{
name: "esm apt source in DEB822 .sources format",
dir: "testdata/ubuntu_esm_deb822",
releaseID: "ubuntu",
expected: true,
},
{
name: "esm evidence in apt auth.conf.d",
dir: "testdata/ubuntu_esm_authconf",
releaseID: "ubuntu",
expected: true,
},
{
name: "active esm service in ubuntu-advantage status",
dir: "testdata/ubuntu_esm_status",
releaseID: "ubuntu",
expected: true,
},
{
name: "only esm-infra enabled (esm-apps disabled)",
dir: "testdata/ubuntu_esm_infra_only",
releaseID: "ubuntu",
expected: true,
},
{
name: "malformed status.json does not panic or match",
dir: "testdata/ubuntu_esm_malformed",
releaseID: "ubuntu",
expected: false,
},
{
name: "installed +esm package version",
dir: "testdata/ubuntu_plain",
releaseID: "ubuntu",
packages: []pkg.Package{
{Name: "openssl", Version: "1.1.1f-1ubuntu2.19+esm1", Type: pkg.DebPkg},
},
expected: true,
},
{
name: "installed ~esm package version",
dir: "testdata/ubuntu_plain",
releaseID: "ubuntu",
packages: []pkg.Package{
{Name: "libcap2", Version: "1:2.32-1ubuntu0.1~esm1", Type: pkg.DebPkg},
},
expected: true,
},
{
name: "plain ubuntu with no esm evidence",
dir: "testdata/ubuntu_plain",
releaseID: "ubuntu",
packages: []pkg.Package{
{Name: "openssl", Version: "1.1.1f-1ubuntu2.19", Type: pkg.DebPkg},
},
expected: false,
},
{
name: "commented esm source and disabled esm service",
dir: "testdata/ubuntu_esm_disabled",
releaseID: "ubuntu",
expected: false,
},
{
name: "esm disabled but +esm package remains installed",
dir: "testdata/ubuntu_esm_disabled",
releaseID: "ubuntu",
packages: []pkg.Package{
{Name: "openssl", Version: "1.1.1f-1ubuntu2.19+esm1", Type: pkg.DebPkg},
},
expected: true, // durable proof: ESM content is on disk even though the channel is now off
},
{
name: "fips-only pro host is not plain esm",
dir: "testdata/ubuntu_fips_only",
releaseID: "ubuntu",
expected: false, // esm.ubuntu.com/fips must not be folded into the base esm channel
},
{
name: "non-ubuntu is unaffected by esm evidence",
dir: "testdata/ubuntu_esm_apt",
releaseID: "debian",
expected: false,
},
{
name: "non-ubuntu is unaffected by esm package version",
dir: "testdata/ubuntu_plain",
releaseID: "debian",
packages: []pkg.Package{
{Name: "openssl", Version: "1.1.1f-1ubuntu2.19+esm1", Type: pkg.DebPkg},
},
expected: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
resolver := fixtureResolverForDir(t, test.dir)
s := sbom.SBOM{}
s.Artifacts.LinuxDistribution = &linux.Release{ID: test.releaseID}
s.Artifacts.Packages = pkg.NewCollection(test.packages...)
err := os.DetectFeatures(context.Background(), resolver, sbomsync.NewBuilder(&s))
require.NoError(t, err)
require.Equal(t, test.expected, s.Artifacts.LinuxDistribution.ExtendedSupport)
})
}
}
// a read error in the apt-source signal must not suppress the ubuntu-advantage status signal; the three
// signals are meant to be independent fallbacks.
func Test_findUbuntuFeatures_signalErrorDoesNotSuppressOthers(t *testing.T) {
resolver := errOnGlobResolver{
fixtureResolver: fixtureResolverForDir(t, "testdata/ubuntu_esm_status").(fixtureResolver),
errOnGlob: "/etc/apt/sources.list.d/*",
}
s := sbom.SBOM{}
s.Artifacts.LinuxDistribution = &linux.Release{ID: "ubuntu"}
s.Artifacts.Packages = pkg.NewCollection()
require.NoError(t, os.DetectFeatures(context.Background(), resolver, sbomsync.NewBuilder(&s)))
// the apt glob errors, but the status.json signal (esm-infra enabled) should still be consulted
require.True(t, s.Artifacts.LinuxDistribution.ExtendedSupport)
}
// errOnGlobResolver injects a failure for a specific glob to simulate an unreadable apt directory.
type errOnGlobResolver struct {
fixtureResolver
errOnGlob string
}
func (r errOnGlobResolver) FilesByGlob(patterns ...string) ([]file.Location, error) {
if slices.Contains(patterns, r.errOnGlob) {
return nil, stdos.ErrPermission
}
return r.fixtureResolver.FilesByGlob(patterns...)
}
// fixtureResolver maps in-image logical paths to on-disk fixture files so DetectFeatures can be driven
// without building a container image.
type fixtureResolver struct {
file.Resolver // unused methods; nil is fine since only the three below are called
files map[string]string
}
func (r fixtureResolver) FilesByGlob(patterns ...string) ([]file.Location, error) {
var out []file.Location
for logical := range r.files {
for _, p := range patterns {
if ok, _ := path.Match(p, logical); ok {
out = append(out, file.NewLocation(logical))
break
}
}
}
return out, nil
}
func (r fixtureResolver) FilesByPath(paths ...string) ([]file.Location, error) {
var out []file.Location
for _, p := range paths {
if _, ok := r.files[p]; ok {
out = append(out, file.NewLocation(p))
}
}
return out, nil
}
func (r fixtureResolver) FileContentsByLocation(l file.Location) (io.ReadCloser, error) {
return stdos.Open(r.files[l.RealPath])
}
func fixtureResolverForDir(t *testing.T, dir string) file.Resolver {
t.Helper()
files := map[string]string{}
require.NoError(t, filepath.WalkDir(dir, func(p string, d stdos.DirEntry, err error) error {
if err != nil || d.IsDir() {
return err
}
rel, err := filepath.Rel(dir, p)
if err != nil {
return err
}
files["/"+filepath.ToSlash(rel)] = p
return nil
}))
return fixtureResolver{files: files}
}
@@ -0,0 +1,6 @@
# Written by ubuntu-advantage-tools
deb https://esm.ubuntu.com/infra/ubuntu focal-infra-security main
# deb-src https://esm.ubuntu.com/infra/ubuntu focal-infra-security main
deb https://esm.ubuntu.com/infra/ubuntu focal-infra-updates main
# deb-src https://esm.ubuntu.com/infra/ubuntu focal-infra-updates main
@@ -0,0 +1,2 @@
machine esm.ubuntu.com/infra/ubuntu/ login bearer password DUMMYTOKENREDACTED # ubuntu-pro-client
machine esm.ubuntu.com/apps/ubuntu/ login bearer password DUMMYTOKENREDACTED # ubuntu-pro-client
@@ -0,0 +1,5 @@
Types: deb
URIs: https://esm.ubuntu.com/infra/ubuntu
Suites: noble-infra-security noble-infra-updates
Components: main
Signed-By: /usr/share/keyrings/ubuntu-pro-esm-infra-keyring.gpg
@@ -0,0 +1,3 @@
# Written by ubuntu-advantage-tools
# deb https://esm.ubuntu.com/infra/ubuntu focal-infra-security main
# deb-src https://esm.ubuntu.com/infra/ubuntu focal-infra-security main
@@ -0,0 +1,18 @@
{
"_schema_version": "0.1",
"attached": true,
"services": [
{
"name": "esm-apps",
"available": "yes",
"entitled": "yes",
"status": "disabled"
},
{
"name": "esm-infra",
"available": "yes",
"entitled": "yes",
"status": "disabled"
}
]
}
@@ -0,0 +1,21 @@
{
"_schema_version": "0.1",
"attached": true,
"execution_status": "success",
"services": [
{
"name": "esm-apps",
"description": "Expanded Security Maintenance for Applications",
"available": "yes",
"entitled": "yes",
"status": "disabled"
},
{
"name": "esm-infra",
"description": "Expanded Security Maintenance for Infrastructure",
"available": "yes",
"entitled": "yes",
"status": "enabled"
}
]
}
@@ -0,0 +1 @@
{ this is not valid json
@@ -0,0 +1,28 @@
{
"_doc": "Content provided in json response is currently considered Experimental and may change",
"_schema_version": "0.1",
"attached": true,
"services": [
{
"name": "esm-apps",
"description": "Expanded Security Maintenance for Applications",
"available": "yes",
"entitled": "yes",
"status": "enabled"
},
{
"name": "esm-infra",
"description": "Expanded Security Maintenance for Infrastructure",
"available": "yes",
"entitled": "yes",
"status": "enabled"
},
{
"name": "fips",
"description": "NIST-certified FIPS crypto packages",
"available": "no",
"entitled": "no",
"status": "n/a"
}
]
}
@@ -0,0 +1,3 @@
# Written by ubuntu-pro-client
deb https://esm.ubuntu.com/fips/ubuntu focal main
# deb-src https://esm.ubuntu.com/fips/ubuntu focal main
@@ -0,0 +1,28 @@
{
"_schema_version": "0.1",
"attached": true,
"execution_status": "success",
"services": [
{
"name": "esm-apps",
"description": "Expanded Security Maintenance for Applications",
"available": "yes",
"entitled": "yes",
"status": "disabled"
},
{
"name": "esm-infra",
"description": "Expanded Security Maintenance for Infrastructure",
"available": "yes",
"entitled": "yes",
"status": "disabled"
},
{
"name": "fips",
"description": "NIST-certified FIPS crypto packages",
"available": "yes",
"entitled": "yes",
"status": "enabled"
}
]
}
@@ -0,0 +1,5 @@
Types: deb
URIs: http://archive.ubuntu.com/ubuntu/
Suites: noble noble-updates noble-backports
Components: main restricted universe multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
@@ -27,6 +27,7 @@ var knownNonMetadataTypeNames = strset.New(
var knownMetadataTypeNames = strset.New(
"DotnetPortableExecutableEntry",
"GGUFFileHeader",
"SafeTensorsModelInfo",
)
func DiscoverTypeNames() ([]string, error) {
+5
View File
@@ -9,6 +9,7 @@ func AllTypes() []any {
return []any{
pkg.AlpmDBEntry{},
pkg.ApkDBEntry{},
pkg.AppleAppBundleEntry{},
pkg.BinarySignature{},
pkg.BitnamiSBOMEntry{},
pkg.BunLockEntry{},
@@ -20,6 +21,8 @@ func AllTypes() []any {
pkg.CondaMetaPackage{},
pkg.DartPubspec{},
pkg.DartPubspecLockEntry{},
pkg.DenoLockEntry{},
pkg.DenoRemoteLockEntry{},
pkg.DotnetDepsEntry{},
pkg.DotnetPackagesLockEntry{},
pkg.DotnetPortableExecutableEntry{},
@@ -65,10 +68,12 @@ func AllTypes() []any {
pkg.RubyGemspec{},
pkg.RustBinaryAuditEntry{},
pkg.RustCargoLockEntry{},
pkg.SafeTensorsModelInfo{},
pkg.SnapEntry{},
pkg.SwiftPackageManagerResolvedEntry{},
pkg.SwiplPackEntry{},
pkg.TerraformLockProviderEntry{},
pkg.VcpkgManifest{},
pkg.WordpressPluginEntry{},
pkg.YarnLockEntry{},
}
+5
View File
@@ -73,6 +73,8 @@ var jsonTypes = makeJSONTypes(
jsonNames(pkg.ConaninfoEntry{}, "c-conan-info-entry"),
jsonNames(pkg.DartPubspecLockEntry{}, "dart-pubspec-lock-entry", "DartPubMetadata"),
jsonNames(pkg.DartPubspec{}, "dart-pubspec"),
jsonNames(pkg.DenoLockEntry{}, "deno-lock-entry"),
jsonNames(pkg.DenoRemoteLockEntry{}, "deno-remote-lock-entry"),
jsonNames(pkg.DotnetDepsEntry{}, "dotnet-deps-entry", "DotnetDepsMetadata"),
jsonNames(pkg.DotnetPortableExecutableEntry{}, "dotnet-portable-executable-entry"),
jsonNames(pkg.DpkgArchiveEntry{}, "dpkg-archive-entry"),
@@ -122,11 +124,14 @@ var jsonTypes = makeJSONTypes(
jsonNames(pkg.SnapEntry{}, "snap-entry"),
jsonNames(pkg.WordpressPluginEntry{}, "wordpress-plugin-entry", "WordpressMetadata"),
jsonNames(pkg.HomebrewFormula{}, "homebrew-formula"),
jsonNames(pkg.AppleAppBundleEntry{}, "apple-app-bundle-entry"),
jsonNames(pkg.LuaRocksPackage{}, "luarocks-package"),
jsonNames(pkg.TerraformLockProviderEntry{}, "terraform-lock-provider-entry"),
jsonNames(pkg.DotnetPackagesLockEntry{}, "dotnet-packages-lock-entry"),
jsonNames(pkg.CondaMetaPackage{}, "conda-metadata-entry", "CondaPackageMetadata"),
jsonNames(pkg.GGUFFileHeader{}, "gguf-file-header"),
jsonNames(pkg.SafeTensorsModelInfo{}, "safetensors-model-info"),
jsonNames(pkg.VcpkgManifest{}, "vcpkg-manifest"),
)
func expandLegacyNameVariants(names ...string) []string {
+50 -2
View File
@@ -65,6 +65,30 @@ func (i *Index) Add(relationships ...artifact.Relationship) {
}
func (i *Index) Remove(id artifact.ID) {
// scrub this node's edges from the adjacent nodes' maps before dropping the node itself. without this, the
// edges live on as dangling pointers inside the other endpoints' mappedRelationships, so query methods
// (From/To/References/Coordinates/Contains) would keep returning relationships that have been removed.
if mapped := i.fromID[id]; mapped != nil {
for _, sr := range mapped.allRelated {
if sr.to == id {
continue // self-edge: this node's own maps are deleted below
}
if other := i.toID[sr.to]; other != nil {
other.remove(id, sr)
}
}
}
if mapped := i.toID[id]; mapped != nil {
for _, sr := range mapped.allRelated {
if sr.from == id {
continue // self-edge: this node's own maps are deleted below
}
if other := i.fromID[sr.from]; other != nil {
other.remove(id, sr)
}
}
}
delete(i.fromID, id)
delete(i.toID, id)
@@ -78,6 +102,12 @@ func (i *Index) Remove(id artifact.ID) {
}
func (i *Index) Replace(ogID artifact.ID, replacement artifact.Identifiable) {
if replacement == nil || replacement.ID() == ogID {
// replacing a node with one that has the same ID would re-add the (deduped) edges and then the trailing
// Remove(ogID) would delete them, silently wiping all of the node's relationships. treat it as a no-op.
return
}
for _, mapped := range fromMappedByID(i.fromID, ogID) {
// the stale relationship(i.e. if there's an elder ID in either side) should be discarded
if len(fromMappedByID(i.toID, mapped.relationship.To.ID())) == 0 {
@@ -91,8 +121,10 @@ func (i *Index) Replace(ogID artifact.ID, replacement artifact.Identifiable) {
}
for _, mapped := range fromMappedByID(i.toID, ogID) {
// same as the above
if len(fromMappedByID(i.fromID, mapped.relationship.To.ID())) == 0 {
// same as the above, but check the surviving other endpoint (the From side, since these are edges TO ogID).
// note: this must reference From (not To, which is ogID itself) so that nodes appearing only on the To side
// of relationships (e.g. a go main module that nothing depends on) keep their edges after an ID change.
if len(fromMappedByID(i.fromID, mapped.relationship.From.ID())) == 0 {
continue
}
i.Add(artifact.Relationship{
@@ -206,6 +238,22 @@ func (m *mappedRelationships) add(id artifact.ID, newRelationship *sortableRelat
typeMap[id] = newRelationship
}
// remove deletes a single relationship (the other endpoint keyed by id) from this node's maps. it is the inverse
// of add and is used to keep adjacent nodes consistent when a node is removed from the index.
func (m *mappedRelationships) remove(id artifact.ID, target *sortableRelationship) {
filtered := m.allRelated[:0]
for _, r := range m.allRelated {
if r != target {
filtered = append(filtered, r)
}
}
m.allRelated = filtered
if typeMap := m.typeMap[target.relationship.Type]; typeMap != nil && typeMap[id] == target {
delete(typeMap, id)
}
}
type sortableRelationship struct {
from artifact.ID
to artifact.ID
+126
View File
@@ -380,6 +380,41 @@ func TestReplace(t *testing.T) {
compareRelationships(t, expectedRels, index.All())
}
func TestReplace_NodeOnlyOnToSide(t *testing.T) {
// regression: a node that only appears on the To side of relationships (nothing depends on it, e.g. a go main
// module that other modules are a dependency-of) must keep its incoming edges when its ID changes (such as when a
// compliance rule stubs a missing version and recomputes the package ID). previously the To-side remap guard
// checked ogID itself instead of the surviving From endpoint, so all such edges were dropped.
main := pkg.Package{Name: "main-module"} // empty version, like a go main module
dep1 := pkg.Package{Name: "dep-1", Version: "1.0.0"}
dep2 := pkg.Package{Name: "dep-2", Version: "2.0.0"}
for _, p := range []*pkg.Package{&main, &dep1, &dep2} {
p.SetID()
}
// both deps are a dependency-of main; main has no outgoing edges
r1 := artifact.Relationship{From: dep1, To: main, Type: artifact.DependencyOfRelationship}
r2 := artifact.Relationship{From: dep2, To: main, Type: artifact.DependencyOfRelationship}
index := NewIndex(r1, r2)
// simulate the version stub: main gets a new ID
stubbedMain := main
stubbedMain.Version = "UNKNOWN"
stubbedMain.SetID()
require.NotEqual(t, main.ID(), stubbedMain.ID())
index.Replace(main.ID(), &stubbedMain)
expectedRels := []artifact.Relationship{
{From: dep1, To: stubbedMain, Type: artifact.DependencyOfRelationship},
{From: dep2, To: stubbedMain, Type: artifact.DependencyOfRelationship},
}
compareRelationships(t, expectedRels, index.All())
}
func compareRelationships(t testing.TB, expected, actual []artifact.Relationship) {
assert.Equal(t, len(expected), len(actual), "number of relationships should match")
for _, e := range expected {
@@ -408,3 +443,94 @@ func TestReplace_NoExistingRelations(t *testing.T) {
allRels := index.All()
assert.Len(t, allRels, 0)
}
// Remove must scrub the removed node's edges from adjacent nodes too, otherwise query methods keep returning
// relationships that no longer exist. the original Remove only deleted the removed node's own maps, leaving
// dangling pointers in the other endpoints.
func TestRemove_NoStaleEdgesInAdjacentNodes(t *testing.T) {
p1 := pkg.Package{Name: "pkg-1", Version: "1"}
p2 := pkg.Package{Name: "pkg-2", Version: "2"}
p3 := pkg.Package{Name: "pkg-3", Version: "3"}
for _, p := range []*pkg.Package{&p1, &p2, &p3} {
p.SetID()
}
r12 := artifact.Relationship{From: p1, To: p2, Type: artifact.DependencyOfRelationship}
r32 := artifact.Relationship{From: p3, To: p2, Type: artifact.DependencyOfRelationship}
index := NewIndex(r12, r32)
index.Remove(p1.ID())
// p2 was on the To side of the removed edge; its incoming view must no longer include the stale p1 edge,
// but must still include the live p3 edge.
compareRelationships(t, []artifact.Relationship{r32}, index.To(p2, artifact.DependencyOfRelationship))
assert.False(t, index.Contains(r12), "Contains must not report a removed edge")
assert.Len(t, index.References(p2), 1, "p2 should only reference the surviving p3 edge")
assert.True(t, index.Contains(r32), "surviving edge should remain")
compareRelationships(t, []artifact.Relationship{r32}, index.All())
}
// Replace must leave no dangling references to the old ID in adjacent nodes' query views.
func TestReplace_NoStaleEdgesAfterReplace(t *testing.T) {
main := pkg.Package{Name: "main-module"} // empty version
dep := pkg.Package{Name: "dep", Version: "1.0.0"}
main.SetID()
dep.SetID()
oldEdge := artifact.Relationship{From: dep, To: main, Type: artifact.DependencyOfRelationship}
index := NewIndex(oldEdge)
stubbed := main
stubbed.Version = "UNKNOWN"
stubbed.SetID()
require.NotEqual(t, main.ID(), stubbed.ID())
index.Replace(main.ID(), &stubbed)
newEdge := artifact.Relationship{From: dep, To: stubbed, Type: artifact.DependencyOfRelationship}
// the dep's outgoing view must point at the stubbed node, not the old one
from := index.From(dep, artifact.DependencyOfRelationship)
require.Len(t, from, 1)
assert.Equal(t, stubbed.ID(), from[0].To.ID())
assert.True(t, index.Contains(newEdge))
assert.False(t, index.Contains(oldEdge), "stale edge to old ID must be gone")
assert.Empty(t, index.To(main), "old node should have no incoming edges")
compareRelationships(t, []artifact.Relationship{newEdge}, index.All())
}
// replacing a node with one that has the same ID must be a no-op, not silently wipe its edges.
func TestReplace_SameIDIsNoOp(t *testing.T) {
a := pkg.Package{Name: "a", Version: "1"}
b := pkg.Package{Name: "b", Version: "2"}
a.SetID()
b.SetID()
edge := artifact.Relationship{From: a, To: b, Type: artifact.DependencyOfRelationship}
index := NewIndex(edge)
// same identity object -> same ID
index.Replace(b.ID(), b)
compareRelationships(t, []artifact.Relationship{edge}, index.All())
assert.True(t, index.Contains(edge))
}
// a self-edge on the replaced node should be remapped to replacement->replacement, not dropped.
func TestReplace_SelfEdge(t *testing.T) {
n := pkg.Package{Name: "n"} // empty version
n.SetID()
index := NewIndex(artifact.Relationship{From: n, To: n, Type: artifact.DependencyOfRelationship})
sn := n
sn.Version = "UNKNOWN"
sn.SetID()
require.NotEqual(t, n.ID(), sn.ID())
index.Replace(n.ID(), &sn)
all := index.All()
require.Len(t, all, 1)
assert.Equal(t, sn.ID(), all[0].From.ID())
assert.Equal(t, sn.ID(), all[0].To.ID())
assert.Empty(t, index.From(n), "no edges should reference the old ID")
assert.Empty(t, index.To(n), "no edges should reference the old ID")
}
+13 -1
View File
@@ -5,6 +5,7 @@ import (
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/ai"
"github.com/anchore/syft/syft/pkg/cataloger/alpine"
"github.com/anchore/syft/syft/pkg/cataloger/apple"
"github.com/anchore/syft/syft/pkg/cataloger/arch"
"github.com/anchore/syft/syft/pkg/cataloger/binary"
bitnamiSbomCataloger "github.com/anchore/syft/syft/pkg/cataloger/bitnami"
@@ -56,6 +57,10 @@ const (
// Python ecosystem labels
Python = "python"
// C/C++ ecosystem labels
Cpp = "cpp"
Vcpkg = "vcpkg"
)
//nolint:funlen
@@ -82,6 +87,11 @@ func DefaultPackageTaskFactories() Factories {
// language-specific package declared catalogers ///////////////////////////////////////////////////////////////////////////
newSimplePackageTaskFactory(cpp.NewConanCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "cpp", "conan"),
newPackageTaskFactory(
func(cfg CatalogingFactoryConfig) pkg.Cataloger {
return cpp.NewVcpkgManifestCataloger(cfg.PackagesConfig.Cpp)
},
pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, Cpp, Vcpkg),
newSimplePackageTaskFactory(dart.NewPubspecLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "dart"),
newSimplePackageTaskFactory(dart.NewPubspecCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "dart"),
newSimplePackageTaskFactory(elixir.NewMixLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "elixir"),
@@ -105,7 +115,7 @@ func DefaultPackageTaskFactories() Factories {
func(cfg CatalogingFactoryConfig) pkg.Cataloger {
return javascript.NewLockCataloger(cfg.PackagesConfig.JavaScript)
},
pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, JavaScript, Node, NPM,
pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, JavaScript, Node, NPM, "deno",
),
newSimplePackageTaskFactory(php.NewComposerLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "php", "composer"),
newSimplePackageTaskFactory(php.NewPearCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, pkgcataloging.ImageTag, "php", "pear"),
@@ -184,9 +194,11 @@ func DefaultPackageTaskFactories() Factories {
newSimplePackageTaskFactory(wordpress.NewWordpressPluginCataloger, pkgcataloging.DirectoryTag, pkgcataloging.ImageTag, "wordpress"),
newSimplePackageTaskFactory(terraform.NewLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, "terraform"),
newSimplePackageTaskFactory(homebrew.NewCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "homebrew"),
newSimplePackageTaskFactory(apple.NewAppBundleCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "apple"),
newSimplePackageTaskFactory(conda.NewCondaMetaCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.PackageTag, "conda"),
newSimplePackageTaskFactory(snap.NewCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "snap"),
newSimplePackageTaskFactory(ai.NewGGUFCataloger, pkgcataloging.DirectoryTag, pkgcataloging.ImageTag, "ai", "model", "gguf", "ml"),
newSimplePackageTaskFactory(ai.NewSafeTensorsCataloger, pkgcataloging.DirectoryTag, pkgcataloging.ImageTag, "ai", "model", "safetensors", "ml"),
// deprecated catalogers ////////////////////////////////////////
// these are catalogers that should not be selectable other than specific inclusion via name or "deprecated" tag (to remain backwards compatible)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+166 -1
View File
@@ -2,7 +2,7 @@
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
elementFormDefault="qualified"
targetNamespace="http://cyclonedx.org/schema/spdx"
version="1.0-3.27.0">
version="1.0-3.28.0">
<xs:simpleType name="licenseId">
<xs:restriction base="xs:string">
@@ -57,6 +57,11 @@
<xs:documentation>Amazon Digital Services License</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="Advanced-Cryptics-Dictionary">
<xs:annotation>
<xs:documentation>Advanced Cryptics Dictionary License</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="AFL-1.1">
<xs:annotation>
<xs:documentation>Academic Free License v1.1</xs:documentation>
@@ -122,6 +127,11 @@
<xs:documentation>Aladdin Free Public License</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="ALGLIB-Documentation">
<xs:annotation>
<xs:documentation>ALGLIB Documentation License</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="AMD-newlib">
<xs:annotation>
<xs:documentation>AMD newlib License</xs:documentation>
@@ -327,6 +337,11 @@
<xs:documentation>Boehm-Demers-Weiser GC License (without fee)</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="BOLA-1.1">
<xs:annotation>
<xs:documentation>Buena Onda License Agreement v1.1</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="Borceux">
<xs:annotation>
<xs:documentation>Borceux license</xs:documentation>
@@ -457,6 +472,11 @@
<xs:documentation>BSD 3-Clause Sun Microsystems</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="BSD-3-Clause-Tso">
<xs:annotation>
<xs:documentation>BSD 3-Clause Tso variant</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="BSD-4-Clause">
<xs:annotation>
<xs:documentation>BSD 4-Clause &quot;Original&quot; or &quot;Old&quot; License</xs:documentation>
@@ -497,6 +517,11 @@
<xs:documentation>BSD-Inferno-Nettverk</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="BSD-Mark-Modifications">
<xs:annotation>
<xs:documentation>BSD Mark Modifications License</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="BSD-Protection">
<xs:annotation>
<xs:documentation>BSD Protection License</xs:documentation>
@@ -527,6 +552,11 @@
<xs:documentation>Boost Software License 1.0</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="Buddy">
<xs:annotation>
<xs:documentation>Buddy License</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="BUSL-1.1">
<xs:annotation>
<xs:documentation>Business Source License 1.1</xs:documentation>
@@ -567,6 +597,11 @@
<xs:documentation>Caldera License (without preamble)</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="CAPEC-tou">
<xs:annotation>
<xs:documentation>Common Attack Pattern Enumeration and Classification License</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="Catharon">
<xs:annotation>
<xs:documentation>Catharon License</xs:documentation>
@@ -1212,6 +1247,21 @@
<xs:documentation>Erlang Public License v1.1</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="ESA-PL-permissive-2.4">
<xs:annotation>
<xs:documentation>European Space Agency Public License – v2.4 – Permissive (Type 3)</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="ESA-PL-strong-copyleft-2.4">
<xs:annotation>
<xs:documentation>European Space Agency Public License (ESA-PL) - V2.4 - Strong Copyleft (Type 1)</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="ESA-PL-weak-copyleft-2.4">
<xs:annotation>
<xs:documentation>European Space Agency Public License – v2.4 – Weak Copyleft (Type 2)</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="etalab-2.0">
<xs:annotation>
<xs:documentation>Etalab Open License 2.0</xs:documentation>
@@ -1737,6 +1787,11 @@
<xs:documentation>Historical Permission Notice and Disclaimer - sell variant</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="HPND-sell-variant-critical-systems">
<xs:annotation>
<xs:documentation>HPND - sell variant with safety critical systems clause</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="HPND-sell-variant-MIT-disclaimer">
<xs:annotation>
<xs:documentation>HPND sell variant with MIT disclaimer</xs:documentation>
@@ -1747,6 +1802,11 @@
<xs:documentation>HPND sell variant with MIT disclaimer - reverse</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="HPND-SMC">
<xs:annotation>
<xs:documentation>Historical Permission Notice and Disclaimer - SMC variant</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="HPND-UC">
<xs:annotation>
<xs:documentation>Historical Permission Notice and Disclaimer - University of California variant</xs:documentation>
@@ -1762,6 +1822,11 @@
<xs:documentation>HTML Tidy License</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="hyphen-bulgarian">
<xs:annotation>
<xs:documentation>hyphen-bulgarian License</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="IBM-pibs">
<xs:annotation>
<xs:documentation>IBM PowerPC Initialization and Boot Software</xs:documentation>
@@ -1852,6 +1917,11 @@
<xs:documentation>ISC Veillard variant</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="ISO-permission">
<xs:annotation>
<xs:documentation>ISO permission notice</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="Jam">
<xs:annotation>
<xs:documentation>Jam License</xs:documentation>
@@ -2237,6 +2307,11 @@
<xs:documentation>MIT Open Group variant</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="MIT-STK">
<xs:annotation>
<xs:documentation>MIT-STK License</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="MIT-testregex">
<xs:annotation>
<xs:documentation>MIT testregex Variant</xs:documentation>
@@ -2257,6 +2332,11 @@
<xs:documentation>MMIXware License</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="MMPL-1.0.1">
<xs:annotation>
<xs:documentation>Minecraft Mod Public License v1.0.1</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="Motosoto">
<xs:annotation>
<xs:documentation>Motosoto License</xs:documentation>
@@ -2422,6 +2502,11 @@
<xs:documentation>NIST Public Domain Notice with license fallback</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="NIST-PD-TNT">
<xs:annotation>
<xs:documentation>NIST Public Domain Notice TNT variant</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="NIST-Software">
<xs:annotation>
<xs:documentation>NIST Software License</xs:documentation>
@@ -2687,6 +2772,11 @@
<xs:documentation>Open Market License</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="OpenMDW-1.0">
<xs:annotation>
<xs:documentation>OpenMDW License Agreement v1.0</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="OpenPBS-2.3">
<xs:annotation>
<xs:documentation>OpenPBS v2.3 Software License</xs:documentation>
@@ -2722,6 +2812,11 @@
<xs:documentation>Open Publication License v1.0</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="OSC-1.0">
<xs:annotation>
<xs:documentation>OSC License 1.0</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="OSET-PL-2.1">
<xs:annotation>
<xs:documentation>OSET Public License version 2.1</xs:documentation>
@@ -2752,11 +2847,21 @@
<xs:documentation>Open Software License 3.0</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="OSSP">
<xs:annotation>
<xs:documentation>OSSP License</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="PADL">
<xs:annotation>
<xs:documentation>PADL License</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="ParaType-Free-Font-1.3">
<xs:annotation>
<xs:documentation>ParaType Free Font Licensing Agreement v1.3</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="Parity-6.0.0">
<xs:annotation>
<xs:documentation>The Parity Public License 6.0.0</xs:documentation>
@@ -2977,6 +3082,11 @@
<xs:documentation>SGI OpenGL License</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="SGMLUG-PM">
<xs:annotation>
<xs:documentation>SGMLUG Parser Materials License</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="SGP4">
<xs:annotation>
<xs:documentation>SGP4 Permission Notice</xs:documentation>
@@ -3162,6 +3272,11 @@
<xs:documentation>TCP Wrappers License</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="TekHVC">
<xs:annotation>
<xs:documentation>TekHVC License</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="TermReadKey">
<xs:annotation>
<xs:documentation>TermReadKey License</xs:documentation>
@@ -3297,6 +3412,11 @@
<xs:documentation>Unlicense - libwhirlpool variant</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="UnRAR">
<xs:annotation>
<xs:documentation>UnRAR License</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="UPL-1.0">
<xs:annotation>
<xs:documentation>Universal Permissive License v1.0</xs:documentation>
@@ -3312,6 +3432,11 @@
<xs:documentation>Vim License</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="Vixie-Cron">
<xs:annotation>
<xs:documentation>Vixie Cron License</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="VOSTROM">
<xs:annotation>
<xs:documentation>VOSTROM Public License for Open Source</xs:documentation>
@@ -3352,11 +3477,21 @@
<xs:documentation>Widget Workshop License</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="WordNet">
<xs:annotation>
<xs:documentation>WordNet License</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="Wsuipa">
<xs:annotation>
<xs:documentation>Wsuipa License</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="WTFNMFPL">
<xs:annotation>
<xs:documentation>Do What The F*ck You Want To But It&apos;s Not My Fault Public License</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="WTFPL">
<xs:annotation>
<xs:documentation>Do What The F*ck You Want To Public License</xs:documentation>
@@ -3382,6 +3517,11 @@
<xs:documentation>X11 License Distribution Modification Variant</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="X11-no-permit-persons">
<xs:annotation>
<xs:documentation>X11 no permit persons clause</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="X11-swapped">
<xs:annotation>
<xs:documentation>X11 swapped final paragraphs</xs:documentation>
@@ -3568,6 +3708,11 @@
<xs:documentation>Classpath exception 2.0</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="Classpath-exception-2.0-short">
<xs:annotation>
<xs:documentation>Classpath exception 2.0 - short</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="CLISP-exception-2.0">
<xs:annotation>
<xs:documentation>CLISP exception 2.0</xs:documentation>
@@ -3718,6 +3863,11 @@
<xs:documentation>KiCad Libraries Exception</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="kvirc-openssl-exception">
<xs:annotation>
<xs:documentation>kvirc OpenSSL Exception</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="LGPL-3.0-linking-exception">
<xs:annotation>
<xs:documentation>LGPL-3.0 Linking Exception</xs:documentation>
@@ -3833,6 +3983,11 @@
<xs:documentation>RRDtool FLOSS exception 2.0</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="rsync-linking-exception">
<xs:annotation>
<xs:documentation>rsync Linking Exception</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="SANE-exception">
<xs:annotation>
<xs:documentation>SANE Exception</xs:documentation>
@@ -3848,6 +4003,16 @@
<xs:documentation>Solderpad Hardware License v2.1</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="Simple-Library-Usage-exception">
<xs:annotation>
<xs:documentation>Simple Library Usage Exception</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="sqlitestudio-OpenSSL-exception">
<xs:annotation>
<xs:documentation>sqlitestudio OpenSSL exception</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="stunnel-exception">
<xs:annotation>
<xs:documentation>stunnel Exception</xs:documentation>
+21
View File
@@ -35,3 +35,24 @@ Create the new schema by running `make generate-json-schema` from the root of th
- If there is an existing schema for the given version and the new schema **does not** match the existing schema, an error is shown indicating to increment the version appropriately (see the "Versioning" section)
***Note: never delete a JSON schema and never change an existing JSON schema once it has been published in a release!*** Only add new schemas with a newly incremented version. All previous schema files must be stored in the `schema/json/` directory.
### Exception: `description`-only corrections
A published schema may be amended in place for one narrow case: the change touches only `description` text and leaves the data shape identical. Descriptions are documentation carried alongside the schema rather than constraints a validator evaluates, so correcting one cannot invalidate a document that already validated against that version. Minting a new version instead would leave the old one permanently describing the tool incorrectly, and spend a version number on no semantic change.
This applies when **every** one of the following holds:
- the only differences are `description` values
- no field, type, enum, `required` entry, or `$ref` is added, removed, or altered
- the `$id` version is unchanged
Anything else, including adding a field that happens to be optional, is a schema change and needs a version bump per the "Versioning" section above.
The generator blocks an in-place edit by design, since it refuses to overwrite a file that differs from what it would produce. To amend one, delete the schema file and regenerate so it is rewritten from the current Go doc comments:
```bash
rm schema/json/schema-$VERSION.json
make generate-json-schema
```
The result is byte-identical to what the generator produces, so do not hand-edit the JSON. Re-running `make generate-json-schema` afterwards should report `No change to the existing schema!`, and `make check-json-schema-drift` should pass. Confirm with `git diff` that the only changes are the intended description lines.
+2 -2
View File
@@ -720,7 +720,7 @@
},
"layerID": {
"type": "string",
"description": "FileSystemID is an ID representing an entire filesystem. For container images, this is a layer digest. For directories or a root filesystem, this is blank. When nested archive cataloging is enabled, a file found inside an archive carries the chain naming that archive's filesystem instead: the ID of the filesystem the archive itself was found in (a layer digest, or blank) joined with the path of each containing archive, e.g. \"sha256:1a2b.../app.war/WEB-INF/lib/dep.jar\". It is a path there rather than a digest, and it is what keeps identically-named files in different archives apart."
"description": "FileSystemID is an ID representing and entire filesystem. For container images, this is a layer digest. For directories or a root filesystem, this is blank."
}
},
"type": "object",
@@ -2482,7 +2482,7 @@
},
"layerID": {
"type": "string",
"description": "FileSystemID is an ID representing an entire filesystem. For container images, this is a layer digest. For directories or a root filesystem, this is blank. When nested archive cataloging is enabled, a file found inside an archive carries the chain naming that archive's filesystem instead: the ID of the filesystem the archive itself was found in (a layer digest, or blank) joined with the path of each containing archive, e.g. \"sha256:1a2b.../app.war/WEB-INF/lib/dep.jar\". It is a path there rather than a digest, and it is what keeps identically-named files in different archives apart."
"description": "FileSystemID is an ID representing and entire filesystem. For container images, this is a layer digest. For directories or a root filesystem, this is blank."
},
"accessPath": {
"type": "string",
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+8
View File
@@ -2,6 +2,7 @@ package pkgcataloging
import (
"github.com/anchore/syft/syft/pkg/cataloger/binary"
"github.com/anchore/syft/syft/pkg/cataloger/cpp"
"github.com/anchore/syft/syft/pkg/cataloger/dotnet"
"github.com/anchore/syft/syft/pkg/cataloger/golang"
"github.com/anchore/syft/syft/pkg/cataloger/java"
@@ -13,6 +14,7 @@ import (
type Config struct {
Binary binary.ClassifierCatalogerConfig `yaml:"binary" json:"binary" mapstructure:"binary"`
Cpp cpp.CatalogerConfig `yaml:"cpp" json:"cpp" mapstructure:"cpp"`
Dotnet dotnet.CatalogerConfig `yaml:"dotnet" json:"dotnet" mapstructure:"dotnet"`
Golang golang.CatalogerConfig `yaml:"golang" json:"golang" mapstructure:"golang"`
JavaArchive java.ArchiveCatalogerConfig `yaml:"java-archive" json:"java-archive" mapstructure:"java-archive"`
@@ -25,6 +27,7 @@ type Config struct {
func DefaultConfig() Config {
return Config{
Binary: binary.DefaultClassifierCatalogerConfig(),
Cpp: cpp.DefaultCatalogerConfig(),
Dotnet: dotnet.DefaultCatalogerConfig(),
Golang: golang.DefaultCatalogerConfig(),
JavaArchive: java.DefaultArchiveCatalogerConfig(),
@@ -44,6 +47,11 @@ func (c Config) WithBinaryConfig(cfg binary.ClassifierCatalogerConfig) Config {
return c
}
func (c Config) WithCppConfig(cfg cpp.CatalogerConfig) Config {
c.Cpp = cfg
return c
}
func (c Config) WithDotnetConfig(cfg dotnet.CatalogerConfig) Config {
c.Dotnet = cfg
return c
+34
View File
@@ -0,0 +1,34 @@
package cataloging
import "strings"
// SymbolScope controls which packages get function symbols (from a binary's symbol table) attached to their metadata.
type SymbolScope string
const (
// SymbolScopeNone disables symbol capture entirely.
SymbolScopeNone SymbolScope = "none"
// SymbolScopeStdlib captures symbols only for the synthetic "stdlib" package, leaving module packages without symbols.
SymbolScopeStdlib SymbolScope = "stdlib"
// SymbolScopeExtendedStdlib captures symbols for the synthetic "stdlib" package as well as every module
// under golang.org/x/ (the extended standard library).
SymbolScopeExtendedStdlib SymbolScope = "extended-stdlib"
// SymbolScopeAll captures symbols for all module packages as well as the synthetic "stdlib" package.
SymbolScopeAll SymbolScope = "all"
)
// Parse normalizes a SymbolScope, treating empty (unset) and unrecognized values as SymbolScopeNone.
func (s SymbolScope) Parse() SymbolScope {
switch strings.ToLower(strings.TrimSpace(string(s))) {
case string(SymbolScopeAll):
return SymbolScopeAll
case string(SymbolScopeExtendedStdlib):
return SymbolScopeExtendedStdlib
case string(SymbolScopeStdlib):
return SymbolScopeStdlib
}
return SymbolScopeNone
}
+35
View File
@@ -0,0 +1,35 @@
package cataloging
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_SymbolScope_Parse(t *testing.T) {
tests := []struct {
input string
expected SymbolScope
}{
{"all", SymbolScopeAll},
{"ALL", SymbolScopeAll},
{" all ", SymbolScopeAll},
{"stdlib", SymbolScopeStdlib},
{"Stdlib", SymbolScopeStdlib},
{"extended-stdlib", SymbolScopeExtendedStdlib},
{"Extended-Stdlib", SymbolScopeExtendedStdlib},
{"EXTENDED-STDLIB", SymbolScopeExtendedStdlib},
{" extended-stdlib ", SymbolScopeExtendedStdlib},
{"extended_stdlib", SymbolScopeNone},
{"none", SymbolScopeNone},
{"", SymbolScopeNone},
{"true", SymbolScopeNone},
{"false", SymbolScopeNone},
{"bogus", SymbolScopeNone},
}
for _, test := range tests {
t.Run(test.input, func(t *testing.T) {
assert.Equal(t, test.expected, SymbolScope(test.input).Parse())
})
}
}
+1 -1
View File
@@ -201,7 +201,7 @@ func (c Attributes) String() string {
// sanitize is a modified version of WFNize function from nvdtools
// that quotes all the allowed punctation chars with a slash and replaces
// spaces with underscores. It differs from the upstream implmentation as
// spaces with underscores. It differs from the upstream implementation as
// it does not use the buggy nvdtools implementation, specifically the "addSlashesAt" part of the
// function which stops the loop as soon as it encounters ":" a valid
// character for a WFN attribute after quoting, but the way nvdtools
+2 -1
View File
@@ -10,11 +10,12 @@ import (
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/unknown"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/elfutil"
"github.com/anchore/syft/syft/internal/unionreader"
)
func findELFFeatures(data *file.Executable, reader unionreader.UnionReader) error {
f, err := elf.NewFile(reader)
f, err := elfutil.NewFile(reader)
if err != nil {
return err
}
+7 -1
View File
@@ -1,6 +1,12 @@
FROM gcc:9.5.0
RUN apt update -y && apt install -y clang cmake git make m4 pkg-config zlib1g-dev
# bullseye is EOL: deb.debian.org dropped it, so pull from archive.debian.org (whose Release files are
# permanently expired, hence Check-Valid-Until). bullseye-security is deliberately omitted -- its index is
# still served but the pool behind it 404s, and it has not been moved to the archive yet. These are throwaway
# fixture build tools, so base bullseye package versions are fine.
RUN printf 'deb http://archive.debian.org/debian bullseye main\ndeb http://archive.debian.org/debian bullseye-updates main\n' > /etc/apt/sources.list \
&& apt -o Acquire::Check-Valid-Until=false update -y \
&& apt install -y clang cmake git make m4 pkg-config zlib1g-dev
## from https://github.com/runsafesecurity/selfrando/blob/tb-v0.4.2/docs/linux-build-instructions.md
#RUN git clone https://github.com/runsafesecurity/selfrando.git && \
+20
View File
@@ -152,6 +152,26 @@ func NewLocationFromImage(accessPath string, ref file.Reference, img *image.Imag
}
}
// NewVirtualLocationFromImage creates a new Location surfaced at realPath but whose contents and metadata are
// described by the given reference. Note that RealPath may differ from ref.RealPath; callers must not assume the two
// agree for these locations. The FileSystemID comes from the given ref's layer.
func NewVirtualLocationFromImage(realPath, accessPath string, ref file.Reference, img *image.Image) Location {
layer := img.FileCatalog.Layer(ref)
return Location{
LocationData: LocationData{
Coordinates: Coordinates{
RealPath: realPath,
FileSystemID: layer.Metadata.Digest,
},
AccessPath: accessPath,
ref: ref,
},
LocationMetadata: LocationMetadata{
Annotations: map[string]string{},
},
}
}
// NewLocationFromDirectory creates a new Location representing the given path (extracted from the Reference) relative to the given directory.
func NewLocationFromDirectory(responsePath string, fd string, ref file.Reference) Location {
return Location{
+16
View File
@@ -25,6 +25,7 @@ type MockResolver struct {
mediaTypeIndex map[string][]Location
extension map[string][]Location
basename map[string][]Location
ociRef string
}
// NewMockResolverForPaths creates a new MockResolver, where the only resolvable
@@ -74,6 +75,16 @@ func NewMockResolverForPathsWithMetadata(metadata map[Coordinates]Metadata) *Moc
}
}
// NewMockResolverForOCIArtifact creates a MockResolver that can resolve files
// by media type AND surfaces the given OCI ref via the ImageReference method.
// Intended for tests that exercise the catalogers' OCI-artifact-aware naming
// code paths.
func NewMockResolverForOCIArtifact(ref string, mediaTypes map[string][]Location) *MockResolver {
r := NewMockResolverForMediaTypes(mediaTypes)
r.ociRef = ref
return r
}
// NewMockResolverForMediaTypes creates a MockResolver that can resolve files by media type.
// The mediaTypes map specifies which locations should be returned for each media type.
func NewMockResolverForMediaTypes(mediaTypes map[string][]Location) *MockResolver {
@@ -102,6 +113,11 @@ func NewMockResolverForMediaTypes(mediaTypes map[string][]Location) *MockResolve
}
}
// ImageReference returns the image reference associated with this mock, if any.
func (r MockResolver) ImageReference() string {
return r.ociRef
}
// HasPath indicates if the given path exists in the underlying source.
func (r MockResolver) HasPath(path string) bool {
for _, l := range r.locations {
@@ -399,7 +399,7 @@ func collectDocRelationships(spdxIDMap map[string]any, doc *spdx.Document) (out
to = toLocation
case helpers.OtherRelationship:
// Encoding uses a specifically formatted comment...
if strings.Index(r.RelationshipComment, string(artifact.EvidentByRelationship)) == 0 {
if strings.HasPrefix(r.RelationshipComment, string(artifact.EvidentByRelationship)) {
typ = artifact.EvidentByRelationship
to = toLocation
}
@@ -418,7 +418,7 @@ func collectDocRelationships(spdxIDMap map[string]any, doc *spdx.Document) (out
to = toPackage
case helpers.OtherRelationship:
// Encoding uses a specifically formatted comment...
if strings.Index(r.RelationshipComment, string(artifact.OwnershipByFileOverlapRelationship)) == 0 {
if strings.HasPrefix(r.RelationshipComment, string(artifact.OwnershipByFileOverlapRelationship)) {
typ = artifact.OwnershipByFileOverlapRelationship
to = toPackage
}
@@ -462,10 +462,10 @@ func collectPackageFileRelationships(spdxIDMap map[string]any, doc *spdx.Documen
func toSyftCoordinates(f *spdx.File) file.Coordinates {
const layerIDPrefix = "layerID: "
var fileSystemID string
if strings.Index(f.FileComment, layerIDPrefix) == 0 {
if strings.HasPrefix(f.FileComment, layerIDPrefix) {
fileSystemID = strings.TrimPrefix(f.FileComment, layerIDPrefix)
}
if strings.Index(string(f.FileSPDXIdentifier), layerIDPrefix) == 0 {
if strings.HasPrefix(string(f.FileSPDXIdentifier), layerIDPrefix) {
fileSystemID = strings.TrimPrefix(string(f.FileSPDXIdentifier), layerIDPrefix)
}
return file.Coordinates{
@@ -481,8 +481,8 @@ func toSyftLocation(f *spdx.File) file.Location {
func requireAndTrimPrefix(val any, prefix string) string {
if v, ok := val.(string); ok {
if i := strings.Index(v, prefix); i == 0 {
return strings.Replace(v, prefix, "", 1)
if strings.HasPrefix(v, prefix) {
return strings.TrimPrefix(v, prefix)
}
}
return ""
@@ -0,0 +1,774 @@
package spdxhelpers
import (
"errors"
"fmt"
"reflect"
"regexp"
"strconv"
"strings"
spdx "github.com/spdx/tools-golang/spdx/v3/v3_0"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/format/internal"
"github.com/anchore/syft/syft/format/internal/spdxutil/helpers"
"github.com/anchore/syft/syft/license"
"github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
func ToSyftModelV3(doc *spdx.Document) (*sbom.SBOM, error) {
if doc == nil {
return nil, errors.New("cannot convert SPDX document to Syft model because document is nil")
}
spdxMap := ptrMap[any]{}
s := &sbom.SBOM{
Source: v3extractSource(spdxMap, doc),
Artifacts: sbom.Artifacts{
Packages: pkg.NewCollection(),
FileMetadata: map[file.Coordinates]file.Metadata{},
FileDigests: map[file.Coordinates][]file.Digest{},
LinuxDistribution: v3findLinuxReleaseByPURL(doc),
},
}
relationships := v3relationshipMap(doc)
v3collectSyftPackages(s, spdxMap, relationships, doc)
v3collectSyftFiles(s, spdxMap, doc)
s.Relationships = v3toSyftRelationships(spdxMap, doc)
return s, nil
}
func v3removePackage(packages spdx.ElementList, remove spdx.AnyPackage) (pkgs spdx.ElementList) {
for _, p := range packages {
if p == remove {
continue
}
pkgs = append(pkgs, p)
}
return pkgs
}
func v3removeRelationships(elements spdx.ElementList, element spdx.AnyElement) (relations spdx.ElementList) {
for _, e := range elements {
if r, ok := e.(spdx.AnyRelationship); ok {
if r != nil && r.GetFrom() == element {
continue
}
if r != nil {
var tos spdx.ElementList
for _, to := range r.GetTo() {
if to == element {
continue
}
tos = append(tos, to)
}
r.SetTo(tos)
if len(r.GetTo()) == 0 {
continue
}
}
relations = append(relations, r)
} else {
relations = append(relations, e)
}
}
return relations
}
func v3findRootPackages(doc spdx.AnyElementCollection) (out spdx.PackageList) {
for _, p := range doc.GetRootElements().Packages() {
out = append(out, p)
}
for _, s := range doc.GetRootElements().SBOMs() {
for _, p := range s.GetRootElements().Packages() {
out = append(out, p)
}
}
return
}
func v3extractSource(spdxMap ptrMap[any], doc *spdx.Document) source.Description {
namespace := doc.ID
if namespace == "" && len(doc.NamespaceMaps) > 0 {
namespace = string(doc.NamespaceMaps[0].GetNamespace())
}
src := extractSourceFromNamespace(namespace)
rootPackages := v3findRootPackages(doc)
if len(rootPackages) != 1 {
return src
}
p := rootPackages[0]
switch p.GetPrimaryPurpose() {
case spdx.SoftwarePurpose_Container:
src = v3containerSource(p)
case spdx.SoftwarePurpose_File:
src = v3fileSource(p)
default:
return src
}
spdxMap.Set(p, src)
doc.Elements = v3removePackage(doc.Elements, p)
doc.Elements = v3removeRelationships(doc.Elements, p)
return src
}
func v3containerSource(p spdx.AnyPackage) source.Description {
container := p.GetName()
v := p.GetVersion()
if v != "" {
container += ":" + v
}
digest := ""
if len(p.GetVerifiedUsing().Hashes()) > 0 {
h := p.GetVerifiedUsing().Hashes()[0]
if h != nil {
digest = fmt.Sprintf("%s:%s", v3fromChecksumAlgorithm(h.GetAlgorithm()), h.GetValue())
}
}
supplier := ""
if p.GetSuppliedBy() != nil {
supplier = v3agentString(p.GetSuppliedBy())
}
return source.Description{
ID: p.GetID(),
Name: p.GetName(),
Version: p.GetVersion(),
Supplier: supplier,
Metadata: source.ImageMetadata{
UserInput: container,
ID: p.GetID(),
Layers: nil, // TODO handle formats with nested layer packages like Tern and K8s BOM tool
ManifestDigest: digest,
},
}
}
func v3fileSource(p spdx.AnyPackage) source.Description {
typeRegex := regexp.MustCompile("DocumentRoot-([^-]+)-.*$")
typeName := typeRegex.ReplaceAllString(p.GetID(), "$1")
var version string
var metadata any
switch {
case typeName == prefixDirectory:
// is a Syft SBOM, explicitly a directory source
metadata, version = v3directorySourceMetadata(p)
case typeName == prefixFile:
// is a Syft SBOM, explicitly a file source
metadata, version = v3fileSourceMetadata(p)
case isDirectory(p.GetName()):
// is a non-Syft SBOM, which looks like a directory
metadata, version = v3directorySourceMetadata(p)
default:
// is a non-Syft SBOM, which is probably a file
metadata, version = v3fileSourceMetadata(p)
}
supplier := ""
if p.GetSuppliedBy() != nil {
supplier = p.GetSuppliedBy().GetName()
}
return source.Description{
ID: p.GetID(),
Name: p.GetName(),
Version: version,
Supplier: supplier,
Metadata: metadata,
}
}
func v3fileSourceMetadata(p spdx.AnyPackage) (any, string) {
version := p.GetVersion()
m := source.FileMetadata{
Path: p.GetName(),
}
// if this is a Syft SBOM, we might have output a digest as the version
checksum := v3toChecksum(p.GetVersion())
for _, d := range p.GetVerifiedUsing().Hashes() {
if checksum != nil && checksum.GetValue() == d.GetValue() {
version = ""
}
m.Digests = append(m.Digests, file.Digest{
Algorithm: v3fromChecksumAlgorithm(d.GetAlgorithm()),
Value: d.GetValue(),
})
}
return m, version
}
func v3directorySourceMetadata(p spdx.AnyPackage) (any, string) {
return source.DirectoryMetadata{
Path: p.GetName(),
Base: "",
}, p.GetVersion()
}
func v3findLinuxReleaseByPURL(doc *spdx.Document) *linux.Release {
for _, p := range doc.Elements.Packages() {
purlValue := v3findPURLValue(p)
if purlValue == "" {
continue
}
purl, err := packageurl.FromString(purlValue)
if err != nil {
log.Warnf("unable to parse purl: %s", purlValue)
continue
}
distro := v3findQualifierValue(purl, pkg.PURLQualifierDistro)
if distro != "" {
parts := strings.Split(distro, "-")
name := parts[0]
version := ""
if len(parts) > 1 {
version = parts[1]
}
return &linux.Release{
PrettyName: name,
Name: name,
ID: name,
IDLike: []string{name},
Version: version,
VersionID: version,
}
}
}
return nil
}
func v3collectSyftPackages(s *sbom.SBOM, spdxMap ptrMap[any], relationships ptrMap[[]spdx.AnyRelationship], doc *spdx.Document) {
skipIDs := v3packageIDsToSkip(doc)
found := ptrMap[struct{}]{}
// tools-golang collects all elements from the JSON LD @graph to the root SpdxDocument.Elements property
for _, elementList := range []spdx.ElementList{doc.Elements, doc.RootElements} {
for _, p := range elementList.Packages() {
if p == nil || skipIDs.Has(p) || found.Has(p) {
continue
}
found.Set(p, struct{}{})
syftPkg := v3toSyftPackage(relationships, p)
spdxMap.Set(p, syftPkg)
s.Artifacts.Packages.Add(syftPkg)
}
}
}
func v3collectSyftFiles(s *sbom.SBOM, spdxMap ptrMap[any], doc *spdx.Document) {
found := ptrMap[struct{}]{}
for _, elementList := range []spdx.ElementList{doc.Elements, doc.RootElements} {
for _, f := range elementList.Files() {
if found.Has(f) {
continue
}
found.Set(f, struct{}{})
l := v3toSyftLocation(f)
spdxMap.Set(f, l)
s.Artifacts.FileMetadata[l.Coordinates] = v3toFileMetadata(f)
s.Artifacts.FileDigests[l.Coordinates] = v3toFileDigests(f)
}
}
}
func v3toFileDigests(f spdx.AnyFile) (digests []file.Digest) {
for _, h := range f.GetVerifiedUsing().Hashes() {
digests = append(digests, file.Digest{
Algorithm: v3fromChecksumAlgorithm(h.GetAlgorithm()),
Value: h.GetValue(),
})
}
return digests
}
func v3fromChecksumAlgorithm(algorithm spdx.HashAlgorithm) string {
// it might be better to have a specific case statement with constants
parts := strings.Split(algorithm.GetID(), "/")
return strings.ToLower(parts[len(parts)-1])
}
func v3toFileMetadata(f spdx.AnyFile) (meta file.Metadata) {
// FIXME Syft is currently lossy due to the SPDX 2.2.1 spec not supporting arbitrary mimetypes
if f.GetContentType() != "" {
meta.MIMEType = f.GetContentType()
}
return meta
}
func v3toSyftRelationships(spdxMap ptrMap[any], doc *spdx.Document) []artifact.Relationship {
out := v3collectDocRelationships(spdxMap, doc)
return out
}
//nolint:gocognit
func v3collectDocRelationships(spdxMap ptrMap[any], doc *spdx.Document) (out []artifact.Relationship) {
for _, r := range doc.Elements.Relationships() {
from := r.GetFrom()
if from == nil || from.GetID() == "" {
log.Debugf("ignoring relationship to external document: %+v", r)
continue
}
a := spdxMap.Get(from)
for _, to := range r.GetTo() {
b := spdxMap.Get(to)
from, fromOk := a.(pkg.Package)
toPackage, toPackageOk := b.(pkg.Package)
toLocation, toLocationOk := b.(file.Location)
//nolint:staticcheck
if !fromOk || !(toPackageOk || toLocationOk) {
log.Debugf("unable to find valid relationship mapping from SPDX, ignoring: (from: %+v) (to: %+v)", a, b)
continue
}
var to artifact.Identifiable
var typ artifact.RelationshipType
if toLocationOk {
switch r.GetType() {
case spdx.RelationshipType_Contains:
typ = artifact.ContainsRelationship
to = toLocation
case spdx.RelationshipType_Other:
// Encoding uses a specifically formatted comment...
if strings.HasPrefix(r.GetComment(), string(artifact.EvidentByRelationship)) {
typ = artifact.EvidentByRelationship
to = toLocation
}
}
} else {
switch r.GetType() {
case spdx.RelationshipType_DependsOn:
typ = artifact.DependencyOfRelationship
to = from
from = toPackage
case spdx.RelationshipType_Contains:
typ = artifact.ContainsRelationship
to = toPackage
case spdx.RelationshipType_Other:
// Encoding uses a specifically formatted comment...
if strings.HasPrefix(r.GetComment(), string(artifact.OwnershipByFileOverlapRelationship)) {
typ = artifact.OwnershipByFileOverlapRelationship
to = toPackage
}
}
}
if typ != "" && to != nil {
out = append(out, artifact.Relationship{
From: from,
To: to,
Type: typ,
})
}
}
}
return out
}
func v3toSyftCoordinates(f spdx.AnyFile) file.Coordinates {
const layerIDPrefix = "layerID: "
var fileSystemID string
if strings.HasPrefix(f.GetComment(), layerIDPrefix) {
fileSystemID = strings.TrimPrefix(f.GetComment(), layerIDPrefix)
}
if strings.HasPrefix(f.GetID(), layerIDPrefix) {
fileSystemID = strings.TrimPrefix(f.GetID(), layerIDPrefix)
}
return file.Coordinates{
RealPath: f.GetName(),
FileSystemID: fileSystemID,
}
}
func v3toSyftLocation(f spdx.AnyFile) file.Location {
l := file.NewVirtualLocationFromCoordinates(v3toSyftCoordinates(f), f.GetName())
return l
}
func v3findQualifierValue(purl packageurl.PackageURL, qualifier string) string {
for _, q := range purl.Qualifiers {
if q.Key == qualifier {
return q.Value
}
}
return ""
}
func v3extractPkgInfo(p spdx.AnyPackage) pkgInfo {
pu := v3findPURLValue(p)
purl, err := packageurl.FromString(pu)
if err != nil {
return pkgInfo{}
}
return pkgInfo{
purl,
pkg.TypeByName(purl.Type),
pkg.LanguageByName(purl.Type),
}
}
func v3toSyftPackage(relationships ptrMap[[]spdx.AnyRelationship], p spdx.AnyPackage) pkg.Package {
info := v3extractPkgInfo(p)
sP := &pkg.Package{
Type: info.typ,
Name: p.GetName(),
Version: p.GetVersion(),
Licenses: pkg.NewLicenseSet(v3parseSPDXLicenses(relationships, p)...),
CPEs: v3extractCPEs(p),
PURL: v3purlValue(info.purl),
Language: info.lang,
Metadata: v3extractMetadata(p, info),
}
internal.Backfill(sP)
if p.GetID() != "" {
// always prefer the IDs from the SBOM over derived IDs
sP.OverrideID(artifact.ID(p.GetID()))
} else {
sP.SetID()
}
return *sP
}
func v3purlValue(purl packageurl.PackageURL) string {
val := purl.String()
if _, err := packageurl.FromString(val); err != nil {
return ""
}
return val
}
func v3parseSPDXLicenses(relationships ptrMap[[]spdx.AnyRelationship], p spdx.AnyPackage) []pkg.License {
licenses := make([]pkg.License, 0)
// licenses are defined with relationships in SPDX 3, see:
// https://github.com/spdx/tools-golang/blob/spdx3/spdx/v3/v3_0/convert.go#L536
rels := relationships.Get(p)
for _, r := range rels {
if r.GetType() == spdx.RelationshipType_HasConcludedLicense {
licenses = append(licenses, v3toSyftLicenses(license.Concluded, r.GetTo().LicenseInfos()...)...)
}
if r.GetType() == spdx.RelationshipType_HasDeclaredLicense {
licenses = append(licenses, v3toSyftLicenses(license.Declared, r.GetTo().LicenseInfos()...)...)
}
}
return licenses
}
func v3toSyftLicenses(licenseType license.Type, licenses ...spdx.AnyLicenseInfo) []pkg.License {
var out []pkg.License
for _, lic := range licenses {
if lic == nil {
continue
}
value := v3licenseInfoToExpression(lic)
if value == "" {
log.Debugf("skipping SPDX license during import: %#v", lic)
continue
}
l := pkg.NewLicense(value)
if l.Value != "" {
l.Type = licenseType
out = append(out, l)
}
}
return out
}
// v3licenseInfoToExpression recursively converts an SPDX 3.0 license info object to an SPDX expression string.
func v3licenseInfoToExpression(info spdx.AnyLicenseInfo) string {
switch li := info.(type) {
case spdx.AnyLicenseExpression:
return li.GetLicenseExpression()
case spdx.AnyOrLaterOperator:
subject := v3licenseInfoToExpression(li.GetSubjectLicense())
if subject != "" && !strings.HasSuffix(subject, "+") {
return subject + "+"
}
case spdx.AnyWithAdditionOperator:
subject := v3licenseInfoToExpression(li.GetSubjectExtendableLicense())
addition := li.GetSubjectAddition()
if subject != "" && addition != nil {
return subject + " WITH " + addition.GetAdditionText()
}
case spdx.AnyConjunctiveLicenseSet:
var parts []string
for _, m := range li.GetMembers() {
if e := v3licenseInfoToExpression(m); e != "" {
parts = append(parts, e)
}
}
if len(parts) > 0 {
return strings.Join(parts, " AND ")
}
case spdx.AnyDisjunctiveLicenseSet:
var parts []string
for _, m := range li.GetMembers() {
if e := v3licenseInfoToExpression(m); e != "" {
parts = append(parts, e)
}
}
if len(parts) > 0 {
return "(" + strings.Join(parts, " OR ") + ")"
}
case spdx.AnyListedLicense:
return li.GetName()
case spdx.AnyCustomLicense:
if li.GetID() != "" {
return li.GetID()
}
return li.GetName()
}
return ""
}
//nolint:funlen
func v3extractMetadata(p spdx.AnyPackage, info pkgInfo) any {
arch := info.qualifierValue(pkg.PURLQualifierArch)
upstreamValue := info.qualifierValue(pkg.PURLQualifierUpstream)
upstream := strings.SplitN(upstreamValue, "@", 2)
upstreamName := upstream[0]
upstreamVersion := ""
if len(upstream) > 1 {
upstreamVersion = upstream[1]
}
supplier := ""
if p.GetSuppliedBy() != nil {
supplier = v3agentString(p.GetSuppliedBy())
}
originator := ""
if len(p.GetOriginatedBy()) > 0 {
// FIXME there could be multiple
originator = v3agentString(p.GetOriginatedBy()[0])
}
switch info.typ {
case pkg.ApkPkg:
return pkg.ApkDBEntry{
Package: p.GetName(),
OriginPackage: upstreamName,
Maintainer: supplier,
Version: p.GetVersion(),
Architecture: arch,
URL: string(p.GetHomePage()),
Description: p.GetDescription(),
}
case pkg.RpmPkg:
converted, err := strconv.Atoi(info.qualifierValue(pkg.PURLQualifierEpoch))
var epoch *int
if err != nil {
epoch = nil
} else {
epoch = &converted
}
return pkg.RpmDBEntry{
Name: p.GetName(),
Version: p.GetVersion(),
Epoch: epoch,
Arch: arch,
SourceRpm: upstreamValue,
Vendor: originator,
}
case pkg.DebPkg:
return pkg.DpkgDBEntry{
Package: p.GetName(),
Source: upstreamName,
Version: p.GetVersion(),
SourceVersion: upstreamVersion,
Architecture: arch,
Maintainer: originator,
}
case pkg.JavaPkg:
var digests []file.Digest
for _, value := range p.GetVerifiedUsing() {
h, _ := value.(spdx.AnyHash)
if h != nil {
digests = append(digests, file.Digest{Algorithm: v3fromChecksumAlgorithm(h.GetAlgorithm()), Value: h.GetValue()})
}
}
return pkg.JavaArchive{
ArchiveDigests: digests,
}
case pkg.GoModulePkg:
var h1Digest string
for _, value := range p.GetVerifiedUsing() {
h, _ := value.(spdx.AnyHash)
if h == nil {
continue
}
digest, err := helpers.HDigestFromSHA(v3fromChecksumAlgorithm(h.GetAlgorithm()), h.GetValue())
if err != nil {
log.Debugf("invalid h1digest: %v %v", value, err)
continue
}
h1Digest = digest
break
}
return pkg.GolangBinaryBuildinfoEntry{
H1Digest: h1Digest,
}
}
return nil
}
func v3agentString(agent spdx.AnyAgent) string {
out := ""
switch o := agent.(type) {
case spdx.AnyOrganization:
out = o.GetName()
case spdx.AnyPerson:
out = o.GetName()
}
if out == helpers.NOASSERTION {
return ""
}
return out
}
func v3findPURLValue(p spdx.AnyPackage) string {
if p.GetPackageURL() != "" {
return string(p.GetPackageURL())
}
for _, r := range p.GetExternalIdentifiers() {
if r.GetType() == spdx.ExternalIdentifierType_PackageURL {
for _, l := range r.GetIdentifierLocators() {
// FIXME multiple values
return string(l)
}
}
}
return ""
}
func v3extractCPEs(p spdx.AnyPackage) (cpes []cpe.CPE) {
for _, r := range p.GetExternalIdentifiers() {
if r.GetType() == spdx.ExternalIdentifierType_Cpe23 || r.GetType() == spdx.ExternalIdentifierType_Cpe22 {
c, err := cpe.New(r.GetIdentifier(), cpe.DeclaredSource)
if err == nil {
cpes = append(cpes, c)
continue
}
log.Tracef("unable to extract SPDX IDENTIFIER CPE=%q: %+v", r.GetIdentifier(), err)
// try the locators
for _, l := range r.GetIdentifierLocators() {
c, err = cpe.New(string(l), cpe.DeclaredSource)
if err != nil {
log.Tracef("unable to extract SPDX CPE=%q: %+v", l, err)
continue
}
cpes = append(cpes, c)
}
}
}
return cpes
}
// v3packageIDsToSkip returns a set of packageIDs that should not be imported
func v3packageIDsToSkip(doc *spdx.Document) ptrMap[struct{}] {
skipIDs := ptrMap[struct{}]{}
for _, r := range doc.Elements.Relationships() {
if r != nil && r.GetFrom() != nil && r.GetType() == spdx.RelationshipType_Generates {
skipIDs.Set(r.GetFrom(), struct{}{}) // flipped from GENERATED_FROM
}
}
return skipIDs
}
// toChecksum takes a checksum in the format <algorithm>:<hash> and returns an spdx.Checksum or nil if the string is invalid
func v3toChecksum(algorithmHash string) spdx.AnyHash {
parts := strings.Split(algorithmHash, ":")
if len(parts) < 2 {
return nil
}
return &spdx.Hash{
Algorithm: v3toChecksumAlgorithm(parts[0]),
Value: parts[1],
}
}
func v3toChecksumAlgorithm(algorithm string) spdx.HashAlgorithm {
// this needs to be an uppercase version of our algorithm
switch strings.ToLower(algorithm) {
case "sha1":
return spdx.HashAlgorithm_Sha1
case "sha256":
return spdx.HashAlgorithm_Sha256
case "sha384":
return spdx.HashAlgorithm_Sha384
case "sha512":
return spdx.HashAlgorithm_Sha512
case "md5":
return spdx.HashAlgorithm_Md5
}
return spdx.HashAlgorithm{}
}
func v3relationshipMap(doc *spdx.Document) ptrMap[[]spdx.AnyRelationship] {
relationships := ptrMap[[]spdx.AnyRelationship]{}
for _, r := range doc.Elements.Relationships() {
existing := relationships.Get(r.GetFrom())
relationships.Set(r.GetFrom(), append(existing, r))
}
return relationships
}
// SPDX 3 values are stored as pointers, and there is a distinct possibility that an ID
// will be blank if it was a JSON-LD blank node in the document, as these IDs are not persisted
// nor should they be persisted from decoding
type ptrMap[T any] map[reflect.Value]T
func (s ptrMap[T]) Set(k any, v T) {
s[ptrTo(k)] = v
}
func (s ptrMap[T]) Get(k any) T {
return s[ptrTo(k)]
}
func (s ptrMap[T]) Remove(k any) {
delete(s, ptrTo(k))
}
func (s ptrMap[T]) Has(k any) bool {
_, ok := s[ptrTo(k)]
return ok
}
func ptrTo(k any) reflect.Value {
rv := reflect.ValueOf(k)
if rv.Kind() != reflect.Pointer {
// this case is a programming problem -- all objects in the SPDX 3 model are pointers and may not have IDs set
panic(fmt.Errorf("value is not a pointer; comparable SPDX 3 elements are pointers, this is probably an implementation issue: %#v", k))
}
return rv
}
@@ -0,0 +1,436 @@
package spdxhelpers
import (
"testing"
spdx "github.com/spdx/tools-golang/spdx/v3/v3_0"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/license"
"github.com/anchore/syft/syft/pkg"
)
func TestToSyftModelV3_documentConversion(t *testing.T) {
// Build SPDX 3.0 elements
pkg1 := &spdx.Package{
ID: "pkg-1-id",
Name: "pkg-1",
Version: "1.0.0",
ExternalIdentifiers: spdx.ExternalIdentifierList{
&spdx.ExternalIdentifier{
Type: spdx.ExternalIdentifierType_PackageURL,
Identifier: "pkg:npm/pkg-1@1.0.0",
IdentifierLocators: []spdx.URI{"pkg:npm/pkg-1@1.0.0"},
},
},
}
pkg2 := &spdx.Package{
ID: "pkg-2-id",
Name: "pkg-2",
Version: "2.0.0",
ExternalIdentifiers: spdx.ExternalIdentifierList{
&spdx.ExternalIdentifier{
Type: spdx.ExternalIdentifierType_PackageURL,
Identifier: "pkg:npm/pkg-2@2.0.0",
IdentifierLocators: []spdx.URI{"pkg:npm/pkg-2@2.0.0"},
},
},
}
file1 := &spdx.File{
ID: "file-1-id",
Name: "/src/main.go",
VerifiedUsing: spdx.IntegrityMethodList{
&spdx.Hash{
Algorithm: spdx.HashAlgorithm_Sha256,
Value: "abc123",
},
},
}
file2 := &spdx.File{
ID: "file-2-id",
Name: "/src/util.go",
}
// licenses
pkg1ConcludedLicense := &spdx.ListedLicense{Name: "MIT"}
pkg1DeclaredLicense := &spdx.DisjunctiveLicenseSet{
Members: spdx.LicenseInfoList{
&spdx.ListedLicense{Name: "MIT"},
&spdx.ListedLicense{Name: "Apache-2.0"},
},
}
pkg2ConcludedLicense := &spdx.ConjunctiveLicenseSet{
Members: spdx.LicenseInfoList{
&spdx.ListedLicense{Name: "BSD-3-Clause"},
&spdx.OrLaterOperator{
SubjectLicense: &spdx.ListedLicense{Name: "GPL-2.0"},
},
},
}
// relationships
pkg1DependsOnPkg2 := &spdx.Relationship{
From: pkg1,
To: spdx.ElementList{pkg2},
Type: spdx.RelationshipType_DependsOn,
}
pkg1ContainsFile1 := &spdx.Relationship{
From: pkg1,
To: spdx.ElementList{file1},
Type: spdx.RelationshipType_Contains,
}
pkg2ContainsFile2 := &spdx.Relationship{
From: pkg2,
To: spdx.ElementList{file2},
Type: spdx.RelationshipType_Contains,
}
pkg1HasConcludedLicense := &spdx.Relationship{
From: pkg1,
To: spdx.ElementList{pkg1ConcludedLicense},
Type: spdx.RelationshipType_HasConcludedLicense,
}
pkg1HasDeclaredLicense := &spdx.Relationship{
From: pkg1,
To: spdx.ElementList{pkg1DeclaredLicense},
Type: spdx.RelationshipType_HasDeclaredLicense,
}
pkg2HasConcludedLicense := &spdx.Relationship{
From: pkg2,
To: spdx.ElementList{pkg2ConcludedLicense},
Type: spdx.RelationshipType_HasConcludedLicense,
}
sbomElement := &spdx.SBOM{
RootElements: spdx.ElementList{pkg1},
Elements: spdx.ElementList{
pkg1, pkg2,
file1, file2,
pkg1DependsOnPkg2,
pkg1ContainsFile1,
pkg2ContainsFile2,
pkg1HasConcludedLicense,
pkg1HasDeclaredLicense,
pkg2HasConcludedLicense,
},
}
doc := &spdx.Document{
SpdxDocument: spdx.SpdxDocument{
ID: "https://example.org/test-doc",
Name: "test-document",
RootElements: spdx.ElementList{sbomElement},
Elements: spdx.ElementList{
sbomElement,
pkg1, pkg2,
file1, file2,
pkg1DependsOnPkg2,
pkg1ContainsFile1,
pkg2ContainsFile2,
pkg1HasConcludedLicense,
pkg1HasDeclaredLicense,
pkg2HasConcludedLicense,
},
},
}
// Convert
result, err := ToSyftModelV3(doc)
require.NoError(t, err)
require.NotNil(t, result)
// Verify packages
allPkgs := result.Artifacts.Packages.Sorted()
require.Len(t, allPkgs, 2, "expected 2 packages")
pkgsByName := map[string]pkg.Package{}
for _, p := range allPkgs {
pkgsByName[p.Name] = p
}
syftPkg1, ok := pkgsByName["pkg-1"]
require.True(t, ok, "pkg-1 not found")
assert.Equal(t, "1.0.0", syftPkg1.Version)
assert.Equal(t, "pkg:npm/pkg-1@1.0.0", syftPkg1.PURL)
syftPkg2, ok := pkgsByName["pkg-2"]
require.True(t, ok, "pkg-2 not found")
assert.Equal(t, "2.0.0", syftPkg2.Version)
assert.Equal(t, "pkg:npm/pkg-2@2.0.0", syftPkg2.PURL)
// Verify pkg-1 licenses: concluded MIT + declared (MIT OR Apache-2.0)
pkg1Licenses := syftPkg1.Licenses.ToSlice()
require.NotEmpty(t, pkg1Licenses, "pkg-1 should have licenses")
var pkg1Concluded, pkg1Declared []pkg.License
for _, l := range pkg1Licenses {
switch l.Type {
case license.Concluded:
pkg1Concluded = append(pkg1Concluded, l)
case license.Declared:
pkg1Declared = append(pkg1Declared, l)
}
}
require.Len(t, pkg1Concluded, 1)
assert.Equal(t, "MIT", pkg1Concluded[0].Value)
require.Len(t, pkg1Declared, 1)
assert.Equal(t, "(MIT OR Apache-2.0)", pkg1Declared[0].Value)
// Verify pkg-2 licenses: concluded BSD-3-Clause AND GPL-2.0-only+
pkg2Licenses := syftPkg2.Licenses.ToSlice()
require.NotEmpty(t, pkg2Licenses, "pkg-2 should have licenses")
var pkg2Concluded []pkg.License
for _, l := range pkg2Licenses {
if l.Type == license.Concluded {
pkg2Concluded = append(pkg2Concluded, l)
}
}
require.Len(t, pkg2Concluded, 1)
assert.Equal(t, "BSD-3-Clause AND GPL-2.0+", pkg2Concluded[0].Value)
// Verify files
coords1 := file.Coordinates{RealPath: "/src/main.go"}
digests, ok := result.Artifacts.FileDigests[coords1]
require.True(t, ok, "file1 digests not found")
require.Len(t, digests, 1)
assert.Equal(t, "sha256", digests[0].Algorithm)
assert.Equal(t, "abc123", digests[0].Value)
coords2 := file.Coordinates{RealPath: "/src/util.go"}
_, ok = result.Artifacts.FileMetadata[coords2]
assert.True(t, ok, "file2 metadata not found")
// Verify relationships
require.NotEmpty(t, result.Relationships)
foundDependsOn := false
foundPkg1ContainsFile := false
foundPkg2ContainsFile := false
for _, rel := range result.Relationships {
fromPkg, fromOk := rel.From.(pkg.Package)
if !fromOk {
continue
}
switch toPkg := rel.To.(type) {
case pkg.Package:
if fromPkg.Name == "pkg-2" && toPkg.Name == "pkg-1" && rel.Type == artifact.DependencyOfRelationship {
foundDependsOn = true
}
case file.Location:
if fromPkg.Name == "pkg-1" && toPkg.RealPath == "/src/main.go" && rel.Type == artifact.ContainsRelationship {
foundPkg1ContainsFile = true
}
if fromPkg.Name == "pkg-2" && toPkg.RealPath == "/src/util.go" && rel.Type == artifact.ContainsRelationship {
foundPkg2ContainsFile = true
}
}
}
assert.True(t, foundDependsOn, "expected pkg-2 DependencyOf pkg-1 relationship")
assert.True(t, foundPkg1ContainsFile, "expected pkg-1 Contains /src/main.go relationship")
assert.True(t, foundPkg2ContainsFile, "expected pkg-2 Contains /src/util.go relationship")
}
func Test_v3licenseInfoToExpression(t *testing.T) {
tests := []struct {
name string
info spdx.AnyLicenseInfo
expected string
}{
{
name: "listed license",
info: &spdx.ListedLicense{Name: "MIT"},
expected: "MIT",
},
{
name: "custom license with ID",
info: &spdx.CustomLicense{ID: "LicenseRef-Custom-1", Name: "Custom License"},
expected: "LicenseRef-Custom-1",
},
{
name: "custom license without ID falls back to name",
info: &spdx.CustomLicense{Name: "Custom License"},
expected: "Custom License",
},
{
name: "license expression",
info: &spdx.LicenseExpression{LicenseExpression: "MIT AND Apache-2.0"},
expected: "MIT AND Apache-2.0",
},
{
name: "or-later operator",
info: &spdx.OrLaterOperator{
SubjectLicense: &spdx.ListedLicense{Name: "GPL-2.0"},
},
expected: "GPL-2.0+",
},
{
name: "conjunctive license set (AND)",
info: &spdx.ConjunctiveLicenseSet{
Members: spdx.LicenseInfoList{
&spdx.ListedLicense{Name: "MIT"},
&spdx.ListedLicense{Name: "Apache-2.0"},
},
},
expected: "MIT AND Apache-2.0",
},
{
name: "disjunctive license set (OR)",
info: &spdx.DisjunctiveLicenseSet{
Members: spdx.LicenseInfoList{
&spdx.ListedLicense{Name: "MIT"},
&spdx.ListedLicense{Name: "Apache-2.0"},
},
},
expected: "(MIT OR Apache-2.0)",
},
{
name: "with-addition operator",
info: &spdx.WithAdditionOperator{
SubjectExtendableLicense: &spdx.ListedLicense{Name: "GPL-2.0-only"},
SubjectAddition: &spdx.ListedLicenseException{AdditionText: "Classpath-exception-2.0"},
},
expected: "GPL-2.0-only WITH Classpath-exception-2.0",
},
{
name: "nested: (MIT OR Apache-2.0) AND GPL-2.0",
info: &spdx.ConjunctiveLicenseSet{
Members: spdx.LicenseInfoList{
&spdx.DisjunctiveLicenseSet{
Members: spdx.LicenseInfoList{
&spdx.ListedLicense{Name: "MIT"},
&spdx.ListedLicense{Name: "Apache-2.0"},
},
},
&spdx.ListedLicense{Name: "GPL-2.0-only"},
},
},
expected: "(MIT OR Apache-2.0) AND GPL-2.0-only",
},
{
name: "nested: GPL-2.0 WITH Classpath OR MIT",
info: &spdx.DisjunctiveLicenseSet{
Members: spdx.LicenseInfoList{
&spdx.WithAdditionOperator{
SubjectExtendableLicense: &spdx.ListedLicense{Name: "GPL-2.0-only"},
SubjectAddition: &spdx.ListedLicenseException{AdditionText: "Classpath-exception-2.0"},
},
&spdx.ListedLicense{Name: "MIT"},
},
},
expected: "(GPL-2.0-only WITH Classpath-exception-2.0 OR MIT)",
},
{
name: "nil returns empty",
info: nil,
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := v3licenseInfoToExpression(tt.info)
assert.Equal(t, tt.expected, got)
})
}
}
func Test_v3toSyftLicenses(t *testing.T) {
tests := []struct {
name string
licenseType license.Type
licenses []spdx.AnyLicenseInfo
expected []string
}{
{
name: "simple listed license",
licenseType: license.Declared,
licenses: []spdx.AnyLicenseInfo{
&spdx.ListedLicense{Name: "MIT"},
},
expected: []string{"MIT"},
},
{
name: "conjunctive set produces single expression",
licenseType: license.Concluded,
licenses: []spdx.AnyLicenseInfo{
&spdx.ConjunctiveLicenseSet{
Members: spdx.LicenseInfoList{
&spdx.ListedLicense{Name: "MIT"},
&spdx.ListedLicense{Name: "Apache-2.0"},
},
},
},
expected: []string{"MIT AND Apache-2.0"},
},
{
name: "multiple license infos",
licenseType: license.Declared,
licenses: []spdx.AnyLicenseInfo{
&spdx.ListedLicense{Name: "MIT"},
&spdx.ListedLicense{Name: "GPL-2.0-only"},
},
expected: []string{"MIT", "GPL-2.0-only"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := v3toSyftLicenses(tt.licenseType, tt.licenses...)
require.Len(t, got, len(tt.expected))
for i, l := range got {
assert.Equal(t, tt.licenseType, l.Type)
assert.Equal(t, tt.expected[i], l.Value)
}
})
}
}
func Test_v3parseSPDXLicenses(t *testing.T) {
p := &spdx.Package{Name: "test-pkg"}
concludedLicense := &spdx.ListedLicense{Name: "MIT"}
declaredLicense := &spdx.ConjunctiveLicenseSet{
Members: spdx.LicenseInfoList{
&spdx.ListedLicense{Name: "Apache-2.0"},
&spdx.ListedLicense{Name: "BSD-3-Clause"},
},
}
relationships := ptrMap[[]spdx.AnyRelationship]{}
relationships.Set(p, []spdx.AnyRelationship{
&spdx.Relationship{
Type: spdx.RelationshipType_HasConcludedLicense,
From: p,
To: spdx.ElementList{concludedLicense},
},
&spdx.Relationship{
Type: spdx.RelationshipType_HasDeclaredLicense,
From: p,
To: spdx.ElementList{declaredLicense},
},
})
licenses := v3parseSPDXLicenses(relationships, p)
require.Len(t, licenses, 2)
var concluded, declared []pkg.License
for _, l := range licenses {
switch l.Type {
case license.Concluded:
concluded = append(concluded, l)
case license.Declared:
declared = append(declared, l)
}
}
require.Len(t, concluded, 1)
assert.Equal(t, "MIT", concluded[0].Value)
require.Len(t, declared, 1)
assert.Equal(t, "Apache-2.0 AND BSD-3-Clause", declared[0].Value)
}
+59
View File
@@ -0,0 +1,59 @@
{
"$schema": "http://cyclonedx.org/schema/bom-1.7.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.7",
"serialNumber": "urn:uuid:5208fea9-73dd-4624-b596-69fddccdb9e7",
"version": 1,
"metadata": {
"timestamp": "2023-09-29T12:02:02-04:00",
"tools": [
{
"vendor": "anchore",
"name": "syft",
"version": "[not provided]"
}
],
"component": {
"bom-ref": "a0ff99a6af10f11f",
"type": "file",
"name": "go.mod",
"version": "sha256:sha256:dc333f342905248a52e424d8dfd061251d01867d01a4f9d7397144a775ff9ebd"
}
},
"components": [
{
"bom-ref": "pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651?package-id=2ff71a67fb024c86",
"type": "library",
"name": "github.com/wagoodman/go-partybus",
"version": "v0.0.0-20230516145632-8ccac152c651",
"cpe": "cpe:2.3:a:wagoodman:go-partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:*",
"purl": "pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651",
"properties": [
{
"name": "syft:package:foundBy",
"value": "go-module-file-cataloger"
},
{
"name": "syft:package:language",
"value": "go"
},
{
"name": "syft:package:metadataType",
"value": "GolangModMetadata"
},
{
"name": "syft:package:type",
"value": "go-module"
},
{
"name": "syft:cpe23",
"value": "cpe:2.3:a:wagoodman:go_partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:*"
},
{
"name": "syft:location:0:path",
"value": "/go.mod"
}
]
}
]
}
@@ -1,7 +1,7 @@
{
"$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json",
"$schema": "http://cyclonedx.org/schema/bom-1.7.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"specVersion": "1.7",
"serialNumber": "urn:uuid:redacted",
"version": 1,
"metadata": {
@@ -1,7 +1,7 @@
{
"$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json",
"$schema": "http://cyclonedx.org/schema/bom-1.7.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"specVersion": "1.7",
"serialNumber": "urn:uuid:redacted",
"version": 1,
"metadata": {
+33
View File
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.7" serialNumber="urn:uuid:098e8516-ecd5-4130-9d5f-c32ba1ddb0dd" version="1">
<metadata>
<timestamp>2023-09-29T11:48:10-04:00</timestamp>
<tools>
<tool>
<vendor>anchore</vendor>
<name>syft</name>
<version>[not provided]</version>
</tool>
</tools>
<component bom-ref="a0ff99a6af10f11f" type="file">
<name>go.mod</name>
<version>sha256:sha256:dc333f342905248a52e424d8dfd061251d01867d01a4f9d7397144a775ff9ebd</version>
</component>
</metadata>
<components>
<component bom-ref="pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651?package-id=2ff71a67fb024c86" type="library">
<name>github.com/wagoodman/go-partybus</name>
<version>v0.0.0-20230516145632-8ccac152c651</version>
<cpe>cpe:2.3:a:wagoodman:go-partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:*</cpe>
<purl>pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651</purl>
<properties>
<property name="syft:package:foundBy">go-module-file-cataloger</property>
<property name="syft:package:language">go</property>
<property name="syft:package:metadataType">GolangModMetadata</property>
<property name="syft:package:type">go-module</property>
<property name="syft:cpe23">cpe:2.3:a:wagoodman:go_partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:*</property>
<property name="syft:location:0:path">/go.mod</property>
</properties>
</component>
</components>
</bom>
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.6" serialNumber="redacted" version="1">
<bom xmlns="http://cyclonedx.org/schema/bom/1.7" serialNumber="redacted" version="1">
<metadata>
<timestamp>redacted</timestamp>
<tools>
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.6" serialNumber="redacted" version="1">
<bom xmlns="http://cyclonedx.org/schema/bom/1.7" serialNumber="redacted" version="1">
<metadata>
<timestamp>redacted</timestamp>
<tools>
+28 -5
View File
@@ -14,12 +14,22 @@ import (
)
type EncoderCollection struct {
encoders []sbom.FormatEncoder
defaultVersions map[sbom.FormatID]string
encoders []sbom.FormatEncoder
}
func NewEncoderCollection(encoders ...sbom.FormatEncoder) *EncoderCollection {
defaultVersions := map[sbom.FormatID]string{}
for _, encoder := range encoders {
if defaultVersion, ok := encoder.(defaultVersionIndicator); ok {
if defaultVersion.DefaultVersion() {
defaultVersions[encoder.ID()] = encoder.Version()
}
}
}
return &EncoderCollection{
encoders: encoders,
defaultVersions: defaultVersions,
encoders: encoders,
}
}
@@ -77,13 +87,20 @@ func (e EncoderCollection) Get(name string, version string) sbom.FormatEncoder {
var mostRecentFormat sbom.FormatEncoder
for _, f := range e.encoders {
defaultVersion := e.defaultVersions[f.ID()]
log.WithFields("name", f.ID(), "version", f.Version(), "aliases", f.Aliases()).Trace("considering format")
names := []string{string(f.ID())}
names = append(names, f.Aliases()...)
for _, n := range names {
if cleanFormatName(n) == name && versionMatches(f.Version(), version) {
if mostRecentFormat == nil || f.Version() > mostRecentFormat.Version() {
mostRecentFormat = f
if cleanFormatName(n) == name {
// if the name or alias matches, and the requested version is any-version, use the default
if version == sbom.AnyVersion && defaultVersion != "" {
version = defaultVersion
}
if versionMatches(f.Version(), version) {
if mostRecentFormat == nil || f.Version() > mostRecentFormat.Version() {
mostRecentFormat = f
}
}
}
}
@@ -141,3 +158,9 @@ func Encode(s sbom.SBOM, f sbom.FormatEncoder) ([]byte, error) {
return buff.Bytes(), nil
}
type defaultVersionIndicator interface {
// DefaultVersion a sbom.FormatEncoder may indicate it is the default version to be used
// when no version is specified for the type
DefaultVersion() bool
}
+75
View File
@@ -4,7 +4,9 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/format/internal/spdxutil"
"github.com/anchore/syft/syft/sbom"
)
@@ -109,3 +111,76 @@ func Test_versionMatches(t *testing.T) {
})
}
}
func Test_EncoderCollection_Get(t *testing.T) {
tests := []struct {
name string
searchName string
searchVersion string
expectedID sbom.FormatID
expectedVersion string
}{
{
name: "explicit name and version",
searchName: "spdx-json",
searchVersion: "2.3",
expectedID: spdxutil.JSONFormatID,
expectedVersion: "2.3",
},
{
name: "explicit name without version gets default",
searchName: "spdx-json",
searchVersion: "",
expectedID: spdxutil.JSONFormatID,
expectedVersion: "2.3",
},
{
name: "alias name with version",
searchName: "spdx",
searchVersion: "2.2",
expectedID: spdxutil.TagValueFormatID,
expectedVersion: "2.2",
},
{
name: "alias name without version gets default",
searchName: "spdx",
searchVersion: "",
expectedID: spdxutil.TagValueFormatID,
expectedVersion: "2.3",
},
{
name: "invalid name gets nothing",
searchName: "json-spdx",
searchVersion: "2.3",
expectedID: "",
},
{
name: "invalid version gets nothing",
searchName: "spdx-json",
searchVersion: "2.0",
expectedID: "",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
config := DefaultEncodersConfig()
// ensure SPDX default is 2.3 for test
config.SPDXJSON.DefaultVersion = "2.3"
encoders, err := config.Encoders()
require.NoError(t, err)
collection := NewEncoderCollection(encoders...)
result := collection.Get(test.searchName, test.searchVersion)
if test.expectedID != "" {
require.NotNil(t, result, "expected to find encoder but got nil")
if result != nil {
require.Equal(t, test.expectedID, result.ID())
require.Equal(t, test.expectedVersion, result.Version())
}
} else {
require.Nil(t, result, "expected nil but found encoder")
}
})
}
}
+98 -13
View File
@@ -48,6 +48,7 @@ func backfillFromPurl(p *pkg.Package) {
var cpes []cpe.CPE
epoch := ""
rpmmod := ""
arch := ""
for _, qualifier := range purl.Qualifiers {
switch qualifier.Key {
@@ -65,6 +66,8 @@ func backfillFromPurl(p *pkg.Package) {
epoch = qualifier.Value
case pkg.PURLQualifierRpmModularity:
rpmmod = qualifier.Value
case pkg.PURLQualifierArch:
arch = qualifier.Value
}
}
@@ -84,9 +87,7 @@ func backfillFromPurl(p *pkg.Package) {
setJavaMetadataFromPurl(p, purl)
}
if p.Type == pkg.RpmPkg {
setRpmMetadataFromPurl(p, rpmmod)
}
setDistroMetadata(p, rpmmod, arch)
for _, c := range cpes {
if slices.Contains(p.CPEs, c) {
@@ -96,6 +97,19 @@ func backfillFromPurl(p *pkg.Package) {
}
}
func setDistroMetadata(p *pkg.Package, rpmmod, arch string) {
switch p.Type {
case pkg.RpmPkg:
setRpmMetadataFromPurl(p, rpmmod, arch)
case pkg.DebPkg:
setDpkgMetadataFromPurl(p, arch)
case pkg.AlpmPkg:
setAlpmMetadataFromPurl(p, arch)
case pkg.ApkPkg:
setApkMetadataFromPurl(p, arch)
}
}
func setJavaMetadataFromPurl(p *pkg.Package, _ packageurl.PackageURL) {
if p.Type != pkg.JavaPkg {
return
@@ -107,32 +121,103 @@ func setJavaMetadataFromPurl(p *pkg.Package, _ packageurl.PackageURL) {
}
}
func setRpmMetadataFromPurl(p *pkg.Package, rpmmod string) {
func setRpmMetadataFromPurl(p *pkg.Package, rpmmod, arch string) {
if p.Type != pkg.RpmPkg {
return
}
if rpmmod == "" {
if rpmmod == "" && arch == "" {
return
}
if p.Metadata == nil {
p.Metadata = pkg.RpmDBEntry{
ModularityLabel: &rpmmod,
}
return
p.Metadata = pkg.RpmDBEntry{}
}
switch m := p.Metadata.(type) {
case pkg.RpmDBEntry:
if m.ModularityLabel == nil {
if m.ModularityLabel == nil && rpmmod != "" {
m.ModularityLabel = &rpmmod
p.Metadata = m
}
if m.Arch == "" {
m.Arch = arch
}
p.Metadata = m
case pkg.RpmArchive:
if m.ModularityLabel == nil {
if m.ModularityLabel == nil && rpmmod != "" {
m.ModularityLabel = &rpmmod
p.Metadata = m
}
if m.Arch == "" {
m.Arch = arch
}
p.Metadata = m
}
}
func setDpkgMetadataFromPurl(p *pkg.Package, arch string) {
if p.Type != pkg.DebPkg {
return
}
if arch == "" {
return
}
if p.Metadata == nil {
p.Metadata = pkg.DpkgDBEntry{}
}
switch m := p.Metadata.(type) {
case pkg.DpkgDBEntry:
if m.Architecture == "" {
m.Architecture = arch
}
p.Metadata = m
case pkg.DpkgArchiveEntry:
if m.Architecture == "" {
m.Architecture = arch
}
p.Metadata = m
}
}
func setAlpmMetadataFromPurl(p *pkg.Package, arch string) {
if p.Type != pkg.AlpmPkg {
return
}
if arch == "" {
return
}
if p.Metadata == nil {
p.Metadata = pkg.AlpmDBEntry{Architecture: arch}
return
}
if m, ok := p.Metadata.(pkg.AlpmDBEntry); ok {
if m.Architecture == "" {
m.Architecture = arch
}
p.Metadata = m
}
}
func setApkMetadataFromPurl(p *pkg.Package, arch string) {
if p.Type != pkg.ApkPkg {
return
}
if arch == "" {
return
}
if p.Metadata == nil {
p.Metadata = pkg.ApkDBEntry{Architecture: arch}
return
}
if m, ok := p.Metadata.(pkg.ApkDBEntry); ok {
if m.Architecture == "" {
m.Architecture = arch
}
p.Metadata = m
}
}
+241
View File
@@ -39,6 +39,9 @@ func Test_Backfill(t *testing.T) {
Type: pkg.RpmPkg,
Name: "dbus-common",
Version: "1.12.8-26.el8",
Metadata: pkg.RpmDBEntry{
Arch: "noarch",
},
},
},
{
@@ -51,6 +54,9 @@ func Test_Backfill(t *testing.T) {
Type: pkg.RpmPkg,
Name: "dbus-common",
Version: "1:1.12.8-26.el8",
Metadata: pkg.RpmDBEntry{
Arch: "noarch",
},
},
},
{
@@ -65,6 +71,22 @@ func Test_Backfill(t *testing.T) {
Version: "2.4.37-51",
Metadata: pkg.RpmDBEntry{
ModularityLabel: strRef("httpd:2.4"),
Arch: "x86_64",
},
},
},
{
name: "rpm with arch and no rpmmod",
in: pkg.Package{
PURL: "pkg:rpm/redhat/httpd@2.4.37-51?arch=x86_64&distro=rhel-8.7",
},
expected: pkg.Package{
PURL: "pkg:rpm/redhat/httpd@2.4.37-51?arch=x86_64&distro=rhel-8.7",
Type: pkg.RpmPkg,
Name: "httpd",
Version: "2.4.37-51",
Metadata: pkg.RpmDBEntry{
Arch: "x86_64",
},
},
},
@@ -121,6 +143,225 @@ func Test_Backfill(t *testing.T) {
Metadata: pkg.JavaArchive{},
},
},
// deb cases
{
name: "deb nil metadata gets arch set",
in: pkg.Package{
PURL: "pkg:deb/debian/curl@7.74.0-1.3+deb11u7?arch=amd64&distro=debian-11",
},
expected: pkg.Package{
PURL: "pkg:deb/debian/curl@7.74.0-1.3+deb11u7?arch=amd64&distro=debian-11",
Type: pkg.DebPkg,
Name: "curl",
Version: "7.74.0-1.3+deb11u7",
Metadata: pkg.DpkgDBEntry{
Architecture: "amd64",
},
},
},
{
name: "deb arch:all preserved (nil metadata)",
in: pkg.Package{
PURL: "pkg:deb/debian/tzdata@2021a-1+deb11u10?arch=all&distro=debian-11",
},
expected: pkg.Package{
PURL: "pkg:deb/debian/tzdata@2021a-1+deb11u10?arch=all&distro=debian-11",
Type: pkg.DebPkg,
Name: "tzdata",
Version: "2021a-1+deb11u10",
Metadata: pkg.DpkgDBEntry{
Architecture: "all",
},
},
},
{
name: "deb populated metadata empty arch gets filled",
in: pkg.Package{
PURL: "pkg:deb/debian/curl@7.74.0-1.3+deb11u7?arch=amd64&distro=debian-11",
Metadata: pkg.DpkgDBEntry{
Package: "curl",
},
},
expected: pkg.Package{
PURL: "pkg:deb/debian/curl@7.74.0-1.3+deb11u7?arch=amd64&distro=debian-11",
Type: pkg.DebPkg,
Name: "curl",
Version: "7.74.0-1.3+deb11u7",
Metadata: pkg.DpkgDBEntry{
Package: "curl",
Architecture: "amd64",
},
},
},
{
name: "deb populated metadata with arch preserved",
in: pkg.Package{
PURL: "pkg:deb/debian/curl@7.74.0-1.3+deb11u7?arch=amd64&distro=debian-11",
Metadata: pkg.DpkgDBEntry{
Package: "curl",
Architecture: "arm64",
},
},
expected: pkg.Package{
PURL: "pkg:deb/debian/curl@7.74.0-1.3+deb11u7?arch=amd64&distro=debian-11",
Type: pkg.DebPkg,
Name: "curl",
Version: "7.74.0-1.3+deb11u7",
Metadata: pkg.DpkgDBEntry{
Package: "curl",
Architecture: "arm64",
},
},
},
{
name: "deb archive entry empty arch gets filled",
in: pkg.Package{
PURL: "pkg:deb/debian/curl@7.74.0-1.3+deb11u7?arch=amd64&distro=debian-11",
Metadata: pkg.DpkgArchiveEntry{
Package: "curl",
},
},
expected: pkg.Package{
PURL: "pkg:deb/debian/curl@7.74.0-1.3+deb11u7?arch=amd64&distro=debian-11",
Type: pkg.DebPkg,
Name: "curl",
Version: "7.74.0-1.3+deb11u7",
Metadata: pkg.DpkgArchiveEntry{
Package: "curl",
Architecture: "amd64",
},
},
},
{
name: "deb archive entry with arch preserved",
in: pkg.Package{
PURL: "pkg:deb/debian/curl@7.74.0-1.3+deb11u7?arch=amd64&distro=debian-11",
Metadata: pkg.DpkgArchiveEntry{
Package: "curl",
Architecture: "arm64",
},
},
expected: pkg.Package{
PURL: "pkg:deb/debian/curl@7.74.0-1.3+deb11u7?arch=amd64&distro=debian-11",
Type: pkg.DebPkg,
Name: "curl",
Version: "7.74.0-1.3+deb11u7",
Metadata: pkg.DpkgArchiveEntry{
Package: "curl",
Architecture: "arm64",
},
},
},
// alpm cases
{
name: "alpm nil metadata gets arch set",
in: pkg.Package{
PURL: "pkg:alpm/arch/curl@7.88.1-1?arch=x86_64&distro=arch-rolling",
},
expected: pkg.Package{
PURL: "pkg:alpm/arch/curl@7.88.1-1?arch=x86_64&distro=arch-rolling",
Type: pkg.AlpmPkg,
Name: "curl",
Version: "7.88.1-1",
Metadata: pkg.AlpmDBEntry{
Architecture: "x86_64",
},
},
},
{
name: "alpm populated metadata empty arch gets filled",
in: pkg.Package{
PURL: "pkg:alpm/arch/curl@7.88.1-1?arch=x86_64&distro=arch-rolling",
Metadata: pkg.AlpmDBEntry{
Package: "curl",
},
},
expected: pkg.Package{
PURL: "pkg:alpm/arch/curl@7.88.1-1?arch=x86_64&distro=arch-rolling",
Type: pkg.AlpmPkg,
Name: "curl",
Version: "7.88.1-1",
Metadata: pkg.AlpmDBEntry{
Package: "curl",
Architecture: "x86_64",
},
},
},
{
name: "alpm populated metadata with arch preserved",
in: pkg.Package{
PURL: "pkg:alpm/arch/curl@7.88.1-1?arch=x86_64&distro=arch-rolling",
Metadata: pkg.AlpmDBEntry{
Package: "curl",
Architecture: "aarch64",
},
},
expected: pkg.Package{
PURL: "pkg:alpm/arch/curl@7.88.1-1?arch=x86_64&distro=arch-rolling",
Type: pkg.AlpmPkg,
Name: "curl",
Version: "7.88.1-1",
Metadata: pkg.AlpmDBEntry{
Package: "curl",
Architecture: "aarch64",
},
},
},
// apk cases
{
name: "apk nil metadata gets arch set",
in: pkg.Package{
PURL: "pkg:apk/alpine/curl@7.83.1-r2?arch=aarch64&distro=alpine-3.16.2",
},
expected: pkg.Package{
PURL: "pkg:apk/alpine/curl@7.83.1-r2?arch=aarch64&distro=alpine-3.16.2",
Type: pkg.ApkPkg,
Name: "curl",
Version: "7.83.1-r2",
Metadata: pkg.ApkDBEntry{
Architecture: "aarch64",
},
},
},
{
name: "apk populated metadata empty arch gets filled",
in: pkg.Package{
PURL: "pkg:apk/alpine/curl@7.83.1-r2?arch=aarch64&distro=alpine-3.16.2",
Metadata: pkg.ApkDBEntry{
Package: "curl",
},
},
expected: pkg.Package{
PURL: "pkg:apk/alpine/curl@7.83.1-r2?arch=aarch64&distro=alpine-3.16.2",
Type: pkg.ApkPkg,
Name: "curl",
Version: "7.83.1-r2",
Metadata: pkg.ApkDBEntry{
Package: "curl",
Architecture: "aarch64",
},
},
},
{
name: "apk populated metadata with arch preserved",
in: pkg.Package{
PURL: "pkg:apk/alpine/curl@7.83.1-r2?arch=aarch64&distro=alpine-3.16.2",
Metadata: pkg.ApkDBEntry{
Package: "curl",
Architecture: "x86_64",
},
},
expected: pkg.Package{
PURL: "pkg:apk/alpine/curl@7.83.1-r2?arch=aarch64&distro=alpine-3.16.2",
Type: pkg.ApkPkg,
Name: "curl",
Version: "7.83.1-r2",
Metadata: pkg.ApkDBEntry{
Package: "curl",
Architecture: "x86_64",
},
},
},
{
name: "target-sw from CPE",
in: pkg.Package{
@@ -9,8 +9,6 @@ import (
"github.com/anchore/syft/syft/sbom"
)
const DefaultVersion = "1.6"
type Encoder struct {
version cyclonedx.SpecVersion
format cyclonedx.BOMFileFormat
@@ -317,7 +317,7 @@ func Test_decodeComponent(t *testing.T) {
},
},
},
wantMetadata: pkg.RpmDBEntry{},
wantMetadata: pkg.RpmDBEntry{Arch: "x86_64"},
wantPURL: "pkg:rpm/centos/acl@2.2.53-1.el8?arch=x86_64&upstream=acl-2.2.53-1.el8.src.rpm&distro=centos-8",
},
{
@@ -341,6 +341,7 @@ func Test_decodeComponent(t *testing.T) {
},
wantMetadata: pkg.RpmDBEntry{
Release: "some-release",
Arch: "x86_64",
},
wantPURL: "pkg:rpm/centos/acl@2.2.53-1.el8?arch=x86_64&upstream=acl-2.2.53-1.el8.src.rpm&distro=centos-8",
},

Some files were not shown because too many files have changed in this diff Show More