Compare commits

..
Author SHA1 Message Date
Will Murphy e0bfd2f98a fix: read only single package.json or package-lock.json document
Previously, this used a for loop over a json decoder, reading N
package.json objects from a file stream. Because a single file stream
containing the JSON for more than one package.json is unexpected, and
because on some filesystems the loop failed to exit, instead read a
single package.json object from the decoder.

Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
2025-02-28 13:07:27 -05:00
1193 changed files with 10479 additions and 163748 deletions
+22 -9
View File
@@ -2,7 +2,7 @@ tools:
# we want to use a pinned version of binny to manage the toolchain (so binny manages itself!)
- name: binny
version:
want: v0.11.1
want: v0.8.0
method: github-release
with:
repo: anchore/binny
@@ -26,7 +26,7 @@ tools:
# used for linting
- name: golangci-lint
version:
want: v2.8.0
want: v1.64.5
method: github-release
with:
repo: golangci/golangci-lint
@@ -34,7 +34,7 @@ tools:
# used for showing the changelog at release
- name: glow
version:
want: v2.1.1
want: v2.0.0
method: github-release
with:
repo: charmbracelet/glow
@@ -42,7 +42,7 @@ tools:
# used for signing the checksums file at release
- name: cosign
version:
want: v3.0.4
want: v2.4.3
method: github-release
with:
repo: sigstore/cosign
@@ -58,7 +58,7 @@ tools:
# used to release all artifacts
- name: goreleaser
version:
want: v2.13.3
want: v2.7.0
method: github-release
with:
repo: goreleaser/goreleaser
@@ -87,10 +87,23 @@ tools:
with:
repo: wagoodman/go-bouncer
# used for showing benchmark testing
- name: benchstat
version:
want: latest
method: go-proxy
with:
module: golang.org/x/perf
allow-unresolved-version: true
method: go-install
with:
entrypoint: cmd/benchstat
module: golang.org/x/perf
# used for running all local and CI tasks
- name: task
version:
want: v3.48.0
want: v3.41.0
method: github-release
with:
repo: go-task/task
@@ -98,7 +111,7 @@ tools:
# used for triggering a release
- name: gh
version:
want: v2.86.0
want: v2.67.0
method: github-release
with:
repo: cli/cli
@@ -106,7 +119,7 @@ tools:
# used to upload test fixture cache
- name: oras
version:
want: v1.3.0
want: v1.2.2
method: github-release
with:
repo: oras-project/oras
@@ -114,7 +127,7 @@ tools:
# used to upload test fixture cache
- name: yq
version:
want: v4.52.2
want: v4.45.1
method: github-release
with:
repo: mikefarah/yq
-3
View File
@@ -9,9 +9,6 @@ permit:
- Unlicense
ignore-packages:
# https://github.com/sorairolake/lzip-go/blob/34a2615d2abf740175c6b0a835baa08364e09430/go.sum.license#L3
# has `SPDX-License-Identifier: Apache-2.0 OR MIT`, both of which are acceptable
- github.com/sorairolake/lzip-go
# packageurl-go is released under the MIT license located in the root of the repo at /mit.LICENSE
- github.com/anchore/packageurl-go
+7 -13
View File
@@ -5,7 +5,7 @@ inputs:
go-version:
description: "Go version to install"
required: true
default: ">= 1.24"
default: "1.24.x"
go-dependencies:
description: "Download go dependencies"
required: true
@@ -13,7 +13,7 @@ inputs:
cache-key-prefix:
description: "Prefix all cache keys with this value"
required: true
default: "53ac821810"
default: "181053ac82"
download-test-fixture-cache:
description: "Download test fixture cache from OCI and github actions"
required: true
@@ -29,7 +29,7 @@ runs:
using: "composite"
steps:
# note: go mod and build is automatically cached on default with v4+
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
- uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
if: inputs.go-version != ''
with:
go-version: ${{ inputs.go-version }}
@@ -37,7 +37,7 @@ runs:
- name: Restore tool cache
if: inputs.tools == 'true'
id: tool-cache
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.2.1
with:
path: ${{ github.workspace }}/.tool
key: ${{ inputs.cache-key-prefix }}-${{ runner.os }}-tool-${{ hashFiles('.binny.yaml') }}
@@ -45,10 +45,7 @@ runs:
- name: Install project tools
shell: bash
if: inputs.tools == 'true'
run: |
make tools
.tool/binny list
.tool/binny check
run: make tools
- name: Install go dependencies
if: inputs.go-dependencies == 'true'
@@ -58,15 +55,12 @@ runs:
- name: Install apt packages
if: inputs.bootstrap-apt-packages != ''
shell: bash
env:
APT_PACKAGES: ${{ inputs.bootstrap-apt-packages }}
run: |
IFS=' ' read -ra packages <<< "$APT_PACKAGES"
DEBIAN_FRONTEND=noninteractive sudo apt update && sudo -E apt install -y "${packages[@]}"
DEBIAN_FRONTEND=noninteractive sudo apt update && sudo -E apt install -y ${{ inputs.bootstrap-apt-packages }}
- name: Restore ORAS cache from github actions
if: inputs.download-test-fixture-cache == 'true'
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.2.1
with:
path: ${{ github.workspace }}/.tmp/oras-cache
key: ${{ inputs.cache-key-prefix }}-oras-cache
+11 -38
View File
@@ -1,54 +1,27 @@
# 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
updates:
- package-ecosystem: gomod
directory: "/"
cooldown:
default-days: 7
schedule:
interval: "weekly"
day: "friday"
interval: "daily"
open-pull-requests-limit: 10
labels:
- "dependencies"
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:
- "/"
- "/.github/actions/bootstrap"
cooldown:
default-days: 7
directory: "/"
schedule:
interval: "weekly"
day: "friday"
interval: "daily"
open-pull-requests-limit: 10
labels:
- "dependencies"
- package-ecosystem: "github-actions"
directory: "/.github/actions/bootstrap"
schedule:
interval: "daily"
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"
+6 -9
View File
@@ -1,10 +1,11 @@
## Description
# Description
<!-- Please include a summary of the changes along with any relevant motivation and context -->
Please include a summary of the changes along with any relevant motivation and context,
or link to an issue where this is explained.
<!-- If CLI output changed, show an example (before/after if helpful) -->
<!-- If this completes an issue, please include: -->
<!-- If this changes application or API configuration, describe new/changed/removed options -->
- Fixes
## Type of change
@@ -17,12 +18,8 @@
- [ ] Chore (improve the developer experience, fix a test flake, etc, without changing the visible behavior of Syft)
- [ ] Performance (make Syft run faster or use less memory, without changing visible behavior much)
## Checklist
# Checklist:
- [ ] I have added unit tests that cover changed behavior
- [ ] I have tested my code in common scenarios and confirmed there are no regressions
- [ ] I have added comments to my code, particularly in hard-to-understand sections
## Issue references
<!-- If this fixes an issue, include "Fixes #<issue-number>" or otherwise list the issue references -->
-17
View File
@@ -1,17 +0,0 @@
#!/usr/bin/env bash
set -u
if [ "$(git status --porcelain | wc -l)" -ne "0" ]; then
echo " 🔴 there are uncommitted changes, please commit them before running this check"
exit 1
fi
if ! make generate-capabilities; then
echo "Generating capability descriptions failed"
exit 1
fi
if [ "$(git status --porcelain | wc -l)" -ne "0" ]; then
echo " 🔴 there is drift in capability descriptions! Please run 'make generate-capabilities' and commit the changes."
exit 1
fi
+1 -1
View File
@@ -24,7 +24,7 @@ while IFS= read -r -d '' file; do
echo "File $file is greater than ${size} bytes."
found_large_files=1
fi
done < <(git ls-files -z "$directory")
done < <(find "$directory" -type f -print0)
if [ "$found_large_files" -eq 1 ]; then
echo "Script failed: Some files are greater than ${size} bytes."
+2 -4
View File
@@ -86,10 +86,8 @@ def main(file_path: str | None):
show("The following paths are missing or have no content, but have corresponding .fingerprint files:")
for path in sorted(missing_content):
show(f"- {path}")
# when adding new cache directories there is a time where it is not possible to have this directory without
# running the tests first... but this step is a prerequisite for running the tests. We should not block on this.
# show("Please ensure these paths exist and have content if they are directories.")
# exit(1)
show("Please ensure these paths exist and have content if they are directories.")
exit(1)
sha256_hash = calculate_sha256(fingerprint_contents)
+1 -1
View File
@@ -12,6 +12,6 @@ if ! make generate-json-schema; then
fi
if [ "$(git status --porcelain | wc -l)" -ne "0" ]; then
echo " 🔴 there is drift in json schema! Please run 'make generate-json-schema' and commit the changes."
echo " 🔴 there are uncommitted changes, please commit them before running this check"
exit 1
fi
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
set -ue
BIN="syft"
VERSION_FILE="VERSION"
VERSION=$1
# the source of truth as to whether we want to notify users of an update is if the release just created is NOT
# flagged as a pre-release on github
if [[ "$(curl -SsL https://api.github.com/repos/anchore/${BIN}/releases/tags/${VERSION} | jq .prerelease)" == "true" ]] ; then
echo "skipping publishing a version file (this is a pre-release: ${VERSION})"
exit 0
fi
echo "creating and publishing version file (${VERSION})"
# create a version file for version-update checks
echo "${VERSION}" | tee "${VERSION_FILE}"
# upload the version file that supports the application version update check
export AWS_DEFAULT_REGION=us-west-2
aws s3 cp "${VERSION_FILE}" s3://toolbox-data.anchore.io/${BIN}/releases/latest/VERSION
+62
View File
@@ -0,0 +1,62 @@
name: "Benchmark testing"
on:
workflow_dispatch:
pull_request:
permissions:
contents: read
jobs:
Benchmark-Test:
name: "Benchmark tests"
runs-on: ubuntu-24.04
# note: we want benchmarks to run on pull_request events in order to publish results to a sticky comment, and
# we also want to run on push such that merges to main are recorded to the cache. For this reason we don't filter
# the job by event.
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Bootstrap environment
uses: ./.github/actions/bootstrap
- name: Restore base benchmark result
uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f #v4.2.1
with:
path: test/results/benchmark-main.txt
# use base sha for PR or new commit hash for main push in benchmark result key
key: ${{ runner.os }}-bench-${{ (github.event.pull_request.base.sha != github.event.after) && github.event.pull_request.base.sha || github.event.after }}
- name: Run benchmark tests
id: benchmark
run: |
REF_NAME=${GITHUB_REF##*/} make benchmark
OUTPUT=$(make show-benchstat)
OUTPUT="${OUTPUT//'%'/'%25'}" # URL encode all '%' characters
OUTPUT="${OUTPUT//$'\n'/'%0A'}" # URL encode all '\n' characters
OUTPUT="${OUTPUT//$'\r'/'%0D'}" # URL encode all '\r' characters
echo "result=$OUTPUT" >> $GITHUB_OUTPUT
- uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
with:
name: benchmark-test-results
path: test/results/**/*
- name: Update PR benchmark results comment
uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 #v2.9.1
continue-on-error: true
with:
header: benchmark
message: |
### Benchmark Test Results
<details>
<summary>Benchmark results from the latest changes vs base branch</summary>
```
${{ steps.benchmark.outputs.result }}
```
</details>
+6 -8
View File
@@ -21,7 +21,7 @@ jobs:
analyze:
name: Analyze
runs-on: ubuntu-22.04-4core-16gb
if: github.repository == 'anchore/syft' # only run for main repo
permissions:
security-events: write
@@ -36,18 +36,16 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
persist-credentials: false
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- name: Install Go
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 #v6.2.0
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 #v5.3.0
with:
go-version-file: go.mod
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
uses: github/codeql-action/init@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d #v3.28.10
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -58,7 +56,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
uses: github/codeql-action/autobuild@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d #v3.28.10
# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -72,4 +70,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
uses: github/codeql-action/analyze@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d #v3.28.10
+3 -9
View File
@@ -28,15 +28,9 @@ jobs:
label:
name: "Label changes"
runs-on: ubuntu-22.04
permissions:
contents: read
pull-requests: write
issues: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
persist-credentials: false
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- run: python .github/scripts/labeler.py
env:
@@ -46,7 +40,7 @@ jobs:
- name: Delete existing comment
if: ${{ hashFiles( env.CI_COMMENT_FILE ) == '' }}
uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 #v2.9.4
uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 #v2.9.1
with:
header: ${{ env.COMMENT_HEADER }}
hide: true
@@ -54,7 +48,7 @@ jobs:
- name: Add comment
if: ${{ hashFiles( env.CI_COMMENT_FILE ) != '' }}
uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 #v2.9.4
uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 #v2.9.1
with:
header: ${{ env.COMMENT_HEADER }}
path: ${{ env.CI_COMMENT_FILE }}
@@ -0,0 +1,31 @@
name: "Release: version file"
on:
workflow_dispatch:
inputs:
version:
description: release version to update the version file with (prefixed with v)
required: true
workflow_call:
inputs:
version:
type: string
description: release version to update the version file with (prefixed with v)
required: true
jobs:
release:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- name: Update version file
run: make ci-release-version-file
env:
RELEASE_VERSION: ${{ github.event.inputs.version }}
# for updating the VERSION file in S3...
AWS_ACCESS_KEY_ID: ${{ secrets.TOOLBOX_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.TOOLBOX_AWS_SECRET_ACCESS_KEY }}
+27 -83
View File
@@ -9,34 +9,13 @@ on:
version:
description: tag the latest commit on main with the given version (prefixed with v)
required: true
phase:
description: the specific workflow phase to run or all
required: true
default: "all"
type: choice
options:
- "all"
- "install-script-only"
jobs:
quality-gate:
environment: release
if: ${{ github.event.inputs.phase == 'all' }}
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
persist-credentials: false
- name: Bootstrap environment
uses: ./.github/actions/bootstrap
- name: Validate Apple notarization credentials
run: .tool/quill submission list
env:
QUILL_NOTARY_ISSUER: ${{ secrets.APPLE_NOTARY_ISSUER }}
QUILL_NOTARY_KEY_ID: ${{ secrets.APPLE_NOTARY_KEY_ID }}
QUILL_NOTARY_KEY: ${{ secrets.APPLE_NOTARY_KEY }}
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- name: Check if running on main
if: github.ref != 'refs/heads/main'
@@ -49,10 +28,8 @@ jobs:
- name: Check if tag already exists
# note: this will fail if the tag already exists
run: |
[[ "$VERSION" == v* ]] || (echo "version '$VERSION' does not have a 'v' prefix" && exit 1)
git tag "$VERSION"
env:
VERSION: ${{ github.event.inputs.version }}
[[ "${{ github.event.inputs.version }}" == v* ]] || (echo "version '${{ github.event.inputs.version }}' does not have a 'v' prefix" && exit 1)
git tag ${{ github.event.inputs.version }}
- name: Check static analysis results
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
@@ -110,53 +87,39 @@ jobs:
- name: Quality gate
if: steps.static-analysis.outputs.conclusion != 'success' || steps.unit.outputs.conclusion != 'success' || steps.integration.outputs.conclusion != 'success' || steps.cli-linux.outputs.conclusion != 'success' || steps.acceptance-linux.outputs.conclusion != 'success' || steps.acceptance-mac.outputs.conclusion != 'success'
env:
STATIC_ANALYSIS_STATUS: ${{ steps.static-analysis.conclusion }}
UNIT_TEST_STATUS: ${{ steps.unit.outputs.conclusion }}
INTEGRATION_TEST_STATUS: ${{ steps.integration.outputs.conclusion }}
ACCEPTANCE_LINUX_STATUS: ${{ steps.acceptance-linux.outputs.conclusion }}
ACCEPTANCE_MAC_STATUS: ${{ steps.acceptance-mac.outputs.conclusion }}
CLI_LINUX_STATUS: ${{ steps.cli-linux.outputs.conclusion }}
run: |
echo "Static Analysis Status: $STATIC_ANALYSIS_STATUS"
echo "Unit Test Status: $UNIT_TEST_STATUS"
echo "Integration Test Status: $INTEGRATION_TEST_STATUS"
echo "Acceptance Test (Linux) Status: $ACCEPTANCE_LINUX_STATUS"
echo "Acceptance Test (Mac) Status: $ACCEPTANCE_MAC_STATUS"
echo "CLI Test (Linux) Status: $CLI_LINUX_STATUS"
echo "Static Analysis Status: ${{ steps.static-analysis.conclusion }}"
echo "Unit Test Status: ${{ steps.unit.outputs.conclusion }}"
echo "Integration Test Status: ${{ steps.integration.outputs.conclusion }}"
echo "Acceptance Test (Linux) Status: ${{ steps.acceptance-linux.outputs.conclusion }}"
echo "Acceptance Test (Mac) Status: ${{ steps.acceptance-mac.outputs.conclusion }}"
echo "CLI Test (Linux) Status: ${{ steps.cli-linux.outputs.conclusion }}"
false
release:
needs: [ quality-gate ]
if: ${{ github.event.inputs.phase == 'all' }}
# 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
# s3-cache: faster actions cache
# tmpfs: faster io-intensive workflows
runs-on: runs-on=${{ github.run_id }}/cpu=16+32/ram=32+128/family=c5+c6+c7+c8/spot=false/extras=s3-cache+tmpfs
needs: [quality-gate]
runs-on: ubuntu-24.04
permissions:
contents: write
packages: write
# required for goreleaser signs section with cosign
id-token: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
with:
fetch-depth: 0
persist-credentials: true
- name: Bootstrap environment
uses: ./.github/actions/bootstrap
- name: Login to Docker Hub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef #v3.6.0
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 #v3.3.0
with:
username: ${{ secrets.ANCHOREOSSWRITE_DH_USERNAME }}
password: ${{ secrets.ANCHOREOSSWRITE_DH_PAT }}
- name: Login to GitHub Container Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef #v3.6.0
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 #v3.3.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -166,11 +129,10 @@ jobs:
run: |
git config --global user.name "anchoreci"
git config --global user.email "anchoreci@users.noreply.github.com"
git tag -a "$VERSION" -m "Release $VERSION"
git tag -a ${{ github.event.inputs.version }} -m "Release ${{ github.event.inputs.version }}"
git push origin --tags
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ github.event.inputs.version }}
- name: Build & publish release artifacts
run: make ci-release
@@ -186,43 +148,25 @@ jobs:
# for updating brew formula in anchore/homebrew-syft
GITHUB_BREW_TOKEN: ${{ secrets.ANCHOREOPS_GITHUB_OSS_WRITE_TOKEN }}
- uses: anchore/sbom-action@0b82b0b1a22399a1c542d4d656f70cd903571b5c #v0.21.1
- uses: anchore/sbom-action@f325610c9f50a54015d37c8d16cb3b0e2c8f4de0 #v0.18.0
continue-on-error: true
with:
file: go.mod
artifact-name: sbom.spdx.json
- name: Notify Slack of new release
uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a #v2.1.1
- uses: 8398a7/action-slack@28ba43ae48961b90635b50953d216767a6bea486 #v3.16.2
continue-on-error: true
with:
webhook: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
text: "A new Syft release has been published: https://github.com/anchore/syft/releases/tag/${{ github.event.inputs.version }}"
blocks:
- type: section
text:
type: mrkdwn
text: |
*A new Syft release has been published* :rocket:
• Release: <https://github.com/anchore/syft/releases/tag/${{ github.event.inputs.version }}|${{ github.event.inputs.version }}>
• Repo: `${{ github.repository }}`
• Workflow: `${{ github.workflow }}`
• Event: `${{ github.event_name }}`
status: ${{ job.status }}
fields: repo,workflow,action,eventName
text: "A new Syft release has been published: https://github.com/anchore/syft/releases/tag/${{ github.event.inputs.version }}"
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }}
if: ${{ success() }}
release-install-script:
needs: [ release ]
if: ${{ always() && (needs.release.result == 'success' || github.event.inputs.phase == 'install-script-only') }}
uses: "anchore/workflows/.github/workflows/release-install-script.yaml@main"
release-version-file:
needs: [release]
uses: ./.github/workflows/release-version-file.yaml
with:
tag: ${{ github.event.inputs.version }}
secrets:
# needed for r2...
R2_INSTALL_ACCESS_KEY_ID: ${{ secrets.OSS_R2_INSTALL_ACCESS_KEY_ID }}
R2_INSTALL_SECRET_ACCESS_KEY: ${{ secrets.OSS_R2_INSTALL_SECRET_ACCESS_KEY }}
R2_ENDPOINT: ${{ secrets.TOOLBOX_CLOUDFLARE_R2_ENDPOINT }}
# needed for s3...
S3_INSTALL_AWS_ACCESS_KEY_ID: ${{ secrets.TOOLBOX_AWS_ACCESS_KEY_ID }}
S3_INSTALL_AWS_SECRET_ACCESS_KEY: ${{ secrets.TOOLBOX_AWS_SECRET_ACCESS_KEY }}
version: ${{ github.event.inputs.version }}
secrets: inherit
@@ -6,10 +6,6 @@ on:
jobs:
run:
permissions:
contents: read
issues: write
pull-requests: write
uses: "anchore/workflows/.github/workflows/remove-awaiting-response-label.yaml@main"
secrets:
token: ${{ secrets.OSS_PROJECT_GH_TOKEN }}
@@ -13,18 +13,12 @@ jobs:
Publish:
name: "Publish test fixture image cache"
# runs-on.com: general purpose instances for building test fixture cache
# spot disabled: can run up to an hour, avoid interruptions
# s3-cache: faster actions cache
# family: note that m*d instances have local nvme storage which is beneficial for building large test fixture images
runs-on: "runs-on=${{ github.run_id }}/cpu=8+16/ram=32+64/family=m5d+m5ad+m5dn+m6gd+m6id+m6idn+m7gd+m8gd/spot=false/extras=s3-cache"
if: github.repository == 'anchore/syft' # only run for main repo
# we use this runner to get enough storage space for docker images and fixture cache
runs-on: ubuntu-22.04-4core-16gb
permissions:
packages: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
persist-credentials: false
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- name: Bootstrap environment
uses: ./.github/actions/bootstrap
@@ -33,16 +27,13 @@ jobs:
download-test-fixture-cache: false
- name: Run all tests
run: |
make test
run: make test
env:
# we want to rebuild the cache with no previous state
DOWNLOAD_TEST_FIXTURE_CACHE: "false"
- name: Login to GitHub Container Registry (ORAS)
run: echo "${{ secrets.GITHUB_TOKEN }}" | .tool/oras login ghcr.io -u "$ACTOR" --password-stdin
env:
ACTOR: ${{ github.actor }}
run: echo "${{ secrets.GITHUB_TOKEN }}" | .tool/oras login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Publish test fixture cache
run: make upload-test-fixture-cache
@@ -15,9 +15,7 @@ jobs:
runs-on: ubuntu-latest
if: github.repository_owner == 'anchore' # only run for main repo (not forks)
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
persist-credentials: false
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- name: Bootstrap environment
uses: ./.github/actions/bootstrap
@@ -31,13 +29,13 @@ jobs:
with:
repos: ${{ github.event.inputs.repos }}
- uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf #v2.2.1
- uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a #v2.1.0
id: generate-token
with:
app-id: ${{ secrets.TOKEN_APP_ID }}
private-key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }}
app_id: ${{ secrets.TOKEN_APP_ID }}
private_key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }}
- uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 #v8.1.0
- uses: peter-evans/create-pull-request@dd2324fc52d5d43c699a5636bcf19fceaa70c284 #v7.0.7
with:
signoff: true
delete-branch: true
+5 -7
View File
@@ -13,9 +13,7 @@ jobs:
runs-on: ubuntu-latest
if: github.repository == 'anchore/syft' # only run for main repo
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
persist-credentials: false
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- name: Bootstrap environment
uses: ./.github/actions/bootstrap
@@ -45,13 +43,13 @@ jobs:
echo "\`\`\`"
} >> $GITHUB_STEP_SUMMARY
- uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf #v2.2.1
- uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a #v2.1.0
id: generate-token
with:
app-id: ${{ secrets.TOKEN_APP_ID }}
private-key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }}
app_id: ${{ secrets.TOKEN_APP_ID }}
private_key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }}
- uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 #v8.1.0
- uses: peter-evans/create-pull-request@dd2324fc52d5d43c699a5636bcf19fceaa70c284 #v7.0.7
with:
signoff: true
delete-branch: true
@@ -14,45 +14,26 @@ env:
jobs:
upgrade-cpe-dictionary-index:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
if: github.repository == 'anchore/syft' # only run for main repo
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
persist-credentials: false
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- name: Bootstrap environment
uses: ./.github/actions/bootstrap
id: bootstrap
- name: Login to GitHub Container Registry
run: echo "${{ secrets.GITHUB_TOKEN }}" | .tool/oras login ghcr.io -u "$ACTOR" --password-stdin
env:
ACTOR: ${{ github.actor }}
- name: Bootstrap environment
uses: ./.github/actions/bootstrap
- name: Pull CPE cache from registry
run: make generate:cpe-index:cache:pull
- run: |
make generate-cpe-dictionary-index
- name: Update CPE cache from NVD API
run: make generate:cpe-index:cache:update
env:
NVD_API_KEY: ${{ secrets.NVD_API_KEY }}
- name: Generate CPE dictionary index
run: make generate:cpe-index:build
- name: Push updated CPE cache to registry
run: make generate:cpe-index:cache:push
- uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf #v2.2.1
- uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a #v2.1.0
id: generate-token
with:
app-id: ${{ secrets.TOKEN_APP_ID }}
private-key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }}
app_id: ${{ secrets.TOKEN_APP_ID }}
private_key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }}
- uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 #v8.1.0
- uses: peter-evans/create-pull-request@dd2324fc52d5d43c699a5636bcf19fceaa70c284 #v7.0.7
with:
signoff: true
delete-branch: true
@@ -64,21 +45,11 @@ jobs:
Update CPE dictionary index based on the latest available CPE dictionary
token: ${{ steps.generate-token.outputs.token }}
- name: Notify Slack on failure
uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a #v2.1.1
- uses: 8398a7/action-slack@28ba43ae48961b90635b50953d216767a6bea486 #v3.16.2
with:
webhook: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
text: "Syft CPE dictionary index update failed"
blocks:
- type: section
text:
type: mrkdwn
text: |
*Syft CPE dictionary index update failed*
• Workflow: `${{ github.workflow }}`
• Event: `${{ github.event_name }}`
• Job Status: `${{ job.status }}`
• <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Run>
status: ${{ job.status }}
fields: workflow,eventName,job
text: Syft CPE dictionary index update failed
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }}
if: ${{ failure() && env.SLACK_NOTIFICATIONS == 'true' }}
@@ -1,64 +0,0 @@
name: PR to update SPDX license list
on:
schedule:
- cron: "0 6 * * 1" # every monday at 6 AM UTC
workflow_dispatch:
permissions:
contents: read
env:
SLACK_NOTIFICATIONS: true
jobs:
upgrade-spdx-license-list:
runs-on: ubuntu-latest
if: github.repository == 'anchore/syft' # only run for main repo
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
persist-credentials: false
- name: Bootstrap environment
uses: ./.github/actions/bootstrap
- run: |
make generate-license-list
- uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a #v2.1.0
id: generate-token
with:
app_id: ${{ secrets.TOKEN_APP_ID }}
private_key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }}
- uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 #v8.1.0
with:
signoff: true
delete-branch: true
branch: auto/latest-spdx-license-list
labels: dependencies
commit-message: "chore(deps): update SPDX license list"
title: "chore(deps): update SPDX license list"
body: |
Update SPDX license list based on the latest available list from spdx.org
token: ${{ steps.generate-token.outputs.token }}
- name: Notify Slack on failure
uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a #v2.1.1
with:
webhook: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
text: "Syft SPDX license list update failed"
blocks:
- type: section
text:
type: mrkdwn
text: |
*Syft SPDX license list update failed*
• Workflow: `${{ github.workflow }}`
• Event: `${{ github.event_name }}`
• Job Status: `${{ job.status }}`
• <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Run>
if: ${{ failure() && env.SLACK_NOTIFICATIONS == 'true' }}
@@ -1,36 +0,0 @@
name: "Validate GitHub Actions"
on:
pull_request:
paths:
- '.github/workflows/**'
- '.github/actions/**'
push:
branches:
- main
paths:
- '.github/workflows/**'
- '.github/actions/**'
permissions:
contents: read
jobs:
zizmor:
name: "Lint"
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write # for uploading SARIF results
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: "Run zizmor"
uses: zizmorcore/zizmor-action@135698455da5c3b3e55f73f4419e481ab68cdd95 # v0.4.1
with:
config: .github/zizmor.yml
# Disable SARIF upload so the step is a simple pass/fail gate
advanced-security: false
inputs: .github
+87 -84
View File
@@ -1,11 +1,5 @@
name: "Validations"
# we should cancel any in-progress runs for the same workflow + PR/ref
# so that we can avoid redundant work / save on CI minutes
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
on:
workflow_dispatch:
pull_request:
@@ -14,41 +8,30 @@ on:
- main
permissions:
contents: read
contents: read
jobs:
Static-Analysis:
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
name: "Static analysis"
# runs-on.com: memory & general purpose instances for testing
# spot enabled: ok to interrupt non-production workloads
# s3-cache: faster actions cache
# tmpfs: faster io-intensive workflows
runs-on: &test-runner "runs-on=${{ github.run_id }}/cpu=4+8/ram=32+128/family=r5+r6+r7+r8+m4+m5+m6+m7+m8/spot=price-capacity-optimized/extras=s3-cache+tmpfs"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
persist-credentials: false
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- name: Bootstrap environment
uses: ./.github/actions/bootstrap
with:
# the self-consistency tests for the output of the capabilities code generation depends on unit test
# output from ./syft/pkg/... packages. Therefore we need to download the test fixture cache here
# so that running the few unit tests as part of static analysis works correctly.
download-test-fixture-cache: true
- name: Run static analysis
run: make static-analysis
Unit-Test:
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
name: "Unit tests"
runs-on: *test-runner
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
persist-credentials: false
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- name: Bootstrap environment
uses: ./.github/actions/bootstrap
@@ -58,14 +41,13 @@ jobs:
- name: Run unit tests
run: make unit
Integration-Test:
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
name: "Integration tests"
runs-on: *test-runner
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
persist-credentials: false
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- name: Bootstrap environment
uses: ./.github/actions/bootstrap
@@ -78,21 +60,12 @@ jobs:
- name: Run integration tests
run: make integration
Build-Snapshot-Artifacts:
name: "Build snapshot artifacts"
# 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
# s3-cache: faster actions cache
# tmpfs: faster io-intensive workflows
runs-on: "runs-on=${{ github.run_id }}/cpu=16+32/ram=32+128/family=c5+c6+c7+c8/spot=false/extras=s3-cache+tmpfs"
runs-on: ubuntu-24.04
steps:
# required for magic-cache from runs-on to function with artifact upload/download (see https://runs-on.com/caching/magic-cache/#actionsupload-artifact-compatibility)
- uses: runs-on/action@cd2b598b0515d39d78c38a02d529db87d2196d1e # v2.0.3
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
persist-credentials: false
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- name: Bootstrap environment
uses: ./.github/actions/bootstrap
@@ -102,42 +75,47 @@ jobs:
- name: Build snapshot artifacts
run: make snapshot
- name: Smoke test snapshot build
run: make snapshot-smoke-test
# why not use actions/upload-artifact? It is very slow (3 minutes to upload ~600MB of data, vs 10 seconds with this approach).
# see https://github.com/actions/upload-artifact/issues/199 for more info
- name: Upload snapshot artifacts
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f #v6.0.0
uses: actions/cache/save@0c907a75c2c80ebcb7f088228285e798b750cf8f #v4.2.1
with:
name: snapshot
path: snapshot/
retention-days: 30
# we need to preserve the snapshot data itself as well as the task data that confirms if the
# snapshot build is stale or not. Otherwise the downstream jobs will attempt to rebuild the snapshot
# even though it already exists.
path: |
snapshot
.task
key: snapshot-build-${{ github.run_id }}
Acceptance-Linux:
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
name: "Acceptance tests (Linux)"
needs: [Build-Snapshot-Artifacts]
runs-on: *test-runner
runs-on: ubuntu-24.04
steps:
# required for magic-cache from runs-on to function with artifact upload/download (see https://runs-on.com/caching/magic-cache/#actionsupload-artifact-compatibility)
- uses: runs-on/action@cd2b598b0515d39d78c38a02d529db87d2196d1e # v2.0.3
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
persist-credentials: false
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- name: Bootstrap environment
uses: ./.github/actions/bootstrap
with:
download-test-fixture-cache: true
- name: Download snapshot artifacts
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 #v7.0.0
- name: Download snapshot build
id: snapshot-cache
uses: actions/cache/restore@0c907a75c2c80ebcb7f088228285e798b750cf8f #v4.2.1
with:
name: snapshot
path: snapshot
path: |
snapshot
.task
fail-on-cache-miss: true
key: snapshot-build-${{ github.run_id }}
- name: Restore binary permissions
run: chmod +x snapshot/*/syft snapshot/*/*.exe 2>/dev/null || true
# workaround for https://github.com/actions/cache/issues/1265
- name: (cache-miss) Snapshot build missing
if: steps.snapshot-cache.outputs.cache-hit != 'true'
run: echo "unable to download snapshots from previous job" && false
- name: Run comparison tests (Linux)
run: make compare-linux
@@ -153,19 +131,17 @@ jobs:
if: steps.install-test-image-cache.outputs.cache-hit != 'true'
run: make install-test-cache-save
Acceptance-Mac:
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
name: "Acceptance tests (Mac)"
needs: [Build-Snapshot-Artifacts]
# note: macos runners aren't supported yet for runs-on managed runners.
runs-on: macos-latest
steps:
- name: Install Cosign
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
uses: sigstore/cosign-installer@v3.8.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
persist-credentials: false
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- name: Bootstrap environment
uses: ./.github/actions/bootstrap
@@ -174,14 +150,20 @@ jobs:
go-dependencies: false
download-test-fixture-cache: true
- name: Download snapshot artifacts
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 #v7.0.0
- name: Download snapshot build
id: snapshot-cache
uses: actions/cache/restore@0c907a75c2c80ebcb7f088228285e798b750cf8f #v4.2.1
with:
name: snapshot
path: snapshot
path: |
snapshot
.task
fail-on-cache-miss: true
key: snapshot-build-${{ github.run_id }}
- name: Restore binary permissions
run: chmod +x snapshot/*/syft 2>/dev/null || true
# workaround for https://github.com/actions/cache/issues/1265
- name: (cache-miss) Snapshot build missing
if: steps.snapshot-cache.outputs.cache-hit != 'true'
run: echo "unable to download snapshots from previous job" && false
- name: Run comparison tests (Mac)
run: make compare-mac
@@ -189,32 +171,53 @@ jobs:
- name: Run install.sh tests (Mac)
run: make install-test-ci-mac
Cli-Linux:
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
name: "CLI tests (Linux)"
needs: [Build-Snapshot-Artifacts]
runs-on: *test-runner
runs-on: ubuntu-24.04
steps:
# required for magic-cache from runs-on to function with artifact upload/download (see https://runs-on.com/caching/magic-cache/#actionsupload-artifact-compatibility)
- uses: runs-on/action@cd2b598b0515d39d78c38a02d529db87d2196d1e # v2.0.3
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
persist-credentials: false
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- name: Bootstrap environment
uses: ./.github/actions/bootstrap
with:
download-test-fixture-cache: true
- name: Download snapshot artifacts
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 #v7.0.0
- name: Download snapshot build
id: snapshot-cache
uses: actions/cache/restore@0c907a75c2c80ebcb7f088228285e798b750cf8f #v4.2.1
with:
name: snapshot
path: snapshot
path: |
snapshot
.task
fail-on-cache-miss: true
key: snapshot-build-${{ github.run_id }}
- name: Restore binary permissions
run: chmod +x snapshot/*/syft snapshot/*/*.exe 2>/dev/null || true
# workaround for https://github.com/actions/cache/issues/1265
- name: (cache-miss) Snapshot build missing
if: steps.snapshot-cache.outputs.cache-hit != 'true'
run: echo "unable to download snapshots from previous job" && false
- name: Run CLI Tests (Linux)
run: make cli
Cleanup-Cache:
name: "Cleanup snapshot cache"
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-24.04
permissions:
actions: write
needs:
- Acceptance-Linux
- Acceptance-Mac
- Cli-Linux
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- name: Delete snapshot cache
run: gh cache delete "snapshot-build-${{ github.run_id }}"
env:
GH_TOKEN: ${{ github.token }}
-11
View File
@@ -1,11 +0,0 @@
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,
# never checks out PR code, needs secrets for labeling PRs from forks
- detect-schema-changes.yaml
-7
View File
@@ -2,7 +2,6 @@
go.work
go.work.sum
.tool-versions
.python-version
# app configuration
/.syft.yaml
@@ -17,9 +16,6 @@ bin/
/snapshot
/.tool
/.task
/generate
/specs
mise.toml
# changelog generation
CHANGELOG.md
@@ -36,7 +32,6 @@ VERSION
/test/results
coverage.txt
*.log
**/test-fixtures/test-observations.json
# probable archives
.images
@@ -75,5 +70,3 @@ cosign.pub
__pycache__/
*.py[cod]
*$py.class
+40 -55
View File
@@ -1,8 +1,15 @@
version: "2"
run:
tests: false
issues:
max-same-issues: 25
uniq-by-line: false
# TODO: enable this when we have coverage on docstring comments
# # The list of ids of default excludes to include or disable.
# include:
# - EXC0002 # disable excluding of issues about comments from golint
linters:
default: none
# inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint
disable-all: true
enable:
- asciicheck
- bodyclose
@@ -15,8 +22,11 @@ linters:
- goconst
- gocritic
- gocyclo
- gofmt
- goimports
- goprintffuncname
- gosec
- gosimple
- govet
- ineffassign
- misspell
@@ -24,41 +34,36 @@ linters:
- nolintlint
- revive
- staticcheck
- stylecheck
- typecheck
- unconvert
- unparam
- unused
- whitespace
settings:
funlen:
lines: 70
statements: 50
gocritic:
enabled-checks:
- deferInLoop
- ruleguard
settings:
ruleguard:
rules: test/rules/rules.go
gosec:
excludes:
- G115
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
rules:
# internal/os contains OS feature detection logic; the name reflects its purpose
- linters:
- revive
path: internal/os/
text: "var-naming: avoid package names that conflict"
paths:
- third_party$
- builtin$
- examples$
linters-settings:
funlen:
# Checks the number of lines in a function.
# If lower than 0, disable the check.
# Default: 60
lines: 70
# Checks the number of statements in a function.
# If lower than 0, disable the check.
# Default: 40
statements: 50
gocritic:
enabled-checks:
- deferInLoop
- ruleguard
settings:
ruleguard:
rules: "test/rules/rules.go"
gosec:
excludes:
- G115
run:
timeout: 10m
tests: false
# do not enable...
# - deadcode # The owner seems to have abandoned the linter. Replaced by "unused".
@@ -85,23 +90,3 @@ linters:
# - testpackage
# - varcheck # The owner seems to have abandoned the linter. Replaced by "unused".
# - wsl # this doens't have an auto-fixer yet and is pretty noisy (https://github.com/bombsimon/wsl/issues/90)
issues:
max-same-issues: 25
uniq-by-line: false
# TODO: enable this when we have coverage on docstring comments
# # The list of ids of default excludes to include or disable.
# include:
# - EXC0002 # disable excluding of issues about comments from golint
formatters:
enable:
- gofmt
- goimports
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
+120 -190
View File
@@ -1,11 +1,11 @@
version: 2
project_name: syft
release:
prerelease: auto
draft: false
env:
# required to support multi architecture docker builds
- DOCKER_CLI_EXPERIMENTAL=enabled
- CGO_ENABLED=0
@@ -13,8 +13,14 @@ builds:
- id: linux-build
dir: ./cmd/syft
binary: syft
goos: [linux]
goarch: [amd64, arm64, ppc64le, s390x]
goos:
- linux
goarch:
- amd64
- arm64
- ppc64le
- s390x
# 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 |
-w
@@ -28,8 +34,11 @@ builds:
- id: darwin-build
dir: ./cmd/syft
binary: syft
goos: [darwin]
goarch: [amd64, arm64]
goos:
- darwin
goarch:
- amd64
- arm64
mod_timestamp: *build-timestamp
ldflags: *build-ldflags
hooks:
@@ -41,43 +50,115 @@ builds:
- id: windows-build
dir: ./cmd/syft
binary: syft
goos: [windows]
goarch: [amd64, arm64]
goos:
- windows
goarch:
- amd64
mod_timestamp: *build-timestamp
ldflags: *build-ldflags
archives:
- id: linux-archives
ids: [linux-build]
builds:
- linux-build
# note: the signing process is depending on tar.gz archives. If this format changes then .github/scripts/apple-signing/*.sh will need to be adjusted
- id: darwin-archives
ids: [darwin-build]
builds:
- darwin-build
- id: windows-archives
formats:
- zip
ids: [windows-build]
format: zip
builds:
- windows-build
nfpms:
- license: "Apache 2.0"
maintainer: "Anchore, Inc"
homepage: &website "https://github.com/anchore/syft"
description: &description "A tool that generates a Software Bill Of Materials (SBOM) from container images and filesystems"
formats: [rpm, deb]
formats:
- rpm
- deb
brews:
- repository:
owner: anchore
name: homebrew-syft
token: "{{.Env.GITHUB_BREW_TOKEN}}"
ids: [darwin-archives, linux-archives]
ids:
- darwin-archives
- linux-archives
homepage: *website
description: *description
license: "Apache License 2.0"
dockers:
# production images...
- image_templates:
- anchore/syft:{{.Tag}}-amd64
- ghcr.io/anchore/syft:{{.Tag}}-amd64
- anchore/syft:debug
- anchore/syft:{{.Tag}}-debug
- ghcr.io/anchore/syft:debug
- ghcr.io/anchore/syft:{{.Tag}}-debug
goarch: amd64
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}}"
- image_templates:
- anchore/syft:debug-arm64v8
- anchore/syft:{{.Tag}}-debug-arm64v8
- ghcr.io/anchore/syft: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:debug-ppc64le
- anchore/syft:{{.Tag}}-debug-ppc64le
- ghcr.io/anchore/syft: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:debug-s390x
- anchore/syft:{{.Tag}}-debug-s390x
- ghcr.io/anchore/syft: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}}"
- image_templates:
- anchore/syft:latest
- anchore/syft:{{.Tag}}
- ghcr.io/anchore/syft:latest
- ghcr.io/anchore/syft:{{.Tag}}
goarch: amd64
dockerfile: Dockerfile
use: buildx
@@ -127,201 +208,51 @@ dockers:
- "--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
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}}"
- 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-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
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}}"
- 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-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}}
- anchore/syft:{{.Tag}}-arm64v8
- anchore/syft:{{.Tag}}-ppc64le
- anchore/syft:{{.Tag}}-s390x
- name_template: anchore/syft:debug
- anchore/syft:{{.Tag}}-debug
- anchore/syft:{{.Tag}}-debug-arm64v8
- anchore/syft:{{.Tag}}-debug-ppc64le
- anchore/syft:{{.Tag}}-debug-s390x
- name_template: anchore/syft:{{.Tag}}
image_templates:
- anchore/syft:{{.Tag}}
- anchore/syft:{{.Tag}}-arm64v8
- anchore/syft:{{.Tag}}-ppc64le
- anchore/syft:{{.Tag}}-s390x
- name_template: ghcr.io/anchore/syft:latest
image_templates:
- ghcr.io/anchore/syft:{{.Tag}}-amd64
- ghcr.io/anchore/syft:{{.Tag}}
- ghcr.io/anchore/syft:{{.Tag}}-arm64v8
- ghcr.io/anchore/syft:{{.Tag}}-ppc64le
- 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}}-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}}-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-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-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-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-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-s390x
- name_template: ghcr.io/anchore/syft:debug
image_templates:
- ghcr.io/anchore/syft:{{.Tag}}-debug-amd64
- ghcr.io/anchore/syft:{{.Tag}}-debug
- ghcr.io/anchore/syft:{{.Tag}}-debug-arm64v8
- ghcr.io/anchore/syft:{{.Tag}}-debug-ppc64le
- ghcr.io/anchore/syft:{{.Tag}}-debug-s390x
- name_template: anchore/syft:{{.Tag}}-debug
- name_template: ghcr.io/anchore/syft:{{.Tag}}
image_templates:
- anchore/syft:{{.Tag}}-debug-amd64
- anchore/syft:{{.Tag}}-debug-arm64v8
- anchore/syft:{{.Tag}}-debug-ppc64le
- 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-s390x
- ghcr.io/anchore/syft:{{.Tag}}
- ghcr.io/anchore/syft:{{.Tag}}-arm64v8
- ghcr.io/anchore/syft:{{.Tag}}-ppc64le
- ghcr.io/anchore/syft:{{.Tag}}-s390x
sboms:
- artifacts: archive
# this is relative to the snapshot/dist directory, not the root of the repo
cmd: ../.tool/syft
documents:
- "{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}.sbom"
@@ -337,7 +268,6 @@ signs:
certificate: "${artifact}.pem"
args:
- "sign-blob"
- "--use-signing-config=false"
- "--oidc-issuer=https://token.actions.githubusercontent.com"
- "--output-certificate=${certificate}"
- "--output-signature=${signature}"
+126 -3
View File
@@ -1,5 +1,128 @@
# Code of Conduct
# Contributor Covenant Code of Conduct
All contributors for any Anchore project must follow the [Contributor Covenant Code of Conduct](https://oss.anchore.com/docs/contributing/code-of-conduct/).
## Our Pledge
**TLDR:** Be kind, be respectful, and assume good intentions. We're all here to build great software together.
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
[opensource@anchore.com](mailto:opensource@anchore.com).
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
+148 -7
View File
@@ -1,13 +1,154 @@
# Contributing
[#](#) Contributing to Syft
Thank you for your interest in contributing to Syft!
If you are looking to contribute to this project and want to open a GitHub pull request ("PR"), there are a few guidelines of what we are looking for in patches. Make sure you go through this document and ensure that your code proposal is aligned.
Please see the [contribution guide](https://oss.anchore.com/docs/contributing/syft/) for development requirements and helpful tips to get started developing in the repo. For a deeper dive, please see the [architecture docs](https://oss.anchore.com/docs/architecture/syft/).
## Setting up your environment
**Have a question or need help?** Check out our [issues and discussions guide](https://oss.anchore.com/docs/contributing/issues-and-discussions/) to find the right place to start a conversation.
Before you can contribute to Syft, you need to configure your development environment.
**Ready to submit code?** Our [pull request guide](https://oss.anchore.com/docs/contributing/pull-requests/) covers everything from title conventions to the review process. Don't forget that ***all commits require a [sign-off](https://oss.anchore.com/docs/contributing/sign-off/)***.
### Debian setup
**Found a security issue?** Please do **not** open a public issue. Instead, see our [security policy](https://oss.anchore.com/docs/contributing/security/) for how to report vulnerabilities responsibly.
You will need to install Go. The version on https://go.dev works best, using the system golang doesn't always work the way you might expect.
**Want to help improve the docs?** Check out the [anchore/oss-docs](https://github.com/anchore/oss-docs) repository.
Refer to the go.mod file in the root of this repo for the recommended version of Go to install.
You will also need Docker. There's no reason the system packages shouldn't work, but we used the official Docker package. You can find instructions for installing Docker in Debian [here](https://docs.docker.com/engine/install/debian/).
You also need to install some Debian packages
```sh
sudo apt-get install build-essential zip bc libxml2-utils git
```
## Configuring Git
You will need to configure your git client with your name and email address. This is easily done from the command line.
```text
$ git config --global user.name "John Doe"
$ git config --global user.email "john.doe@example.com"
```
This username and email address will matter later in this guide.
## Fork the repo
You should fork the Syft repo using the "Fork" button at the top right of the Syft GitHub [site](https://github.com/anchore/syft/). You will be doing your development in your fork, then submit a pull request to Syft. There are many resources how to use GitHub effectively, we will not cover those here.
## Adding a feature or fix
If you look at the Syft [Issue](https://github.com/anchore/syft/issues) there are plenty of bugs and feature requests. Maybe look at the [good first issue](https://github.com/anchore/syft/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) list if you're not sure where to start.
## Commit guidelines
In the Syft project we like commits and pull requests (PR) to be easy to understand and review. Open source thrives best when everything happening is over documented and small enough to be understood.
### Granular commits
Please try to make every commit as simple as possible, but no simpler. The idea is that each commit should be a logical unit of code. Try not to commit too many tiny changes, for example every line changed in a file as a separate commit. And also try not to make a commit enormous, for example committing all your work at the end of the day.
Rather than try to follow a strict guide on what is or is not best, we try to be flexible and simple in this space. Do what makes the most sense for the changes you are trying to include.
### Commit title and description
Remember that the message you leave for a commit is for the reviewer in the present, and for someone (maybe you) changing something in the future. Please make sure the title and description used is easy to understand and explains what was done. Jokes and clever comments generally don't age well in commit messages. Just the facts please.
## Sign off your work
The `sign-off` is an added line at the end of the explanation for the commit, certifying that you wrote it or otherwise have the right to submit it as an open-source patch. By submitting a contribution, you agree to be bound by the terms of the DCO Version 1.1 and Apache License Version 2.0.
Signing off a commit certifies the below Developer's Certificate of Origin (DCO):
```text
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
```
All contributions to this project are licensed under the [Apache License Version 2.0, January 2004](http://www.apache.org/licenses/).
When committing your change, you can add the required line manually so that it looks like this:
```text
Signed-off-by: John Doe <john.doe@example.com>
```
Creating a signed-off commit is then possible with `-s` or `--signoff`:
```text
$ git commit -s -m "this is a commit message"
```
To double-check that the commit was signed-off, look at the log output:
```text
$ git log -1
commit 37ceh170e4hb283bb73d958f2036ee5k07e7fde7 (HEAD -> issue-35, origin/main, main)
Author: John Doe <john.doe@example.com>
Date: Mon Aug 1 11:27:13 2020 -0400
this is a commit message
Signed-off-by: John Doe <john.doe@example.com>
```
## Test your changes
This project has a `Makefile` which includes many helpers running both unit and integration tests. You can run `make help` to see all the options. Although PRs will have automatic checks for these, it is useful to run them locally, ensuring they pass before submitting changes. Ensure you've bootstrapped once before running tests:
```text
$ make bootstrap
```
You only need to bootstrap once. After the bootstrap process, you can run the tests as many times as needed:
```text
$ make unit
$ make integration
```
You can also run `make all` to run a more extensive test suite, but there is additional configuration that will be needed for those tests to run correctly. We will not cover the extra steps here.
## Pull Request
If you made it this far and all the tests are passing, it's time to submit a Pull Request (PR) for Syft. Submitting a PR is always a scary moment as what happens next can be an unknown. The Syft project strives to be easy to work with, we appreciate all contributions. Nobody is going to yell at you or try to make you feel bad. We love contributions and know how scary that first PR can be.
### PR Title and Description
Just like the commit title and description mentioned above, the PR title and description is very important for letting others know what's happening. Please include any details you think a reviewer will need to more properly review your PR.
A PR that is very large or poorly described has a higher likelihood of being pushed to the end of the list. Reviewers like PRs they can understand and quickly review.
### What to expect next
Please be patient with the project. We try to review PRs in a timely manner, but this is highly dependent on all the other tasks we have going on. It's OK to ask for a status update every week or two, it's not OK to ask for a status update every day.
It's very likely the reviewer will have questions and suggestions for changes to your PR. If your changes don't match the current style and flow of the other code, expect a request to change what you've done.
## Document your changes
And lastly, when proposed changes are modifying user-facing functionality or output, it is expected the PR will include updates to the documentation as well. Syft is not a project that is heavy on documentation. This will mostly be updating the README and help for the tool.
If nobody knows new features exist, they can't use them!
+382
View File
@@ -0,0 +1,382 @@
# Developing
## Getting started
In order to test and develop in this repo you will need the following dependencies installed:
- Golang
- docker
- make
- Python (>= 3.9)
### Docker settings for getting started
Make sure you've updated your docker settings so the default docker socket path is available.
Go to:
docker -> settings -> advanced
Make sure:
```
Allow the default Docker socket to be used
```
is checked.
Also double check that the docker context being used is the default context. If it is not, run:
`docker context use default`
After cloning the following step can help you get setup:
1. run `make bootstrap` to download go mod dependencies, create the `/.tmp` dir, and download helper utilities.
2. run `make` to view the selection of developer commands in the Makefile
3. run `make build` to build the release snapshot binaries and packages
4. for an even quicker start you can run `go run cmd/syft/main.go` to print the syft help.
- this command `go run cmd/syft/main.go alpine:latest` will compile and run syft against `alpine:latest`
5. view the README or syft help output for more output options
The main make tasks for common static analysis and testing are `lint`, `format`, `lint-fix`, `unit`, `integration`, and `cli`.
See `make help` for all the current make tasks.
### Internal Artifactory Settings
**Not always applicable**
Some companies have Artifactory setup internally as a solution for sourcing secure dependencies.
If you're seeing an issue where the unit tests won't run because of the below error then this section might be relevant for your use case.
```
[ERROR] [ERROR] Some problems were encountered while processing the POMs
```
If you're dealing with an issue where the unit tests will not pull/build certain java fixtures check some of these settings:
- a `settings.xml` file should be available to help you communicate with your internal artifactory deployment
- this can be moved to `syft/pkg/cataloger/java/test-fixtures/java-builds/example-jenkins-plugin/` to help build the unit test-fixtures
- you'll also want to modify the `build-example-jenkins-plugin.sh` to use `settings.xml`
For more information on this setup and troubleshooting see [issue 1895](https://github.com/anchore/syft/issues/1895#issuecomment-1610085319)
## Architecture
At a high level, this is the package structure of syft:
```
./cmd/syft/
│ ├── cli/
│ │ ├── cli.go // where all commands are wired up
│ │ ├── commands/ // all command implementations
│ │ ├── options/ // all command flags and configuration options
│ │ └── ui/ // all handlers for events that are shown on the UI
│ └── main.go // entrypoint for the application
└── syft/ // the "core" syft library
├── format/ // contains code to encode or decode to and from SBOM formats
├── pkg/ // contains code to catalog packages from a source
├── sbom/ // contains the definition of an SBOM
└── source/ // contains code to create a source object for some input type (e.g. container image, directory, etc)
```
Syft's core library is implemented in the `syft` package and subpackages, where the major packages are:
- the `syft/source` package produces a `source.Source` object that can be used to catalog a directory, container, and other source types.
- the `syft` package contains a single function that can take a `source.Source` object and catalog it, producing an `sbom.SBOM` object
- the `syft/format` package contains the ability to encode and decode SBOMs to and from different SBOM formats (such as SPDX and CycloneDX)
The `cmd` package at the highest level execution flow wires up [`spf13/cobra`](https://github.com/spf13/cobra) commands for execution in the main application:
```mermaid
sequenceDiagram
participant main as cmd/syft/main
participant cli as cli.New()
participant root as root.Execute()
participant cmd as <command>.Execute()
main->>+cli:
Note right of cli: wire ALL CLI commands
Note right of cli: add flags for ALL commands
cli-->>-main: root command
main->>+root:
root->>+cmd:
cmd-->>-root: (error)
root-->>-main: (error)
Note right of cmd: Execute SINGLE command from USER
```
The `packages` command uses the core library to generate an SBOM for the given user input:
```mermaid
sequenceDiagram
participant source as source.New(ubuntu:latest)
participant sbom as sbom.SBOM
participant catalog as syft.CatalogPackages(src)
participant encoder as syft.Encode(sbom, format)
Note right of source: use "ubuntu:latest" as SBOM input
source-->>+sbom: add source to SBOM struct
source-->>+catalog: pass src to generate catalog
catalog-->-sbom: add cataloging results onto SBOM
sbom-->>encoder: pass SBOM and format desiered to syft encoder
encoder-->>source: return bytes that are the SBOM of the original input
Note right of catalog: cataloger configuration is done based on src
```
Additionally, here is a [gist of using syft as a library](https://gist.github.com/spiffcs/3027638b7ba904d07e482a712bc00d3d) to generate a SBOM for a docker image.
### `pkg.Package` object
The `pkg.Package` object is a core data structure that represents a software package. Fields like `name` and `version` probably don't need
a detailed explanation, but some of the other fields are worth a quick overview:
- `FoundBy`: the name of the cataloger that discovered this package (e.g. `python-pip-cataloger`).
- `Locations`: these are the set of paths and layer ids that were parsed to discover this package (e.g. `python-pip-cataloger`).
- `Language`: the language of the package (e.g. `python`).
- `Type`: this is a high-level categorization of the ecosystem the package resides in. For instance, even if the package is a egg, wheel, or requirements.txt reference, it is still logically a "python" package. Not all package types align with a language (e.g. `rpm`) but it is common.
- `Metadata`: specialized data for specific location(s) parsed. We should try and raise up as much raw information that seems useful. As a rule of thumb the object here should be as flat as possible and use the raw names and values from the underlying source material parsed.
When `pkg.Package` is serialized an additional `MetadataType` is shown. This is a label that helps consumers understand the datashape of the `Metadata` field.
By convention the `MetadataType` value should follow these rules of thumb:
- Only use lowercase letters, numbers, and hyphens. Use hyphens to separate words.
- **Try to anchor the name in the ecosystem, language, or packaging tooling it belongs to**. For a package manager for a language ecosystem the language, framework or runtime should be used as a prefix. For instance `pubspec-lock` is an OK name, but `dart-pubspec-lock` is better. For an OS package manager this is not necessary (e.g. `apk-db-entry` is a good name, but `alpine-apk-db-entry` is not since `alpine` and the `a` in `apk` is redundant).
- **Be as specific as possible to what the data represents**. For instance `ruby-gem` is NOT a good `MetadataType` value, but `ruby-gemspec` is. Why? Ruby gem information can come from a gemspec file or a Gemfile.lock, which are very different. The latter name provides more context as to what to expect.
- **Should describe WHAT the data is, NOT HOW it's used**. For instance `r-description-installed-file` is NOT a good `MetadataType` value since it's trying to convey that we use the DESCRIPTION file in the R ecosystem to detect installed packages. Instead simply describe what the DESCRIPTION file is itself without context of how it's used: `r-description`.
- **Use the `lock` suffix** to distinct between manifest files that loosely describe package version requirements vs files that strongly specify one and only one version of a package ("lock" files). These should only be used with respect to package managers that have the guide and lock distinction, but would not be appropriate otherwise (e.g. `rpm` does not have a guide vs lock, so `lock` should NOT be used to describe a db entry).
- **Use the `archive` suffix to indicate a package archive** (e.g. rpm file, apk file, etc) that describes the contents of the package. For example an RPM file that was cataloged would have a `rpm-archive` metadata type (not to be confused with an RPM DB record entry which would be `rpm-db-entry`).
- **Use the `entry` suffix** to indicate information about a package that was found as a single entry within file that has multiple package entries. If the entry was found within a DB or a flat-file store for an OS package manager, you should use `db-entry`.
- **Should NOT contain the phrase `package`**, though exceptions are allowed (say if the canonical name literally has the phrase package in it).
- **Should NOT contain have a `file` suffix** unless the canonical name has the term "file", such as a `pipfile` or `gemfile`. An example of a bad name for this rule is`ruby-gemspec-file`; a better name would be `ruby-gemspec`.
- **Should NOT contain the exact filename+extensions**. For instance `pipfile.lock` shouldn't really be in the name, instead try and describe what the file is: `python-pipfile-lock` (but shouldn't this be `python-pip-lock` you might ask? No, since the `pip` package manger is not related to the `pipfile` project).
- **Should NOT contain the phrase `metadata`**, unless the canonical name has this term.
- **Should represent a single use case**. For example, trying to describe Hackage metadata with a single `HackageMetadata` struct (and thus `MetadataType`) is not allowed since it represents 3 mutually exclusive use cases: representing a `stack.yaml`, `stack.lock`, or `cabal.project` file. Instead, each of these should have their own struct types and `MetadataType` values.
There are other cases that are not covered by these rules... and that's ok! The goal is to provide a consistent naming scheme that is easy to understand and use when it's applicable. If the rules do not exactly apply in your situation then just use your best judgement (or amend these rules as needed whe new common cases come up).
What if the underlying parsed data represents multiple files? There are two approaches to this:
- use the primary file to represent all the data. For instance, though the `dpkg-cataloger` looks at multiple files to get all information about a package, it's the `status` file that gets represented.
- nest each individual file's data under the `Metadata` field. For instance, the `java-archive-cataloger` may find information from on or all of the files: `pom.xml`, `pom.properties`, and `MANIFEST.MF`. However, the metadata is simply `java-metadata' with each possibility as a nested optional field.
### Syft Catalogers
Catalogers are the way in which syft is able to identify and construct packages given a set a targeted list of files.
For example, a cataloger can ask syft for all `package-lock.json` files in order to parse and raise up javascript packages
(see [how file globs](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/cataloger/javascript/cataloger.go#L16-L21) and
[file parser functions](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/cataloger/javascript/cataloger.go#L16-L21) are used
for a quick example).
From a high level catalogers have the following properties:
- _They are independent from one another_. The java cataloger has no idea of the processes, assumptions, or results of the python cataloger, for example.
- _They do not know what source is being analyzed_. Are we analyzing a local directory? an image? if so, the squashed representation or all layers? The catalogers do not know the answers to these questions. Only that there is an interface to query for file paths and contents from an underlying "source" being scanned.
- _Packages created by the cataloger should not be mutated after they are created_. There is one exception made for adding CPEs to a package after the cataloging phase, but that will most likely be moved back into the cataloger in the future.
Cataloger names should be unique and named with the following rules of thumb in mind:
- Must end with `-cataloger`
- Use lowercase letters, numbers, and hyphens only
- Use hyphens to separate words
- Catalogers for language ecosystems should start with the language name (e.g. `python-` for a cataloger that raises up python packages)
- Distinct between when the cataloger is searching for evidence of installed packages vs declared packages. For example, there are currently two different gemspec-based catalogers, the `ruby-gemspec-cataloger` and `ruby-installed-gemspec-cataloger`, where the latter requires that the gemspec is found within a `specifications` directory (which means it was installed, not just at the root of a source repo).
#### Building a new Cataloger
Catalogers must fulfill the [`pkg.Cataloger` interface](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/cataloger.go) in order to add packages to the SBOM.
All catalogers should be added to:
- the [global list of catalogers](https://github.com/anchore/syft/blob/9995950c70e849f9921919faffbfcf46401f71f3/syft/pkg/cataloger/cataloger.go#L92-L125)
- at least one source-specific list, today the two lists are [directory catalogers and image catalogers](https://github.com/anchore/syft/blob/9995950c70e849f9921919faffbfcf46401f71f3/syft/pkg/cataloger/cataloger.go#L39-L89)
For reference, catalogers are [invoked within syft](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/cataloger/catalog.go#L41-L100) one after the other, and can be invoked in parallel.
`generic.NewCataloger` is an abstraction syft used to make writing common components easier (see the [apkdb cataloger](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/cataloger/apkdb/cataloger.go) for example usage).
It takes the following information as input:
- A `catalogerName` to identify the cataloger uniquely among all other catalogers.
- Pairs of file globs as well as parser functions to parse those files. These parser functions return a slice of [`pkg.Package`](https://github.com/anchore/syft/blob/9995950c70e849f9921919faffbfcf46401f71f3/syft/pkg/package.go#L19) as well as a slice of [`artifact.Relationship`](https://github.com/anchore/syft/blob/9995950c70e849f9921919faffbfcf46401f71f3/syft/artifact/relationship.go#L31) to describe how the returned packages are related. See this [the apkdb cataloger parser function](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/cataloger/apkdb/parse_apk_db.go#L22-L102) as an example.
Identified packages share a common `pkg.Package` struct so be sure that when the new cataloger is constructing a new package it is using the [`Package` struct](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/package.go#L16-L31).
If you want to return more information than what is available on the `pkg.Package` struct then you can do so in the `pkg.Package.Metadata` section of the struct, which is unique for each [`pkg.Type`](https://github.com/anchore/syft/blob/v0.70.0/syft/pkg/type.go).
See [the `pkg` package](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg) for examples of the different metadata types that are supported today.
These are plugged into the `MetadataType` and `Metadata` fields in the above struct. `MetadataType` informs which type is being used. `Metadata` is an interface converted to that type.
Finally, here is an example of where the package construction is done within the apk cataloger:
- [Calling the APK package constructor from the parser function](https://github.com/anchore/syft/blob/v0.70.0/syft/pkg/cataloger/apkdb/parse_apk_db.go#L106)
- [The APK package constructor itself](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/cataloger/apkdb/package.go#L12-L27)
Interested in building a new cataloger? Checkout the [list of issues with the `new-cataloger` label](https://github.com/anchore/syft/issues?q=is%3Aopen+is%3Aissue+label%3Anew-cataloger+no%3Aassignee)!
If you have questions about implementing a cataloger feel free to file an issue or reach out to us [on discourse](https://anchore.com/discourse)!
#### Searching for files
All catalogers are provided an instance of the [`file.Resolver`](https://github.com/anchore/syft/blob/v0.70.0/syft/source/file_resolver.go#L8) to interface with the image and search for files. The implementations for these
abstractions leverage [`stereoscope`](https://github.com/anchore/stereoscope) in order to perform searching. Here is a
rough outline how that works:
1. a stereoscope `file.Index` is searched based on the input given (a path, glob, or MIME type). The index is relatively fast to search, but requires results to be filtered down to the files that exist in the specific layer(s) of interest. This is done automatically by the `filetree.Searcher` abstraction. This abstraction will fallback to searching directly against the raw `filetree.FileTree` if the index does not contain the file(s) of interest. Note: the `filetree.Searcher` is used by the `file.Resolver` abstraction.
2. Once the set of files are returned from the `filetree.Searcher` the results are filtered down further to return the most unique file results. For example, you may have requested for files by a glob that returns multiple results. These results are filtered down to deduplicate by real files, so if a result contains two references to the same file, say one accessed via symlink and one accessed via the real path, then the real path reference is returned and the symlink reference is filtered out. If both were accessed by symlink then the first (by lexical order) is returned. This is done automatically by the `file.Resolver` abstraction.
3. By the time results reach the `pkg.Cataloger` you are guaranteed to have a set of unique files that exist in the layer(s) of interest (relative to what the resolver supports).
## Testing
### Levels of testing
- `unit`: The default level of test which is distributed throughout the repo are unit tests. Any `_test.go` file that
does not reside somewhere within the `/test` directory is a unit test. Other forms of testing should be organized in
the `/test` directory. These tests should focus on correctness of functionality in depth. % test coverage metrics
only considers unit tests and no other forms of testing.
- `integration`: located within `cmd/syft/internal/test/integration`, these tests focus on the behavior surfaced by the common library
entrypoints from the `syft` package and make light assertions about the results surfaced. Additionally, these tests
tend to make diversity assertions for enum-like objects, ensuring that as enum values are added to a definition
that integration tests will automatically fail if no test attempts to use that enum value. For more details see
the "Data diversity and freshness assertions" section below.
- `cli`: located with in `test/cli`, these are tests that test the correctness of application behavior from a
snapshot build. This should be used in cases where a unit or integration test will not do or if you are looking
for in-depth testing of code in the `cmd/` package (such as testing the proper behavior of application configuration,
CLI switches, and glue code before syft library calls).
- `acceptance`: located within `test/compare` and `test/install`, these are smoke-like tests that ensure that application
packaging and installation works as expected. For example, during release we provide RPM packages as a download
artifact. We also have an accompanying RPM acceptance test that installs the RPM from a snapshot build and ensures the
output of a syft invocation matches canned expected output. New acceptance tests should be added for each release artifact
and architecture supported (when possible).
### Data diversity and freshness assertions
It is important that tests against the codebase are flexible enough to begin failing when they do not cover "enough"
of the objects under test. "Cover" in this case does not mean that some percentage of the code has been executed
during testing, but instead that there is enough diversity of data input reflected in testing relative to the
definitions available.
For instance, consider an enum-like value like so:
```go
type Language string
const (
Java Language = "java"
JavaScript Language = "javascript"
Python Language = "python"
Ruby Language = "ruby"
Go Language = "go"
)
```
Say we have a test that exercises all the languages defined today:
```go
func TestCatalogPackages(t *testing.T) {
testTable := []struct {
// ... the set of test cases that test all languages
}
for _, test := range cases {
t.Run(test.name, func (t *testing.T) {
// use inputFixturePath and assert that syft.CatalogPackages() returns the set of expected Package objects
// ...
})
}
}
```
Where each test case has a `inputFixturePath` that would result with packages from each language. This test is
brittle since it does not assert that all languages were exercised directly and future modifications (such as
adding a new language) won't be covered by any test cases.
To address this the enum-like object should have a definition of all objects that can be used in testing:
```go
type Language string
// const( Java Language = ..., ... )
var AllLanguages = []Language{
Java,
JavaScript,
Python,
Ruby,
Go,
Rust,
}
```
Allowing testing to automatically fail when adding a new language:
```go
func TestCatalogPackages(t *testing.T) {
testTable := []struct {
// ... the set of test cases that (hopefully) covers all languages
}
// new stuff...
observedLanguages := strset.New()
for _, test := range cases {
t.Run(test.name, func (t *testing.T) {
// use inputFixturePath and assert that syft.CatalogPackages() returns the set of expected Package objects
// ...
// new stuff...
for _, actualPkg := range actual {
observedLanguages.Add(string(actualPkg.Language))
}
})
}
// new stuff...
for _, expectedLanguage := range pkg.AllLanguages {
if !observedLanguages.Contains(expectedLanguage) {
t.Errorf("failed to test language=%q", expectedLanguage)
}
}
}
```
This is a better test since it will fail when someone adds a new language but fails to write a test case that should
exercise that new language. This method is ideal for integration-level testing, where testing correctness in depth
is not needed (that is what unit tests are for) but instead testing in breadth to ensure that units are well integrated.
A similar case can be made for data freshness; if the quality of the results will be diminished if the input data
is not kept up to date then a test should be written (when possible) to assert any input data is not stale.
An example of this is the static list of licenses that is stored in `internal/spdxlicense` for use by the SPDX
presenters. This list is updated and published periodically by an external group and syft can grab and update this
list by running `go generate ./...` from the root of the repo.
An integration test has been written to grabs the latest license list version externally and compares that version
with the version generated in the codebase. If they differ, the test fails, indicating to someone that there is an
action needed to update it.
**_The key takeaway is to try and write tests that fail when data assumptions change and not just when code changes.**_
### Snapshot tests
The format objects make a lot of use of "snapshot" testing, where you save the expected output bytes from a call into the
git repository and during testing make a comparison of the actual bytes from the subject under test with the golden
copy saved in the repo. The "golden" files are stored in the `test-fixtures/snapshot` directory relative to the go
package under test and should always be updated by invoking `go test` on the specific test file with a specific CLI
update flag provided.
Many of the `Format` tests make use of this approach, where the raw SBOM report is saved in the repo and the test
compares that SBOM with what is generated from the latest presenter code. The following command can be used to
update the golden files for the various snapshot tests:
```bash
make update-format-golden-files
```
These flags are defined at the top of the test files that have tests that use the snapshot files.
Snapshot testing is only as good as the manual verification of the golden snapshot file saved to the repo! Be careful
and diligent when updating these files.
+1 -1
View File
@@ -1,4 +1,4 @@
FROM gcr.io/distroless/static-debian12:latest AS build
FROM gcr.io/distroless/static-debian11:debug@sha256:a0a404776dec98be120089ae42bbdfbe48c177921d856937d124d48eb8c0b951 AS build
FROM scratch
# needed for version check HTTPS request
+1 -3
View File
@@ -1,12 +1,10 @@
FROM gcr.io/distroless/static-debian12:debug-nonroot
FROM gcr.io/distroless/static-debian11:debug@sha256:a0a404776dec98be120089ae42bbdfbe48c177921d856937d124d48eb8c0b951
# 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
-27
View File
@@ -1,27 +0,0 @@
FROM gcr.io/distroless/static-debian12:nonroot
# 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"
ENTRYPOINT ["/syft"]
+2 -2
View File
@@ -12,7 +12,7 @@ TASK = $(TOOL_DIR)/task
# note: we need to assume that binny and task have not already been installed
$(BINNY):
@mkdir -p $(TOOL_DIR)
@curl -sSfL https://get.anchore.io/binny | sh -s -- -b $(TOOL_DIR)
@curl -sSfL https://raw.githubusercontent.com/$(OWNER)/binny/main/install.sh | sh -s -- -b $(TOOL_DIR)
# note: we need to assume that binny and task have not already been installed
.PHONY: task
@@ -36,7 +36,7 @@ make-default: $(TASK)
@$(TASK)
# for those of us that can't seem to kick the habit of typing `make ...` lets wrap the superior `task` tool
TASKS := $(shell bash -c "test -f $(TASK) && NO_COLOR=1 $(TASK) -l | grep '^\* ' | cut -d' ' -f2 | tr -d ':' | tr '\n' ' '" ) $(shell bash -c "test -f $(TASK) && NO_COLOR=1 $(TASK) -l | grep 'aliases:' | cut -d ':' -f 3 | tr '\n' ' ' | tr -d ','")
TASKS := $(shell bash -c "test -f $(TASK) && $(TASK) -l | grep '^\* ' | cut -d' ' -f2 | tr -d ':' | tr '\n' ' '" ) $(shell bash -c "test -f $(TASK) && $(TASK) -l | grep 'aliases:' | cut -d ':' -f 3 | tr '\n' ' ' | tr -d ','")
.PHONY: $(TASKS)
$(TASKS): $(TASK)
+144 -48
View File
@@ -18,74 +18,170 @@
![syft-demo](https://user-images.githubusercontent.com/590471/90277200-2a253000-de33-11ea-893f-32c219eea11a.gif)
## Introduction
Syft is a powerful and easy-to-use open-source tool for generating Software Bill of Materials (SBOMs) for container images and filesystems. It provides detailed visibility into the packages and dependencies in your software, helping you manage vulnerabilities, license compliance, and software supply chain security.
Syft development is sponsored by [Anchore](https://anchore.com/), and is released under the [Apache-2.0 License](https://github.com/anchore/syft?tab=Apache-2.0-1-ov-file). For commercial support options with Syft or Grype, please [contact Anchore](https://get.anchore.com/contact/).
## Features
- Generates SBOMs for **container images**, **filesystems**, **archives** (see the docs for a full list of [supported scan targets](https://oss.anchore.com/docs/guides/sbom/scan-targets/))
- Supports dozens of packaging ecosystems (e.g. Alpine (apk), Debian (dpkg), RPM, Go, Python, Java, JavaScript, Ruby, Rust, PHP, .NET, and [many more](https://oss.anchore.com/docs/capabilities/all-packages/))
- Supports OCI, Docker, [Singularity](https://github.com/sylabs/singularity), and [more image formats](https://oss.anchore.com/docs/guides/sbom/scan-targets/)
- Works seamlessly with [Grype](https://github.com/anchore/grype) for vulnerability scanning
- Multiple output formats (**CycloneDX**, **SPDX**, **Syft JSON**, and [more](https://oss.anchore.com/docs/guides/sbom/formats/)) including the ability to [convert between SBOM formats](https://oss.anchore.com/docs/guides/sbom/conversion/)
- Create signed SBOM attestations using the [in-toto specification](https://github.com/in-toto/attestation/blob/main/spec/README.md)
> [!TIP]
> **New to Syft? Check out the [Getting Started guide](https://oss.anchore.com/docs/guides/sbom/getting-started/) for a walkthrough!**
- Generates SBOMs for container images, filesystems, archives, and more to discover packages and libraries
- Supports OCI, Docker and [Singularity](https://github.com/sylabs/singularity) image formats
- Linux distribution identification
- Works seamlessly with [Grype](https://github.com/anchore/grype) (a fast, modern vulnerability scanner)
- Able to create signed SBOM attestations using the [in-toto specification](https://github.com/in-toto/attestation/blob/main/spec/README.md)
- Convert between SBOM formats, such as CycloneDX, SPDX, and Syft's own format.
## Installation
The quickest way to get up and going:
Syft binaries are provided for Linux, macOS and Windows.
### Recommended
> ```bash
> curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
> ```
Install script options:
- `-b`: Specify a custom installation directory (defaults to `./bin`)
- `-d`: More verbose logging levels (`-d` for debug, `-dd` for trace)
- `-v`: Verify the signature of the downloaded artifact before installation (requires [`cosign`](https://github.com/sigstore/cosign) to be installed)
### Homebrew
```bash
curl -sSfL https://get.anchore.io/syft | sudo sh -s -- -b /usr/local/bin
brew install syft
```
> [!TIP]
> **See [Installation docs](https://oss.anchore.com/docs/installation/syft/) for more ways to get Syft, including Homebrew, Docker, Scoop, Chocolatey, Nix, and more!**
### Scoop
## The basics
See the packages within a container image or directory:
```bash
# container image
syft alpine:latest
# directory
syft ./my-project
```powershell
scoop install syft
```
To get an SBOM, specify one or more output formats:
### Chocolatey
```bash
# SBOM to stdout
syft <image> -o cyclonedx-json
The chocolatey distribution of Syft is community-maintained and not distributed by the Anchore team
# Multiple SBOMs to files
syft <image> -o spdx-json=./spdx.json -o cyclonedx-json=./cdx.json
```powershell
choco install syft -y
```
### Nix
> [!TIP]
> **Check out the [Getting Started guide](https://oss.anchore.com/docs/guides/sbom/getting-started/)** to explore all of the capabilities and features.
>
> **Want to know all of the ins-and-outs of Syft?** Check out the [CLI docs](https://oss.anchore.com/docs/reference/syft/cli/), [configuration docs](https://oss.anchore.com/docs/reference/syft/configuration/), and [JSON schema](https://oss.anchore.com/docs/reference/syft/json/latest/).
**Note**: Nix packaging of Syft is [community maintained](https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/sy/syft/package.nix). Syft is available in the [stable channel](https://wiki.nixos.org/wiki/Nix_channels#The_official_channels) since NixOS `22.05`.
```bash
nix-env -i syft
```
... or, just try it out in an ephemeral nix shell:
```bash
nix-shell -p syft
```
## Getting started
### SBOM
To generate an SBOM for a container image:
```bash
syft <image>
```
The above output includes only software that is visible in the container (i.e., the squashed representation of the image). To include software from all image layers in the SBOM, regardless of its presence in the final image, provide `--scope all-layers`:
```bash
syft <image> --scope all-layers
```
### Output formats
The output format for Syft is configurable as well using the `-o` (or `--output`) option:
```
syft <image> -o <format>
```
Where the `formats` available are:
- `syft-json`: Use this to get as much information out of Syft as possible!
- `syft-text`: A row-oriented, human-and-machine-friendly output.
- `cyclonedx-xml`: A XML report conforming to the [CycloneDX 1.6 specification](https://cyclonedx.org/specification/overview/).
- `cyclonedx-xml@1.5`: A XML report conforming to the [CycloneDX 1.5 specification](https://cyclonedx.org/specification/overview/).
- `cyclonedx-json`: A JSON report conforming to the [CycloneDX 1.6 specification](https://cyclonedx.org/specification/overview/).
- `cyclonedx-json@1.5`: A JSON report conforming to the [CycloneDX 1.5 specification](https://cyclonedx.org/specification/overview/).
- `spdx-tag-value`: A tag-value formatted report conforming to the [SPDX 2.3 specification](https://spdx.github.io/spdx-spec/v2.3/).
- `spdx-tag-value@2.2`: A tag-value formatted report conforming to the [SPDX 2.2 specification](https://spdx.github.io/spdx-spec/v2.2.2/).
- `spdx-json`: A JSON report conforming to the [SPDX 2.3 JSON Schema](https://github.com/spdx/spdx-spec/blob/v2.3/schemas/spdx-schema.json).
- `spdx-json@2.2`: A JSON report conforming to the [SPDX 2.2 JSON Schema](https://github.com/spdx/spdx-spec/blob/v2.2/schemas/spdx-schema.json).
- `github-json`: A JSON report conforming to GitHub's dependency snapshot format.
- `syft-table`: A columnar summary (default).
- `template`: Lets the user specify the output format. See ["Using templates"](#using-templates) below.
Note that flags using the @<version> can be used for earlier versions of each specification as well.
### Supported Ecosystems
- Alpine (apk)
- Bitnami packages
- C (conan)
- C++ (conan)
- Dart (pubs)
- Debian (dpkg)
- Dotnet (deps.json)
- Objective-C (cocoapods)
- Elixir (mix)
- Erlang (rebar3)
- Go (go.mod, Go binaries)
- Haskell (cabal, stack)
- Java (jar, ear, war, par, sar, nar, native-image)
- JavaScript (npm, yarn)
- Jenkins Plugins (jpi, hpi)
- Linux kernel archives (vmlinz)
- Linux kernel modules (ko)
- Nix (outputs in /nix/store)
- PHP (composer)
- Python (wheel, egg, poetry, requirements.txt)
- Red Hat (rpm)
- Ruby (gem)
- Rust (cargo.lock)
- Swift (cocoapods, swift-package-manager)
- Wordpress plugins
- Terraform providers (.terraform.lock.hcl)
## Documentation
Our [wiki](https://github.com/anchore/syft/wiki) contains further details on the following topics:
* [Supported Sources](https://github.com/anchore/syft/wiki/supported-sources)
* [File Selection](https://github.com/anchore/syft/wiki/file-selection)
* [Excluding file paths](https://github.com/anchore/syft/wiki/excluding-file-paths)
* [Output formats](https://github.com/anchore/syft/wiki/output-formats)
* [Package Cataloger Selection](https://github.com/anchore/syft/wiki/package-cataloger-selection)
* [Concepts](https://github.com/anchore/syft/wiki/package-cataloger-selection#concepts)
* [Examples](https://github.com/anchore/syft/wiki/package-cataloger-selection#examples)
* [Using templates](https://github.com/anchore/syft/wiki/using-templates)
* [Multiple outputs](https://github.com/anchore/syft/wiki/multiple-outputs)
* [Private Registry Authentication](https://github.com/anchore/syft/wiki/private-registry-authentication)
* [Local Docker Credentials](https://github.com/anchore/syft/wiki/private-registry-authentication#local-docker)
* [Docker Credentials in Kubernetes](https://github.com/anchore/syft/wiki/private-registry-authentication#docker-credentials-in-kubernetes)
* [Attestation (experimental)](https://github.com/anchore/syft/wiki/attestation)
* [Keyless Support](https://github.com/anchore/syft/wiki/attestation#keyless-support)
* [Local private key support](https://github.com/anchore/syft/wiki/attestation#local-private-key-support)
* [Adding an SBOM to an image as an attestation using Syft](https://github.com/anchore/syft/wiki/attestation#adding-an-sbom-to-an-image-as-an-attestation-using-syft)
* [Configuration](https://github.com/anchore/syft/wiki/configuration)
## Contributing
We encourage users to help make these tools better by [submitting issues](https://github.com/anchore/syft/issues) when you find a bug or want a new feature.
Check out our [contributing overview](https://oss.anchore.com/docs/contributing/) and [developer-specific documentation](https://oss.anchore.com/docs/contributing/syft/) if you are interested in providing code contributions.
Check out our [contributing](/CONTRIBUTING.md) guide and [developer](/DEVELOPING.md) docs.
## Syft Team Meetings
<p xmlns:cc="http://creativecommons.org/ns#" xmlns:dct="http://purl.org/dc/terms/">
Syft development is sponsored by <a href="https://anchore.com/">Anchore</a>, and is released under the <a href="https://github.com/anchore/syft?tab=Apache-2.0-1-ov-file">Apache-2.0 License</a>.
The <a property="dct:title" rel="cc:attributionURL" href="https://anchore.com/wp-content/uploads/2024/11/syft-logo.svg">Syft logo</a> by <a rel="cc:attributionURL dct:creator" property="cc:attributionName" href="https://anchore.com/">Anchore</a> is licensed under <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="license noopener noreferrer" style="display:inline-block;">CC BY 4.0<img style="height:22px!important;margin-left:3px;vertical-align:text-bottom;" src="https://mirrors.creativecommons.org/presskit/icons/cc.svg" alt=""><img style="height:22px!important;margin-left:3px;vertical-align:text-bottom;" src="https://mirrors.creativecommons.org/presskit/icons/by.svg" alt=""></a>
</p>
For commercial support options with Syft or Grype, please [contact Anchore](https://get.anchore.com/contact/).
## Come talk to us!
The Syft Team holds regular community meetings online. All are welcome to join to bring topics for discussion.
- Check the [calendar](https://calendar.google.com/calendar/u/0/r?cid=Y182OTM4dGt0MjRtajI0NnNzOThiaGtnM29qNEBncm91cC5jYWxlbmRhci5nb29nbGUuY29t) for the next meeting date.
The Syft Team hold regular community meetings online. All are welcome to join to bring topics for discussion.
- Check the [calendar](https://calendar.google.com/calendar/u/0/r?cid=Y182OTM4dGt0MjRtajI0NnNzOThiaGtnM29qNEBncm91cC5jYWxlbmRhci5nb29nbGUuY29t) for the next meeting date.
- Add items to the [agenda](https://docs.google.com/document/d/1ZtSAa6fj2a6KRWviTn3WoJm09edvrNUp4Iz_dOjjyY8/edit?usp=sharing) (join [this group](https://groups.google.com/g/anchore-oss-community) for write access to the [agenda](https://docs.google.com/document/d/1ZtSAa6fj2a6KRWviTn3WoJm09edvrNUp4Iz_dOjjyY8/edit?usp=sharing))
- See you there!
## Syft Logo
<p xmlns:cc="http://creativecommons.org/ns#" xmlns:dct="http://purl.org/dc/terms/"><a property="dct:title" rel="cc:attributionURL" href="https://anchore.com/wp-content/uploads/2024/11/syft-logo.svg">Syft Logo</a> by <a rel="cc:attributionURL dct:creator" property="cc:attributionName" href="https://anchore.com/">Anchore</a> is licensed under <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="license noopener noreferrer" style="display:inline-block;">CC BY 4.0<img style="height:22px!important;margin-left:3px;vertical-align:text-bottom;" src="https://mirrors.creativecommons.org/presskit/icons/cc.svg" alt=""><img style="height:22px!important;margin-left:3px;vertical-align:text-bottom;" src="https://mirrors.creativecommons.org/presskit/icons/by.svg" alt=""></a></p>
+19 -3
View File
@@ -2,15 +2,31 @@
## Supported Versions
<!-- Use this section to tell people about which versions of your project are
currently being supported with security updates.
| Version | Supported |
| ------- | ------------------ |
| 5.1.x | :white_check_mark: |
| 5.0.x | :x: |
| 4.0.x | :white_check_mark: |
| < 4.0 | :x: |
-->
Security updates are applied only to the most recent release, try to always be up to date.
## Reporting a Vulnerability
<!-- Use this section to tell people how to report a vulnerability.
Tell them where to go, how often they can expect to get an update on a
reported vulnerability, what to expect if the vulnerability is accepted or
declined, etc. -->
To report a security issue, please email
[security@anchore.com](mailto:security@anchore.com)
with a description of the issue, the steps you took to create the issue,
affected versions, and, if known, mitigations for the issue.
All support will be made on a best effort basis, so please indicate the "urgency level" of the vulnerability as Critical, High, Medium or Low.
For more details, see our [security policy documentation](https://oss.anchore.com/docs/contributing/security/).
All support will be made on the best effort base, so please indicate the "urgency level" of the vulnerability as Critical, High, Medium or Low.
+67 -84
View File
@@ -1,9 +1,5 @@
version: "3"
includes:
generate:cpe-index: ./task.d/generate/cpe-index.yaml
vars:
OWNER: anchore
PROJECT: syft
@@ -31,12 +27,10 @@ vars:
sh: uname -s | tr '[:upper:]' '[:lower:]'
ARCH:
sh: |
[ "$(uname -m)" = "x86_64" ] && echo "amd64_v1" || { [ "$(uname -m)" = "aarch64" ] && echo "arm64_v8.0" || [ "$(uname -m)" = "arm64" ] && echo "arm64_v8.0" || echo $(uname -m); }
[ "$(uname -m)" = "x86_64" ] && echo "amd64_v1" || { [ "$(uname -m)" = "aarch64" ] && echo "arm64_v8.0" || echo $(uname -m); }
PROJECT_ROOT:
sh: echo $PWD
# 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
SNAPSHOT_BIN: "{{ .PROJECT_ROOT }}/{{ .SNAPSHOT_DIR }}/{{ .OS }}-build_{{ .OS }}_{{ .ARCH }}/{{ .PROJECT }}"
SNAPSHOT_CMD: "{{ .TOOL_DIR }}/goreleaser release --config {{ .TMP_DIR }}/goreleaser.yaml --clean --snapshot --skip=publish --skip=sign"
@@ -73,7 +67,6 @@ tasks:
- task: check-licenses
- task: lint
- task: check-json-schema-drift
- task: check-capability-drift
- task: check-binary-fixture-size
test:
@@ -82,8 +75,8 @@ tasks:
- task: unit
- task: integration
- task: validate-cyclonedx-schema
- task: benchmark
- task: test-utils
- task: snapshot
- task: cli
- task: check-docker-cache
@@ -96,7 +89,7 @@ tasks:
- "{{ .TOOL_DIR }}/binny"
status:
- "test -f {{ .TOOL_DIR }}/binny"
cmd: "curl -sSfL https://get.anchore.io/binny | sh -s -- -b .tool"
cmd: "curl -sSfL https://raw.githubusercontent.com/anchore/binny/main/install.sh | sh -s -- -b .tool"
silent: true
tools:
@@ -191,11 +184,6 @@ tasks:
cmds:
- .github/scripts/json-schema-drift-check.sh
check-capability-drift:
desc: Ensure there is no drift between the capabilities data and the code
cmds:
- .github/scripts/capability-drift-check.sh
check-binary-fixture-size:
desc: Ensure that the binary test fixtures are not too large
cmds:
@@ -224,7 +212,6 @@ tasks:
# unit test coverage threshold (in % coverage)
COVERAGE_THRESHOLD: 62
cmds:
- task: clean-test-observations
- "go test -coverprofile {{ .TMP_DIR }}/unit-coverage-details.txt {{ .TEST_PKGS }}"
- cmd: ".github/scripts/coverage.py {{ .COVERAGE_THRESHOLD }} {{ .TMP_DIR }}/unit-coverage-details.txt"
silent: true
@@ -234,17 +221,19 @@ tasks:
cmds:
- "go test -v ./cmd/syft/internal/test/integration"
# exercise most of the CLI with the data race detector
# we use a larger image to ensure we're using multiple catalogers at a time
- "go run -race cmd/syft/main.go anchore/test_images:grype-quality-dotnet-69f15d2"
- "go run -race cmd/syft/main.go alpine:latest"
validate-cyclonedx-schema:
desc: Validate that Syft produces valid CycloneDX documents
desc: Run integration tests
cmds:
- "cd schema/cyclonedx && make"
cli:
desc: Run CLI tests
deps: [tools]
# note: we don't want to regenerate the snapshot unless we have to. In CI it's probable
# that the cache being restored with the correct binary will be rebuilt since the timestamps
# and local checksums will not line up.
deps: [tools, snapshot]
cmds:
- cmd: "echo 'testing binary: {{ .SNAPSHOT_BIN }}'"
silent: true
@@ -261,6 +250,27 @@ tasks:
- cmd: .github/scripts/labeler_test.py
## Benchmark test targets #################################
benchmark:
deps: [tmpdir]
generates:
- "{{ .TMP_DIR }}/benchmark-main.txt"
cmds:
- "go test -p 1 -run=^Benchmark -bench=. -count=7 -benchmem ./... | tee {{ .TMP_DIR }}/benchmark-{{ .VERSION }}.txt"
- |
bash -c "(test -s {{ .TMP_DIR }}/benchmark-main.txt && \
{{ .TOOL_DIR }}/benchstat {{ .TMP_DIR }}/benchmark-main.txt {{ .TMP_DIR }}/benchmark-{{ .VERSION }}.txt || \
{{ .TOOL_DIR }}/benchstat {{ .TMP_DIR }}/benchmark-{{ .VERSION }}.txt) \
| tee {{ .TMP_DIR }}/benchstat.txt"
show-benchstat:
deps: [benchmark, tmpdir]
cmds:
- cmd: "cat {{ .TMP_DIR }}/benchstat.txt"
silent: true
## Test-fixture-related targets #################################
fingerprints:
@@ -276,12 +286,12 @@ tasks:
YELLOW='\033[0;33m'
RESET='\033[0m'
echo -e "${YELLOW}creating fingerprint files for non-docker fixtures...${RESET}"
echo -e "${YELLOW}creating fingerprint files for non-docker fixtures...${RESET}"
for dir in $(find . -type d -name 'test-fixtures'); do
if [ -f "$dir/Makefile" ]; then
# for debugging...
#echo -e "${YELLOW}• calculating fingerprints in $dir... ${RESET}"
(make -C "$dir" fingerprint)
fi
done
@@ -310,18 +320,18 @@ tasks:
BOLD='\033[1m'
PURPLE='\033[0;35m'
RESET='\033[0m'
# if DOWNLOAD_TEST_FIXTURE_CACHE is set to 'false', then skip the cache download and always build
if [ "$DOWNLOAD_TEST_FIXTURE_CACHE" = "false" ]; then
echo -e "${BOLD}${PURPLE}skipping cache download, rebuilding cache...${RESET}"
{{ .TASK }} build-fixtures
exit 0
fi
LATEST_FINGERPRINT=$(docker manifest inspect {{ .CACHE_IMAGE }} | {{ .YQ }} -r '.annotations.fingerprint')
echo "latest cache: $LATEST_FINGERPRINT"
if [ -f {{ .LAST_CACHE_PULL_FILE }} ]; then
LAST_PULL_FINGERPRINT=$(cat {{ .LAST_CACHE_PULL_FILE }} | {{ .YQ }} -r '.digest')
else
@@ -329,41 +339,41 @@ tasks:
{{ .TASK }} download-test-fixture-cache
exit 0
fi
{{ .TASK }} fingerprints
WANT_FINGERPRINT=$(cat {{ .CACHE_PATHS_FILE }} | {{ .YQ }} -r '.digest')
WANT_FINGERPRINT=$(cat {{ .CACHE_PATHS_FILE }} | {{ .YQ }} -r '.digest')
echo "desired cache: $WANT_FINGERPRINT"
echo "last pulled cache: $LAST_PULL_FINGERPRINT"
# if we already have the latest cache, skip the refresh
if [ "$LAST_PULL_FINGERPRINT" = "$WANT_FINGERPRINT" ]; then
echo -e "${BOLD}${PURPLE}already have the latest cache (skipping cache download)${RESET}"
exit 0
fi
# at this point we only refresh the cache if we want the same cache that is currently available.
# we don't by default refresh the cache if the cache if it is simply different from what we have,
# because we may be working on a code change that doesn't require a cache refresh (but could trigger one,
# which would be annoying to deal with in a development workflow).
if [ "$LATEST_FINGERPRINT" = "$WANT_FINGERPRINT" ]; then
echo -e "${BOLD}${PURPLE}found newer cache! downloading cache...${RESET}"
{{ .TASK }} download-test-fixture-cache
else
echo -e "${BOLD}${PURPLE}found different cache, but isn't clear if it's newer (skipping cache download and manually building)${RESET}"
{{ .YQ }} eval '.paths[] | "\(.digest) \(.path)"' {{ .LAST_CACHE_PULL_FILE }} > .tmp/last_cache_lines
{{ .YQ }} eval '.paths[] | "\(.digest) \(.path)"' {{ .CACHE_PATHS_FILE }} > .tmp/cache_lines
diff .tmp/last_cache_lines .tmp/cache_lines || true
echo -e "${BOLD}${PURPLE}diff with more context...${RESET}"
diff -U10000 {{ .LAST_CACHE_PULL_FILE }} {{ .CACHE_PATHS_FILE }} || true
echo -e "${BOLD}${PURPLE}detected changes to input material, manually building fixtures...${RESET}"
{{ .TASK }} build-fixtures
fi
@@ -373,18 +383,15 @@ tasks:
# this will look for `test-fixtures/Makefile` and invoke the `fixtures` target to generate any and all test fixtures
cmds:
- |
# we want to stop on the first build error
set -e
BOLD='\033[1m'
YELLOW='\033[0;33m'
RESET='\033[0m'
# Use a for loop with command substitution to avoid subshell issues
for dir in $(find . -type d -name 'test-fixtures'); do
if [ -f "$dir/Makefile" ]; then
echo -e "${YELLOW}${BOLD}generating fixtures in $dir${RESET}"
make -C "$dir" fixtures
(make -C "$dir" fixtures)
fi
done
echo -e "${BOLD}generated all fixtures${RESET}"
@@ -417,15 +424,15 @@ tasks:
set -eu
oras_command="{{ .ORAS }} push {{ .CACHE_IMAGE }}"
paths=$(cat {{ .CACHE_PATHS_FILE }} | {{ .YQ }} -r '.paths[].path')
paths=$(cat {{ .CACHE_PATHS_FILE }} | {{ .YQ }} -r '.paths[].path')
for path in $paths; do
oras_command+=" $path"
done
oras_command+=" {{ .CACHE_PATHS_FILE }}"
oras_command+=" --annotation org.opencontainers.image.source=https://github.com/{{ .OWNER }}/{{ .PROJECT }}"
oras_command+=" --annotation fingerprint=$(cat {{ .CACHE_PATHS_FILE }} | {{ .YQ }} -r '.digest')"
echo "Executing: $oras_command"
eval $oras_command
@@ -445,8 +452,8 @@ tasks:
find . | grep cache | grep tar | xargs du
echo "total $total_size KB"
if [ "$total_size" -gt 5242880 ]; then
echo 'docker cache is larger than 5GB'
if [ "$total_size" -gt 1048576 ]; then
echo 'docker cache is larger than 1GB'
exit 1
fi
@@ -510,14 +517,13 @@ tasks:
desc: Add data generation tasks
cmds:
- task: generate-json-schema
- task: generate-capabilities
- task: generate-license-list
- task: generate-cpe-dictionary-index
generate-json-schema:
desc: Generate a new JSON schema
cmds:
- "cd ./internal && go generate . && cd ./jsonschema && go run . && go fmt ../..."
- "cd syft/internal && go generate . && cd jsonschema && go run . && go fmt ../..."
generate-license-list:
desc: Generate an updated license processing code off of the latest available SPDX license list
@@ -526,26 +532,10 @@ tasks:
- "gofmt -s -w ./internal/spdxlicense"
generate-cpe-dictionary-index:
desc: Generate the CPE index from local cache
desc: Generate the CPE index based off of the latest available CPE dictionary
dir: "syft/pkg/cataloger/internal/cpegenerate/dictionary"
cmds:
- task: generate:cpe-index:cache:pull
- task: generate:cpe-index:cache:update
- task: generate:cpe-index:build
generate-capabilities:
desc: Generate the capabilities data file
deps:
- tmpdir
- fixtures
cmds:
# remove all test observations prior to regenerating
- task: clean-test-observations
# this is required to update test observations; such evidence is used to update the packages/*.yaml
- "go test ./syft/pkg/... -count=1"
- "go generate ./internal/capabilities/..."
- "gofmt -s -w ./internal/capabilities"
# now that we have the latest capabilities, run completeness tests to ensure this is self-consistent
- "SYFT_ENABLE_COMPLETENESS_TESTS=true go test ./internal/capabilities/... -count=1"
- "go generate"
## Build-related targets #################################
@@ -583,18 +573,6 @@ tasks:
- "{{ .SNAPSHOT_CMD }}"
snapshot-smoke-test:
desc: Run a smoke test on the snapshot builds + docker images
cmds:
- cmd: "echo 'testing snapshot binary: {{ .SNAPSHOT_BIN }}'"
silent: true
- cmd: "test -f {{ .SNAPSHOT_BIN }} || (find {{ .SNAPSHOT_DIR }} && echo '\nno snapshot found for {{ .SNAPSHOT_BIN }}' && false)"
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
changelog:
desc: Generate a changelog
deps: [tools]
@@ -635,6 +613,16 @@ tasks:
silent: true
- "{{ .RELEASE_CMD }}"
ci-release-version-file:
# desc: "[CI only] Update the version file"
deps: [tools]
cmds:
- task: ci-check
- ".github/scripts/update-version-file.sh {{ .RELEASE_VERSION }}"
requires:
vars:
- RELEASE_VERSION
## Cleanup targets #################################
@@ -662,7 +650,7 @@ tasks:
BOLD='\033[1m'
YELLOW='\033[0;33m'
RESET='\033[0m'
# Use a for loop with command substitution to avoid subshell issues
for dir in $(find . -type d -name 'test-fixtures'); do
if [ -f "$dir/Makefile" ]; then
@@ -672,8 +660,3 @@ tasks:
done
echo -e "${BOLD}Deleted all ephemeral test fixtures${RESET}"
- rm -f {{ .LAST_CACHE_PULL_FILE }} {{ .CACHE_PATHS_FILE }}
clean-test-observations:
desc: Remove all test observations (i.e. test-fixtures/test-observations.json)
cmds:
- find . -type f -wholename "**/test-fixtures/test-observations.json" | xargs rm -f
+1 -6
View File
@@ -10,7 +10,6 @@ import (
"github.com/anchore/clio"
"github.com/anchore/syft/cmd/syft/internal"
"github.com/anchore/syft/cmd/syft/internal/commands"
constants "github.com/anchore/syft/internal"
)
// Application constructs the `syft packages` command and aliases the root command to `syft packages`.
@@ -48,7 +47,7 @@ func create(id clio.Identification, out io.Writer) (clio.Application, *cobra.Com
commands.Cataloger(app),
commands.Attest(app),
commands.Convert(app),
clio.VersionCommand(id, schemaVersion),
clio.VersionCommand(id),
clio.ConfigCommand(app, nil),
cranecmd.NewCmdAuthLogin(id.Name), // syft login uses the same command as crane
)
@@ -60,7 +59,3 @@ func create(id clio.Identification, out io.Writer) (clio.Application, *cobra.Com
return app, rootCmd
}
func schemaVersion() (string, any) {
return "SchemaVersion", constants.JSONSchemaVersion
}
@@ -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 +0,0 @@
[TestHandler_handlePullSourceStarted/snap_download_in_progress - 1]
⠋ Downloading snap file... ━━━━━━━━━━━━━━━━━━━━ example-app_1.0_amd64.snap
---
[TestHandler_handlePullSourceStarted/snap_download_complete - 1]
✔ 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
---
@@ -26,7 +26,7 @@ func TestHandler_handleCatalogerTaskStarted(t *testing.T) {
{
name: "cataloging task in progress",
eventFn: func(t *testing.T) partybus.Event {
value := &monitor.TaskProgress{
value := &monitor.CatalogerTaskProgress{
AtomicStage: progress.NewAtomicStage("some stage"),
Manual: progress.NewManual(100),
}
@@ -48,7 +48,7 @@ func TestHandler_handleCatalogerTaskStarted(t *testing.T) {
{
name: "cataloging sub task in progress",
eventFn: func(t *testing.T) partybus.Event {
value := &monitor.TaskProgress{
value := &monitor.CatalogerTaskProgress{
AtomicStage: progress.NewAtomicStage("some stage"),
Manual: progress.NewManual(100),
}
@@ -71,7 +71,7 @@ func TestHandler_handleCatalogerTaskStarted(t *testing.T) {
{
name: "cataloging sub task complete",
eventFn: func(t *testing.T) partybus.Event {
value := &monitor.TaskProgress{
value := &monitor.CatalogerTaskProgress{
AtomicStage: progress.NewAtomicStage("some stage"),
Manual: progress.NewManual(100),
}
@@ -94,7 +94,7 @@ func TestHandler_handleCatalogerTaskStarted(t *testing.T) {
{
name: "cataloging sub task complete -- hide stage",
eventFn: func(t *testing.T) partybus.Event {
value := &monitor.TaskProgress{
value := &monitor.CatalogerTaskProgress{
AtomicStage: progress.NewAtomicStage("some stage"),
Manual: progress.NewManual(100),
}
@@ -117,7 +117,7 @@ func TestHandler_handleCatalogerTaskStarted(t *testing.T) {
{
name: "cataloging sub task complete with removal",
eventFn: func(t *testing.T) partybus.Event {
value := &monitor.TaskProgress{
value := &monitor.CatalogerTaskProgress{
AtomicStage: progress.NewAtomicStage("some stage"),
Manual: progress.NewManual(100),
}
@@ -162,7 +162,7 @@ func TestHandler_handleCatalogerTaskStarted(t *testing.T) {
}
// note: this line / event is not under test, only needed to show a sub status
kickoffEvent := &monitor.TaskProgress{
kickoffEvent := &monitor.CatalogerTaskProgress{
AtomicStage: progress.NewAtomicStage(""),
Manual: progress.NewManual(-1),
}
-37
View File
@@ -1,37 +0,0 @@
package ui
import (
tea "github.com/charmbracelet/bubbletea"
"github.com/wagoodman/go-partybus"
"github.com/anchore/bubbly/bubbles/taskprogress"
"github.com/anchore/syft/internal/log"
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
)
func (m *Handler) handlePullSourceStarted(e partybus.Event) []tea.Model {
prog, info, err := syftEventParsers.ParsePullSourceStarted(e)
if err != nil {
log.WithFields("error", err).Debug("unable to parse event")
return nil
}
tsk := m.newTaskProgress(
taskprogress.Title{
Default: info.Title.Default,
Running: info.Title.WhileRunning,
Success: info.Title.OnSuccess,
},
taskprogress.WithStagedProgressable(prog),
)
tsk.HideOnSuccess = info.HideOnSuccess
tsk.HideStageOnSuccess = info.HideStageOnSuccess
tsk.HideProgressOnSuccess = true
if info.Context != "" {
tsk.Context = []string{info.Context}
}
return []tea.Model{tsk}
}
-121
View File
@@ -1,121 +0,0 @@
package ui
import (
"testing"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/gkampitakis/go-snaps/snaps"
"github.com/stretchr/testify/require"
"github.com/wagoodman/go-partybus"
"github.com/wagoodman/go-progress"
"github.com/anchore/bubbly/bubbles/taskprogress"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/event/monitor"
)
func TestHandler_handlePullSourceStarted(t *testing.T) {
tests := []struct {
name string
eventFn func(*testing.T) partybus.Event
iterations int
}{
{
name: "snap download in progress",
eventFn: func(t *testing.T) partybus.Event {
stage := progress.NewAtomicStage("")
manual := progress.NewManual(0)
manual.SetTotal(1000000) // 1MB file
manual.Set(250000) // 25% downloaded
taskProg := &monitor.TaskProgress{
AtomicStage: stage,
Manual: manual,
}
genericTask := monitor.GenericTask{
Title: monitor.Title{
Default: "Downloading snap",
WhileRunning: "Downloading snap file...",
OnSuccess: "Snap downloaded",
},
Context: "example-app_1.0_amd64.snap",
HideOnSuccess: false,
HideStageOnSuccess: true,
ID: "snap-download-123",
}
return partybus.Event{
Type: event.PullSourceStarted,
Source: genericTask,
Value: taskProg,
}
},
iterations: 5,
},
{
name: "snap download complete",
eventFn: func(t *testing.T) partybus.Event {
stage := progress.NewAtomicStage("")
manual := progress.NewManual(0)
manual.SetTotal(1000000) // 1MB file
manual.Set(1000000) // 100% downloaded
manual.SetCompleted()
taskProg := &monitor.TaskProgress{
AtomicStage: stage,
Manual: manual,
}
genericTask := monitor.GenericTask{
Title: monitor.Title{
Default: "Downloading snap",
WhileRunning: "Downloading snap file...",
OnSuccess: "Snap downloaded successfully",
},
Context: "example-app_1.0_amd64.snap",
HideOnSuccess: false,
HideStageOnSuccess: true,
ID: "snap-download-123",
}
return partybus.Event{
Type: event.PullSourceStarted,
Source: genericTask,
Value: taskProg,
}
},
iterations: 3,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
event := tt.eventFn(t)
handler := New(DefaultHandlerConfig())
handler.WindowSize = tea.WindowSizeMsg{
Width: 100,
Height: 80,
}
models := handler.handlePullSourceStarted(event)
require.Len(t, models, 1)
model := models[0]
tsk, ok := model.(taskprogress.Model)
require.True(t, ok)
gotModel := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
Time: time.Now(),
Sequence: tsk.Sequence(),
ID: tsk.ID(),
})
got := gotModel.View()
t.Log(got)
snaps.MatchSnapshot(t, got)
})
}
}
-1
View File
@@ -57,7 +57,6 @@ func New(cfg HandlerConfig) *Handler {
stereoscopeEvent.FetchImage: simpleHandler(h.handleFetchImage),
syftEvent.FileIndexingStarted: simpleHandler(h.handleFileIndexingStarted),
syftEvent.AttestationStarted: simpleHandler(h.handleAttestationStarted),
syftEvent.PullSourceStarted: simpleHandler(h.handlePullSourceStarted),
syftEvent.CatalogerTaskStarted: h.handleCatalogerTaskStarted,
})
+1 -1
View File
@@ -44,7 +44,7 @@ func AppClioSetupConfig(id clio.Identification, out io.Writer) *clio.SetupConfig
redact.Set(state.RedactStore)
log.Set(state.Logger)
stereoscope.SetLogger(state.Logger.Nested("from", "stereoscope"))
stereoscope.SetLogger(state.Logger)
return nil
},
).
+7 -5
View File
@@ -35,7 +35,6 @@ const (
attestSchemeHelp = "\n " + schemeHelpHeader + "\n" + imageSchemeHelp
attestHelp = attestExample + attestSchemeHelp
cosignBinName = "cosign"
jsonFormat = "json"
)
type attestOptions struct {
@@ -53,7 +52,7 @@ func Attest(app clio.Application) *cobra.Command {
opts := defaultAttestOptions()
// template format explicitly not allowed
opts.Template.Enabled = false
opts.Format.Template.Enabled = false
return app.SetupCommand(&cobra.Command{
Use: "attest --output [FORMAT] <IMAGE>",
@@ -94,7 +93,9 @@ func defaultAttestOutputOptions() options.Output {
string(spdxtagvalue.ID),
},
Outputs: []string{syftjson.ID.String()},
// OutputFile is omitted (defaults to Enabled: false, explicitly not allowed for attestation)
OutputFile: options.OutputFile{ //nolint:staticcheck
Enabled: false, // explicitly not allowed
},
Format: options.DefaultFormat(),
}
}
@@ -135,7 +136,7 @@ func writeSBOMToFormattedFile(s *sbom.SBOM, sbomFile io.Writer, opts *attestOpti
return fmt.Errorf("no output file provided")
}
encs, err := opts.Encoders()
encs, err := opts.Format.Encoders()
if err != nil {
return fmt.Errorf("unable to create encoders: %w", err)
}
@@ -239,7 +240,7 @@ func predicateType(outputName string) string {
return "cyclonedx"
case "spdx-tag-value", "spdx-tv":
return "spdx"
case "spdx-json", jsonFormat:
case "spdx-json", "json":
return "spdxjson"
default:
return "custom"
@@ -252,6 +253,7 @@ func generateSBOMForAttestation(ctx context.Context, id clio.Identification, opt
}
src, err := getSource(ctx, opts, userInput, stereoscope.RegistryTag)
if err != nil {
return nil, err
}
-11
View File
@@ -1,8 +1,6 @@
package commands
import (
"os"
"github.com/spf13/cobra"
"github.com/anchore/clio"
@@ -18,14 +16,5 @@ func Cataloger(app clio.Application) *cobra.Command {
CatalogerList(app),
)
// only add cataloger info command if experimental capabilities feature is enabled
if isCapabilitiesExperimentEnabled() {
cmd.AddCommand(CatalogerCaps(app))
}
return cmd
}
func isCapabilitiesExperimentEnabled() bool {
return os.Getenv("SYFT_EXP_CAPABILITIES") == "true"
}
@@ -1,589 +0,0 @@
package commands
import (
"bytes"
"encoding/json"
"fmt"
"os"
"sort"
"strings"
"github.com/charmbracelet/lipgloss"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/renderer"
"github.com/olekukonko/tablewriter/tw"
"github.com/scylladb/go-set/strset"
"github.com/spf13/cobra"
"github.com/anchore/clio"
"github.com/anchore/syft/cmd/syft/internal/options"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/capabilities"
"github.com/anchore/syft/internal/task"
"github.com/anchore/syft/syft/cataloging"
_ "github.com/anchore/syft/syft/pkg/cataloger" // register all cataloger capabilities, otherwise the info command will not function
)
var (
yesStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("10")) // hi green
noStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("8")) // dark grey
criteriaWidth = 60
criteriaMargin = 10
)
// types for JSON cataloger info output
type (
configFieldInfo struct {
Key string `json:"key"`
Description string `json:"description"`
AppKey string `json:"app_key,omitempty"`
}
configInfo struct {
Type string `json:"type"`
Fields []configFieldInfo `json:"fields,omitempty"`
}
detectorPackageInfo struct {
Class string `json:"class"`
Name string `json:"name"`
PURL string `json:"purl"`
CPEs []string `json:"cpes"`
Type string `json:"type"`
}
patternInfo struct {
Method string `json:"method"`
Criteria []string `json:"criteria"`
Conditions []capabilities.DetectorCondition `json:"conditions,omitempty"`
Packages []detectorPackageInfo `json:"packages,omitempty"`
Comment string `json:"comment,omitempty"`
PackageTypes []string `json:"package_types,omitempty"`
JSONSchemaTypes []string `json:"json_schema_types,omitempty"`
Capabilities capabilities.CapabilitySet `json:"capabilities,omitempty"`
}
catalogerInfo struct {
Ecosystem string `json:"ecosystem,omitempty"`
Name string `json:"name"`
Type string `json:"type"`
Selectors []string `json:"selectors,omitempty"`
Deprecated bool `json:"deprecated,omitempty"`
Patterns []patternInfo `json:"patterns,omitempty"`
Capabilities capabilities.CapabilitySet `json:"capabilities,omitempty"`
Config *configInfo `json:"config,omitempty"`
}
)
type catalogerInfoOptions struct {
Output string `yaml:"output" json:"output" mapstructure:"output"`
options.CatalogerSelection `yaml:",inline" json:",inline" mapstructure:",squash"`
Names []string // cataloger names from args
}
func (o *catalogerInfoOptions) setNames(args []string) error {
o.Names = args
usingLegacyCatalogers := len(o.Catalogers) > 0
usingNewCatalogers := len(o.DefaultCatalogers) > 0 || len(o.SelectCatalogers) > 0
usingSelection := usingNewCatalogers || usingLegacyCatalogers
if usingSelection && len(o.Names) > 0 {
return fmt.Errorf("cannot use both cataloger name arguments and '--catalogers'/'--select-catalogers'/'--default-catalogers' flags")
}
if usingSelection {
// get all available package cataloger tasks
pkgTaskFactories := task.DefaultPackageTaskFactories()
allPkgTasks, err := pkgTaskFactories.Tasks(task.DefaultCatalogingFactoryConfig())
if err != nil {
return fmt.Errorf("unable to create pkg cataloger tasks: %w", err)
}
// make the selection based on user input
defaultCatalogers := options.FlattenAndSort(o.DefaultCatalogers)
selectCatalogers := options.FlattenAndSort(o.SelectCatalogers)
selectedTaskGroups, _, err := task.SelectInGroups(
[][]task.Task{allPkgTasks},
cataloging.NewSelectionRequest().
WithDefaults(defaultCatalogers...).
WithExpression(selectCatalogers...),
)
if err != nil {
return fmt.Errorf("unable to select catalogers: %w", err)
}
// build the list of cataloger names based on the selection
for _, g := range selectedTaskGroups {
for _, t := range g {
o.Names = append(o.Names, t.Name())
}
}
}
return nil
}
func (o *catalogerInfoOptions) AddFlags(flags clio.FlagSet) {
flags.StringVarP(&o.Output, "output", "o", "format to output the cataloger info (available: table, json)")
}
func defaultCatalogerCapsOptions() *catalogerInfoOptions {
return &catalogerInfoOptions{
CatalogerSelection: options.CatalogerSelection{
// this is different than the default behavior where a scan will automatically detect the default set
DefaultCatalogers: []string{"all"},
},
}
}
func CatalogerCaps(app clio.Application) *cobra.Command {
opts := defaultCatalogerCapsOptions()
return app.SetupCommand(&cobra.Command{
Use: "info [OPTIONS] [CATALOGER_NAMES...]",
Short: "Show detailed capabilities of catalogers",
Args: cobra.ArbitraryArgs,
PreRunE: disableUI(app, os.Stdout),
RunE: func(_ *cobra.Command, args []string) error {
if err := opts.setNames(args); err != nil {
return err
}
return runCatalogerInfo(opts)
},
}, opts)
}
func runCatalogerInfo(opts *catalogerInfoOptions) error {
doc, err := capabilities.LoadDocument()
if err != nil {
return fmt.Errorf("unable to load cataloger capabilities: %w", err)
}
// filter by cataloger names if provided
catalogers := doc.Catalogers
if len(opts.Names) > 0 {
catalogers = filterCatalogersByName(catalogers, opts.Names)
}
report, err := catalogerInfoReport(opts, doc, catalogers)
if err != nil {
return fmt.Errorf("unable to generate cataloger info report: %w", err)
}
bus.Report(report)
bus.Notify("Note: the `cataloger info` command is experimental and may change or be removed without notice. Do not depend on its output in production systems.")
return nil
}
func filterCatalogersByName(catalogers []capabilities.CatalogerEntry, names []string) []capabilities.CatalogerEntry {
nameSet := make(map[string]bool)
for _, name := range names {
nameSet[name] = true
}
var filtered []capabilities.CatalogerEntry
for _, cat := range catalogers {
if nameSet[cat.Name] {
filtered = append(filtered, cat)
}
}
return filtered
}
func catalogerInfoReport(opts *catalogerInfoOptions, doc *capabilities.Document, catalogers []capabilities.CatalogerEntry) (string, error) {
switch opts.Output {
case jsonFormat:
return renderCatalogerInfoJSON(doc, catalogers)
case "table", "":
return renderCatalogerInfoTable(doc, catalogers), nil
default:
return "", fmt.Errorf("invalid output format %q, must be one of: table, json", opts.Output)
}
}
func renderCatalogerInfoJSON(doc *capabilities.Document, catalogers []capabilities.CatalogerEntry) (string, error) {
type document struct {
Catalogers []catalogerInfo `json:"catalogers"`
}
docOut := document{}
// sort catalogers by ecosystem then name
sort.Slice(catalogers, func(i, j int) bool {
if catalogers[i].Ecosystem != catalogers[j].Ecosystem {
return catalogers[i].Ecosystem < catalogers[j].Ecosystem
}
return catalogers[i].Name < catalogers[j].Name
})
for _, cat := range catalogers {
info := catalogerInfo{
Ecosystem: cat.Ecosystem,
Name: cat.Name,
Type: cat.Type,
Selectors: cat.Selectors,
Deprecated: isDeprecatedCataloger(cat.Selectors),
}
// convert parsers to patterns if available
info.Patterns = convertParsersToPatterns(cat.Parsers)
// 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.Config = getConfigInfoFromDocument(doc, cat.Config)
docOut.Catalogers = append(docOut.Catalogers, info)
}
by, err := json.Marshal(docOut)
return string(by), err
}
// isDeprecatedCataloger checks if a cataloger is deprecated based on its selectors
func isDeprecatedCataloger(selectors []string) bool {
for _, selector := range selectors {
if selector == "deprecated" {
return true
}
}
return false
}
// convertDetectorPackages converts detector package info to the JSON output format
func convertDetectorPackages(pkgs []capabilities.DetectorPackageInfo) []detectorPackageInfo {
var result []detectorPackageInfo
for _, pkg := range pkgs {
result = append(result, detectorPackageInfo{
Class: pkg.Class,
Name: pkg.Name,
PURL: pkg.PURL,
CPEs: pkg.CPEs,
Type: pkg.Type,
})
}
return result
}
// convertParsersToPatterns converts parser entries to pattern info for JSON output
func convertParsersToPatterns(parsers []capabilities.Parser) []patternInfo {
var patterns []patternInfo
for _, parser := range parsers {
patterns = append(patterns, patternInfo{
Method: string(parser.Detector.Method),
Criteria: parser.Detector.Criteria,
Conditions: parser.Detector.Conditions,
Packages: convertDetectorPackages(parser.Detector.Packages),
Comment: parser.Detector.Comment,
PackageTypes: parser.PackageTypes,
JSONSchemaTypes: parser.JSONSchemaTypes,
Capabilities: parser.Capabilities,
})
}
return patterns
}
// convertDetectorsToPatterns converts detector entries to pattern info for JSON output (for non-parser catalogers)
func convertDetectorsToPatterns(detectors []capabilities.Detector, packageTypes, jsonSchemaTypes []string) []patternInfo {
var patterns []patternInfo
for _, det := range detectors {
patterns = append(patterns, patternInfo{
Method: string(det.Method),
Criteria: det.Criteria,
Conditions: det.Conditions,
Packages: convertDetectorPackages(det.Packages),
Comment: det.Comment,
PackageTypes: packageTypes,
JSONSchemaTypes: jsonSchemaTypes,
})
}
return patterns
}
// getConfigInfoFromDocument retrieves config info from the capabilities document
func getConfigInfoFromDocument(doc *capabilities.Document, configType string) *configInfo {
if configType == "" {
return nil
}
configEntry, ok := doc.Configs[configType]
if !ok {
return nil
}
cfg := &configInfo{
Type: configType,
}
for _, field := range configEntry.Fields {
cfg.Fields = append(cfg.Fields, configFieldInfo{
Key: field.Key,
Description: field.Description,
AppKey: field.AppKey,
})
}
return cfg
}
func renderCatalogerInfoTable(_ *capabilities.Document, catalogers []capabilities.CatalogerEntry) string {
if len(catalogers) == 0 {
return noStyle.Render("No catalogers found")
}
// sort catalogers by ecosystem then name
sort.Slice(catalogers, func(i, j int) bool {
if catalogers[i].Ecosystem != catalogers[j].Ecosystem {
return catalogers[i].Ecosystem < catalogers[j].Ecosystem
}
return catalogers[i].Name < catalogers[j].Name
})
var buf bytes.Buffer
// create table with hierarchical merging
table := tablewriter.NewTable(&buf,
tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{
Settings: tw.Settings{Separators: tw.Separators{BetweenRows: tw.On}},
})),
tablewriter.WithConfig(tablewriter.Config{
Row: tw.CellConfig{
Formatting: tw.CellFormatting{MergeMode: tw.MergeHierarchical},
Alignment: tw.CellAlignment{Global: tw.AlignLeft},
},
}),
)
// set headers
table.Header("ECOSYSTEM", "CATALOGER", "CRITERIA", "LICENSE", "NODES", "EDGES", "KINDS", "LISTING", "DIGESTS", "HASH")
// build rows for each cataloger
var data [][]string
for _, cat := range catalogers {
ecosystem := cat.Ecosystem
if ecosystem == "" {
ecosystem = "other"
}
if cat.Type == "generic" {
// 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)
data = append(data, row)
}
} else {
// custom catalogers: one row with all detectors
criteria := formatCriteria(cat.Detectors)
row := buildTableRowFromCapabilities(ecosystem, cat.Name, criteria, cat.Capabilities)
data = append(data, row)
}
}
// add all rows at once for better merging
_ = table.Bulk(data)
_ = table.Render()
return buf.String()
}
// buildTableRowFromCapabilities builds a table row from capability values
func buildTableRowFromCapabilities(ecosystem, name, criteria string, caps capabilities.CapabilitySet) []string {
// extract capability default values
license := extractBoolCapability(caps, "license")
nodes := extractNodesCapability(caps)
edges := extractStringCapability(caps, "dependency.edges")
kinds := extractArrayCapability(caps, "dependency.kinds")
listing := extractBoolCapability(caps, "package_manager.files.listing")
digests := extractBoolCapability(caps, "package_manager.files.digests")
hash := extractBoolCapability(caps, "package_manager.package_integrity_hash")
return []string{
ecosystem,
name,
criteria,
license,
nodes,
edges,
kinds,
listing,
digests,
hash,
}
}
// extractBoolCapability extracts a boolean capability value and formats it
func extractBoolCapability(caps capabilities.CapabilitySet, name string) string {
for _, cap := range caps {
if cap.Name == name {
if b, ok := cap.Default.(bool); ok {
if b {
return yesStyle.Render("✔")
}
return noStyle.Render("·")
}
return noStyle.Render("·")
}
}
return noStyle.Render("·")
}
// extractStringCapability extracts a string capability value
func extractStringCapability(caps capabilities.CapabilitySet, name string) string {
for _, cap := range caps {
if cap.Name == name {
if s, ok := cap.Default.(string); ok && s != "" {
return s
}
return noStyle.Render("·")
}
}
return noStyle.Render("·")
}
// extractArrayCapability extracts an array capability value and formats it as comma-separated
func extractArrayCapability(caps capabilities.CapabilitySet, name string) string {
for _, cap := range caps {
if cap.Name == name {
// handle various array types
switch v := cap.Default.(type) {
case []string:
if len(v) > 0 {
return strings.Join(v, ", ")
}
case []interface{}:
if len(v) > 0 {
strs := make([]string, 0, len(v))
for _, item := range v {
strs = append(strs, fmt.Sprintf("%v", item))
}
return strings.Join(strs, ", ")
}
}
return noStyle.Render("·")
}
}
return noStyle.Render("·")
}
// extractNodesCapability extracts dependency.depth and formats it
// maps ["direct", "indirect"] to "transitive"
func extractNodesCapability(caps capabilities.CapabilitySet) string {
for _, cap := range caps {
if cap.Name == "dependency.depth" {
switch v := cap.Default.(type) {
case []string:
return formatDepthStringArray(v)
case []interface{}:
return formatDepthInterfaceArray(v)
}
return noStyle.Render("·")
}
}
return noStyle.Render("·")
}
// formatDepthStringArray formats a []string dependency depth value
func formatDepthStringArray(v []string) string {
if len(v) == 0 {
return noStyle.Render("·")
}
if hasBothDirectAndIndirect(v) {
return "transitive"
}
return strings.Join(v, ", ")
}
// formatDepthInterfaceArray formats a []interface{} dependency depth value
func formatDepthInterfaceArray(v []interface{}) string {
if len(v) == 0 {
return noStyle.Render("·")
}
strs := make([]string, 0, len(v))
for _, item := range v {
strs = append(strs, fmt.Sprintf("%v", item))
}
if hasBothDirectAndIndirect(strs) {
return "transitive"
}
return strings.Join(strs, ", ")
}
// hasBothDirectAndIndirect checks if a slice contains both "direct" and "indirect" strings
func hasBothDirectAndIndirect(items []string) bool {
hasDirect := false
hasIndirect := false
for _, item := range items {
if item == "direct" {
hasDirect = true
}
if item == "indirect" {
hasIndirect = true
}
}
return hasDirect && hasIndirect
}
func formatCriteria(detectors []capabilities.Detector) string {
var allCriteria []string
methods := strset.New()
for _, det := range detectors {
allCriteria = append(allCriteria, det.Criteria...)
methods.Add(string(det.Method))
}
// smart word wrapping: wrap by word (criterion) up to criteriaWidth,
// allow up to criteriaWidth+criteriaMargin before forcing a new line
var lines []string
var currentLine []string
currentLength := 0
for _, criterion := range allCriteria {
// calculate length including the criterion plus ", " separator (except for first item)
itemLength := len(criterion)
if len(currentLine) > 0 {
itemLength += 2 // for ", "
}
newLength := currentLength + itemLength
if len(currentLine) > 0 && newLength > criteriaWidth {
// check if it's within margin
if newLength <= criteriaWidth+criteriaMargin {
// close enough, add it anyway
currentLine = append(currentLine, criterion)
currentLength = newLength
} else {
// too long, finalize current line and start new one
lines = append(lines, strings.Join(currentLine, ", "))
currentLine = []string{criterion}
currentLength = len(criterion)
}
} else {
// fits within width or first item
currentLine = append(currentLine, criterion)
currentLength = newLength
}
}
// add final line
if len(currentLine) > 0 {
lines = append(lines, strings.Join(currentLine, ", "))
}
methodsList := methods.List()
sort.Strings(methodsList)
method := strings.Join(methodsList, ", ")
if len(lines) == 0 {
return ""
}
joined := strings.Join(lines, "\n")
if method != string(capabilities.GlobDetection) {
return fmt.Sprintf("%s (%s)", joined, method)
}
return joined
}
@@ -1,373 +0,0 @@
package commands
import (
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/internal/capabilities"
)
func Test_isDeprecatedCataloger(t *testing.T) {
tests := []struct {
name string
selectors []string
want bool
}{
{
name: "empty selectors",
selectors: nil,
want: false,
},
{
name: "no deprecated selector",
selectors: []string{"python", "pip", "package"},
want: false,
},
{
name: "has deprecated selector",
selectors: []string{"python", "deprecated", "package"},
want: true,
},
{
name: "only deprecated selector",
selectors: []string{"deprecated"},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := isDeprecatedCataloger(tt.selectors)
assert.Equal(t, tt.want, got)
})
}
}
func Test_hasBothDirectAndIndirect(t *testing.T) {
tests := []struct {
name string
items []string
want bool
}{
{
name: "empty slice",
items: nil,
want: false,
},
{
name: "only direct",
items: []string{"direct"},
want: false,
},
{
name: "only indirect",
items: []string{"indirect"},
want: false,
},
{
name: "both direct and indirect",
items: []string{"direct", "indirect"},
want: true,
},
{
name: "both with other items",
items: []string{"other", "direct", "more", "indirect", "stuff"},
want: true,
},
{
name: "unrelated items",
items: []string{"foo", "bar", "baz"},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := hasBothDirectAndIndirect(tt.items)
assert.Equal(t, tt.want, got)
})
}
}
func Test_filterCatalogersByName(t *testing.T) {
catalogers := []capabilities.CatalogerEntry{
{Name: "python-cataloger", Ecosystem: "python"},
{Name: "java-cataloger", Ecosystem: "java"},
{Name: "go-cataloger", Ecosystem: "golang"},
{Name: "npm-cataloger", Ecosystem: "javascript"},
}
tests := []struct {
name string
catalogers []capabilities.CatalogerEntry
names []string
want []capabilities.CatalogerEntry
}{
{
name: "empty names returns empty",
catalogers: catalogers,
names: nil,
want: nil,
},
{
name: "single match",
catalogers: catalogers,
names: []string{"python-cataloger"},
want: []capabilities.CatalogerEntry{
{Name: "python-cataloger", Ecosystem: "python"},
},
},
{
name: "multiple matches",
catalogers: catalogers,
names: []string{"python-cataloger", "java-cataloger"},
want: []capabilities.CatalogerEntry{
{Name: "python-cataloger", Ecosystem: "python"},
{Name: "java-cataloger", Ecosystem: "java"},
},
},
{
name: "no matches",
catalogers: catalogers,
names: []string{"nonexistent"},
want: nil,
},
{
name: "partial matches",
catalogers: catalogers,
names: []string{"python-cataloger", "nonexistent"},
want: []capabilities.CatalogerEntry{
{Name: "python-cataloger", Ecosystem: "python"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := filterCatalogersByName(tt.catalogers, tt.names)
if diff := cmp.Diff(tt.want, got); diff != "" {
t.Errorf("filterCatalogersByName() mismatch (-want +got):\n%s", diff)
}
})
}
}
func Test_formatCriteria(t *testing.T) {
tests := []struct {
name string
detectors []capabilities.Detector
want string
}{
{
name: "empty detectors",
detectors: nil,
want: "",
},
{
name: "single glob detector",
detectors: []capabilities.Detector{
{
Method: capabilities.GlobDetection,
Criteria: []string{"**/*.py"},
},
},
want: "**/*.py",
},
{
name: "multiple criteria single detector",
detectors: []capabilities.Detector{
{
Method: capabilities.GlobDetection,
Criteria: []string{"**/*.py", "**/*.pyc", "**/requirements.txt"},
},
},
want: "**/*.py, **/*.pyc, **/requirements.txt",
},
{
name: "path detection shows method",
detectors: []capabilities.Detector{
{
Method: capabilities.PathDetection,
Criteria: []string{"/usr/bin/python"},
},
},
want: "/usr/bin/python (path)",
},
{
name: "mimetype detection shows method",
detectors: []capabilities.Detector{
{
Method: capabilities.MIMETypeDetection,
Criteria: []string{"application/x-python"},
},
},
want: "application/x-python (mimetype)",
},
{
name: "multiple detectors combine criteria",
detectors: []capabilities.Detector{
{
Method: capabilities.GlobDetection,
Criteria: []string{"**/*.py"},
},
{
Method: capabilities.GlobDetection,
Criteria: []string{"**/*.pyc"},
},
},
want: "**/*.py, **/*.pyc",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := formatCriteria(tt.detectors)
assert.Equal(t, tt.want, got)
})
}
}
func testDocument() *capabilities.Document {
return &capabilities.Document{
Configs: map[string]capabilities.CatalogerConfigEntry{
"test-config": {
Fields: []capabilities.CatalogerConfigFieldEntry{
{Key: "field1", Description: "test field"},
},
},
},
}
}
func Test_catalogerInfoReport(t *testing.T) {
tests := []struct {
name string
opts *catalogerInfoOptions
doc *capabilities.Document
catalogers []capabilities.CatalogerEntry
wantErr require.ErrorAssertionFunc
assertions func(t *testing.T, got string)
}{
{
name: "empty catalogers table",
opts: &catalogerInfoOptions{Output: "table"},
doc: testDocument(),
assertions: func(t *testing.T, got string) {
assert.Contains(t, got, "No catalogers found")
},
},
{
name: "empty catalogers json",
opts: &catalogerInfoOptions{Output: "json"},
doc: testDocument(),
assertions: func(t *testing.T, got string) {
assert.Contains(t, got, `"catalogers":null`)
},
},
{
name: "single cataloger table output",
opts: &catalogerInfoOptions{Output: "table"},
doc: testDocument(),
catalogers: []capabilities.CatalogerEntry{
{
Name: "test-cataloger",
Ecosystem: "test",
Type: "custom",
Detectors: []capabilities.Detector{
{Method: capabilities.GlobDetection, Criteria: []string{"**/*.test"}},
},
},
},
assertions: func(t *testing.T, got string) {
assert.Contains(t, got, "test-cataloger")
assert.Contains(t, got, "ECOSYSTEM")
assert.Contains(t, got, "CATALOGER")
},
},
{
name: "single cataloger json output",
opts: &catalogerInfoOptions{Output: "json"},
doc: testDocument(),
catalogers: []capabilities.CatalogerEntry{
{
Name: "test-cataloger",
Ecosystem: "test",
Type: "custom",
Selectors: []string{"test", "custom"},
},
},
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"`)
},
},
{
name: "deprecated cataloger json output",
opts: &catalogerInfoOptions{Output: "json"},
doc: testDocument(),
catalogers: []capabilities.CatalogerEntry{
{
Name: "old-cataloger",
Ecosystem: "legacy",
Type: "custom",
Selectors: []string{"legacy", "deprecated"},
},
},
assertions: func(t *testing.T, got string) {
assert.Contains(t, got, `"deprecated":true`)
},
},
{
name: "invalid output format",
opts: &catalogerInfoOptions{Output: "invalid"},
doc: testDocument(),
wantErr: require.Error,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.wantErr == nil {
tt.wantErr = require.NoError
}
got, err := catalogerInfoReport(tt.opts, tt.doc, tt.catalogers)
tt.wantErr(t, err)
if err != nil {
return
}
if tt.assertions != nil {
tt.assertions(t, got)
}
})
}
}
func Test_formatCriteria_wordWrapping(t *testing.T) {
// test word wrapping behavior with long criteria lists
longCriteria := make([]string, 20)
for i := range longCriteria {
longCriteria[i] = "**/file" + strings.Repeat("x", 10)
}
detector := capabilities.Detector{
Method: capabilities.GlobDetection,
Criteria: longCriteria,
}
got := formatCriteria([]capabilities.Detector{detector})
// should contain newlines for wrapping
assert.Contains(t, got, "\n")
// should contain all criteria (joined with ", " and possibly split across lines)
for _, c := range longCriteria {
assert.Contains(t, got, c)
}
}
+26 -20
View File
@@ -13,9 +13,7 @@ import (
"github.com/spf13/cobra"
"github.com/anchore/clio"
"github.com/anchore/syft/cmd/syft/internal/options"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/capabilities"
"github.com/anchore/syft/internal/task"
"github.com/anchore/syft/syft/cataloging"
)
@@ -29,23 +27,25 @@ var (
)
type catalogerListOptions struct {
Output string `yaml:"output" json:"output" mapstructure:"output"`
options.CatalogerSelection `yaml:",inline" json:",inline" mapstructure:",squash"`
ShowHidden bool `yaml:"show-hidden" json:"show-hidden" mapstructure:"show-hidden"`
Output string `yaml:"output" json:"output" mapstructure:"output"`
DefaultCatalogers []string `yaml:"default-catalogers" json:"default-catalogers" mapstructure:"default-catalogers"`
SelectCatalogers []string `yaml:"select-catalogers" json:"select-catalogers" mapstructure:"select-catalogers"`
ShowHidden bool `yaml:"show-hidden" json:"show-hidden" mapstructure:"show-hidden"`
}
func (o *catalogerListOptions) AddFlags(flags clio.FlagSet) {
flags.StringVarP(&o.Output, "output", "o", "format to output the cataloger list (available: table, json)")
flags.StringArrayVarP(&o.DefaultCatalogers, "override-default-catalogers", "", "override the default catalogers with an expression")
flags.StringArrayVarP(&o.SelectCatalogers, "select-catalogers", "", "select catalogers with an expression")
flags.BoolVarP(&o.ShowHidden, "show-hidden", "s", "show catalogers that have been de-selected")
}
func defaultCatalogerListOptions() *catalogerListOptions {
return &catalogerListOptions{
CatalogerSelection: options.CatalogerSelection{
// this is different than the default behavior where a scan will automatically detect the default set
DefaultCatalogers: []string{"all"},
},
DefaultCatalogers: []string{"all"},
}
}
@@ -86,13 +86,11 @@ func runCatalogerList(opts *catalogerListOptions) error {
}
func catalogerListReport(opts *catalogerListOptions, allTaskGroups [][]task.Task) (string, error) {
defaultCatalogers := options.FlattenAndSort(opts.DefaultCatalogers)
selectCatalogers := options.FlattenAndSort(opts.SelectCatalogers)
selectedTaskGroups, selectionEvidence, err := task.SelectInGroups(
allTaskGroups,
cataloging.NewSelectionRequest().
WithDefaults(defaultCatalogers...).
WithExpression(selectCatalogers...),
WithDefaults(opts.DefaultCatalogers...).
WithExpression(opts.SelectCatalogers...),
)
if err != nil {
return "", fmt.Errorf("unable to select catalogers: %w", err)
@@ -100,8 +98,8 @@ func catalogerListReport(opts *catalogerListOptions, allTaskGroups [][]task.Task
var report string
switch opts.Output {
case jsonFormat:
report, err = renderCatalogerListJSON(flattenTaskGroups(selectedTaskGroups), selectionEvidence, defaultCatalogers, selectCatalogers)
case "json":
report, err = renderCatalogerListJSON(flattenTaskGroups(selectedTaskGroups), selectionEvidence, opts.DefaultCatalogers, opts.SelectCatalogers)
case "table", "":
if opts.ShowHidden {
report = renderCatalogerListTables(allTaskGroups, selectionEvidence)
@@ -305,14 +303,22 @@ func formatToken(token string, selection *task.TokenSelection, included bool, de
}
func extractTaskInfo(tasks []task.Task) ([]string, map[string][]string) {
infos := capabilities.ExtractCatalogerInfo(tasks)
tagsByName := make(map[string][]string)
var names []string
for _, info := range infos {
tagsByName[info.Name] = info.Selectors
names = append(names, info.Name)
for _, tsk := range tasks {
var tags []string
name := tsk.Name()
if s, ok := tsk.(task.Selector); ok {
set := strset.New(s.Selectors()...)
set.Remove(name)
tags = set.List()
sort.Strings(tags)
}
tagsByName[name] = tags
names = append(names, name)
}
sort.Strings(names)
@@ -184,11 +184,11 @@ Default selections: 1
• 'image'
Selection expressions: 6
• 'b' (intersect)
• '-c' (remove)
• '-directory' (remove)
• '-c' (remove)
• '-file' (remove)
• '+file-task1' (add)
• '+task3' (add)
• '+file-task1' (add)
┌────────────────┬──────────────────┐
│ FILE CATALOGER │ TAGS │
├────────────────┼──────────────────┤
@@ -200,43 +200,6 @@ Selection expressions: 6
│ task1 │ 1, a, b, image │
│ task3 │ 3, c, d, directory │
└───────────────────┴────────────────────┘
`,
},
{
name: "with comma separated expressions, table",
options: func() *catalogerListOptions {
c := defaultCatalogerListOptions()
c.Output = "table"
c.DefaultCatalogers = []string{
"image,task3,file-task1",
}
c.SelectCatalogers = []string{
"-task2,c,b",
}
return c
}(),
want: `
Default selections: 4
• 'file-task1'
• 'image'
• 'task3'
• 'file'
Selection expressions: 3
• 'b' (intersect)
• 'c' (intersect)
• '-task2' (remove)
┌────────────────┬──────────────────┐
│ FILE CATALOGER │ TAGS │
├────────────────┼──────────────────┤
│ file-task1 │ file, ft, ft-1-b │
│ file-task2 │ file, ft, ft-2-b │
└────────────────┴──────────────────┘
┌───────────────────┬────────────────────┐
│ PACKAGE CATALOGER │ TAGS │
├───────────────────┼────────────────────┤
│ task1 │ 1, a, b, image │
│ task3 │ 3, c, d, directory │
└───────────────────┴────────────────────┘
`,
},
{
@@ -256,7 +219,7 @@ Selection expressions: 3
return c
}(),
want: `
{"default":["image"],"selection":["+task3","-c","-directory","b"],"catalogers":[{"name":"file-task1","tags":["file"]},{"name":"file-task2","tags":["file"]},{"name":"task1","tags":["b","image"]},{"name":"task3","tags":["task3"]}]}
{"default":["image"],"selection":["-directory","+task3","-c","b"],"catalogers":[{"name":"file-task1","tags":["file"]},{"name":"file-task2","tags":["file"]},{"name":"task1","tags":["b","image"]},{"name":"task3","tags":["task3"]}]}
`,
},
}
+6 -6
View File
@@ -10,7 +10,7 @@ import (
"github.com/hashicorp/go-multierror"
"github.com/spf13/cobra"
"go.yaml.in/yaml/v3"
"gopkg.in/yaml.v3"
"github.com/anchore/clio"
"github.com/anchore/fangs"
@@ -110,7 +110,7 @@ func (o *scanOptions) PostLoad() error {
}
func (o *scanOptions) validateLegacyOptionsNotUsed() error {
if len(fangs.Flatten(o.ConfigFile)) == 0 {
if len(fangs.Flatten(o.Config.ConfigFile)) == 0 {
return nil
}
@@ -122,7 +122,7 @@ func (o *scanOptions) validateLegacyOptionsNotUsed() error {
File any `yaml:"file" json:"file" mapstructure:"file"`
}
for _, f := range fangs.Flatten(o.ConfigFile) {
for _, f := range fangs.Flatten(o.Config.ConfigFile) {
by, err := os.ReadFile(f)
if err != nil {
return fmt.Errorf("unable to read config file during validations %q: %w", f, err)
@@ -185,6 +185,7 @@ func runScan(ctx context.Context, id clio.Identification, opts *scanOptions, use
}
src, err := getSource(ctx, &opts.Catalog, userInput, sources...)
if err != nil {
return err
}
@@ -217,9 +218,8 @@ func getSource(ctx context.Context, opts *options.Catalog, userInput string, sou
cfg := syft.DefaultGetSourceConfig().
WithRegistryOptions(opts.Registry.ToOptions()).
WithAlias(source.Alias{
Name: opts.Source.Name,
Version: opts.Source.Version,
Supplier: opts.Source.Supplier,
Name: opts.Source.Name,
Version: opts.Source.Version,
}).
WithExcludeConfig(source.ExcludeConfig{
Paths: opts.Exclusions,
+1 -1
View File
@@ -10,9 +10,9 @@ import (
"time"
"github.com/adrg/xdg"
"github.com/mitchellh/go-homedir"
"github.com/anchore/clio"
"github.com/anchore/go-homedir"
"github.com/anchore/syft/internal/cache"
"github.com/anchore/syft/internal/log"
)
+25 -35
View File
@@ -9,9 +9,9 @@ import (
"time"
"github.com/adrg/xdg"
"github.com/mitchellh/go-homedir"
"github.com/stretchr/testify/require"
"github.com/anchore/go-homedir"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/cache"
)
@@ -57,13 +57,35 @@ func Test_defaultDir(t *testing.T) {
env[k] = os.Getenv(k)
}
unsetEnv := func(t *testing.T) {
for k := range env {
t.Setenv(k, "")
}
}
resetEnv := func() {
for k, v := range env {
if v == "" {
_ = os.Unsetenv(k)
} else {
_ = os.Setenv(k, v)
}
}
homedir.Reset()
xdg.Reload()
}
t.Cleanup(resetEnv)
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
restoreCache(t)
defer resetEnv()
unsetEnv(t)
for k, v := range test.env {
patchEnv(t, k, v)
t.Setenv(k, v)
}
homedir.Reset()
xdg.Reload()
got := defaultDir()
@@ -289,35 +311,3 @@ func Test_durationToString(t *testing.T) {
})
}
}
// restoreCache ensures cache settings are restored after test
func restoreCache(t testing.TB) {
t.Helper()
origEnabled := homedir.CacheEnabled()
t.Cleanup(func() {
homedir.SetCacheEnable(origEnabled)
homedir.Reset()
})
}
// patchEnv modifies an environment variable for the duration of the test
func patchEnv(t testing.TB, key, value string) {
t.Helper()
original := os.Getenv(key)
if value != "" {
os.Setenv(key, value)
} else {
os.Unsetenv(key)
}
t.Cleanup(func() {
if original != "" {
os.Setenv(key, original)
} else {
os.Unsetenv(key)
}
xdg.Reload()
})
}
+53 -47
View File
@@ -24,23 +24,23 @@ import (
"github.com/anchore/syft/syft/pkg/cataloger/java"
"github.com/anchore/syft/syft/pkg/cataloger/javascript"
"github.com/anchore/syft/syft/pkg/cataloger/kernel"
"github.com/anchore/syft/syft/pkg/cataloger/nix"
"github.com/anchore/syft/syft/pkg/cataloger/python"
"github.com/anchore/syft/syft/source"
)
type Catalog struct {
// high-level cataloger configuration
CatalogerSelection `yaml:",inline" json:",inline" mapstructure:",squash"`
Package packageConfig `yaml:"package" json:"package" mapstructure:"package"`
License licenseConfig `yaml:"license" json:"license" mapstructure:"license"`
File fileConfig `yaml:"file" json:"file" mapstructure:"file"`
Scope string `yaml:"scope" json:"scope" mapstructure:"scope"`
Parallelism int `yaml:"parallelism" json:"parallelism" mapstructure:"parallelism"` // the number of catalog workers to run in parallel
Relationships relationshipsConfig `yaml:"relationships" json:"relationships" mapstructure:"relationships"`
Compliance complianceConfig `yaml:"compliance" json:"compliance" mapstructure:"compliance"`
Enrich []string `yaml:"enrich" json:"enrich" mapstructure:"enrich"`
Catalogers []string `yaml:"-" json:"catalogers" mapstructure:"catalogers"` // deprecated and not shown in yaml output
DefaultCatalogers []string `yaml:"default-catalogers" json:"default-catalogers" mapstructure:"default-catalogers"`
SelectCatalogers []string `yaml:"select-catalogers" json:"select-catalogers" mapstructure:"select-catalogers"`
Package packageConfig `yaml:"package" json:"package" mapstructure:"package"`
License licenseConfig `yaml:"license" json:"license" mapstructure:"license"`
File fileConfig `yaml:"file" json:"file" mapstructure:"file"`
Scope string `yaml:"scope" json:"scope" mapstructure:"scope"`
Parallelism int `yaml:"parallelism" json:"parallelism" mapstructure:"parallelism"` // the number of catalog workers to run in parallel
Relationships relationshipsConfig `yaml:"relationships" json:"relationships" mapstructure:"relationships"`
Compliance complianceConfig `yaml:"compliance" json:"compliance" mapstructure:"compliance"`
Enrich []string `yaml:"enrich" json:"enrich" mapstructure:"enrich"`
// ecosystem-specific cataloger configuration
Dotnet dotnetConfig `yaml:"dotnet" json:"dotnet" mapstructure:"dotnet"`
@@ -48,7 +48,6 @@ type Catalog struct {
Java javaConfig `yaml:"java" json:"java" mapstructure:"java"`
JavaScript javaScriptConfig `yaml:"javascript" json:"javascript" mapstructure:"javascript"`
LinuxKernel linuxKernelConfig `yaml:"linux-kernel" json:"linux-kernel" mapstructure:"linux-kernel"`
Nix nixConfig `yaml:"nix" json:"nix" mapstructure:"nix"`
Python pythonConfig `yaml:"python" json:"python" mapstructure:"python"`
// configuration for the source (the subject being analyzed)
@@ -69,16 +68,12 @@ var _ interface {
} = (*Catalog)(nil)
func DefaultCatalog() Catalog {
cfg := syft.DefaultCreateSBOMConfig()
return Catalog{
Compliance: defaultComplianceConfig(),
Scope: source.SquashedScope.String(),
Package: defaultPackageConfig(),
License: defaultLicenseConfig(),
LinuxKernel: defaultLinuxKernelConfig(),
JavaScript: defaultJavaScriptConfig(),
Python: defaultPythonConfig(),
Nix: defaultNixConfig(),
Dotnet: defaultDotnetConfig(),
Golang: defaultGolangConfig(),
Java: defaultJavaConfig(),
@@ -86,7 +81,7 @@ func DefaultCatalog() Catalog {
Relationships: defaultRelationshipsConfig(),
Unknowns: defaultUnknowns(),
Source: defaultSourceConfig(),
Parallelism: cfg.Parallelism,
Parallelism: 1,
}
}
@@ -159,8 +154,8 @@ func (cfg Catalog) ToFilesConfig() filecataloging.Config {
func (cfg Catalog) ToLicenseConfig() cataloging.LicenseConfig {
return cataloging.LicenseConfig{
IncludeContent: cfg.License.Content,
Coverage: cfg.License.Coverage,
IncludeUnkownLicenseContent: cfg.License.IncludeUnknownLicenseContent,
Coverage: cfg.License.LicenseCoverage,
}
}
@@ -172,10 +167,7 @@ func (cfg Catalog) ToPackagesConfig() pkgcataloging.Config {
return pkgcataloging.Config{
Binary: binary.DefaultClassifierCatalogerConfig(),
Dotnet: dotnet.DefaultCatalogerConfig().
WithDepPackagesMustHaveDLL(cfg.Dotnet.DepPackagesMustHaveDLL).
WithDepPackagesMustClaimDLL(cfg.Dotnet.DepPackagesMustClaimDLL).
WithPropagateDLLClaimsToParents(cfg.Dotnet.PropagateDLLClaimsToParents).
WithRelaxDLLClaimsWhenBundlingDetected(cfg.Dotnet.RelaxDLLClaimsWhenBundlingDetected),
WithCertificateValidation(cfg.Dotnet.EnableCertificateValidation),
Golang: golang.DefaultCatalogerConfig().
WithSearchLocalModCacheLicenses(*multiLevelOption(false, enrichmentEnabled(cfg.Enrich, task.Go, task.Golang), cfg.Golang.SearchLocalModCacheLicenses)).
WithLocalModCacheDir(cfg.Golang.LocalModCacheDir).
@@ -189,8 +181,7 @@ func (cfg Catalog) ToPackagesConfig() pkgcataloging.Config {
WithFromContents(cfg.Golang.MainModuleVersion.FromContents).
WithFromBuildSettings(cfg.Golang.MainModuleVersion.FromBuildSettings).
WithFromLDFlags(cfg.Golang.MainModuleVersion.FromLDFlags),
).
WithUsePackagesLib(*multiLevelOption(true, enrichmentEnabled(cfg.Enrich, task.Go, task.Golang), cfg.Golang.UsePackagesLib)),
),
JavaScript: javascript.DefaultCatalogerConfig().
WithIncludeDevDependencies(*multiLevelOption(false, cfg.JavaScript.IncludeDevDependencies)).
WithSearchRemoteLicenses(*multiLevelOption(false, enrichmentEnabled(cfg.Enrich, task.JavaScript, task.Node, task.NPM), cfg.JavaScript.SearchRemoteLicenses)).
@@ -198,12 +189,9 @@ func (cfg Catalog) ToPackagesConfig() pkgcataloging.Config {
LinuxKernel: kernel.LinuxKernelCatalogerConfig{
CatalogModules: cfg.LinuxKernel.CatalogModules,
},
Nix: nix.DefaultConfig().
WithCaptureOwnedFiles(cfg.Nix.CaptureOwnedFiles),
Python: python.DefaultCatalogerConfig().
WithSearchRemoteLicenses(*multiLevelOption(false, enrichmentEnabled(cfg.Enrich, task.Python), cfg.Python.SearchRemoteLicenses)).
WithPypiBaseURL(cfg.Python.PypiBaseURL).
WithGuessUnpinnedRequirements(*multiLevelOption(false, enrichmentEnabled(cfg.Enrich, task.Python), cfg.Python.GuessUnpinnedRequirements)),
Python: python.CatalogerConfig{
GuessUnpinnedRequirements: cfg.Python.GuessUnpinnedRequirements,
},
JavaArchive: java.DefaultArchiveCatalogerConfig().
WithUseMavenLocalRepository(*multiLevelOption(false, enrichmentEnabled(cfg.Enrich, task.Java, task.Maven), cfg.Java.UseMavenLocalRepository)).
WithMavenLocalRepositoryDir(cfg.Java.MavenLocalRepositoryDir).
@@ -231,8 +219,22 @@ func (cfg *Catalog) AddFlags(flags clio.FlagSet) {
flags.StringArrayVarP(&cfg.Exclusions, "exclude", "",
"exclude paths from being scanned using a glob expression")
flags.IntVarP(&cfg.Parallelism, "parallelism", "",
"number of cataloger workers to run in parallel")
flags.StringArrayVarP(&cfg.Catalogers, "catalogers", "",
"enable one or more package catalogers")
if pfp, ok := flags.(fangs.PFlagSetProvider); ok {
if err := pfp.PFlagSet().MarkDeprecated("catalogers", "use: override-default-catalogers and select-catalogers"); err != nil {
panic(err)
}
} else {
panic("unable to mark flags as deprecated")
}
flags.StringArrayVarP(&cfg.DefaultCatalogers, "override-default-catalogers", "",
"set the base set of catalogers to use (defaults to 'image' or 'directory' depending on the scan source)")
flags.StringArrayVarP(&cfg.SelectCatalogers, "select-catalogers", "",
"add, remove, and filter the catalogers to be used")
flags.StringArrayVarP(&cfg.Enrich, "enrich", "",
fmt.Sprintf("enable package data enrichment from local and online sources (options: %s)", strings.Join(publicisedEnrichmentOptions, ", ")))
@@ -245,14 +247,10 @@ func (cfg *Catalog) AddFlags(flags clio.FlagSet) {
flags.StringVarP(&cfg.Source.BasePath, "base-path", "",
"base directory for scanning, no links will be followed above this directory, and all paths will be reported relative to this directory")
flags.StringVarP(&cfg.Source.Supplier, "source-supplier", "",
"the organization that supplied the component, which often may be the manufacturer, distributor, or repackager")
}
func (cfg *Catalog) DescribeFields(descriptions fangs.FieldDescriptionSet) {
descriptions.Add(&cfg.Parallelism, `number of cataloger workers to run in parallel
by default, when set to 0: this will be based on runtime.NumCPU * 4, if set to less than 0 it will be unbounded`)
descriptions.Add(&cfg.Parallelism, "number of cataloger workers to run in parallel")
descriptions.Add(&cfg.Enrich, fmt.Sprintf(`Enable data enrichment operations, which can utilize services such as Maven Central and NPM.
By default all enrichment is disabled, use: all to enable everything.
@@ -260,8 +258,22 @@ Available options are: %s`, strings.Join(publicisedEnrichmentOptions, ", ")))
}
func (cfg *Catalog) PostLoad() error {
cfg.From = Flatten(cfg.From)
cfg.Enrich = FlattenAndSort(cfg.Enrich)
usingLegacyCatalogers := len(cfg.Catalogers) > 0
usingNewCatalogers := len(cfg.DefaultCatalogers) > 0 || len(cfg.SelectCatalogers) > 0
if usingLegacyCatalogers && usingNewCatalogers {
return fmt.Errorf("cannot use both 'catalogers' and 'select-catalogers'/'default-catalogers' flags")
}
cfg.From = flatten(cfg.From)
cfg.Catalogers = flatten(cfg.Catalogers)
cfg.DefaultCatalogers = flatten(cfg.DefaultCatalogers)
cfg.SelectCatalogers = flatten(cfg.SelectCatalogers)
cfg.Enrich = flatten(cfg.Enrich)
// for backwards compatibility
cfg.DefaultCatalogers = append(cfg.DefaultCatalogers, cfg.Catalogers...)
s := source.ParseScope(cfg.Scope)
if s == source.UnknownScope {
@@ -276,18 +288,13 @@ func (cfg *Catalog) PostLoad() error {
return nil
}
func Flatten(commaSeparatedEntries []string) []string {
func flatten(commaSeparatedEntries []string) []string {
var out []string
for _, v := range commaSeparatedEntries {
for _, s := range strings.Split(v, ",") {
out = append(out, strings.TrimSpace(s))
}
}
return out
}
func FlattenAndSort(commaSeparatedEntries []string) []string {
out := Flatten(commaSeparatedEntries)
sort.Strings(out)
return out
}
@@ -297,7 +304,6 @@ var publicisedEnrichmentOptions = []string{
task.Golang,
task.Java,
task.JavaScript,
task.Python,
}
func enrichmentEnabled(enrichDirectives []string, features ...string) *bool {
+44 -93
View File
@@ -8,12 +8,55 @@ import (
)
func TestCatalog_PostLoad(t *testing.T) {
tests := []struct {
name string
options Catalog
assert func(t *testing.T, options Catalog)
wantErr assert.ErrorAssertionFunc
}{
{
name: "mutually exclusive cataloger flags (cat / def-cat)",
options: Catalog{
Catalogers: []string{"foo,bar", "42"},
DefaultCatalogers: []string{"some,thing"},
Scope: "squashed",
},
wantErr: assert.Error,
},
{
name: "mutually exclusive cataloger flags (cat / sel-cat)",
options: Catalog{
Catalogers: []string{"foo,bar", "42"},
SelectCatalogers: []string{"some,thing"},
Scope: "squashed",
},
wantErr: assert.Error,
},
{
name: "allow old cataloger flags",
options: Catalog{
Catalogers: []string{"foo,bar"},
Scope: "squashed",
},
assert: func(t *testing.T, options Catalog) {
assert.Equal(t, []string{"bar", "foo"}, options.DefaultCatalogers) // note: sorted order
assert.Equal(t, []string{"bar", "foo"}, options.Catalogers) // note: sorted order
},
},
{
name: "allow new cataloger flags",
options: Catalog{
SelectCatalogers: []string{"foo,bar", "42"},
DefaultCatalogers: []string{"some,thing"},
Scope: "squashed",
},
assert: func(t *testing.T, options Catalog) {
assert.Equal(t, []string{"42", "bar", "foo"}, options.SelectCatalogers) // note: sorted order
assert.Equal(t, []string{"some", "thing"}, options.DefaultCatalogers) // note: sorted order
assert.Empty(t, options.Catalogers)
},
},
{
name: "must have package overlap flag when pruning binaries by overlap",
options: Catalog{
@@ -36,98 +79,6 @@ func TestCatalog_PostLoad(t *testing.T) {
}
}
func TestFlatten(t *testing.T) {
tests := []struct {
name string
input []string
expected []string
}{
{
name: "preserves order of comma-separated values",
input: []string{"registry,docker,oci-dir"},
expected: []string{"registry", "docker", "oci-dir"},
},
{
name: "preserves order across multiple entries",
input: []string{"registry,docker", "oci-dir"},
expected: []string{"registry", "docker", "oci-dir"},
},
{
name: "trims whitespace",
input: []string{" registry , docker ", " oci-dir "},
expected: []string{"registry", "docker", "oci-dir"},
},
{
name: "handles single value",
input: []string{"registry"},
expected: []string{"registry"},
},
{
name: "handles empty input",
input: []string{},
expected: nil,
},
{
name: "preserves reverse alphabetical order",
input: []string{"zebra,yankee,xray"},
expected: []string{"zebra", "yankee", "xray"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Flatten(tt.input)
assert.Equal(t, tt.expected, got)
})
}
}
func TestFlattenAndSort(t *testing.T) {
tests := []struct {
name string
input []string
expected []string
}{
{
name: "sorts comma-separated values",
input: []string{"registry,docker,oci-dir"},
expected: []string{"docker", "oci-dir", "registry"},
},
{
name: "sorts across multiple entries",
input: []string{"registry,docker", "oci-dir"},
expected: []string{"docker", "oci-dir", "registry"},
},
{
name: "trims whitespace and sorts",
input: []string{" registry , docker ", " oci-dir "},
expected: []string{"docker", "oci-dir", "registry"},
},
{
name: "handles single value",
input: []string{"registry"},
expected: []string{"registry"},
},
{
name: "handles empty input",
input: []string{},
expected: nil,
},
{
name: "sorts reverse alphabetical order",
input: []string{"zebra,yankee,xray"},
expected: []string{"xray", "yankee", "zebra"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := FlattenAndSort(tt.input)
assert.Equal(t, tt.expected, got)
})
}
}
func Test_enrichmentEnabled(t *testing.T) {
tests := []struct {
directives string
@@ -188,7 +139,7 @@ func Test_enrichmentEnabled(t *testing.T) {
for _, test := range tests {
t.Run(test.directives, func(t *testing.T) {
got := enrichmentEnabled(FlattenAndSort([]string{test.directives}), test.test)
got := enrichmentEnabled(flatten([]string{test.directives}), test.test)
assert.Equal(t, test.expected, got)
})
}
@@ -1,57 +0,0 @@
package options
import (
"fmt"
"github.com/anchore/clio"
"github.com/anchore/fangs"
)
type CatalogerSelection struct {
// high-level cataloger configuration
Catalogers []string `yaml:"-" json:"catalogers" mapstructure:"catalogers"` // deprecated and not shown in yaml output
DefaultCatalogers []string `yaml:"default-catalogers" json:"default-catalogers" mapstructure:"default-catalogers"`
SelectCatalogers []string `yaml:"select-catalogers" json:"select-catalogers" mapstructure:"select-catalogers"`
}
var _ interface {
clio.FlagAdder
clio.PostLoader
} = (*CatalogerSelection)(nil)
func (cfg *CatalogerSelection) AddFlags(flags clio.FlagSet) {
flags.StringArrayVarP(&cfg.Catalogers, "catalogers", "",
"enable one or more package catalogers")
if pfp, ok := flags.(fangs.PFlagSetProvider); ok {
if err := pfp.PFlagSet().MarkDeprecated("catalogers", "use: override-default-catalogers and select-catalogers"); err != nil {
panic(err)
}
} else {
panic("unable to mark flags as deprecated")
}
flags.StringArrayVarP(&cfg.DefaultCatalogers, "override-default-catalogers", "",
"set the base set of catalogers to use (defaults to 'image' or 'directory' depending on the scan source)")
flags.StringArrayVarP(&cfg.SelectCatalogers, "select-catalogers", "",
"add, remove, and filter the catalogers to be used")
}
func (cfg *CatalogerSelection) PostLoad() error {
usingLegacyCatalogers := len(cfg.Catalogers) > 0
usingNewCatalogers := len(cfg.DefaultCatalogers) > 0 || len(cfg.SelectCatalogers) > 0
if usingLegacyCatalogers && usingNewCatalogers {
return fmt.Errorf("cannot use both 'catalogers' and 'select-catalogers'/'default-catalogers' flags %q | %q %q", cfg.Catalogers, cfg.SelectCatalogers, cfg.DefaultCatalogers)
}
cfg.Catalogers = FlattenAndSort(cfg.Catalogers)
cfg.DefaultCatalogers = FlattenAndSort(cfg.DefaultCatalogers)
cfg.SelectCatalogers = FlattenAndSort(cfg.SelectCatalogers)
// for backwards compatibility
cfg.DefaultCatalogers = append(cfg.DefaultCatalogers, cfg.Catalogers...)
return nil
}
@@ -1,68 +0,0 @@
package options
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestCatalogerSelection_PostLoad(t *testing.T) {
tests := []struct {
name string
options CatalogerSelection
assert func(t *testing.T, options CatalogerSelection)
wantErr assert.ErrorAssertionFunc
}{
{
name: "mutually exclusive cataloger flags (cat / def-cat)",
options: CatalogerSelection{
Catalogers: []string{"foo,bar", "42"},
DefaultCatalogers: []string{"some,thing"},
},
wantErr: assert.Error,
},
{
name: "mutually exclusive cataloger flags (cat / sel-cat)",
options: CatalogerSelection{
Catalogers: []string{"foo,bar", "42"},
SelectCatalogers: []string{"some,thing"},
},
wantErr: assert.Error,
},
{
name: "allow old cataloger flags",
options: CatalogerSelection{
Catalogers: []string{"foo,bar"},
},
assert: func(t *testing.T, options CatalogerSelection) {
assert.Equal(t, []string{"bar", "foo"}, options.DefaultCatalogers) // note: sorted order
assert.Equal(t, []string{"bar", "foo"}, options.Catalogers) // note: sorted order
},
},
{
name: "allow new cataloger flags",
options: CatalogerSelection{
SelectCatalogers: []string{"foo,bar", "42"},
DefaultCatalogers: []string{"some,thing"},
},
assert: func(t *testing.T, options CatalogerSelection) {
assert.Equal(t, []string{"42", "bar", "foo"}, options.SelectCatalogers) // note: sorted order
assert.Equal(t, []string{"some", "thing"}, options.DefaultCatalogers) // note: sorted order
assert.Empty(t, options.Catalogers)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.wantErr == nil {
tt.wantErr = assert.NoError
}
tt.wantErr(t, tt.options.PostLoad(), fmt.Sprintf("PostLoad()"))
if tt.assert != nil {
tt.assert(t, tt.options)
}
})
}
}
+3 -19
View File
@@ -6,15 +6,7 @@ import (
)
type dotnetConfig struct {
DepPackagesMustHaveDLL bool `mapstructure:"dep-packages-must-have-dll" json:"dep-packages-must-have-dll" yaml:"dep-packages-must-have-dll"`
DepPackagesMustClaimDLL bool `mapstructure:"dep-packages-must-claim-dll" json:"dep-packages-must-claim-dll" yaml:"dep-packages-must-claim-dll"`
PropagateDLLClaimsToParents bool `mapstructure:"propagate-dll-claims-to-parents" json:"propagate-dll-claims-to-parents" yaml:"propagate-dll-claims-to-parents"`
RelaxDLLClaimsWhenBundlingDetected bool `mapstructure:"relax-dll-claims-when-bundling-detected" json:"relax-dll-claims-when-bundling-detected" yaml:"relax-dll-claims-when-bundling-detected"`
ExcludeProjectReferences bool `mapstructure:"exclude-project-references" json:"exclude-project-references" yaml:"exclude-project-references"`
EnableCertificateValidation bool `json:"enable-certificate-validation" yaml:"enable-certificate-validation" mapstructure:"enable-certificate-validation"`
}
var _ interface {
@@ -22,20 +14,12 @@ var _ interface {
} = (*dotnetConfig)(nil)
func (o *dotnetConfig) DescribeFields(descriptions clio.FieldDescriptionSet) {
descriptions.Add(&o.DepPackagesMustHaveDLL, `only keep dep.json packages which an executable on disk is found. The package is also included if a DLL is found for any child package, even if the package itself does not have a DLL.`)
descriptions.Add(&o.DepPackagesMustClaimDLL, `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.`)
descriptions.Add(&o.PropagateDLLClaimsToParents, `treat DLL claims or on-disk evidence for child packages as DLL claims or on-disk evidence for any parent package`)
descriptions.Add(&o.RelaxDLLClaimsWhenBundlingDetected, `show all packages from the deps.json if bundling tooling is present as a dependency (e.g. ILRepack)`)
descriptions.Add(&o.ExcludeProjectReferences, `exclude packages with type "project" from deps.json output (these are internal project references, not NuGet packages)`)
descriptions.Add(&o.EnableCertificateValidation, `enable certificate validation -- this requires an active internet connection to download certificates and CRLs`)
}
func defaultDotnetConfig() dotnetConfig {
def := dotnet.DefaultCatalogerConfig()
return dotnetConfig{
DepPackagesMustHaveDLL: def.DepPackagesMustHaveDLL,
DepPackagesMustClaimDLL: def.DepPackagesMustClaimDLL,
PropagateDLLClaimsToParents: def.PropagateDLLClaimsToParents,
RelaxDLLClaimsWhenBundlingDetected: def.RelaxDLLClaimsWhenBundlingDetected,
ExcludeProjectReferences: def.ExcludeProjectReferences,
EnableCertificateValidation: def.EnableCertificateValidation,
}
}
+2 -2
View File
@@ -43,9 +43,9 @@ over to the final syft 1.0 json output this option can be used to ease the trans
Note: long term support for this option is not guaranteed (it may change or break at any time)`)
descriptions.Add(&o.Template.Path, `path to the template file to use when rendering the output with the template output format.
descriptions.Add(&o.Template.Path, `path to the template file to use when rendering the output with the template output format.
Note that all template paths are based on the current syft-json schema`)
descriptions.Add(&o.Template.Legacy, `if true, uses the go structs for the syft-json format for templating.
descriptions.Add(&o.Template.Legacy, `if true, uses the go structs for the syft-json format for templating.
if false, uses the syft-json output for templating (which follows the syft JSON schema exactly).
Note: long term support for this option is not guaranteed (it may change or break at any time)`)
-3
View File
@@ -16,7 +16,6 @@ type golangConfig struct {
Proxy string `json:"proxy" yaml:"proxy" mapstructure:"proxy"`
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"`
}
var _ interface {
@@ -38,7 +37,6 @@ if unset this defaults to $GONOPROXY`)
descriptions.Add(&o.MainModuleVersion, `the go main module version discovered from binaries built with the go compiler will
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`)
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
(e.g. v0.0.0-20220308212642-53e6d0aaf6fb) when a more accurate version cannot be found otherwise`)
@@ -66,6 +64,5 @@ func defaultGolangConfig() golangConfig {
FromContents: def.MainModuleVersion.FromContents,
FromBuildSettings: def.MainModuleVersion.FromBuildSettings,
},
UsePackagesLib: nil, // this defaults to true, which is the API default
}
}
+1 -22
View File
@@ -1,9 +1,6 @@
package options
import (
"github.com/anchore/clio"
"github.com/anchore/syft/syft/pkg/cataloger/javascript"
)
import "github.com/anchore/clio"
type javaScriptConfig struct {
SearchRemoteLicenses *bool `json:"search-remote-licenses" yaml:"search-remote-licenses" mapstructure:"search-remote-licenses"`
@@ -15,24 +12,6 @@ var _ interface {
clio.FieldDescriber
} = (*javaScriptConfig)(nil)
func defaultJavaScriptConfig() javaScriptConfig {
def := javascript.DefaultCatalogerConfig()
var includeDevDependencies *bool
if def.IncludeDevDependencies {
includeDevDependencies = &def.IncludeDevDependencies
}
var searchRemoteLicenses *bool
if def.SearchRemoteLicenses {
searchRemoteLicenses = &def.SearchRemoteLicenses
}
return javaScriptConfig{
NpmBaseURL: def.NPMBaseURL,
SearchRemoteLicenses: searchRemoteLicenses,
IncludeDevDependencies: includeDevDependencies,
}
}
func (o *javaScriptConfig) DescribeFields(descriptions clio.FieldDescriptionSet) {
descriptions.Add(&o.SearchRemoteLicenses, `enables Syft to use the network to fill in more detailed license information`)
descriptions.Add(&o.NpmBaseURL, `base NPM url to use`)
+7 -26
View File
@@ -1,17 +1,12 @@
package options
import (
"fmt"
"github.com/anchore/clio"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/cataloging"
)
type licenseConfig struct {
Content cataloging.LicenseContent `yaml:"content" json:"content" mapstructure:"content"`
Coverage float64 `yaml:"coverage" json:"coverage" mapstructure:"coverage"`
AvailableLicenseContent []cataloging.LicenseContent `yaml:"-" json:"-" mapstructure:"-"`
IncludeUnknownLicenseContent bool `yaml:"include-unknown-license-content" json:"include-unknown-license-content" mapstructure:"include-unknown-license-content"`
LicenseCoverage float64 `yaml:"license-coverage" json:"license-coverage" mapstructure:"license-coverage"`
}
var _ interface {
@@ -19,29 +14,15 @@ var _ interface {
} = (*licenseConfig)(nil)
func (o *licenseConfig) DescribeFields(descriptions clio.FieldDescriptionSet) {
descriptions.Add(&o.Content, fmt.Sprintf("include the content of licenses in the SBOM for a given syft scan; valid values are: %s", o.AvailableLicenseContent))
descriptions.Add(&o.Coverage, `adjust the percent as a fraction of the total text, in normalized words, that
descriptions.Add(&o.IncludeUnknownLicenseContent, `include the content of a license in the SBOM when syft
cannot determine a valid SPDX ID for the given license`)
descriptions.Add(&o.LicenseCoverage, `adjust the percent as a fraction of the total text, in normalized words, that
matches any valid license for the given inputs, expressed as a percentage across all of the licenses matched.`)
}
func (o *licenseConfig) PostLoad() error {
validContent := internal.NewSet(o.AvailableLicenseContent...)
if !validContent.Contains(o.Content) {
return fmt.Errorf("could not use %q as license content option; valid values are: %v", o.Content, validContent.ToSlice())
}
return nil
}
func defaultLicenseConfig() licenseConfig {
cfg := cataloging.DefaultLicenseConfig()
return licenseConfig{
Content: cfg.IncludeContent,
Coverage: cfg.Coverage,
AvailableLicenseContent: []cataloging.LicenseContent{
cataloging.LicenseContentIncludeAll,
cataloging.LicenseContentIncludeUnknown,
cataloging.LicenseContentExcludeAll,
},
IncludeUnknownLicenseContent: false,
LicenseCoverage: 75,
}
}
+2 -6
View File
@@ -1,18 +1,14 @@
package options
import (
"github.com/anchore/clio"
"github.com/anchore/syft/syft/pkg/cataloger/kernel"
)
import "github.com/anchore/clio"
type linuxKernelConfig struct {
CatalogModules bool `json:"catalog-modules" yaml:"catalog-modules" mapstructure:"catalog-modules"`
}
func defaultLinuxKernelConfig() linuxKernelConfig {
def := kernel.DefaultLinuxKernelCatalogerConfig()
return linuxKernelConfig{
CatalogModules: def.CatalogModules,
CatalogModules: true,
}
}
-25
View File
@@ -1,25 +0,0 @@
package options
import (
"github.com/anchore/clio"
"github.com/anchore/syft/syft/pkg/cataloger/nix"
)
type nixConfig struct {
CaptureOwnedFiles bool `json:"capture-owned-files" yaml:"capture-owned-files" mapstructure:"capture-owned-files"`
}
func defaultNixConfig() nixConfig {
def := nix.DefaultConfig()
return nixConfig{
def.CaptureOwnedFiles,
}
}
var _ interface {
clio.FieldDescriber
} = (*nixConfig)(nil)
func (o *nixConfig) DescribeFields(descriptions clio.FieldDescriptionSet) {
descriptions.Add(&o.CaptureOwnedFiles, `enumerate all files owned by packages found within Nix store paths`)
}
+1 -3
View File
@@ -12,7 +12,6 @@ import (
"github.com/anchore/syft/syft/format/cyclonedxjson"
"github.com/anchore/syft/syft/format/cyclonedxxml"
"github.com/anchore/syft/syft/format/github"
"github.com/anchore/syft/syft/format/purls"
"github.com/anchore/syft/syft/format/spdxjson"
"github.com/anchore/syft/syft/format/spdxtagvalue"
"github.com/anchore/syft/syft/format/syftjson"
@@ -90,7 +89,7 @@ func (o Output) SBOMWriter() (sbom.Writer, error) {
usesTemplateOutput := names.Has(string(template.ID))
if usesTemplateOutput && o.Template.Path == "" {
if usesTemplateOutput && o.Format.Template.Path == "" {
return nil, fmt.Errorf(`must specify path to template file when using "template" output format`)
}
@@ -128,7 +127,6 @@ func supportedIDs() []sbom.FormatID {
table.ID,
text.ID,
template.ID,
purls.ID,
// encoders that support multiple versions
cyclonedxxml.ID,
+2 -1
View File
@@ -3,9 +3,10 @@ package options
import (
"fmt"
"github.com/mitchellh/go-homedir"
"github.com/anchore/clio"
"github.com/anchore/fangs"
"github.com/anchore/go-homedir"
"github.com/anchore/syft/syft/sbom"
)
+2 -16
View File
@@ -1,30 +1,16 @@
package options
import (
"github.com/anchore/clio"
"github.com/anchore/syft/syft/pkg/cataloger/python"
)
import "github.com/anchore/clio"
type pythonConfig struct {
SearchRemoteLicenses *bool `json:"search-remote-licenses" yaml:"search-remote-licenses" mapstructure:"search-remote-licenses"`
PypiBaseURL string `json:"pypi-base-url" yaml:"pypi-base-url" mapstructure:"pypi-base-url"`
GuessUnpinnedRequirements *bool `json:"guess-unpinned-requirements" yaml:"guess-unpinned-requirements" mapstructure:"guess-unpinned-requirements"`
GuessUnpinnedRequirements bool `json:"guess-unpinned-requirements" yaml:"guess-unpinned-requirements" mapstructure:"guess-unpinned-requirements"`
}
var _ interface {
clio.FieldDescriber
} = (*pythonConfig)(nil)
func defaultPythonConfig() pythonConfig {
def := python.DefaultCatalogerConfig()
return pythonConfig{
GuessUnpinnedRequirements: &def.GuessUnpinnedRequirements,
}
}
func (o *pythonConfig) DescribeFields(descriptions clio.FieldDescriptionSet) {
descriptions.Add(&o.SearchRemoteLicenses, `enables Syft to use the network to fill in more detailed license information`)
descriptions.Add(&o.PypiBaseURL, `base Pypi url to use`)
descriptions.Add(&o.GuessUnpinnedRequirements, `when running across entries in requirements.txt that do not specify a specific version
(e.g. "sqlalchemy >= 1.0.0, <= 2.0.0, != 3.0.0, <= 3.0.0"), attempt to guess what the version could
be based on the version requirements specified (e.g. "1.0.0"). When enabled the lowest expressible version
-2
View File
@@ -16,8 +16,6 @@ import (
type sourceConfig struct {
Name string `json:"name" yaml:"name" mapstructure:"name"`
Version string `json:"version" yaml:"version" mapstructure:"version"`
Supplier string `json:"supplier" yaml:"supplier" mapstructure:"supplier"`
Source string `json:"source" yaml:"source" mapstructure:"source"`
BasePath string `yaml:"base-path" json:"base-path" mapstructure:"base-path"` // specify base path for all file paths
File fileSource `json:"file" yaml:"file" mapstructure:"file"`
Image imageSource `json:"image" yaml:"image" mapstructure:"image"`
+1 -1
View File
@@ -10,9 +10,9 @@ import (
"strings"
"github.com/hashicorp/go-multierror"
"github.com/mitchellh/go-homedir"
"github.com/scylladb/go-set/strset"
"github.com/anchore/go-homedir"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/format"
+2 -4
View File
@@ -6,10 +6,10 @@ import (
"strings"
"testing"
"github.com/docker/docker/pkg/homedir"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/go-homedir"
"github.com/anchore/syft/syft/sbom"
)
@@ -228,8 +228,6 @@ func Test_newSBOMMultiWriter(t *testing.T) {
}
func Test_newSBOMWriterDescription(t *testing.T) {
h, err := homedir.Dir()
require.NoError(t, err)
tests := []struct {
name string
path string
@@ -238,7 +236,7 @@ func Test_newSBOMWriterDescription(t *testing.T) {
{
name: "expand home dir",
path: "~/place.txt",
expected: filepath.Join(h, "place.txt"),
expected: filepath.Join(homedir.Get(), "place.txt"),
},
{
name: "passthrough other paths",
@@ -78,27 +78,11 @@ var imageOnlyTestCases = []testCase{
},
},
{
name: "find .NET packages (deps.json + .dlls)",
name: "find dot net executable",
pkgType: pkg.DotnetPkg,
pkgLanguage: pkg.Dotnet,
pkgInfo: map[string]string{
// executable
"DocuSign.eSign": "6.8.0.0",
// deps.json
"AWSSDK.Core": "3.7.10.6",
"Microsoft.Extensions.DependencyInjection": "6.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0",
"Microsoft.Extensions.Logging": "6.0.0",
"Microsoft.Extensions.Logging.Abstractions": "6.0.0",
"Microsoft.Extensions.Options": "6.0.0",
"Microsoft.Extensions.Primitives": "6.0.0",
"Newtonsoft.Json": "13.0.1",
"Serilog": "2.10.0",
"Serilog.Sinks.Console": "4.0.1",
//"System.Diagnostics.DiagnosticSource": "6.0.0", // no dll claims in deps.json targets section
//"System.Runtime.CompilerServices.Unsafe": "6.0.0", // no dll claims in deps.json targets section
//"TestCommon": "1.0.0",
"TestLibrary": "1.0.0",
},
},
}
@@ -257,11 +241,10 @@ var dirOnlyTestCases = []testCase{
},
},
{
name: "find dotnet packages (.deps.json)",
name: "find dotnet packages",
pkgType: pkg.DotnetPkg,
pkgLanguage: pkg.Dotnet,
pkgInfo: map[string]string{
// all from deps.json
"AWSSDK.Core": "3.7.10.6",
"Microsoft.Extensions.DependencyInjection": "6.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0",
@@ -272,10 +255,10 @@ var dirOnlyTestCases = []testCase{
"Newtonsoft.Json": "13.0.1",
"Serilog": "2.10.0",
"Serilog.Sinks.Console": "4.0.1",
//"System.Diagnostics.DiagnosticSource": "6.0.0", // no dll claims in deps.json targets section
//"System.Runtime.CompilerServices.Unsafe": "6.0.0", // no dll claims in deps.json targets section
//"TestCommon": "1.0.0",
"TestLibrary": "1.0.0",
"System.Diagnostics.DiagnosticSource": "6.0.0",
"System.Runtime.CompilerServices.Unsafe": "6.0.0",
"TestCommon": "1.0.0",
"TestLibrary": "1.0.0",
},
},
{
@@ -502,15 +485,8 @@ var commonTestCases = []testCase{
},
},
{
name: "find homebrew",
pkgType: pkg.HomebrewPkg,
pkgInfo: map[string]string{
"afflib": "1.2.3",
},
},
{
name: "find php pear/pecl package",
pkgType: pkg.PhpPearPkg,
name: "find php pecl package",
pkgType: pkg.PhpPeclPkg,
pkgLanguage: pkg.PHP,
pkgInfo: map[string]string{
"memcached": "3.2.0",
@@ -12,7 +12,6 @@ import (
"github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
@@ -25,6 +24,7 @@ func BenchmarkImagePackageCatalogers(b *testing.B) {
// get the source object for the image
theSource, err := syft.GetSource(context.Background(), tarPath, syft.DefaultGetSourceConfig().WithSources("docker-archive"))
require.NoError(b, err)
b.Cleanup(func() {
require.NoError(b, theSource.Close())
})
@@ -85,9 +85,6 @@ func TestPkgCoverageImage(t *testing.T) {
definedPkgs.Remove(string(pkg.GithubActionPkg))
definedPkgs.Remove(string(pkg.GithubActionWorkflowPkg))
definedPkgs.Remove(string(pkg.TerraformPkg))
definedPkgs.Remove(string(pkg.PhpPeclPkg)) // we have coverage for pear instead
definedPkgs.Remove(string(pkg.CondaPkg))
definedPkgs.Remove(string(pkg.ModelPkg))
var cases []testCase
cases = append(cases, commonTestCases...)
@@ -95,7 +92,40 @@ func TestPkgCoverageImage(t *testing.T) {
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
assertPackages(t, sbom, c, observedLanguages, observedPkgs)
pkgCount := 0
for a := range sbom.Artifacts.Packages.Enumerate(c.pkgType) {
if a.Language.String() != "" {
observedLanguages.Add(a.Language.String())
}
observedPkgs.Add(string(a.Type))
expectedVersion, ok := c.pkgInfo[a.Name]
if !ok {
t.Errorf("unexpected package found: %s", a.Name)
}
if expectedVersion != a.Version {
t.Errorf("unexpected package version (pkg=%s): %s, expected: %s", a.Name, a.Version, expectedVersion)
}
if a.Language != c.pkgLanguage {
t.Errorf("bad language (pkg=%+v): %+v", a.Name, a.Language)
}
if a.Type != c.pkgType {
t.Errorf("bad package type (pkg=%+v): %+v", a.Name, a.Type)
}
pkgCount++
}
if pkgCount != len(c.pkgInfo)+c.duplicates {
t.Logf("Discovered packages of type %+v", c.pkgType)
for a := range sbom.Artifacts.Packages.Enumerate(c.pkgType) {
t.Log(" ", a)
}
t.Fatalf("unexpected package count: %d!=%d", pkgCount, len(c.pkgInfo))
}
})
}
@@ -145,7 +175,44 @@ func TestPkgCoverageDirectory(t *testing.T) {
for _, test := range cases {
t.Run(test.name, func(t *testing.T) {
assertPackages(t, sbom, test, observedLanguages, observedPkgs)
actualPkgCount := 0
for actualPkg := range sbom.Artifacts.Packages.Enumerate(test.pkgType) {
observedLanguages.Add(actualPkg.Language.String())
observedPkgs.Add(string(actualPkg.Type))
expectedVersion, ok := test.pkgInfo[actualPkg.Name]
if !ok {
t.Errorf("unexpected package found: %s", actualPkg.Name)
}
if expectedVersion != actualPkg.Version {
t.Errorf("unexpected package version (pkg=%s): %s", actualPkg.Name, actualPkg.Version)
}
var foundLang bool
for _, lang := range strings.Split(test.pkgLanguage.String(), ",") {
if actualPkg.Language.String() == lang {
foundLang = true
break
}
}
if !foundLang {
t.Errorf("bad language (pkg=%+v): %+v", actualPkg.Name, actualPkg.Language)
}
if actualPkg.Type != test.pkgType {
t.Errorf("bad package type (pkg=%+v): %+v", actualPkg.Name, actualPkg.Type)
}
actualPkgCount++
}
if actualPkgCount != len(test.pkgInfo)+test.duplicates {
for actualPkg := range sbom.Artifacts.Packages.Enumerate(test.pkgType) {
t.Log(" ", actualPkg)
}
t.Fatalf("unexpected package count: %d!=%d", actualPkgCount, len(test.pkgInfo))
}
})
}
@@ -160,9 +227,6 @@ func TestPkgCoverageDirectory(t *testing.T) {
definedPkgs.Remove(string(pkg.LinuxKernelModulePkg))
definedPkgs.Remove(string(pkg.Rpkg))
definedPkgs.Remove(string(pkg.UnknownPkg))
definedPkgs.Remove(string(pkg.CondaPkg))
definedPkgs.Remove(string(pkg.PhpPeclPkg)) // this is covered as pear packages
definedPkgs.Remove(string(pkg.ModelPkg))
// for directory scans we should not expect to see any of the following package types
definedPkgs.Remove(string(pkg.KbPkg))
@@ -177,61 +241,6 @@ func TestPkgCoverageDirectory(t *testing.T) {
}
}
func assertPackages(t *testing.T, sbom sbom.SBOM, test testCase, observedLanguages *strset.Set, observedPkgs *strset.Set) {
actualPkgCount := 0
for actualPkg := range sbom.Artifacts.Packages.Enumerate(test.pkgType) {
observedLanguages.Add(actualPkg.Language.String())
observedPkgs.Add(string(actualPkg.Type))
expectedVersion, ok := test.pkgInfo[actualPkg.Name]
if !ok {
t.Errorf("unexpected package found: %s", actualPkg.Name)
}
if expectedVersion != actualPkg.Version {
t.Errorf("unexpected package version (pkg=%s): %s", actualPkg.Name, actualPkg.Version)
}
var foundLang bool
for _, lang := range strings.Split(test.pkgLanguage.String(), ",") {
if actualPkg.Language.String() == lang {
foundLang = true
break
}
}
if !foundLang {
t.Errorf("bad language (pkg=%+v): %+v", actualPkg.Name, actualPkg.Language)
}
if actualPkg.Type != test.pkgType {
t.Errorf("bad package type (pkg=%+v): %+v", actualPkg.Name, actualPkg.Type)
}
actualPkgCount++
// all packages should have at least one location associated with it, and of those locations at least one should be primary evidence
locs := actualPkg.Locations.ToSlice()
assert.NotEmpty(t, locs, "package %q has no locations (type=%q)", actualPkg.Name, actualPkg.Type)
var primaryEvidenceFound bool
for _, l := range locs {
if _, exists := l.Annotations[pkg.EvidenceAnnotationKey]; !exists {
t.Errorf("missing evidence annotation (pkg=%s type=%s)", actualPkg.Name, actualPkg.Type)
}
if l.Annotations[pkg.EvidenceAnnotationKey] == pkg.PrimaryEvidenceAnnotation {
primaryEvidenceFound = true
}
}
assert.True(t, primaryEvidenceFound, "no primary evidence found for package %q", actualPkg.Name)
}
if actualPkgCount != len(test.pkgInfo)+test.duplicates {
for actualPkg := range sbom.Artifacts.Packages.Enumerate(test.pkgType) {
t.Log(" ", actualPkg)
}
t.Fatalf("unexpected package count: %d!=%d", actualPkgCount, len(test.pkgInfo))
}
}
func TestPkgCoverageImage_HasEvidence(t *testing.T) {
sbom, _ := catalogFixtureImage(t, "image-pkg-coverage", source.SquashedScope)
@@ -78,10 +78,7 @@ func TestEncodeDecodeEncodeCycleComparison(t *testing.T) {
t.Run(test.name, func(t *testing.T) {
for _, image := range images {
originalSBOM, _ := catalogFixtureImage(t, image, source.SquashedScope)
// we need a way to inject supplier into this test
// supplier is not available as part of the SBOM Config API since the flag
// is used in conjunction with the SourceConfig which is injected into generateSBOM during scan
originalSBOM.Source.Supplier = "anchore"
f := encoders.GetByString(test.name)
require.NotNil(t, f)
@@ -52,18 +52,8 @@ func TestAllPackageCatalogersReachableInTasks(t *testing.T) {
assert.Equal(t, len(taskTagsByName), constructorCount, "mismatch in number of cataloger constructors and task names")
exceptions := strset.New(
// not reachable since they are deprecated
"dotnet-portable-executable-cataloger",
"dotnet-deps-cataloger",
"nix-store-cataloger",
"php-pecl-serialized-cataloger",
// not reachable by design
"sbom-cataloger",
)
for taskName, tags := range taskTagsByName {
if exceptions.Has(taskName) {
if taskName == "sbom-cataloger" {
continue // this is a special case
}
if !strset.New(tags...).HasAny(pkgcataloging.ImageTag, pkgcataloging.DirectoryTag) {
@@ -1,6 +1,7 @@
package integration
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
@@ -10,52 +11,47 @@ import (
)
func TestPackageDeduplication(t *testing.T) {
// this test verifies that package deduplication works correctly across layers.
// The test fixture installs/upgrades packages in multiple stages, creating
// duplicate RPM DB entries across layers. Without deduplication, we'd see ~600 packages.
//
// Note: we index by package name (not name-version) to be resilient to Rocky Linux
// repo updates. Location counts are summed across all versions of each package.
tests := []struct {
scope source.Scope
packageCount int
instanceCount map[string]int // how many distinct package instances (by name)
locationCount map[string]int // total locations across ALL versions of each package
instanceCount map[string]int
locationCount map[string]int
}{
{
scope: source.AllLayersScope,
packageCount: 176, // without deduplication this would be ~600
packageCount: 178, // without deduplication this would be ~600
instanceCount: map[string]int{
"basesystem": 1,
"wget": 1,
"curl-minimal": 2, // base + upgraded (2 different versions)
"curl-minimal": 2, // upgraded in the image
"vsftpd": 1,
"httpd": 1,
"httpd": 1, // rpm, - we exclude binary
},
locationCount: map[string]int{
"basesystem": 5, // in all layers
"curl-minimal": 5, // total across both versions (2 + 3)
"wget": 4, // wget + all above layers
"vsftpd": 2, // vsftpd + all above layers
"httpd": 1, // last layer
"basesystem-11-13.el9": 5, // in all layers
"curl-minimal-7.76.1-26.el9_3.2.0.1": 2, // base + wget layer
"curl-minimal-7.76.1-29.el9_4.1": 3, // curl upgrade layer + all above layers
"wget-1.21.1-8.el9_4": 4, // wget + all above layers
"vsftpd-3.0.5-5.el9": 2, // vsftpd + all above layers
"httpd-2.4.57-11.el9_4.1": 1, // last layer
},
},
{
scope: source.SquashedScope,
packageCount: 170,
packageCount: 172,
instanceCount: map[string]int{
"basesystem": 1,
"wget": 1,
"curl-minimal": 1, // deduped to latest
"curl-minimal": 1, // upgraded, but the most recent
"vsftpd": 1,
"httpd": 1,
"httpd": 1, // rpm, binary is now excluded by overlap
},
locationCount: map[string]int{
"basesystem": 1,
"curl-minimal": 1,
"wget": 1,
"vsftpd": 1,
"httpd": 1,
"basesystem-11-13.el9": 1,
"curl-minimal-7.76.1-29.el9_4.1": 1, // upgrade
"wget-1.21.1-8.el9_4": 1,
"vsftpd-3.0.5-5.el9": 1,
"httpd-2.4.57-11.el9_4.1": 1,
},
},
}
@@ -63,32 +59,35 @@ func TestPackageDeduplication(t *testing.T) {
for _, tt := range tests {
t.Run(string(tt.scope), func(t *testing.T) {
sbom, _ := catalogFixtureImage(t, "image-vertical-package-dups", tt.scope)
// verify binary packages have names
for _, p := range sbom.Artifacts.Packages.Sorted() {
if p.Type == pkg.BinaryPkg {
assert.NotEmpty(t, p.Name)
}
}
// verify exact package count
assert.Equal(t, tt.packageCount, sbom.Artifacts.Packages.PackageCount())
// verify instance count by package name
for name, expectedCount := range tt.instanceCount {
for name, expectedInstanceCount := range tt.instanceCount {
pkgs := sbom.Artifacts.Packages.PackagesByName(name)
assert.Len(t, pkgs, expectedCount, "unexpected instance count for %s", name)
}
// verify total location count across all versions of each package
for name, expectedLocCount := range tt.locationCount {
pkgs := sbom.Artifacts.Packages.PackagesByName(name)
totalLocations := 0
for _, p := range pkgs {
totalLocations += len(p.Locations.ToSlice())
// with multiple packages with the same name, something is wrong (or this is the wrong fixture)
if assert.Len(t, pkgs, expectedInstanceCount, "unexpected package count for %s", name) {
for _, p := range pkgs {
nameVersion := fmt.Sprintf("%s-%s", name, p.Version)
expectedLocationCount, ok := tt.locationCount[nameVersion]
if !ok {
t.Errorf("missing name-version: %s", nameVersion)
continue
}
// we should see merged locations (assumption, there was 1 location for each package)
assert.Len(t, p.Locations.ToSlice(), expectedLocationCount, "unexpected location count for %s", nameVersion)
// all paths should match
assert.Len(t, p.Locations.CoordinateSet().Paths(), 1, "unexpected location count for %s", nameVersion)
}
}
assert.Equal(t, expectedLocCount, totalLocations, "unexpected total location count for %s", name)
}
})
}
}
@@ -3,18 +3,18 @@ package integration
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
func TestPhotonPackageRegression(t *testing.T) { // Regression: https://github.com/anchore/syft/pull/1997
sbom, _ := catalogFixtureImage(t, "image-photon-all-layers", source.AllLayersScope)
var count int
for range sbom.Artifacts.Packages.Enumerate(pkg.RpmPkg) {
count++
var packages []pkg.Package
for p := range sbom.Artifacts.Packages.Enumerate() {
packages = append(packages, p)
}
assert.Greater(t, count, 0, "expected to find RPM packages in the SBOM (but did not)")
if len(packages) < 1 {
t.Errorf("failed to find packages for photon distro; wanted > 0 got 0")
}
}
@@ -1,2 +0,0 @@
desc "Advanced Forensic Format"
homepage "https://github.com/sshock/AFFLIBv3"
@@ -1 +1 @@
a:6:{s:4:"name";s:9:"memcached";s:7:"channel";s:12:"pecl.php.net";s:4:"date";s:10:"2022-01-11";s:4:"time";s:8:"15:23:47";s:7:"version";a:2:{s:7:"release";s:5:"3.2.0";s:3:"api";s:5:"3.2.0";}s:7:"license";a:2:{s:7:"attribs";a:1:{s:3:"uri";s:26:"http://www.php.net/license";}s:8:"_content";s:11:"PHP License";}}
a:5:{s:4:"name";s:9:"memcached";s:4:"date";s:10:"2022-01-11";s:4:"time";s:8:"15:23:47";s:7:"version";a:2:{s:7:"release";s:5:"3.2.0";s:3:"api";s:5:"3.2.0";}s:7:"license";a:2:{s:7:"attribs";a:1:{s:3:"uri";s:26:"http://www.php.net/license";}s:8:"_content";s:11:"PHP License";}}
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
set -eux
docker create --name generate-rpmdb-fixture rockylinux:9 sh -c 'tail -f /dev/null'
docker create --name generate-rpmdb-fixture centos:latest sh -c 'tail -f /dev/null'
function cleanup {
docker kill generate-rpmdb-fixture
@@ -38,11 +38,11 @@ func catalogFixtureImageWithConfig(t *testing.T, fixtureImageName string, cfg *s
// get the source to build an SBOM against
theSource, err := syft.GetSource(context.Background(), tarPath, syft.DefaultGetSourceConfig().WithSources("docker-archive"))
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, theSource.Close())
})
// build the SBOM
s, err := syft.CreateSBOM(context.Background(), theSource, cfg)
require.NoError(t, err)
@@ -66,7 +66,7 @@ func catalogDirectory(t *testing.T, dir string, catalogerSelection ...string) (s
func catalogDirectoryWithConfig(t *testing.T, dir string, cfg *syft.CreateSBOMConfig) (sbom.SBOM, source.Source) {
cfg.CatalogerSelection = cfg.CatalogerSelection.WithDefaults(pkgcataloging.DirectoryTag)
// get the source to build an SBOM against
// get the source to build an sbom against
theSource, err := syft.GetSource(context.Background(), dir, syft.DefaultGetSourceConfig().WithSources("dir"))
require.NoError(t, err)
t.Cleanup(func() {
+5 -1
View File
@@ -36,7 +36,11 @@ func capture(target **os.File, writer io.Writer, bufSize int) func() {
}()
buf := make([]byte, bufSize)
for original != nil {
for {
if original == nil {
break
}
n, err := r.Read(buf)
if n > 0 {
_, _ = writer.Write(buf[0:n])
@@ -6,7 +6,6 @@ import (
"io"
"path"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
@@ -77,6 +76,7 @@ func newAlpineConfiguration(resolver file.Resolver) (*AlpineConfiguration, []fil
return &AlpineConfiguration{
APKKeys: keys,
}, locations, nil
}
func getVersion(resolver file.Resolver) (string, []file.Location, error) {
@@ -92,7 +92,6 @@ func getVersion(resolver file.Resolver) (string, []file.Location, error) {
if err != nil {
return "", nil, fmt.Errorf("unable to read alpine version: %w", err)
}
defer internal.CloseAndLogError(reader, locations[0].RealPath)
version, err := io.ReadAll(reader)
if err != nil {
@@ -112,11 +111,7 @@ func getAPKKeys(resolver file.Resolver) (map[string]string, []file.Location, err
}
for _, location := range locations {
basename := path.Base(location.RealPath)
//nolint:gocritic
reader, err := resolver.FileContentsByLocation(location)
if err != nil {
return nil, nil, fmt.Errorf("unable to resolve file contents by location at %s: %w", location.RealPath, err)
}
content, err := io.ReadAll(reader)
if err != nil {
return nil, nil, fmt.Errorf("unable to read apk key content at %s: %w", location.RealPath, err)
+3 -3
View File
@@ -6,8 +6,7 @@ import (
"fmt"
"os"
"go.yaml.in/yaml/v3"
_ "modernc.org/sqlite" // required for rpmdb and other features
"gopkg.in/yaml.v3"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/cataloging"
@@ -23,7 +22,6 @@ const defaultImage = "alpine:3.19"
func main() {
// automagically get a source.Source for arbitrary string input
src := getSource(imageReference())
defer src.Close()
// will catalog the given source and return a SBOM keeping in mind several configurable options
sbom := getSBOM(src)
@@ -47,6 +45,7 @@ func getSource(input string) source.Source {
fmt.Println("detecting source type for input:", input, "...")
src, err := syft.GetSource(context.Background(), input, nil)
if err != nil {
panic(err)
}
@@ -125,4 +124,5 @@ func showAlpineConfiguration(s sbom.SBOM) {
panic(err)
}
fmt.Println(string(meta))
}
+1 -3
View File
@@ -5,8 +5,6 @@ import (
"fmt"
"os"
_ "modernc.org/sqlite" // required for rpmdb and other features
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/format"
"github.com/anchore/syft/syft/format/syftjson"
@@ -19,7 +17,6 @@ const defaultImage = "alpine:3.19"
func main() {
// automagically get a source.Source for arbitrary string input
src := getSource(imageReference())
defer src.Close()
// catalog the given source and return a SBOM
sbom := getSBOM(src)
@@ -41,6 +38,7 @@ func imageReference() string {
func getSource(input string) source.Source {
src, err := syft.GetSource(context.Background(), input, nil)
if err != nil {
panic(err)
}
+2 -3
View File
@@ -5,8 +5,6 @@ import (
"encoding/json"
"os"
_ "modernc.org/sqlite" // required for rpmdb and other features
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/cataloging"
"github.com/anchore/syft/syft/cataloging/pkgcataloging"
@@ -19,7 +17,6 @@ const defaultImage = "alpine:3.19"
func main() {
// automagically get a source.Source for arbitrary string input
src := getSource(imageReference())
defer src.Close()
// catalog the given source and return a SBOM
// let's explicitly use catalogers that are:
@@ -33,6 +30,7 @@ func main() {
if err := enc.Encode(sbom.Descriptor.Configuration); err != nil {
panic(err)
}
}
func imageReference() string {
@@ -45,6 +43,7 @@ func imageReference() string {
func getSource(input string) source.Source {
src, err := syft.GetSource(context.Background(), input, nil)
if err != nil {
panic(err)
}
-3
View File
@@ -4,8 +4,6 @@ import (
"context"
"os"
_ "modernc.org/sqlite" // required for rpmdb and other features
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/format/syftjson"
)
@@ -15,7 +13,6 @@ func main() {
image := "alpine:3.19"
src, _ := syft.GetSource(context.Background(), image, syft.DefaultGetSourceConfig().WithSources("registry"))
defer src.Close()
sbom, _ := syft.CreateSBOM(context.Background(), src, syft.DefaultCreateSBOMConfig())
+140 -223
View File
@@ -1,344 +1,261 @@
module github.com/anchore/syft
go 1.24.6
go 1.24.0
require (
github.com/BurntSushi/toml v1.6.0
github.com/CycloneDX/cyclonedx-go v0.9.3
github.com/Masterminds/semver/v3 v3.4.0
github.com/CycloneDX/cyclonedx-go v0.9.2
github.com/Masterminds/semver v1.5.0
github.com/Masterminds/sprig/v3 v3.3.0
github.com/OneOfOne/xxhash v1.2.8
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
github.com/acobaugh/osrelease v0.1.0
github.com/adrg/xdg v0.5.3
github.com/anchore/bubbly v0.0.0-20231115134915-def0aba654a9
github.com/anchore/clio v0.0.0-20250319180342-2cfe4b0cb716
github.com/anchore/fangs v0.0.0-20250319222917-446a1e748ec2
github.com/anchore/go-collections v0.0.0-20251016125210-a3c352120e8c
github.com/anchore/go-homedir v0.0.0-20250319154043-c29668562e4d
github.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722
github.com/anchore/clio v0.0.0-20241115144204-29e89f9fa837
github.com/anchore/fangs v0.0.0-20241014201141-b6e4b3469f10
github.com/anchore/go-collections v0.0.0-20240216171411-9321230ce537
github.com/anchore/go-logger v0.0.0-20230725134548-c21dafa1ec5a
github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb
github.com/anchore/go-rpmdb v0.0.0-20250516171929-f77691e1faec
github.com/anchore/go-sync v0.0.0-20250326131806-4eda43a485b6
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115
github.com/anchore/stereoscope v0.1.19
github.com/anchore/stereoscope v0.0.13
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
// we are hinting brotli to latest due to warning when installing archiver v3:
// go: warning: github.com/andybalholm/brotli@v1.0.1: retracted by module author: occasional panics and data corruption
github.com/aquasecurity/go-pep440-version v0.0.1
github.com/bitnami/go-version v0.0.0-20250131085805-b1f57a8634ef
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
github.com/bmatcuk/doublestar/v4 v4.9.2
github.com/charmbracelet/bubbles v0.21.0
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0
github.com/bmatcuk/doublestar/v4 v4.8.1
github.com/charmbracelet/bubbles v0.20.0
github.com/charmbracelet/bubbletea v1.2.4
github.com/charmbracelet/lipgloss v1.0.0
github.com/dave/jennifer v1.7.1
github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da
github.com/diskfs/go-diskfs v1.7.0
github.com/distribution/reference v0.6.0
github.com/docker/docker v28.0.0+incompatible
github.com/dustin/go-humanize v1.0.1
github.com/elliotchance/phpserialize v1.4.0
github.com/facebookincubator/nvdtools v0.1.5
github.com/github/go-spdx/v2 v2.3.6
github.com/gkampitakis/go-snaps v0.5.19
github.com/go-git/go-billy/v5 v5.7.0
github.com/go-git/go-git/v5 v5.16.4
github.com/github/go-spdx/v2 v2.3.2
github.com/gkampitakis/go-snaps v0.5.11
github.com/go-git/go-billy/v5 v5.6.2
github.com/go-git/go-git/v5 v5.13.2
github.com/go-test/deep v1.1.1
github.com/go-viper/mapstructure/v2 v2.5.0
github.com/gohugoio/hashstructure v0.6.0
github.com/google/go-cmp v0.7.0
github.com/google/go-containerregistry v0.20.7
github.com/google/go-cmp v0.6.0
github.com/google/go-containerregistry v0.20.3
github.com/google/licensecheck v0.3.1
github.com/google/uuid v1.6.0
github.com/gookit/color v1.6.0
github.com/hashicorp/go-cleanhttp v0.5.2
github.com/hashicorp/go-getter v1.8.4
github.com/gookit/color v1.5.4
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/hcl/v2 v2.24.0
github.com/iancoleman/strcase v0.3.0
github.com/invopop/jsonschema v0.13.0
github.com/jedib0t/go-pretty/v6 v6.7.8
github.com/invopop/jsonschema v0.7.0
github.com/jedib0t/go-pretty/v6 v6.6.6
github.com/jinzhu/copier v0.4.0
github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953
github.com/magiconair/properties v1.8.10
github.com/mholt/archives v0.1.5
github.com/knqyf263/go-rpmdb v0.1.1
github.com/microsoft/go-rustaudit v0.0.0-20220730194248-4b17361d90a5
github.com/mitchellh/go-homedir v1.1.0
github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/mitchellh/mapstructure v1.5.0
github.com/moby/sys/mountinfo v0.7.2
github.com/nix-community/go-nix v0.0.0-20250101154619-4bdde671e0a1
github.com/olekukonko/tablewriter v1.1.3
github.com/olekukonko/tablewriter v0.0.5
github.com/opencontainers/go-digest v1.0.0
github.com/pelletier/go-toml v1.9.5
github.com/quasilyte/go-ruleguard/dsl v0.3.23
github.com/rust-secure-code/go-rustaudit v0.0.0-20250226111315-e20ec32e963c
github.com/quasilyte/go-ruleguard/dsl v0.3.22
github.com/saferwall/pe v1.5.6
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d
github.com/sanity-io/litter v1.5.8
github.com/sassoftware/go-rpmutils v0.4.0
// pinned to pull in 386 arch fix: https://github.com/scylladb/go-set/commit/cc7b2070d91ebf40d233207b633e28f5bd8f03a5
github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e
github.com/sergi/go-diff v1.4.0
github.com/spdx/gordf v0.0.0-20201111095634-7098f93598fb
github.com/spdx/tools-golang v0.5.7
github.com/spf13/afero v1.15.0
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1
github.com/vbatts/go-mtree v0.7.0
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3
github.com/spdx/tools-golang v0.5.5
github.com/spf13/afero v1.12.0
github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.10.0
github.com/vbatts/go-mtree v0.5.4
github.com/vifraa/gopom v1.0.0
github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651
github.com/wagoodman/go-progress v0.0.0-20230925121702-07e42b3cdba0
github.com/xeipuuv/gojsonschema v1.2.0
github.com/zyedidia/generic v1.2.2-0.20230320175451-4410d2372cb1
go.uber.org/goleak v1.3.0
go.yaml.in/yaml/v3 v3.0.4
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546
golang.org/x/mod v0.32.0
golang.org/x/net v0.49.0
modernc.org/sqlite v1.44.3
golang.org/x/mod v0.23.0
golang.org/x/net v0.35.0
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.35.0
)
require google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect
require (
github.com/BurntSushi/toml v1.4.0
github.com/OneOfOne/xxhash v1.2.8
github.com/adrg/xdg v0.5.3
github.com/anchore/archiver/v3 v3.5.3-0.20241210171143-5b1d8d1c7c51
github.com/bitnami/go-version v0.0.0-20250131085805-b1f57a8634ef
github.com/hashicorp/hcl/v2 v2.23.0
github.com/magiconair/properties v1.8.9
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
)
require (
cloud.google.com/go v0.123.0 // indirect
cloud.google.com/go/auth v0.17.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
cloud.google.com/go/iam v1.5.3 // indirect
cloud.google.com/go/storage v1.58.0 // indirect
dario.cat/mergo v1.0.2 // indirect
dario.cat/mergo v1.0.1 // indirect
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0 // indirect
github.com/DataDog/zstd v1.5.5 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.3.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/Microsoft/hcsshim v0.14.0-rc.1 // indirect
github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/STARRY-S/zip v0.2.3 // indirect
github.com/Microsoft/hcsshim v0.11.7 // indirect
github.com/ProtonMail/go-crypto v1.1.5 // indirect
github.com/agext/levenshtein v1.2.1 // indirect; indirectt
github.com/anchore/go-lzo v0.1.0 // indirect
github.com/anchore/go-struct-converter v0.1.0 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/aquasecurity/go-version v0.0.1 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/becheran/wildmatch-go v1.0.0 // indirect
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
github.com/bodgit/plumbing v1.3.0 // indirect
github.com/bodgit/sevenzip v1.6.1 // indirect
github.com/bodgit/windows v1.0.1 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/harmonica v0.2.0 // indirect
github.com/charmbracelet/x/ansi v0.10.1 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
github.com/charmbracelet/x/ansi v0.4.5 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/containerd/containerd/api v1.10.0 // indirect
github.com/containerd/continuity v0.4.5 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/cloudflare/circl v1.3.8 // indirect
github.com/containerd/cgroups v1.1.0 // indirect
github.com/containerd/containerd v1.7.24 // indirect
github.com/containerd/containerd/api v1.7.19 // indirect
github.com/containerd/continuity v0.4.2 // indirect
github.com/containerd/errdefs 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.2 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.18.1 // indirect
github.com/containerd/ttrpc v1.2.7 // indirect
github.com/containerd/typeurl/v2 v2.2.3 // indirect
github.com/cyphar/filepath-securejoin v0.6.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect
github.com/containerd/ttrpc v1.2.5 // indirect
github.com/containerd/typeurl/v2 v2.1.1 // indirect
github.com/cyphar/filepath-securejoin v0.3.6 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/docker/cli v29.1.5+incompatible // indirect
github.com/docker/cli v27.5.0+incompatible // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker v28.5.2+incompatible // indirect
github.com/docker/docker-credential-helpers v0.9.4 // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/docker-credential-helpers v0.8.2 // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
github.com/edsrzf/mmap-go v1.1.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/felixge/fgprof v0.9.5 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/gkampitakis/ciinfo v0.3.2 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gkampitakis/ciinfo v0.3.1 // indirect
github.com/gkampitakis/go-diff v1.3.2 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-restruct/restruct v1.2.0-alpha // indirect
github.com/goccy/go-yaml v1.19.2
github.com/goccy/go-yaml v1.15.13 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-version v1.8.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/maruel/natural v1.1.1 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mikelolasagasti/xz v1.0.1 // indirect
github.com/minio/minlz v1.0.1 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/locker v1.0.1 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/moby/sys/signal v0.7.1 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect
github.com/moby/sys/signal v0.7.0 // indirect
github.com/moby/sys/user v0.3.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/opencontainers/runtime-spec v1.3.0 // indirect
github.com/opencontainers/selinux v1.13.1 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/nwaples/rardecode v1.1.3 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/opencontainers/runtime-spec v1.1.0 // indirect
github.com/opencontainers/selinux v1.11.0 // indirect
github.com/pborman/indent v1.2.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pkg/profile v1.7.0 // indirect
github.com/pkg/xattr v0.4.9 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sahilm/fuzzy v0.1.1 // indirect
github.com/secDre4mer/pkcs7 v0.0.0-20240322103146-665324a4461d // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
github.com/sorairolake/lzip-go v0.3.8 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/skeema/knownhosts v1.3.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/spf13/viper v1.20.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/spf13/cast v1.7.0 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/spf13/viper v1.19.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/sylabs/sif/v2 v2.22.0 // indirect
github.com/sylabs/squashfs v1.0.6 // indirect
github.com/sylabs/sif/v2 v2.20.2 // indirect
github.com/sylabs/squashfs v1.0.4 // indirect
github.com/therootcompany/xz v1.0.1 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/ulikunitz/xz v0.5.15
github.com/vbatts/tar-split v0.12.2 // indirect
github.com/ulikunitz/xz v0.5.12 // indirect
github.com/vbatts/tar-split v0.11.6 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/zclconf/go-cty v1.16.3 // indirect
github.com/zclconf/go-cty v1.13.0 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect
go.opentelemetry.io/otel v1.33.0 // indirect
go.opentelemetry.io/otel/metric v1.33.0 // indirect
go.opentelemetry.io/otel/trace v1.33.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/oauth2 v0.33.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/term v0.39.0 // indirect
golang.org/x/text v0.33.0 // indirect
golang.org/x/time v0.14.0
golang.org/x/tools v0.41.0
golang.org/x/crypto v0.33.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/term v0.29.0 // indirect
golang.org/x/text v0.22.0 // indirect
golang.org/x/tools v0.29.0 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
google.golang.org/api v0.256.0 // indirect
google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect
google.golang.org/grpc v1.76.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect
google.golang.org/grpc v1.67.3 // indirect
google.golang.org/protobuf v1.36.3 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1
modernc.org/libc v1.67.6 // indirect
modernc.org/libc v1.61.13 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
)
require (
github.com/cespare/xxhash/v2 v2.3.0
github.com/gpustack/gguf-parser-go v0.23.1
github.com/wk8/go-ordered-map/v2 v2.1.8
)
require (
cel.dev/expr v0.24.0 // indirect
cloud.google.com/go/monitoring v1.24.2 // indirect
cyphar.com/go-pathrs v0.2.1 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 // indirect
github.com/aws/aws-sdk-go-v2 v1.41.0 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect
github.com/aws/aws-sdk-go-v2/config v1.32.6 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.6 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 // indirect
github.com/aws/smithy-go v1.24.0 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/clipperhouse/displaywidth v0.6.2 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect
github.com/containerd/cgroups/v3 v3.1.2 // indirect
github.com/containerd/containerd/v2 v2.2.1 // indirect
github.com/containerd/plugin v1.0.0 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
github.com/go-jose/go-jose/v4 v4.1.2 // indirect
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.70 // indirect
github.com/henvic/httpretty v0.1.4 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/nwaples/rardecode/v2 v2.2.0 // indirect
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
github.com/olekukonko/errors v1.1.0 // indirect
github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/smallnest/ringbuffer v0.0.0-20241116012123-461381446e3d // indirect
github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect
github.com/zeebo/errs v1.4.0 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect
go.opentelemetry.io/otel/sdk v1.39.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect
gonum.org/v1/gonum v0.16.0 // indirect
modernc.org/memory v1.8.2 // indirect
)
retract (
v1.25.0 // published with a replace directive (confusing for API users)
v0.53.2
v0.53.1 // published accidentally with incorrect license in depdencies
v0.53.1 // Published accidentally with incorrect license in depdencies
)
+293 -479
View File
File diff suppressed because it is too large Load Diff
+2 -7
View File
@@ -6,8 +6,7 @@ PROJECT_NAME=syft
OWNER=anchore
REPO="${PROJECT_NAME}"
GITHUB_DOWNLOAD_PREFIX=https://github.com/${OWNER}/${REPO}/releases/download
INSTALL_SH_BASE_URL=https://get.anchore.io/${PROJECT_NAME}
LEGACY_INSTALL_SH_BASE_URL=https://raw.githubusercontent.com/${OWNER}/${PROJECT_NAME}
INSTALL_SH_BASE_URL=https://raw.githubusercontent.com/${OWNER}/${PROJECT_NAME}
PROGRAM_ARGS=$@
# signature verification options
@@ -837,11 +836,7 @@ EOF
if [ "${DOWNLOAD_TAG_INSTALL_SCRIPT}" = "true" ]; then
export DOWNLOAD_TAG_INSTALL_SCRIPT=false
log_info "fetching release script for tag='${tag}'"
if ! install_script=$(http_copy "${INSTALL_SH_BASE_URL}/${tag}/install.sh" ""); then
log_warn "failed to fetch from ${INSTALL_SH_BASE_URL}, trying fallback URL"
install_script=$(http_copy "${LEGACY_INSTALL_SH_BASE_URL}/${tag}/install.sh" "")
fi
echo "${install_script}" | sh -s -- ${PROGRAM_ARGS}
http_copy "${INSTALL_SH_BASE_URL}/${tag}/install.sh" "" | sh -s -- ${PROGRAM_ARGS}
exit $?
fi
+2 -32
View File
@@ -36,8 +36,8 @@ func Notify(message string) {
})
}
func StartCatalogerTask(info monitor.GenericTask, size int64, initialStage string) *monitor.TaskProgress {
t := &monitor.TaskProgress{
func StartCatalogerTask(info monitor.GenericTask, size int64, initialStage string) *monitor.CatalogerTaskProgress {
t := &monitor.CatalogerTaskProgress{
AtomicStage: progress.NewAtomicStage(initialStage),
Manual: progress.NewManual(size),
}
@@ -50,33 +50,3 @@ func StartCatalogerTask(info monitor.GenericTask, size int64, initialStage strin
return t
}
func StartPullSourceTask(info monitor.GenericTask, size int64, initialStage string) *monitor.TaskProgress {
t := &monitor.TaskProgress{
AtomicStage: progress.NewAtomicStage(initialStage),
Manual: progress.NewManual(size),
}
Publish(partybus.Event{
Type: event.PullSourceStarted,
Source: info,
Value: progress.StagedProgressable(t),
})
return t
}
func StartIndexingFiles(path string) *monitor.TaskProgress {
t := &monitor.TaskProgress{
AtomicStage: progress.NewAtomicStage(""),
Manual: progress.NewManual(-1),
}
Publish(partybus.Event{
Type: event.FileIndexingStarted,
Source: path,
Value: progress.StagedProgressable(t),
})
return t
}
+1 -1
View File
@@ -18,7 +18,7 @@ based on the _structure_ of the provided type.
If the structure changes in any way it will end up with a new version key and all will re populate this new key,
ignoring cached values from older, different versions.
The resolver will store items using the `json` package to serialize/deserialize values, so to save space
it is encouraged to use `omitempty`. For example:
it is encouraged to use `omitempty`. For example:
```go
type myCacheItem struct {
+2 -2
View File
@@ -4,7 +4,7 @@ import (
"fmt"
"reflect"
"github.com/gohugoio/hashstructure"
"github.com/mitchellh/hashstructure/v2"
)
// hashType returns a stable hash based on the structure of the type
@@ -12,7 +12,7 @@ func hashType[T any]() string {
// get the base type and hash an empty instance
var t T
empty := emptyValue(reflect.TypeOf(t)).Interface()
hash, err := hashstructure.Hash(empty, &hashstructure.HashOptions{
hash, err := hashstructure.Hash(empty, hashstructure.FormatV2, &hashstructure.HashOptions{
ZeroNil: false,
IgnoreZeroValue: false,
SlicesAsSets: false,
+2 -2
View File
@@ -4,7 +4,7 @@ import (
"fmt"
"testing"
"github.com/gohugoio/hashstructure"
"github.com/mitchellh/hashstructure/v2"
"github.com/stretchr/testify/require"
)
@@ -99,7 +99,7 @@ func Test_hashType(t *testing.T) {
func Test_hashIgnores(t *testing.T) {
hash := func(v any) string {
v, err := hashstructure.Hash(v, &hashstructure.HashOptions{})
v, err := hashstructure.Hash(v, hashstructure.FormatV2, &hashstructure.HashOptions{})
require.NoError(t, err)
return fmt.Sprintf("%x", v)
}
-227
View File
@@ -1,227 +0,0 @@
# Cataloger Capabilities Documentation
This documentation describes the format and structure of cataloger capabilities YAML files.
## File Organization
Capabilities are organized as follows:
- **Cataloger capabilities**: Located in `syft/pkg/cataloger/*/capabilities.yaml` (one file per ecosystem, alongside the cataloger source code: `golang/capabilities.yaml`, `python/capabilities.yaml`, etc.)
- **Application configuration**: Located in `internal/capabilities/appconfig.yaml`
Each `capabilities.yaml` file is partially auto-generated. Run `go generate ./internal/capabilities` to regenerate.
- Fields marked **AUTO-GENERATED** will be updated during regeneration
- All **capabilities** sections are **MANUAL** - edit these to describe cataloger behavior
## Capability Sections
There are two types of capability sections depending on cataloger type:
### 1. Generic Catalogers (`type: generic`)
- Have capabilities at the **PARSER level**
- Each parser function has its own capabilities section
- Allows different parsers within the same cataloger to have different capabilities
### 2. Custom Catalogers (`type: custom`)
- Have capabilities at the **CATALOGER level**
- Single capabilities section for the entire cataloger
## Capabilities Format
Capabilities use a field-based format with defaults and optional conditional overrides:
```yaml
capabilities:
- field: <field-name> # dot-notation path (e.g., "license", "dependency.depth")
default: <value> # value when no conditions match
conditions: # optional - conditional overrides evaluated in order
- when: {ConfigField: val} # when these config fields match (AND logic)
value: <override-value> # use this value instead
comment: "explanation" # optional - why this condition exists
evidence: # optional - source code references
- "StructName.FieldName"
comment: "explanation" # optional - general field explanation
```
## Detector Conditions
Detectors (used by custom catalogers) can have optional conditions that control when they are active. This allows a single cataloger to have different detection behavior based on configuration.
### Structure
```yaml
detectors:
- method: glob # AUTO-GENERATED - detection method
criteria: ["**/*.jar"] # AUTO-GENERATED - patterns to match
comment: "always active" # MANUAL - optional explanation
- method: glob
criteria: ["**/*.zip"]
conditions: # MANUAL - when this detector is active
- when: {IncludeZipFiles: true} # config fields that must match
comment: "optional explanation"
comment: "ZIP detection requires config"
```
### Notes
- Conditions reference fields from the cataloger's config struct
- Multiple conditions in the array use **OR logic** (any condition can activate)
- Multiple fields in a `when` clause use **AND logic** (all must match)
- Detectors without conditions are always active
- Only custom catalogers support detectors with conditions
## Condition Evaluation
- Conditions are evaluated in array order (first match wins)
- Multiple fields in a `when` clause use **AND logic** (all must match)
- Multiple conditions in the array use **OR logic** (first matching condition)
- If no conditions match, the default value is used
## Capability Fields
Standard capability field names and their value types:
### `license` (boolean)
Whether license information is available.
**Examples:**
```yaml
default: true # always available
default: false # never available
default: false # requires configuration
conditions:
- when: {SearchRemoteLicenses: true}
value: true
```
### `dependency.depth` (array of strings)
Which dependency depths can be discovered.
**Values:** `direct` (immediate deps), `indirect` (transitive deps)
**Examples:**
```yaml
default: [direct] # only immediate dependencies
default: [direct, indirect] # full transitive closure
default: [] # no dependency information
```
### `dependency.edges` (string)
Relationships between nodes and completeness of the dependency graph.
**Values:**
- `""` - dependencies found but no edges between them
- `"flat"` - single level of dependencies with edges to root package only
- `"reduced"` - transitive reduction (redundant edges removed)
- `"complete"` - all relationships with accurate direct and indirect edges
**Examples:**
```yaml
default: complete
default: ""
```
### `dependency.kinds` (array of strings)
Types of dependencies that can be discovered.
**Values:** `runtime`, `dev`, `build`, `test`, `optional`
**Examples:**
```yaml
default: [runtime] # production dependencies only
default: [runtime, dev] # production and development
default: [runtime, dev, build] # all dependency types
default: [runtime] # with conditional dev deps
conditions:
- when: {IncludeDevDeps: true}
value: [runtime, dev]
```
### `package_manager.files.listing` (boolean)
Whether file listings are available (which files belong to the package).
**Examples:**
```yaml
default: true
default: false
conditions:
- when: {CaptureOwnedFiles: true}
value: true
```
### `package_manager.files.digests` (boolean)
Whether file digests/checksums are included in listings.
**Examples:**
```yaml
default: true
default: false
```
### `package_manager.package_integrity_hash` (boolean)
Whether a hash for verifying package integrity is available.
**Examples:**
```yaml
default: true
default: false
```
## Examples
### Simple cataloger with no configuration
```yaml
capabilities:
- name: license
default: true
comment: "license field always present in package.json"
- name: dependency.depth
default: [direct]
- name: dependency.edges
default: ""
- name: dependency.kinds
default: [runtime]
comment: "devDependencies not parsed by this cataloger"
- name: package_manager.files.listing
default: false
- name: package_manager.files.digests
default: false
- name: package_manager.package_integrity_hash
default: false
```
### Cataloger with configuration-dependent capabilities
```yaml
capabilities:
- name: license
default: false
conditions:
- when: {SearchLocalModCacheLicenses: true}
value: true
comment: "searches for licenses in GOPATH mod cache"
- when: {SearchRemoteLicenses: true}
value: true
comment: "fetches licenses from proxy.golang.org"
comment: "license scanning requires configuration"
- name: dependency.depth
default: [direct, indirect]
- name: dependency.edges
default: flat
- name: dependency.kinds
default: [runtime, dev]
- name: package_manager.files.listing
default: false
- name: package_manager.files.digests
default: false
- name: package_manager.package_integrity_hash
default: true
evidence:
- "GolangBinaryBuildinfoEntry.H1Digest"
```
-64
View File
@@ -1,64 +0,0 @@
# Application-level configuration. See README.md for documentation.
# This file is partially auto-generated. Run 'go generate ./internal/capabilities' to regenerate.
application: # AUTO-GENERATED - application-level config keys
- 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
description: only keep dep.json packages which an executable on disk is found. The package is also included if a DLL is found for any child package, even if the package itself does not have a DLL.
- key: dotnet.exclude-project-references
description: exclude packages with type "project" from deps.json output (these are internal project references, not NuGet packages)
- key: dotnet.propagate-dll-claims-to-parents
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.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
description: specify an explicit go vendor directory, if unset this defaults to ./vendor
- key: golang.main-module-version.from-build-settings
description: 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
- key: golang.main-module-version.from-contents
description: search for semver-like strings in the binary contents
- key: golang.main-module-version.from-ld-flags
description: look for LD flags that appear to be setting a version (e.g. -X main.version=1.0.0)
- key: golang.no-proxy
description: specifies packages which should not be fetched by proxy if unset this defaults to $GONOPROXY
- key: golang.proxy
description: remote proxy to use when retrieving go packages from the network, if unset this defaults to $GOPROXY followed by https://proxy.golang.org
- key: golang.search-local-mod-cache-licenses
description: search for go package licences in the GOPATH of the system running Syft, note that this is outside the container filesystem and potentially outside the root of a local directory scan
- key: golang.search-local-vendor-licenses
description: search for go package licences in the vendor folder on the system running Syft, note that this is outside the container filesystem and potentially outside the root of a local directory scan
- key: golang.search-remote-licenses
description: search for go package licences by retrieving the package from a network proxy
- key: golang.use-packages-lib
description: 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
- key: java.maven-local-repository-dir
description: override the default location of the local Maven repository. the default is the subdirectory '.m2/repository' in your home directory
- key: java.maven-url
description: maven repository to use, defaults to Maven central
- key: java.max-parent-recursive-depth
description: depth to recursively resolve parent POMs, no limit if <= 0
- key: java.resolve-transitive-dependencies
description: resolve transient dependencies such as those defined in a dependency's POM on Maven central
- key: java.use-maven-local-repository
description: 'use the local Maven repository to retrieve pom files. When Maven is installed and was previously used for building the software that is being scanned, then most pom files will be available in this repository on the local file system. this greatly speeds up scans. when all pom files are available in the local repository, then ''use-network'' is not needed. TIP: If you want to download all required pom files to the local repository without running a full build, run ''mvn help:effective-pom'' before performing the scan with syft.'
- key: java.use-network
description: enables Syft to use the network to fetch version and license information for packages when a parent or imported pom file is not found in the local maven repository. the pom files are downloaded from the remote Maven repository at 'maven-url'
- key: javascript.include-dev-dependencies
description: include development-scoped dependencies
- key: javascript.npm-base-url
description: base NPM url to use
- key: javascript.search-remote-licenses
description: enables Syft to use the network to fill in more detailed license information
- key: linux-kernel.catalog-modules
description: whether to catalog linux kernel modules found within lib/modules/** directories
- key: nix.capture-owned-files
description: enumerate all files owned by packages found within Nix store paths
- key: python.guess-unpinned-requirements
description: when running across entries in requirements.txt that do not specify a specific version (e.g. "sqlalchemy >= 1.0.0, <= 2.0.0, != 3.0.0, <= 3.0.0"), attempt to guess what the version could be based on the version requirements specified (e.g. "1.0.0"). When enabled the lowest expressible version when given an arbitrary constraint will be used (even if that version may not be available/published).
- key: python.pypi-base-url
description: base Pypi url to use
- key: python.search-remote-licenses
description: enables Syft to use the network to fill in more detailed license information
-132
View File
@@ -1,132 +0,0 @@
// Package capabilities provides discovery and tracking of cataloger capabilities.
package capabilities
import (
"embed"
"fmt"
"io/fs"
"path/filepath"
"sort"
"github.com/scylladb/go-set/strset"
"gopkg.in/yaml.v3"
"github.com/anchore/syft/internal/task"
)
//go:generate go run ./generate
//go:embed appconfig.yaml
var appconfigYAML []byte
var catalogerFiles *embed.FS
func RegisterCatalogerFiles(f embed.FS) {
catalogerFiles = &f
}
// LoadDocument loads and returns the complete document including configs and app-configs
func LoadDocument() (*Document, error) {
if catalogerFiles == nil {
return nil, fmt.Errorf("cataloger files not registered")
}
// parse application config
var appDoc struct {
Application []ApplicationConfigField `yaml:"application"`
}
if err := yaml.Unmarshal(appconfigYAML, &appDoc); err != nil {
return nil, fmt.Errorf("failed to parse appconfig.yaml: %w", err)
}
// walk the embedded filesystem to find all cataloger capabilities.yaml files
var catalogersDoc Document
catalogersDoc.ApplicationConfig = appDoc.Application
catalogersDoc.Configs = make(map[string]CatalogerConfigEntry)
err := fs.WalkDir(catalogerFiles, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
// skip non-yaml files and directories
if d.IsDir() || filepath.Ext(path) != ".yaml" || path == "." {
return nil
}
// read the file
data, err := catalogerFiles.ReadFile(path)
if err != nil {
return fmt.Errorf("failed to read %s: %w", path, err)
}
// parse the file
var capDoc struct {
Configs map[string]CatalogerConfigEntry `yaml:"configs"`
Catalogers []CatalogerEntry `yaml:"catalogers"`
}
if err := yaml.Unmarshal(data, &capDoc); err != nil {
return fmt.Errorf("failed to parse %s: %w", path, err)
}
// merge configs
for k, v := range capDoc.Configs {
catalogersDoc.Configs[k] = v
}
// merge catalogers
catalogersDoc.Catalogers = append(catalogersDoc.Catalogers, capDoc.Catalogers...)
return nil
})
if err != nil {
return nil, fmt.Errorf("failed to walk cataloger capabilities: %w", err)
}
// sort catalogers by name for consistency
sort.Slice(catalogersDoc.Catalogers, func(i, j int) bool {
return catalogersDoc.Catalogers[i].Name < catalogersDoc.Catalogers[j].Name
})
return &catalogersDoc, nil
}
// Packages loads and returns all cataloger capabilities from the embedded YAML file
func Packages() ([]CatalogerEntry, error) {
doc, err := LoadDocument()
if err != nil {
return nil, err
}
return doc.Catalogers, nil
}
// CatalogerInfo represents a cataloger's name and selection tags
type CatalogerInfo struct {
Name string
Selectors []string // tags for cataloger name selection
}
// ExtractCatalogerInfo extracts cataloger names and their selection tags from tasks
func ExtractCatalogerInfo(tasks []task.Task) []CatalogerInfo {
var infos []CatalogerInfo
for _, tsk := range tasks {
var selectors []string
name := tsk.Name()
if s, ok := tsk.(task.Selector); ok {
set := strset.New(s.Selectors()...)
set.Remove(name)
selectors = set.List()
sort.Strings(selectors)
}
infos = append(infos, CatalogerInfo{
Name: name,
Selectors: selectors,
})
}
return infos
}

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