diff --git a/.goreleaser.yaml b/.goreleaser.yaml index aca74945112..f401e400e8f 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -1,11 +1,13 @@ version: 2 +project_name: grype + release: prerelease: auto draft: false env: - # required to support multi architecture docker builds + # required to support multi-architecture docker builds - DOCKER_CLI_EXPERIMENTAL=enabled - CGO_ENABLED=0 @@ -16,10 +18,18 @@ builds: goos: - linux goarch: + # Full arch matrix matches the RapidFort Trivy fork's release surface — + # covers 386/arm for legacy systems, amd64/arm64 for the majority, and + # s390x/ppc64le for IBM enterprise targets. modernc.org/sqlite (via + # glebarez/sqlite in go.mod) supports all six on linux. + - "386" + - arm - amd64 - arm64 - - ppc64le - s390x + - ppc64le + goarm: + - "7" # set the modified timestamp on the output binary to the git timestamp to ensure a reproducible build mod_timestamp: &build-timestamp '{{ .CommitTimestamp }}' ldflags: &build-ldflags | @@ -31,6 +41,19 @@ builds: -X main.buildDate={{.Date}} -X main.gitDescription={{.Summary}} + - id: freebsd-build + dir: ./cmd/grype + binary: grype + goos: + - freebsd + goarch: + # modernc.org/sqlite doesn't support freebsd/arm64. + # freebsd/386 is no longer supported in modernc.org/libc. + # See https://gitlab.com/cznic/libc/-/issues/45 + - amd64 + mod_timestamp: *build-timestamp + ldflags: *build-ldflags + - id: darwin-build dir: ./cmd/grype binary: grype @@ -43,7 +66,13 @@ builds: ldflags: *build-ldflags hooks: post: - - cmd: .tool/quill sign-and-notarize "{{ .Path }}" --dry-run={{ .IsSnapshot }} --ad-hoc={{ .IsSnapshot }} -vv + # RapidFort fork: hard-code --ad-hoc=true so we never require an Apple + # Developer ID cert (which anchore's QUILL_SIGN_P12 secret would provide + # but we don't have). End-user Macs will show a Gatekeeper warning on + # first run; right-click → Open works around it. For proper signing + # later, buy a Developer ID ($99/yr), store the P12 as QUILL_SIGN_P12 + # in the release workflow, and change --ad-hoc back to {{ .IsSnapshot }}. + - cmd: .tool/quill sign-and-notarize "{{ .Path }}" --dry-run={{ .IsSnapshot }} --ad-hoc=true -vv env: - QUILL_LOG_FILE=/tmp/quill-{{ .Target }}.log @@ -53,308 +82,208 @@ builds: goos: - windows goarch: + # modernc.org/sqlite doesn't support windows/386 or windows/arm. - amd64 mod_timestamp: *build-timestamp ldflags: *build-ldflags archives: - - id: linux-archives - ids: - - linux-build - - - id: darwin-archives - ids: - - darwin-build - - - id: windows-archives - formats: [zip] - ids: - - windows-build + - id: archive + formats: [tar.gz] + # Humanized filenames: `grype_0.115.0-rf.1_Linux-64bit.tar.gz` matches the + # convention used by the RapidFort Trivy fork so users of both scanners see + # the same shape. Original grype convention was `grype_0.115.0-rf.1_linux_amd64.tar.gz`. + name_template: >- + {{ .ProjectName }}_{{ .Version }}_ + {{- if eq .Os "darwin" }}macOS + {{- else if eq .Os "linux" }}Linux + {{- else if eq .Os "freebsd" }}FreeBSD + {{- else if eq .Os "windows" }}Windows + {{- else }}{{ .Os }}{{ end }}- + {{- if eq .Arch "amd64" }}64bit + {{- else if eq .Arch "386" }}32bit + {{- else if eq .Arch "arm" }}ARM + {{- else if eq .Arch "arm64" }}ARM64 + {{- else if eq .Arch "ppc64le" }}PPC64LE + {{- else }}{{ .Arch }}{{ end }} + files: + - README.md + - LICENSE + - templates/*.tmpl + format_overrides: + - goos: windows + formats: [zip] nfpms: - - license: "Apache 2.0" - maintainer: "Anchore, Inc" - homepage: &website "https://github.com/anchore/grype" - description: &description "A vulnerability scanner for container images and filesystems" + - license: "Apache-2.0" + maintainer: "RapidFort" + vendor: "RapidFort" + homepage: "https://github.com/rfJanit/grype" + description: "RapidFort's Grype fork — A vulnerability scanner for container images and filesystems, extended with RapidFort curated advisories." formats: - rpm - deb - -homebrew_casks: - - repository: - owner: anchore - name: homebrew-grype - token: "{{.Env.GITHUB_BREW_TOKEN}}" - ids: - - darwin-archives - - linux-archives - homepage: *website - description: *description - license: "Apache License 2.0" - conflicts: - # claim that the previous grype formula (in the root of homebrew-grype) conflicts with the new cask - # see https://goreleaser.com/deprecations/#brews for more information - - formula: grype - + file_name_template: >- + {{ .ProjectName }}_{{ .Version }}_ + {{- if eq .Os "darwin" }}macOS + {{- else if eq .Os "linux" }}Linux + {{- else if eq .Os "freebsd" }}FreeBSD + {{- else }}{{ title .Os }}{{ end }}- + {{- if eq .Arch "amd64" }}64bit + {{- else if eq .Arch "386" }}32bit + {{- else if eq .Arch "arm" }}ARM + {{- else if eq .Arch "arm64" }}ARM64 + {{- else if eq .Arch "ppc64le" }}PPC64LE + {{- else }}{{ .Arch }}{{ end }} + # Ship the output-format templates so `grype -o template -t ` works + # out of the box for anyone who installed via .deb/.rpm. + contents: + - src: templates/*.tmpl + dst: /usr/local/share/grype/templates + # RPM signing intentionally omitted — no signing key configured for the first release. + +# Per-arch container images pushed only to GHCR under the fork's owner. +# Docker Hub removed for the first release; add back later if a rapidfort/grype +# repo is created with DOCKERHUB_USERNAME/TOKEN secrets in the release workflow. +# +# Only production images ship — -nonroot and -debug variants dropped for first +# release (add back on request if users need PodSecurityPolicy-friendly image +# or in-container shell tools). +# +# Note: grype's Dockerfile has hardcoded LABELs including image.vendor="Anchore, Inc." +# and a generic image.description. The `--label=` flags below override these at build +# time — docker takes the latest label with a matching key. To fix at the source +# instead of overriding, edit the LABEL directives in grype/Dockerfile directly. dockers: - # production images... - image_templates: - - anchore/grype:{{.Tag}}-amd64 - - ghcr.io/anchore/grype:{{.Tag}}-amd64 + - "ghcr.io/rfjanit/grype:{{ .Version }}-amd64" + - "ghcr.io/rfjanit/grype:latest-amd64" + use: buildx + goos: linux goarch: amd64 + ids: + - linux-build dockerfile: Dockerfile - use: buildx build_flag_templates: - "--platform=linux/amd64" - "--provenance=false" + - "--label=org.opencontainers.image.title={{ .ProjectName }}" + - "--label=org.opencontainers.image.description=RapidFort's Grype fork with curated advisories" + - "--label=org.opencontainers.image.vendor=RapidFort" + - "--label=org.opencontainers.image.version={{ .Version }}" + - "--label=org.opencontainers.image.created={{ .Date }}" + - "--label=org.opencontainers.image.source=https://github.com/rfJanit/grype" + - "--label=org.opencontainers.image.revision={{ .FullCommit }}" + - "--label=org.opencontainers.image.url=https://github.com/rfJanit/grype" + # Grype's Dockerfile has ARG BUILD_DATE / BUILD_VERSION / VCS_REF / VCS_URL + # that populate additional LABELs. Missing --build-arg's would leave those + # labels empty; not fatal but visually ugly on `docker inspect`. - "--build-arg=BUILD_DATE={{.Date}}" - "--build-arg=BUILD_VERSION={{.Version}}" - "--build-arg=VCS_REF={{.FullCommit}}" - "--build-arg=VCS_URL={{.GitURL}}" - image_templates: - - anchore/grype:{{.Tag}}-arm64v8 - - ghcr.io/anchore/grype:{{.Tag}}-arm64v8 + - "ghcr.io/rfjanit/grype:{{ .Version }}-arm64" + - "ghcr.io/rfjanit/grype:latest-arm64" + use: buildx + goos: linux goarch: arm64 + ids: + - linux-build dockerfile: Dockerfile - use: buildx build_flag_templates: - - "--platform=linux/arm64/v8" + - "--platform=linux/arm64" - "--provenance=false" + - "--label=org.opencontainers.image.title={{ .ProjectName }}" + - "--label=org.opencontainers.image.description=RapidFort's Grype fork with curated advisories" + - "--label=org.opencontainers.image.vendor=RapidFort" + - "--label=org.opencontainers.image.version={{ .Version }}" + - "--label=org.opencontainers.image.created={{ .Date }}" + - "--label=org.opencontainers.image.source=https://github.com/rfJanit/grype" + - "--label=org.opencontainers.image.revision={{ .FullCommit }}" + - "--label=org.opencontainers.image.url=https://github.com/rfJanit/grype" - "--build-arg=BUILD_DATE={{.Date}}" - "--build-arg=BUILD_VERSION={{.Version}}" - "--build-arg=VCS_REF={{.FullCommit}}" - "--build-arg=VCS_URL={{.GitURL}}" - image_templates: - - anchore/grype:{{.Tag}}-ppc64le - - ghcr.io/anchore/grype:{{.Tag}}-ppc64le - goarch: ppc64le - dockerfile: Dockerfile + - "ghcr.io/rfjanit/grype:{{ .Version }}-s390x" + - "ghcr.io/rfjanit/grype:latest-s390x" use: buildx - build_flag_templates: - - "--platform=linux/ppc64le" - - "--provenance=false" - - "--build-arg=BUILD_DATE={{.Date}}" - - "--build-arg=BUILD_VERSION={{.Version}}" - - "--build-arg=VCS_REF={{.FullCommit}}" - - "--build-arg=VCS_URL={{.GitURL}}" - - - image_templates: - - anchore/grype:{{.Tag}}-s390x - - ghcr.io/anchore/grype:{{.Tag}}-s390x + goos: linux goarch: s390x + ids: + - linux-build dockerfile: Dockerfile - use: buildx build_flag_templates: - "--platform=linux/s390x" - "--provenance=false" - - "--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/grype:{{.Tag}}-nonroot-amd64 - - ghcr.io/anchore/grype:{{.Tag}}-nonroot-amd64 - goarch: amd64 - dockerfile: Dockerfile.nonroot - use: buildx - build_flag_templates: - - "--platform=linux/amd64" - - "--provenance=false" + - "--label=org.opencontainers.image.title={{ .ProjectName }}" + - "--label=org.opencontainers.image.description=RapidFort's Grype fork with curated advisories" + - "--label=org.opencontainers.image.vendor=RapidFort" + - "--label=org.opencontainers.image.version={{ .Version }}" + - "--label=org.opencontainers.image.created={{ .Date }}" + - "--label=org.opencontainers.image.source=https://github.com/rfJanit/grype" + - "--label=org.opencontainers.image.revision={{ .FullCommit }}" + - "--label=org.opencontainers.image.url=https://github.com/rfJanit/grype" - "--build-arg=BUILD_DATE={{.Date}}" - "--build-arg=BUILD_VERSION={{.Version}}" - "--build-arg=VCS_REF={{.FullCommit}}" - "--build-arg=VCS_URL={{.GitURL}}" - image_templates: - - anchore/grype:{{.Tag}}-nonroot-arm64v8 - - ghcr.io/anchore/grype:{{.Tag}}-nonroot-arm64v8 - goarch: arm64 - dockerfile: Dockerfile.nonroot + - "ghcr.io/rfjanit/grype:{{ .Version }}-ppc64le" + - "ghcr.io/rfjanit/grype:latest-ppc64le" use: buildx - build_flag_templates: - - "--platform=linux/arm64/v8" - - "--provenance=false" - - "--build-arg=BUILD_DATE={{.Date}}" - - "--build-arg=BUILD_VERSION={{.Version}}" - - "--build-arg=VCS_REF={{.FullCommit}}" - - "--build-arg=VCS_URL={{.GitURL}}" - - - image_templates: - - anchore/grype:{{.Tag}}-nonroot-ppc64le - - ghcr.io/anchore/grype:{{.Tag}}-nonroot-ppc64le + goos: linux goarch: ppc64le - dockerfile: Dockerfile.nonroot - use: buildx - build_flag_templates: - - "--platform=linux/ppc64le" - - "--provenance=false" - - "--build-arg=BUILD_DATE={{.Date}}" - - "--build-arg=BUILD_VERSION={{.Version}}" - - "--build-arg=VCS_REF={{.FullCommit}}" - - "--build-arg=VCS_URL={{.GitURL}}" - - - image_templates: - - anchore/grype:{{.Tag}}-nonroot-s390x - - ghcr.io/anchore/grype:{{.Tag}}-nonroot-s390x - goarch: s390x - dockerfile: Dockerfile.nonroot - use: buildx - build_flag_templates: - - "--platform=linux/s390x" - - "--provenance=false" - - "--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/grype:{{.Tag}}-debug-amd64 - - ghcr.io/anchore/grype:{{.Tag}}-debug-amd64 - goarch: amd64 - dockerfile: Dockerfile.debug - use: buildx - build_flag_templates: - - "--platform=linux/amd64" - - "--provenance=false" - - "--build-arg=BUILD_DATE={{.Date}}" - - "--build-arg=BUILD_VERSION={{.Version}}" - - "--build-arg=VCS_REF={{.FullCommit}}" - - "--build-arg=VCS_URL={{.GitURL}}" - - - image_templates: - - anchore/grype:{{.Tag}}-debug-arm64v8 - - ghcr.io/anchore/grype:{{.Tag}}-debug-arm64v8 - goarch: arm64 - dockerfile: Dockerfile.debug - use: buildx - build_flag_templates: - - "--platform=linux/arm64/v8" - - "--provenance=false" - - "--build-arg=BUILD_DATE={{.Date}}" - - "--build-arg=BUILD_VERSION={{.Version}}" - - "--build-arg=VCS_REF={{.FullCommit}}" - - "--build-arg=VCS_URL={{.GitURL}}" - - - image_templates: - - anchore/grype:{{.Tag}}-debug-ppc64le - - ghcr.io/anchore/grype:{{.Tag}}-debug-ppc64le - goarch: ppc64le - dockerfile: Dockerfile.debug - use: buildx + ids: + - linux-build + dockerfile: Dockerfile build_flag_templates: - "--platform=linux/ppc64le" - "--provenance=false" + - "--label=org.opencontainers.image.title={{ .ProjectName }}" + - "--label=org.opencontainers.image.description=RapidFort's Grype fork with curated advisories" + - "--label=org.opencontainers.image.vendor=RapidFort" + - "--label=org.opencontainers.image.version={{ .Version }}" + - "--label=org.opencontainers.image.created={{ .Date }}" + - "--label=org.opencontainers.image.source=https://github.com/rfJanit/grype" + - "--label=org.opencontainers.image.revision={{ .FullCommit }}" + - "--label=org.opencontainers.image.url=https://github.com/rfJanit/grype" - "--build-arg=BUILD_DATE={{.Date}}" - "--build-arg=BUILD_VERSION={{.Version}}" - "--build-arg=VCS_REF={{.FullCommit}}" - "--build-arg=VCS_URL={{.GitURL}}" - - image_templates: - - anchore/grype:{{.Tag}}-debug-s390x - - ghcr.io/anchore/grype:{{.Tag}}-debug-s390x - goarch: s390x - dockerfile: Dockerfile.debug - use: buildx - build_flag_templates: - - "--platform=linux/s390x" - - "--provenance=false" - - "--build-arg=BUILD_DATE={{.Date}}" - - "--build-arg=BUILD_VERSION={{.Version}}" - - "--build-arg=VCS_REF={{.FullCommit}}" - - "--build-arg=VCS_URL={{.GitURL}}" - +# Multi-arch manifests published only to GHCR under the fork's owner. +# End users pull one of: +# docker pull ghcr.io/rfjanit/grype:latest # newest release +# docker pull ghcr.io/rfjanit/grype:0.115.0-rf.1 # pinned version +# The daemon picks the arch that matches its host. docker_manifests: - - name_template: anchore/grype:latest - image_templates: - - anchore/grype:{{.Tag}}-amd64 - - anchore/grype:{{.Tag}}-arm64v8 - - anchore/grype:{{.Tag}}-ppc64le - - anchore/grype:{{.Tag}}-s390x - - - name_template: ghcr.io/anchore/grype:latest - image_templates: - - ghcr.io/anchore/grype:{{.Tag}}-amd64 - - ghcr.io/anchore/grype:{{.Tag}}-arm64v8 - - ghcr.io/anchore/grype:{{.Tag}}-ppc64le - - ghcr.io/anchore/grype:{{.Tag}}-s390x - - - name_template: anchore/grype:{{.Tag}} + - name_template: 'ghcr.io/rfjanit/grype:{{ .Version }}' image_templates: - - anchore/grype:{{.Tag}}-amd64 - - anchore/grype:{{.Tag}}-arm64v8 - - anchore/grype:{{.Tag}}-ppc64le - - anchore/grype:{{.Tag}}-s390x + - 'ghcr.io/rfjanit/grype:{{ .Version }}-amd64' + - 'ghcr.io/rfjanit/grype:{{ .Version }}-arm64' + - 'ghcr.io/rfjanit/grype:{{ .Version }}-s390x' + - 'ghcr.io/rfjanit/grype:{{ .Version }}-ppc64le' - - name_template: ghcr.io/anchore/grype:{{.Tag}} + - name_template: 'ghcr.io/rfjanit/grype:latest' image_templates: - - ghcr.io/anchore/grype:{{.Tag}}-amd64 - - ghcr.io/anchore/grype:{{.Tag}}-arm64v8 - - ghcr.io/anchore/grype:{{.Tag}}-ppc64le - - ghcr.io/anchore/grype:{{.Tag}}-s390x - - # nonroot images... - - name_template: anchore/grype:nonroot - image_templates: - - anchore/grype:{{.Tag}}-nonroot-amd64 - - anchore/grype:{{.Tag}}-nonroot-arm64v8 - - anchore/grype:{{.Tag}}-nonroot-ppc64le - - anchore/grype:{{.Tag}}-nonroot-s390x - - - name_template: ghcr.io/anchore/grype:nonroot - image_templates: - - ghcr.io/anchore/grype:{{.Tag}}-nonroot-amd64 - - ghcr.io/anchore/grype:{{.Tag}}-nonroot-arm64v8 - - ghcr.io/anchore/grype:{{.Tag}}-nonroot-ppc64le - - ghcr.io/anchore/grype:{{.Tag}}-nonroot-s390x - - - name_template: anchore/grype:{{.Tag}}-nonroot - image_templates: - - anchore/grype:{{.Tag}}-nonroot-amd64 - - anchore/grype:{{.Tag}}-nonroot-arm64v8 - - anchore/grype:{{.Tag}}-nonroot-ppc64le - - anchore/grype:{{.Tag}}-nonroot-s390x - - - name_template: ghcr.io/anchore/grype:{{.Tag}}-nonroot - image_templates: - - ghcr.io/anchore/grype:{{.Tag}}-nonroot-amd64 - - ghcr.io/anchore/grype:{{.Tag}}-nonroot-arm64v8 - - ghcr.io/anchore/grype:{{.Tag}}-nonroot-ppc64le - - ghcr.io/anchore/grype:{{.Tag}}-nonroot-s390x - - # debug images... - - name_template: anchore/grype:debug - image_templates: - - anchore/grype:{{.Tag}}-debug-amd64 - - anchore/grype:{{.Tag}}-debug-arm64v8 - - anchore/grype:{{.Tag}}-debug-ppc64le - - anchore/grype:{{.Tag}}-debug-s390x - - - name_template: ghcr.io/anchore/grype:debug - image_templates: - - ghcr.io/anchore/grype:{{.Tag}}-debug-amd64 - - ghcr.io/anchore/grype:{{.Tag}}-debug-arm64v8 - - ghcr.io/anchore/grype:{{.Tag}}-debug-ppc64le - - ghcr.io/anchore/grype:{{.Tag}}-debug-s390x - - - name_template: anchore/grype:{{.Tag}}-debug - image_templates: - - anchore/grype:{{.Tag}}-debug-amd64 - - anchore/grype:{{.Tag}}-debug-arm64v8 - - anchore/grype:{{.Tag}}-debug-ppc64le - - anchore/grype:{{.Tag}}-debug-s390x - - - name_template: ghcr.io/anchore/grype:{{.Tag}}-debug - image_templates: - - ghcr.io/anchore/grype:{{.Tag}}-debug-amd64 - - ghcr.io/anchore/grype:{{.Tag}}-debug-arm64v8 - - ghcr.io/anchore/grype:{{.Tag}}-debug-ppc64le - - ghcr.io/anchore/grype:{{.Tag}}-debug-s390x - + - 'ghcr.io/rfjanit/grype:{{ .Version }}-amd64' + - 'ghcr.io/rfjanit/grype:{{ .Version }}-arm64' + - 'ghcr.io/rfjanit/grype:{{ .Version }}-s390x' + - 'ghcr.io/rfjanit/grype:{{ .Version }}-ppc64le' + +# Cosign keyless signing of the checksums file. Works with GitHub's OIDC +# provider — no external secrets needed, but the release workflow MUST +# declare `permissions: id-token: write` (§3.2 of RELEASE-GUIDE-GRYPE.md). +# This is one place where the Grype fork diverges from the Trivy fork: Trivy +# disabled signing for the first release; Grype keeps it since it's zero-cost +# once the workflow permission is in place. signs: - cmd: .tool/cosign signature: "${artifact}.sig" diff --git a/cmd/grype/cli/commands/root.go b/cmd/grype/cli/commands/root.go index b39a841404e..0f1dcbd6716 100644 --- a/cmd/grype/cli/commands/root.go +++ b/cmd/grype/cli/commands/root.go @@ -223,12 +223,14 @@ func runGrype(ctx context.Context, app clio.Application, opts *options.Grype, us return fmt.Errorf("failed to create VEX processor: %w", err) } + matchers := getMatchers(opts, pkgContext) + vulnMatcher := grype.VulnerabilityMatcher{ VulnerabilityProvider: vp, IgnoreRules: opts.Ignore, NormalizeByCVE: opts.ByCVE, FailSeverity: opts.FailOnSeverity(), - Matchers: getMatchers(opts), + Matchers: matchers, VexProcessor: vexProcessor, Alerts: grype.AlertsConfig{ EnableEOLDistroWarnings: opts.Alerts.EnableEOLDistroWarnings, @@ -394,8 +396,8 @@ func getMatcherConfig(opts *options.Grype) matcher.Config { } } -func getMatchers(opts *options.Grype) []match.Matcher { - return matcher.NewDefaultMatchers(getMatcherConfig(opts)) +func getMatchers(opts *options.Grype, pkgContext pkg.Context) []match.Matcher { + return matcher.ApplySelectionPolicy(matcher.NewDefaultMatchers(getMatcherConfig(opts)), pkgContext) } func getProviderConfig(opts *options.Grype) pkg.ProviderConfig { diff --git a/grype/db/internal/provider/unmarshal/os_vulnerability.go b/grype/db/internal/provider/unmarshal/os_vulnerability.go index 80b42d7a9b3..f44d1c791fd 100644 --- a/grype/db/internal/provider/unmarshal/os_vulnerability.go +++ b/grype/db/internal/provider/unmarshal/os_vulnerability.go @@ -14,6 +14,7 @@ type OSFixedIn struct { Module *string `json:"Module,omitempty"` Name string `json:"Name"` NamespaceName string `json:"NamespaceName"` + Identifier string `json:"Identifier,omitempty"` VendorAdvisory struct { AdvisorySummary []struct { ID string `json:"ID"` diff --git a/grype/db/v6/build/transformers/os/testdata/rapidfort-redhat-9.json b/grype/db/v6/build/transformers/os/testdata/rapidfort-redhat-9.json new file mode 100644 index 00000000000..f283410223e --- /dev/null +++ b/grype/db/v6/build/transformers/os/testdata/rapidfort-redhat-9.json @@ -0,0 +1,44 @@ +[ + { + "Vulnerability": { + "CVSS": [], + "Description": "curl wildcard certificate validation issue", + "FixedIn": [ + { + "Identifier": "el9", + "Name": "curl", + "NamespaceName": "rapidfort-redhat:9", + "Version": "None", + "VersionFormat": "rpm", + "VulnerableRange": ">= 0", + "VendorAdvisory": { + "AdvisorySummary": [ + { + "ID": "RHSA-TEST-EL9", + "Link": "https://access.redhat.com/errata/RHSA-TEST-EL9" + } + ], + "NoAdvisory": false + } + }, + { + "Identifier": "fc36", + "Name": "curl", + "NamespaceName": "rapidfort-redhat:9", + "Version": "7.78.0-4.fc36", + "VersionFormat": "rpm", + "VulnerableRange": ">= 0, < 7.78.0-4.fc36", + "Available": { + "Date": "2024-01-01T00:00:00Z", + "Kind": "advisory" + } + } + ], + "Link": "https://www.cve.org/CVERecord?id=CVE-2014-0139", + "Metadata": {}, + "Name": "CVE-2014-0139", + "NamespaceName": "rapidfort-redhat:9", + "Severity": "Low" + } + } +] diff --git a/grype/db/v6/build/transformers/os/testdata/rapidfort-ubuntu-20.04.json b/grype/db/v6/build/transformers/os/testdata/rapidfort-ubuntu-20.04.json new file mode 100644 index 00000000000..8999f2437da --- /dev/null +++ b/grype/db/v6/build/transformers/os/testdata/rapidfort-ubuntu-20.04.json @@ -0,0 +1,26 @@ +[ + { + "Vulnerability": { + "CVSS": [], + "Description": "curl: OAUTH2 bearer bypass in connection re-use", + "FixedIn": [ + { + "Name": "curl", + "NamespaceName": "rapidfort-ubuntu:20.04", + "Version": "7.68.0-1ubuntu2.10", + "VersionFormat": "dpkg", + "VulnerableRange": ">= 7.68.0, < 7.68.0-1ubuntu2.10", + "Available": { + "Date": "2022-05-01T00:00:00Z", + "Kind": "advisory" + } + } + ], + "Link": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-22576", + "Metadata": {}, + "Name": "CVE-2022-22576", + "NamespaceName": "rapidfort-ubuntu:20.04", + "Severity": "Medium" + } + } +] diff --git a/grype/db/v6/build/transformers/os/transform.go b/grype/db/v6/build/transformers/os/transform.go index be135ca42bf..48023c9aa57 100644 --- a/grype/db/v6/build/transformers/os/transform.go +++ b/grype/db/v6/build/transformers/os/transform.go @@ -170,6 +170,21 @@ func getFix(fixedInEntry unmarshal.OSFixedIn) *db.Fix { } var refs []db.Reference + if fixedInEntry.Identifier != "" { + rid := releaseIdentifierReferenceID(fixedInEntry.Identifier) + refURL := rid + for _, adv := range advisoryOrder { + if adv.link != "" { + refURL = adv.link + break + } + } + refs = append(refs, db.Reference{ + ID: rid, + URL: refURL, // prefer vendor advisory URL; else rid so toAdvisories includes the ref + Tags: []string{db.AdvisoryReferenceTag}, + }) + } for _, adv := range advisoryOrder { refs = append(refs, db.Reference{ ID: adv.id, @@ -211,6 +226,10 @@ func getFixAvailability(fixedInEntry unmarshal.OSFixedIn) *db.FixAvailability { } } +func releaseIdentifierReferenceID(identifier string) string { + return "release-identifier:" + strings.ToLower(strings.TrimSpace(identifier)) +} + func enforceConstraint(fixedVersion, vulnerableRange, format, vulnerabilityID string) string { if len(vulnerableRange) > 0 { return vulnerableRange @@ -250,15 +269,16 @@ func deriveConstraintFromFix(fixVersion, vulnerabilityID string) string { } type groupIndex struct { - name string - id string - osName string - osVersion string - osChannel string - hasModule bool - module string - format string - arch string + name string + id string + osName string + osVersion string + osChannel string + identifier string + hasModule bool + module string + format string + arch string } func groupFixedIns(vuln unmarshal.OSVulnerability) map[groupIndex][]unmarshal.OSFixedIn { @@ -275,14 +295,15 @@ func groupFixedIns(vuln unmarshal.OSVulnerability) map[groupIndex][]unmarshal.OS arch = *fixedIn.Arch } g := groupIndex{ - name: fixedIn.Name, - id: oi.id, - osName: oi.name, - osVersion: oi.version, - osChannel: oi.channel, - hasModule: fixedIn.Module != nil, - module: mod, - format: fixedIn.VersionFormat, + name: fixedIn.Name, + id: oi.id, + osName: oi.name, + osVersion: oi.version, + osChannel: oi.channel, + identifier: fixedIn.Identifier, + hasModule: fixedIn.Module != nil, + module: mod, + format: fixedIn.VersionFormat, // arch splits a per-arch fix into its own affected package handle so the architecture // qualifier can scope it; empty means the fix applies to all arches. arch: arch, @@ -305,6 +326,12 @@ func getPackageType(osName string) pkg.Type { return pkg.ApkPkg case "windows": return pkg.KbPkg + case "rapidfort-ubuntu": + return pkg.DebPkg + case "rapidfort-alpine": + return pkg.ApkPkg + case "rapidfort-redhat": + return pkg.RpmPkg } return "" diff --git a/grype/db/v6/build/transformers/os/transform_test.go b/grype/db/v6/build/transformers/os/transform_test.go index e536656d296..c6fb1dcd271 100644 --- a/grype/db/v6/build/transformers/os/transform_test.go +++ b/grype/db/v6/build/transformers/os/transform_test.go @@ -44,6 +44,18 @@ func expectedProvider(name string) *db.Provider { func TestTransform(t *testing.T) { + rapidfortUbuntu2004OS := &db.OperatingSystem{ + Name: "rapidfort-ubuntu", + ReleaseID: "rapidfort-ubuntu", + MajorVersion: "20", + MinorVersion: "04", + } + rapidfortRedHat9OS := &db.OperatingSystem{ + Name: "rapidfort-redhat", + ReleaseID: "rapidfort-redhat", + MajorVersion: "9", + } + alpineOS := &db.OperatingSystem{ Name: "alpine", ReleaseID: "alpine", @@ -1138,6 +1150,147 @@ func TestTransform(t *testing.T) { }, }, }, + { + name: "testdata/rapidfort-ubuntu-20.04.json", + provider: "rapidfort", + want: []transformers.RelatedEntries{ + { + VulnerabilityHandle: &db.VulnerabilityHandle{ + Name: "CVE-2022-22576", + ProviderID: "rapidfort", + Provider: expectedProvider("rapidfort"), + Status: "active", + BlobValue: &db.VulnerabilityBlob{ + ID: "CVE-2022-22576", + Description: "curl: OAUTH2 bearer bypass in connection re-use", + References: []db.Reference{ + {URL: "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-22576"}, + }, + Severities: []db.Severity{ + {Scheme: db.SeveritySchemeCHMLN, Value: "medium", Rank: 1}, + }, + }, + }, + Related: affectedPkgSlice( + db.AffectedPackageHandle{ + OperatingSystem: rapidfortUbuntu2004OS, + Package: &db.Package{Ecosystem: "deb", Name: "curl"}, + BlobValue: &db.PackageBlob{ + Ranges: []db.Range{ + { + Version: db.Version{ + Type: "dpkg", + Constraint: ">= 7.68.0, < 7.68.0-1ubuntu2.10", + }, + Fix: &db.Fix{ + Version: "7.68.0-1ubuntu2.10", + State: db.FixedStatus, + Detail: &db.FixDetail{ + Available: &db.FixAvailability{ + Date: timeRef(time.Date(2022, 5, 1, 0, 0, 0, 0, time.UTC)), + Kind: "advisory", + }, + }, + }, + }, + }, + }, + }, + ), + }, + }, + }, + { + name: "testdata/rapidfort-redhat-9.json", + provider: "rapidfort", + want: []transformers.RelatedEntries{ + { + VulnerabilityHandle: &db.VulnerabilityHandle{ + Name: "CVE-2014-0139", + ProviderID: "rapidfort", + Provider: expectedProvider("rapidfort"), + Status: "active", + BlobValue: &db.VulnerabilityBlob{ + ID: "CVE-2014-0139", + Description: "curl wildcard certificate validation issue", + References: []db.Reference{ + {URL: "https://www.cve.org/CVERecord?id=CVE-2014-0139"}, + }, + Severities: []db.Severity{ + {Scheme: db.SeveritySchemeCHMLN, Value: "low", Rank: 1}, + }, + }, + }, + Related: affectedPkgSlice( + db.AffectedPackageHandle{ + OperatingSystem: rapidfortRedHat9OS, + Package: &db.Package{Ecosystem: "rpm", Name: "curl"}, + BlobValue: &db.PackageBlob{ + Qualifiers: &db.PackageQualifiers{RpmModularity: strRef("")}, + Ranges: []db.Range{ + { + Version: db.Version{ + Type: "rpm", + Constraint: ">= 0", + }, + Fix: &db.Fix{ + Version: "", + State: db.NotFixedStatus, + Detail: &db.FixDetail{ + References: []db.Reference{ + { + ID: "release-identifier:el9", + URL: "https://access.redhat.com/errata/RHSA-TEST-EL9", + Tags: []string{db.AdvisoryReferenceTag}, + }, + { + ID: "RHSA-TEST-EL9", + URL: "https://access.redhat.com/errata/RHSA-TEST-EL9", + Tags: []string{db.AdvisoryReferenceTag}, + }, + }, + }, + }, + }, + }, + }, + }, + db.AffectedPackageHandle{ + OperatingSystem: rapidfortRedHat9OS, + Package: &db.Package{Ecosystem: "rpm", Name: "curl"}, + BlobValue: &db.PackageBlob{ + Qualifiers: &db.PackageQualifiers{RpmModularity: strRef("")}, + Ranges: []db.Range{ + { + Version: db.Version{ + Type: "rpm", + Constraint: ">= 0, < 7.78.0-4.fc36", + }, + Fix: &db.Fix{ + Version: "7.78.0-4.fc36", + State: db.FixedStatus, + Detail: &db.FixDetail{ + Available: &db.FixAvailability{ + Date: timeRef(time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)), + Kind: "advisory", + }, + References: []db.Reference{ + { + ID: "release-identifier:fc36", + URL: "release-identifier:fc36", + Tags: []string{db.AdvisoryReferenceTag}, + }, + }, + }, + }, + }, + }, + }, + }, + ), + }, + }, + }, { name: "testdata/fedora-39.json", provider: "fedora", @@ -1506,6 +1659,33 @@ func TestGetOSInfo(t *testing.T) { channel: "eus", }, }, + { + name: "rapidfort ubuntu 20.04 (provider-curated, 2-part namespace)", + group: "rapidfort-ubuntu:20.04", + expected: osInfo{ + name: "rapidfort-ubuntu", + id: "rapidfort-ubuntu", + version: "20.04", + }, + }, + { + name: "rapidfort alpine 3.20 (provider-curated, 2-part namespace)", + group: "rapidfort-alpine:3.20", + expected: osInfo{ + name: "rapidfort-alpine", + id: "rapidfort-alpine", + version: "3.20", + }, + }, + { + name: "rapidfort redhat 9 (provider-curated, 2-part namespace)", + group: "rapidfort-redhat:9", + expected: osInfo{ + name: "rapidfort-redhat", + id: "rapidfort-redhat", + version: "9", + }, + }, } for _, tt := range tests { diff --git a/grype/db/v6/distribution/client.go b/grype/db/v6/distribution/client.go index 92ec2e4afb7..188096335ad 100644 --- a/grype/db/v6/distribution/client.go +++ b/grype/db/v6/distribution/client.go @@ -53,7 +53,7 @@ type client struct { func DefaultConfig() Config { return Config{ - LatestURL: "https://grype.anchore.io/databases", + LatestURL: "https://github.com/rfJanit/grype-db-rf/releases/download", RequireUpdateCheck: false, CheckTimeout: 30 * time.Second, UpdateTimeout: 300 * time.Second, diff --git a/grype/distro/distro_test.go b/grype/distro/distro_test.go index f7ed8373c77..8f8d8f45528 100644 --- a/grype/distro/distro_test.go +++ b/grype/distro/distro_test.go @@ -342,6 +342,9 @@ func Test_NewDistroFromRelease_Coverage(t *testing.T) { // Somewhat cheating with Windows. There is no support for detecting/parsing a Windows OS, so it is not // possible to comply with this test unless it is added manually to the "observed distros" definedDistros.Remove(string(Windows)) + definedDistros.Remove(string(RapidFortUbuntu)) + definedDistros.Remove(string(RapidFortAlpine)) + definedDistros.Remove(string(RapidFortRedHat)) tests := []struct { Name string diff --git a/grype/distro/type.go b/grype/distro/type.go index efac2c7900d..5acdcaffe1f 100644 --- a/grype/distro/type.go +++ b/grype/distro/type.go @@ -10,34 +10,37 @@ type Type string const ( // represents the set of supported Linux Distributions - Debian Type = "debian" - Ubuntu Type = "ubuntu" - RedHat Type = "redhat" - CentOS Type = "centos" - Fedora Type = "fedora" - Alpine Type = "alpine" - Busybox Type = "busybox" - AmazonLinux Type = "amazonlinux" - OracleLinux Type = "oraclelinux" - ArchLinux Type = "archlinux" - OpenSuseLeap Type = "opensuseleap" - SLES Type = "sles" - Photon Type = "photon" - Echo Type = "echo" - Windows Type = "windows" - Mariner Type = "mariner" - Azure Type = "azurelinux" - RockyLinux Type = "rockylinux" - AlmaLinux Type = "almalinux" - Gentoo Type = "gentoo" - Wolfi Type = "wolfi" - Chainguard Type = "chainguard" - MinimOS Type = "minimos" - Raspbian Type = "raspbian" - Scientific Type = "scientific" - SecureOS Type = "secureos" - PostmarketOS Type = "postmarketos" - Hummingbird Type = "hummingbird" + Debian Type = "debian" + Ubuntu Type = "ubuntu" + RedHat Type = "redhat" + CentOS Type = "centos" + Fedora Type = "fedora" + Alpine Type = "alpine" + Busybox Type = "busybox" + AmazonLinux Type = "amazonlinux" + OracleLinux Type = "oraclelinux" + ArchLinux Type = "archlinux" + OpenSuseLeap Type = "opensuseleap" + SLES Type = "sles" + Photon Type = "photon" + Echo Type = "echo" + Windows Type = "windows" + Mariner Type = "mariner" + Azure Type = "azurelinux" + RockyLinux Type = "rockylinux" + AlmaLinux Type = "almalinux" + Gentoo Type = "gentoo" + Wolfi Type = "wolfi" + Chainguard Type = "chainguard" + MinimOS Type = "minimos" + Raspbian Type = "raspbian" + Scientific Type = "scientific" + SecureOS Type = "secureos" + PostmarketOS Type = "postmarketos" + Hummingbird Type = "hummingbird" + RapidFortUbuntu Type = "rapidfort-ubuntu" + RapidFortAlpine Type = "rapidfort-alpine" + RapidFortRedHat Type = "rapidfort-redhat" ) // All contains all Linux distribution options @@ -70,37 +73,43 @@ var All = []Type{ SecureOS, PostmarketOS, Hummingbird, + RapidFortUbuntu, + RapidFortAlpine, + RapidFortRedHat, } // IDMapping maps a distro ID from the /etc/os-release (e.g. like "ubuntu") to a Distro type. var IDMapping = map[string]Type{ - "debian": Debian, - "ubuntu": Ubuntu, - "rhel": RedHat, - "centos": CentOS, - "fedora": Fedora, - "alpine": Alpine, - "busybox": Busybox, - "amzn": AmazonLinux, - "ol": OracleLinux, - "arch": ArchLinux, - "opensuse-leap": OpenSuseLeap, - "sles": SLES, - "photon": Photon, - "echo": Echo, - "mariner": Mariner, - "azurelinux": Azure, - "rocky": RockyLinux, - "almalinux": AlmaLinux, - "gentoo": Gentoo, - "wolfi": Wolfi, - "chainguard": Chainguard, - "minimos": MinimOS, - "raspbian": Raspbian, - "scientific": Scientific, - "secureos": SecureOS, - "postmarketos": PostmarketOS, - "hummingbird": Hummingbird, + "debian": Debian, + "ubuntu": Ubuntu, + "rhel": RedHat, + "centos": CentOS, + "fedora": Fedora, + "alpine": Alpine, + "busybox": Busybox, + "amzn": AmazonLinux, + "ol": OracleLinux, + "arch": ArchLinux, + "opensuse-leap": OpenSuseLeap, + "sles": SLES, + "photon": Photon, + "echo": Echo, + "mariner": Mariner, + "azurelinux": Azure, + "rocky": RockyLinux, + "almalinux": AlmaLinux, + "gentoo": Gentoo, + "wolfi": Wolfi, + "chainguard": Chainguard, + "minimos": MinimOS, + "raspbian": Raspbian, + "scientific": Scientific, + "secureos": SecureOS, + "postmarketos": PostmarketOS, + "hummingbird": Hummingbird, + "rapidfort-ubuntu": RapidFortUbuntu, + "rapidfort-alpine": RapidFortAlpine, + "rapidfort-redhat": RapidFortRedHat, } // aliasTypes maps common aliases to their corresponding Type. diff --git a/grype/match/matcher_type.go b/grype/match/matcher_type.go index 58a864918a6..a8948356500 100644 --- a/grype/match/matcher_type.go +++ b/grype/match/matcher_type.go @@ -20,6 +20,7 @@ const ( BitnamiMatcher MatcherType = "bitnami-matcher" PacmanMatcher MatcherType = "pacman-matcher" HexMatcher MatcherType = "hex-matcher" + RapidFortMatcher MatcherType = "rapidfort-matcher" ) var AllMatcherTypes = []MatcherType{ diff --git a/grype/matcher/matchers.go b/grype/matcher/matchers.go index 7ca44d65804..ce09744dce5 100644 --- a/grype/matcher/matchers.go +++ b/grype/matcher/matchers.go @@ -14,10 +14,12 @@ import ( "github.com/anchore/grype/grype/matcher/pacman" "github.com/anchore/grype/grype/matcher/portage" "github.com/anchore/grype/grype/matcher/python" + "github.com/anchore/grype/grype/matcher/rapidfort" "github.com/anchore/grype/grype/matcher/rpm" "github.com/anchore/grype/grype/matcher/ruby" "github.com/anchore/grype/grype/matcher/rust" "github.com/anchore/grype/grype/matcher/stock" + "github.com/anchore/grype/grype/pkg" ) // Config contains values used by individual matcher structs for advanced configuration @@ -55,3 +57,44 @@ func NewDefaultMatchers(mc Config) []match.Matcher { &pacman.Matcher{}, } } + +func ApplySelectionPolicy(matchers []match.Matcher, ctx pkg.Context) []match.Matcher { + if !rapidfort.IsRapidFortImage(ctx.Source) { + return matchers + } + + rfMatcher := rapidfort.NewMatcher() + replacements := map[match.MatcherType]match.Matcher{ + match.DpkgMatcher: rfMatcher, + match.ApkMatcher: rfMatcher, + match.RpmMatcher: rfMatcher, + } + + return applyMatcherOverrides(matchers, replacements) +} + +func applyMatcherOverrides(matchers []match.Matcher, replacements map[match.MatcherType]match.Matcher) []match.Matcher { + if len(replacements) == 0 { + return matchers + } + + var out []match.Matcher + added := make(map[match.MatcherType]struct{}) + + for _, m := range matchers { + replacement, ok := replacements[m.Type()] + if !ok { + out = append(out, m) + continue + } + + if _, exists := added[replacement.Type()]; exists { + continue + } + + out = append(out, replacement) + added[replacement.Type()] = struct{}{} + } + + return out +} diff --git a/grype/matcher/matchers_test.go b/grype/matcher/matchers_test.go new file mode 100644 index 00000000000..9792a5c7d80 --- /dev/null +++ b/grype/matcher/matchers_test.go @@ -0,0 +1,62 @@ +package matcher + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/anchore/grype/grype/match" + "github.com/anchore/grype/grype/pkg" + "github.com/anchore/syft/syft/source" +) + +func TestApplySelectionPolicy_UsesDefaultOSMatchersForNonRapidFortSources(t *testing.T) { + matchers := ApplySelectionPolicy(NewDefaultMatchers(Config{}), pkg.Context{}) + + assert.Contains(t, matcherTypes(matchers), match.DpkgMatcher) + assert.Contains(t, matcherTypes(matchers), match.ApkMatcher) + assert.NotContains(t, matcherTypes(matchers), match.RapidFortMatcher) +} + +func TestApplySelectionPolicy_UsesDefaultOSMatchersForNonRapidFortImage(t *testing.T) { + matchers := ApplySelectionPolicy(NewDefaultMatchers(Config{}), pkg.Context{ + Source: &source.Description{ + Metadata: source.ImageMetadata{ + Labels: map[string]string{ + "maintainer": "Other Vendor ", + }, + }, + }, + }) + + assert.Contains(t, matcherTypes(matchers), match.DpkgMatcher) + assert.Contains(t, matcherTypes(matchers), match.ApkMatcher) + assert.NotContains(t, matcherTypes(matchers), match.RapidFortMatcher) +} + +func TestApplySelectionPolicy_UsesRapidFortMatcherInsteadOfDebAndApkMatchers(t *testing.T) { + matchers := ApplySelectionPolicy(NewDefaultMatchers(Config{}), pkg.Context{ + Source: &source.Description{ + Metadata: source.ImageMetadata{ + Labels: map[string]string{ + "maintainer": "RapidFort Curation Team ", + }, + }, + }, + }) + + assert.Contains(t, matcherTypes(matchers), match.RapidFortMatcher) + assert.NotContains(t, matcherTypes(matchers), match.DpkgMatcher) + assert.NotContains(t, matcherTypes(matchers), match.ApkMatcher) + assert.NotContains(t, matcherTypes(matchers), match.RpmMatcher) + assert.Contains(t, matcherTypes(matchers), match.JavaMatcher) + assert.Contains(t, matcherTypes(matchers), match.StockMatcher) +} + +func matcherTypes(matchers []match.Matcher) []match.MatcherType { + out := make([]match.MatcherType, 0, len(matchers)) + for _, m := range matchers { + out = append(out, m.Type()) + } + return out +} diff --git a/grype/matcher/rapidfort/label.go b/grype/matcher/rapidfort/label.go new file mode 100644 index 00000000000..2e57519580a --- /dev/null +++ b/grype/matcher/rapidfort/label.go @@ -0,0 +1,32 @@ +package rapidfort + +import ( + "strings" + + "github.com/anchore/syft/syft/source" +) + +const maintainerLabel = "maintainer" + +// rapidFortMaintainerPrefix is the case-insensitive prefix used by all RapidFort +// maintainer label values (e.g. "RapidFort Curation Team "). +const rapidFortMaintainerPrefix = "rapidfort" + +// IsRapidFortImage returns true when the source is a container image whose +// "maintainer" label starts with "RapidFort" (case-insensitive) — the indicator +// that it is a RapidFort-curated image and should be scanned against RF advisories. +func IsRapidFortImage(src *source.Description) bool { + if src == nil { + return false + } + meta, ok := src.Metadata.(source.ImageMetadata) + if !ok { + return false + } + for k, v := range meta.Labels { + if strings.EqualFold(k, maintainerLabel) && strings.HasPrefix(strings.ToLower(v), rapidFortMaintainerPrefix) { + return true + } + } + return false +} diff --git a/grype/matcher/rapidfort/label_test.go b/grype/matcher/rapidfort/label_test.go new file mode 100644 index 00000000000..223d12639c9 --- /dev/null +++ b/grype/matcher/rapidfort/label_test.go @@ -0,0 +1,89 @@ +package rapidfort + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/anchore/syft/syft/source" +) + +func TestIsRapidFortImage(t *testing.T) { + tests := []struct { + name string + src *source.Description + expected bool + }{ + { + name: "nil source returns false", + src: nil, + expected: false, + }, + { + name: "non-image source (no ImageMetadata) returns false", + src: &source.Description{ + Metadata: source.DirectoryMetadata{}, + }, + expected: false, + }, + { + name: "image with rapidfort maintainer label matches", + src: &source.Description{ + Metadata: source.ImageMetadata{ + Labels: map[string]string{ + "maintainer": "RapidFort Curation Team ", + }, + }, + }, + expected: true, + }, + { + name: "label key is case-insensitive", + src: &source.Description{ + Metadata: source.ImageMetadata{ + Labels: map[string]string{ + "Maintainer": "RapidFort Curation Team ", + }, + }, + }, + expected: true, + }, + { + name: "label value prefix is case-insensitive", + src: &source.Description{ + Metadata: source.ImageMetadata{ + Labels: map[string]string{ + "maintainer": "RAPIDFORT Automation", + }, + }, + }, + expected: true, + }, + { + name: "non-rapidfort maintainer label does not match", + src: &source.Description{ + Metadata: source.ImageMetadata{ + Labels: map[string]string{ + "maintainer": "Other Vendor ", + }, + }, + }, + expected: false, + }, + { + name: "image with no labels returns false", + src: &source.Description{ + Metadata: source.ImageMetadata{ + Labels: map[string]string{}, + }, + }, + expected: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expected, IsRapidFortImage(test.src)) + }) + } +} diff --git a/grype/matcher/rapidfort/matcher.go b/grype/matcher/rapidfort/matcher.go new file mode 100644 index 00000000000..ab98ae69f32 --- /dev/null +++ b/grype/matcher/rapidfort/matcher.go @@ -0,0 +1,371 @@ +// Package rapidfort provides vulnerability matching for RapidFort-curated images. +// It is activated only when the scanned image is identified as RapidFort-curated +// (via the "maintainer=RapidFort" Docker label). It queries the DB for advisories +// stored under the "rapidfort-" OS name (e.g. "rapidfort-ubuntu"), +// which are never returned for standard distro scans. +package rapidfort + +import ( + "fmt" + "regexp" + "strings" + + "github.com/anchore/grype/grype/distro" + "github.com/anchore/grype/grype/match" + "github.com/anchore/grype/grype/matcher/internal" + "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/search" + "github.com/anchore/grype/grype/version" + "github.com/anchore/grype/grype/vulnerability" + "github.com/anchore/grype/internal/log" + syftPkg "github.com/anchore/syft/syft/pkg" +) + +// rfDistroMap maps a detected base OS type to its RF-prefixed counterpart in the DB. +// Extend this (plus getPackageType in v6 transform.go) when RF curates a new base distro. +var rfDistroMap = map[distro.Type]distro.Type{ + distro.Ubuntu: distro.RapidFortUbuntu, + distro.Alpine: distro.RapidFortAlpine, + distro.RedHat: distro.RapidFortRedHat, +} + +var ( + fedoraReleasePattern = regexp.MustCompile(`\.fc\d+(?:[._-].*)?$`) + rhelReleasePattern = regexp.MustCompile(`\.el(\d+)`) + rfReleasePattern = regexp.MustCompile(`\.rf(?:[._-].*)?$`) +) + +const ( + reasonNoReleaseIdentifier = "no release identifier on vulnerability" + reasonCannotDeriveReleaseID = "unable to derive rpm release identifier and no el release identifier on vulnerability" + reasonReleaseIdentifierMismatch = "package release identifier did not match vulnerability" +) + +// Matcher matches packages against RapidFort-specific advisories. +// It is only instantiated when the image under scan is detected as RF-curated. +type Matcher struct{} + +func NewMatcher() *Matcher { + return &Matcher{} +} + +func (m *Matcher) PackageTypes() []syftPkg.Type { + return []syftPkg.Type{syftPkg.DebPkg, syftPkg.ApkPkg, syftPkg.RpmPkg} +} + +func (m *Matcher) Type() match.MatcherType { + return match.RapidFortMatcher +} + +func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) { + if p.Distro == nil { + return nil, nil, nil + } + + // Only match against supported RF-curated base distros (ubuntu/alpine/redhat). + rfDistroType, ok := rfDistroMap[p.Distro.Type] + if !ok { + return nil, nil, nil + } + + // Build the RF-prefixed distro for DB queries. + // Intentionally omit the codename (e.g. "jammy") — the RF DB stores no codenames, + // and passing one would add a codename filter that yields 0 results. + rfDistro := distro.New(rfDistroType, rapidfortDistroVersion(*p.Distro, rfDistroType), "") + log.WithFields( + "package", p.Name, + "version", p.Version, + "sourceDistro", p.Distro.String(), + "rapidfortDistro", rfDistro.String(), + ).Debug("rapidfort matcher: remapped distro for RapidFort query") + + var all []match.Match + + // 1. Match by source/upstream package names first, when syft populated Upstreams (same idea as + // dpkg/rpm matchers): e.g. Debian binary → source, or RPM subpackage → source name from + // SourceRPM metadata. If Upstreams is empty, this step is a no-op. + sourceMatches, err := m.matchUpstreamPackages(store, p, rfDistro) + if err != nil { + return nil, nil, fmt.Errorf("rapidfort matcher failed upstream lookup for %q: %w", p.Name, err) + } + all = append(all, sourceMatches...) + + // 2. Direct lookup by installed package name (covers RF-prefixed binaries and advisories + // keyed on the subpackage/binary name rather than the source). + rfPkg := p + rfPkg.Distro = rfDistro + binaryMatches, err := m.matchPackageByDistro(store, rfPkg, &p, match.ExactDirectMatch) + if err != nil { + return nil, nil, fmt.Errorf("rapidfort matcher failed binary lookup for %q: %w", p.Name, err) + } + all = append(all, binaryMatches...) + + // 3. Post-filter: suppress any match where the installed version is already at or + // beyond the recorded fix version. This is an explicit safety net — well-formed + // RF advisories use strict "< fixVersion" constraints so the DB query already + // excludes fixed packages, but this guards against edge cases (e.g. advisory + // has only an "introduced" event and was later patched without updating the DB). + // + // Priority: fix-version check first, range constraint second. + format := pkg.VersionFormat(p) + filtered := filterAlreadyFixed(all, p.Version, format) + + // 4. Dedup: upstream and direct lookups can surface the same CVE for the same package. + return dedupMatches(filtered), nil, nil +} + +func rapidfortDistroVersion(baseDistro distro.Distro, rfDistroType distro.Type) string { + if rfDistroType == distro.RapidFortRedHat && baseDistro.MajorVersion() != "" { + return baseDistro.MajorVersion() + } + return baseDistro.Version +} + +// matchUpstreamPackages searches the RF DB using pkg.UpstreamPackages(p) — names from syft’s +// Upstreams field (Debian source packages, RPM source names derived from SourceRPM, etc.). +// Works for DebPkg, ApkPkg, and RpmPkg whenever the catalog includes upstream metadata. +func (m *Matcher) matchUpstreamPackages(store vulnerability.Provider, p pkg.Package, rfDistro *distro.Distro) ([]match.Match, error) { + var matches []match.Match + + for _, upstream := range pkg.UpstreamPackages(p) { + upstream.Distro = rfDistro + found, err := m.matchPackageByDistro(store, upstream, &p, match.ExactIndirectMatch) + if err != nil { + return nil, fmt.Errorf("failed RF upstream lookup for source %q: %w", upstream.Name, err) + } + matches = append(matches, found...) + } + + // Mark all upstream-sourced matches as indirect (the artifact is the binary pkg). + match.ConvertToIndirectMatches(matches, p) + return matches, nil +} + +func (m *Matcher) matchPackageByDistro(store vulnerability.Provider, searchPkg pkg.Package, catalogPkg *pkg.Package, ty match.Type) ([]match.Match, error) { + if searchPkg.Distro == nil { + return nil, nil + } + + pkgVersion := version.New(searchPkg.Version, pkg.VersionFormat(searchPkg)) + criteria := []vulnerability.Criteria{ + search.ByPackageName(searchPkg.Name), + search.ByDistro(*searchPkg.Distro), + internal.OnlyQualifiedPackages(searchPkg), + internal.OnlyVulnerableVersions(pkgVersion), + } + + if searchPkg.Distro.Type == distro.RapidFortRedHat { + criteria = append(criteria, byRPMReleaseIdentifier(searchPkg)) + } + + vulns, err := store.FindVulnerabilities(criteria...) + if err != nil { + return nil, fmt.Errorf("matcher failed to fetch distro=%q pkg=%q: %w", searchPkg.Distro, searchPkg.Name, err) + } + + log.WithFields( + "package", searchPkg.Name, + "version", searchPkg.Version, + "distro", searchPkg.Distro.String(), + "matches", len(vulns), + ).Debug("rapidfort matcher: fetched vulnerabilities for package") + + var matches []match.Match + for _, vuln := range vulns { + matches = append(matches, match.Match{ + Vulnerability: vuln, + Package: matchPackage(searchPkg, catalogPkg), + Details: distroMatchDetails(m.Type(), ty, searchPkg, catalogPkg, vuln), + }) + } + + return matches, nil +} + +func byRPMReleaseIdentifier(p pkg.Package) vulnerability.Criteria { + return search.ByFunc(func(vuln vulnerability.Vulnerability) (bool, string, error) { + expected := installedReleaseIdentifier(p) + if expected == "" { + for _, advisory := range vuln.Advisories { + id := advisoryID(advisory) + if strings.HasPrefix(id, "release-identifier:el") { + return true, "", nil + } + } + return false, reasonCannotDeriveReleaseID, nil + } + + found := false + for _, advisory := range vuln.Advisories { + id := advisoryID(advisory) + if !strings.HasPrefix(id, "release-identifier:") { + continue + } + found = true + if strings.TrimPrefix(id, "release-identifier:") == expected { + return true, "", nil + } + } + + if !found { + return false, reasonNoReleaseIdentifier, nil + } + + return false, reasonReleaseIdentifierMismatch, nil + }) +} + +func advisoryID(advisory vulnerability.Advisory) string { + return strings.ToLower(strings.TrimSpace(advisory.ID)) +} + +func installedReleaseIdentifier(p pkg.Package) string { + version := strings.ToLower(strings.TrimSpace(p.Version)) + + if id := fedoraReleaseID(p, version); id != "" { + return id + } + if id := rfReleaseID(p, version); id != "" { + return id + } + if id := rhelReleaseID(p, version); id != "" { + return id + } + if id := rfNameReleaseID(p); id != "" { + return id + } + + return "" +} + +func fedoraReleaseID(p pkg.Package, version string) string { + if !fedoraReleasePattern.MatchString(version) { + return "" + } + + idx := strings.LastIndex(version, ".fc") + if idx < 0 { + return "" + } + + id := version[idx+1:] + return id +} + +func rfReleaseID(p pkg.Package, version string) string { + if !rfReleasePattern.MatchString(version) { + return "" + } + + return "rf" +} + +func rhelReleaseID(p pkg.Package, version string) string { + if !rhelReleasePattern.MatchString(version) { + return "" + } + + match := rhelReleasePattern.FindStringSubmatch(version) + if len(match) != 2 { + return "" + } + id := "el" + match[1] + return id +} + +func rfNameReleaseID(p pkg.Package) string { + if !strings.HasPrefix(strings.ToLower(p.Name), "rf-") { + return "" + } + return "rf" +} + +func matchPackage(searchPkg pkg.Package, catalogPkg *pkg.Package) pkg.Package { + if catalogPkg != nil { + return *catalogPkg + } + return searchPkg +} + +func distroMatchDetails(upstreamMatcher match.MatcherType, ty match.Type, searchPkg pkg.Package, catalogPkg *pkg.Package, vuln vulnerability.Vulnerability) []match.Detail { + return []match.Detail{ + { + Type: ty, + Matcher: upstreamMatcher, + SearchedBy: match.DistroParameters{ + Distro: match.DistroIdentification{ + Type: searchPkg.Distro.Type.String(), + Version: searchPkg.Distro.Version, + }, + Package: match.PackageParameter{ + Name: searchPkg.Name, + Version: searchPkg.Version, + }, + Namespace: vuln.Namespace, + }, + Found: match.DistroResult{ + VulnerabilityID: vuln.ID, + VersionConstraint: vuln.Constraint.String(), + }, + Confidence: 1.0, + }, + } +} + +// filterAlreadyFixed removes matches where the installed version exactly equals +// a recorded fix version, meaning the package has been patched. +// +// Logic (in priority order): +// 1. If Fix.State == FixStateFixed AND installedVersion == fixVersion → suppress. +// 2. Otherwise the range constraint (already evaluated by FindVulnerabilities) decides. +func filterAlreadyFixed(matches []match.Match, installedVer string, format version.Format) []match.Match { + installed := version.New(installedVer, format) + var out []match.Match + for _, m := range matches { + fix := m.Vulnerability.Fix + if fix.State == vulnerability.FixStateFixed { + suppressed := false + for _, fixVer := range fix.Versions { + if fixVer == "" || fixVer == "None" { + continue + } + // cmp = fixVersion.Compare(installed): 0 means installed == fixVersion + cmp, err := version.New(fixVer, format).Compare(installed) + if err != nil { + log.WithFields("pkg", installedVer, "fixVersion", fixVer, "err", err). + Trace("rapidfort: could not compare fix version, keeping match") + continue + } + if cmp == 0 { + // installed version equals the fix — package is patched + log.WithFields("vuln", m.Vulnerability.ID, "pkg", installedVer, "fixVersion", fixVer). + Trace("rapidfort: suppressing match — installed version == fix version") + suppressed = true + break + } + } + if suppressed { + vulnerability.LogDropped(m.Vulnerability.ID, "rapidfort-matcher", + "installed version equals fix version", installedVer) + continue + } + } + out = append(out, m) + } + return out +} + +// dedupMatches removes duplicate matches with the same vulnerability ID. +// Source and binary package lookups can surface the same CVE for the same package. +func dedupMatches(matches []match.Match) []match.Match { + seen := make(map[string]struct{}, len(matches)) + out := make([]match.Match, 0, len(matches)) + for _, m := range matches { + if _, ok := seen[m.Vulnerability.ID]; ok { + continue + } + seen[m.Vulnerability.ID] = struct{}{} + out = append(out, m) + } + return out +} diff --git a/grype/matcher/rapidfort/matcher_test.go b/grype/matcher/rapidfort/matcher_test.go new file mode 100644 index 00000000000..72b2167ca2b --- /dev/null +++ b/grype/matcher/rapidfort/matcher_test.go @@ -0,0 +1,172 @@ +package rapidfort + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/anchore/grype/grype/distro" + "github.com/anchore/grype/grype/match" + "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/version" + "github.com/anchore/grype/grype/vulnerability" +) + +func TestInstalledReleaseIdentifier(t *testing.T) { + tests := []struct { + name string + pkg pkg.Package + expected string + }{ + { + name: "fedora release", + pkg: pkg.Package{Name: "curl", Version: "8.6.0-7.fc41"}, + expected: "fc41", + }, + { + name: "rapidfort release from version suffix", + pkg: pkg.Package{Name: "curl", Version: "3.6.1-11.rf"}, + expected: "rf", + }, + { + name: "rhel release with minor suffix", + pkg: pkg.Package{Name: "curl", Version: "7.88.1-5.el9_4.1"}, + expected: "el9", + }, + { + name: "rf package fallback", + pkg: pkg.Package{Name: "rf-curl", Version: "1.2.3-1"}, + expected: "rf", + }, + { + name: "unknown release", + pkg: pkg.Package{Name: "curl", Version: "1.2.3-1"}, + expected: "", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expected, installedReleaseIdentifier(test.pkg)) + }) + } +} + +func TestByRPMReleaseIdentifier(t *testing.T) { + criteria := byRPMReleaseIdentifier(pkg.Package{ + Name: "curl", + Version: "8.6.0-7.fc41", + Distro: distro.New(distro.RapidFortRedHat, "9", ""), + }) + + matched, reason, err := criteria.MatchesVulnerability(vulnerability.Vulnerability{ + Advisories: []vulnerability.Advisory{ + {ID: "release-identifier:fc41"}, + }, + }) + assert.NoError(t, err) + assert.True(t, matched) + assert.Empty(t, reason) + + matched, reason, err = criteria.MatchesVulnerability(vulnerability.Vulnerability{ + Advisories: []vulnerability.Advisory{ + {ID: "release-identifier:el9"}, + }, + }) + assert.NoError(t, err) + assert.False(t, matched) + assert.Equal(t, reasonReleaseIdentifierMismatch, reason) +} + +func TestByRPMReleaseIdentifier_FallsBackToELWhenInstalledIdentifierUnknown(t *testing.T) { + criteria := byRPMReleaseIdentifier(pkg.Package{ + Name: "curl", + Version: "1.2.3-1", + Distro: distro.New(distro.RapidFortRedHat, "9", ""), + }) + + matched, reason, err := criteria.MatchesVulnerability(vulnerability.Vulnerability{ + Advisories: []vulnerability.Advisory{ + {ID: "release-identifier:el9"}, + }, + }) + assert.NoError(t, err) + assert.True(t, matched) + assert.Empty(t, reason) + + matched, reason, err = criteria.MatchesVulnerability(vulnerability.Vulnerability{ + Advisories: []vulnerability.Advisory{ + {ID: "release-identifier:fc41"}, + }, + }) + assert.NoError(t, err) + assert.False(t, matched) + assert.Contains(t, reason, "no el release identifier") +} + +func TestRapidfortDistroVersion(t *testing.T) { + tests := []struct { + name string + baseDistro distro.Distro + rfDistro distro.Type + expectedVer string + }{ + { + name: "rapidfort redhat uses major version only", + baseDistro: *distro.New(distro.RedHat, "9.5", ""), + rfDistro: distro.RapidFortRedHat, + expectedVer: "9", + }, + { + name: "rapidfort redhat keeps major only when already major", + baseDistro: *distro.New(distro.RedHat, "9", ""), + rfDistro: distro.RapidFortRedHat, + expectedVer: "9", + }, + { + name: "rapidfort ubuntu keeps full version", + baseDistro: *distro.New(distro.Ubuntu, "20.04", ""), + rfDistro: distro.RapidFortUbuntu, + expectedVer: "20.04", + }, + { + name: "rapidfort alpine keeps full version", + baseDistro: *distro.New(distro.Alpine, "3.15", ""), + rfDistro: distro.RapidFortAlpine, + expectedVer: "3.15", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expectedVer, rapidfortDistroVersion(test.baseDistro, test.rfDistro)) + }) + } +} + +func TestFilterAlreadyFixed_KeepsVulnBeforeFixVersionAcrossReleases(t *testing.T) { + installed := "1.7.2-1.fc41" + format := version.RpmFormat + + matches := []match.Match{ + { + Vulnerability: vulnerability.Vulnerability{ + Reference: vulnerability.Reference{ + ID: "CVE-TEST-FC-CROSS-RELEASE", + }, + Fix: vulnerability.Fix{ + State: vulnerability.FixStateFixed, + Versions: []string{"1.7.5-1.fc45"}, + }, + }, + }, + } + + filtered := filterAlreadyFixed(matches, installed, format) + + // Installed version is before the fixed version in a later Fedora release, + // so the match must be kept and fix metadata preserved for reporting. + if assert.Len(t, filtered, 1) { + assert.Equal(t, matches[0].Vulnerability.Fix, filtered[0].Vulnerability.Fix) + } +}