Compare commits

..
Author SHA1 Message Date
Alex Goodman 677f4c0110 remove workflows that update the code
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-03-27 12:24:02 -04:00
898 changed files with 7476 additions and 86901 deletions
+99 -9
View File
@@ -1,10 +1,52 @@
# only pull in version updates that were released more than a week ago (low-pass filter for quickly-retracted releases)
cooldown: 7d
# Most tools (binny, chronicle, cosign, golangci-lint, goreleaser, gosimports,
# bouncer, quill, syft, task, gh) are inherited from anchore/go-make's embedded
# .binny.yaml — see https://github.com/anchore/go-make. Only syft-specific tools
# or version overrides should live here.
tools:
# we want to use a pinned version of binny to manage the toolchain (so binny manages itself!)
- name: binny
version:
want: v0.12.0
method: github-release
with:
repo: anchore/binny
# used to produce SBOMs during release
- name: syft
version:
want: latest
method: github-release
with:
repo: anchore/syft
# used to sign mac binaries at release
- name: quill
version:
want: v0.7.1
method: github-release
with:
repo: anchore/quill
# used for linting
- name: golangci-lint
version:
want: v2.11.4
method: github-release
with:
repo: golangci/golangci-lint
# used for showing the changelog at release
- name: glow
version:
want: v2.1.1
method: github-release
with:
repo: charmbracelet/glow
# used for signing the checksums file at release
- name: cosign
version:
want: v3.0.5
method: github-release
with:
repo: sigstore/cosign
# used in integration tests to verify JSON schemas
- name: yajsv
version:
@@ -13,18 +55,66 @@ tools:
with:
repo: neilpa/yajsv
# used to release all artifacts
- name: goreleaser
version:
want: v2.14.3
method: github-release
with:
repo: goreleaser/goreleaser
# used for organizing imports during static analysis
- name: gosimports
version:
want: v0.3.8
method: github-release
with:
repo: rinchsan/gosimports
# used at release to generate the changelog
- name: chronicle
version:
want: v0.8.0
method: github-release
with:
repo: anchore/chronicle
# used during static analysis for license compliance
- name: bouncer
version:
want: v0.4.0
method: github-release
with:
repo: wagoodman/go-bouncer
# used for running all local and CI tasks
- name: task
version:
want: v3.49.1
method: github-release
with:
repo: go-task/task
# used for triggering a release
- name: gh
version:
want: v2.88.1
method: github-release
with:
repo: cli/cli
# used to upload test fixture cache
- name: oras
version:
want: v1.3.4
want: v1.3.1
method: github-release
with:
repo: oras-project/oras
# used to parse JSON/YAML annotations on the fixture cache image
# used to upload test fixture cache
- name: yq
version:
want: v4.53.6
want: v4.52.4
method: github-release
with:
repo: mikefarah/yq
-7
View File
@@ -1,7 +0,0 @@
# check out text files with lf on every platform (regardless of core.autocrlf), letting git detect binary files
* text=auto eol=lf
# fixtures that must keep their crlf line endings byte for byte
syft/linux/testdata/unprintable -text
syft/pkg/cataloger/bitnami/testdata/no-rel/opt/bitnami/redis/.spdx-redis.spdx -text
syft/pkg/cataloger/java/testdata/jar-metadata/**/META-INF/MANIFEST.MF -text
+38 -41
View File
@@ -1,27 +1,26 @@
name: "Bootstrap"
description: "Bootstrap all syft tools and dependencies on top of go-make's setup action"
# This action is a thin wrapper around anchore/go-make/.github/actions/setup which
# already handles checkout, setup-go, restore-only build/mod cache, and tool cache.
# We add the syft-specific extras here: apt packages and the test fixture cache.
description: "Bootstrap all tools and dependencies"
inputs:
go-version:
description: "Go version to install (passed to go-make/setup)"
description: "Go version to install"
required: true
default: "1.26.2"
cache-key-prefix:
description: "Prefix all cache keys with this value (passed to go-make/setup)"
required: true
default: "v1"
cache-enabled:
description: "Enable build/mod and tool caching (passed to go-make/setup)"
default: "1.26.x"
go-dependencies:
description: "Download go dependencies"
required: true
default: "true"
cache-key-prefix:
description: "Prefix all cache keys with this value"
required: true
default: "53ac821810"
download-test-fixture-cache:
description: "Download test fixture cache from OCI and github actions"
required: true
default: "false"
tools:
description: "whether to install tools"
default: "true"
bootstrap-apt-packages:
description: "Space delimited list of tools to install via apt"
default: "libxml2-utils"
@@ -29,16 +28,33 @@ inputs:
runs:
using: "composite"
steps:
- name: Setup go + go-make tooling
uses: anchore/go-make/.github/actions/setup@fa5421b3bf24c9d18f11a736c5eb4172f2b7d683 # v0.8.1
# note: go mod and build is automatically cached on default with v4+
- uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
if: inputs.go-version != ''
with:
go-version: ${{ inputs.go-version }}
cache-key-prefix: ${{ inputs.cache-key-prefix }}
cache-enabled: ${{ inputs.cache-enabled }}
check-latest: true
- name: Install binny-managed tools
- name: Restore tool cache
if: inputs.tools == 'true'
id: tool-cache
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: ${{ github.workspace }}/.tool
key: ${{ inputs.cache-key-prefix }}-${{ runner.os }}-tool-${{ hashFiles('.binny.yaml') }}
- name: Install project tools
shell: bash
run: make binny:install
if: inputs.tools == 'true'
run: |
make tools
.tool/binny list
.tool/binny check
- name: Install go dependencies
if: inputs.go-dependencies == 'true'
shell: bash
run: make ci-bootstrap-go
- name: Install apt packages
if: inputs.bootstrap-apt-packages != ''
@@ -47,30 +63,11 @@ runs:
APT_PACKAGES: ${{ inputs.bootstrap-apt-packages }}
run: |
IFS=' ' read -ra packages <<< "$APT_PACKAGES"
# the AMI already ships most of these, and `apt update` costs ~40MB of index (minutes,
# when archive.ubuntu.com throttles us) to find that out. only touch apt if we must.
missing=()
for pkg in "${packages[@]}"; do
dpkg -s "$pkg" >/dev/null 2>&1 || missing+=("$pkg")
done
if [ ${#missing[@]} -eq 0 ]; then
echo "all apt packages already present: ${packages[*]}"
exit 0
fi
DEBIAN_FRONTEND=noninteractive sudo apt update && sudo -E apt install -y "${missing[@]}"
DEBIAN_FRONTEND=noninteractive sudo apt update && sudo -E apt install -y "${packages[@]}"
# ORAS cache: restore-only on non-default branches / forks
- name: Restore ORAS cache from github actions
if: ${{ inputs.download-test-fixture-cache == 'true' && (github.ref != format('refs/heads/{0}', github.event.repository.default_branch) || github.event.repository.fork == true) }}
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ github.workspace }}/.tmp/oras-cache
key: ${{ inputs.cache-key-prefix }}-oras-cache
# ORAS cache: restore + save on the default branch of the canonical repo only.
- name: Restore and save ORAS cache from github actions
if: ${{ inputs.download-test-fixture-cache == 'true' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && github.event.repository.fork == false }}
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
if: inputs.download-test-fixture-cache == 'true'
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: ${{ github.workspace }}/.tmp/oras-cache
key: ${{ inputs.cache-key-prefix }}-oras-cache
+27 -7
View File
@@ -1,13 +1,20 @@
# 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
directories:
- "/"
- "/.make"
directory: "/"
cooldown:
default-days: 7
schedule:
@@ -21,14 +28,19 @@ updates:
- dependency-name: "github.com/aquasecurity/go-version"
- dependency-name: "github.com/knqyf263/go-apk-version"
- dependency-name: "github.com/knqyf263/go-deb-version"
# v1 changed SlicesAsSets hashing, which changes every artifact ID syft produces
- dependency-name: "github.com/gohugoio/hashstructure"
update-types: ["version-update:semver-major"]
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/*"
- "/.github/actions/bootstrap"
cooldown:
default-days: 7
schedule:
@@ -37,3 +49,11 @@ updates:
open-pull-requests-limit: 10
labels:
- "dependencies"
groups:
actions-minor-patch:
applies-to: version-updates # security updates get individual PRs
patterns:
- "*"
update-types: # major omitted, gets individual PRs
- "minor"
- "patch"
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
red=$(tput setaf 1)
bold=$(tput bold)
normal=$(tput sgr0)
# assert we are running in CI (or die!)
if [[ -z "$CI" ]]; then
echo "${bold}${red}This step should ONLY be run in CI. Exiting...${normal}"
exit 1
fi
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env python3
import subprocess
import sys
import shlex
class bcolors:
HEADER = '\033[95m'
OKBLUE = '\033[94m'
OKCYAN = '\033[96m'
OKGREEN = '\033[92m'
WARNING = '\033[93m'
FAIL = '\033[91m'
ENDC = '\033[0m'
BOLD = '\033[1m'
UNDERLINE = '\033[4m'
if len(sys.argv) < 3:
print("Usage: coverage.py [threshold] [go-coverage-report]")
sys.exit(1)
threshold = float(sys.argv[1])
report = sys.argv[2]
args = shlex.split(f"go tool cover -func {report}")
p = subprocess.run(args, capture_output=True, text=True)
percent_coverage = float(p.stdout.splitlines()[-1].split()[-1].replace("%", ""))
print(f"{bcolors.BOLD}Coverage: {percent_coverage}%{bcolors.ENDC}")
if percent_coverage < threshold:
print(f"{bcolors.BOLD}{bcolors.FAIL}Coverage below threshold of {threshold}%{bcolors.ENDC}")
sys.exit(1)
+34 -51
View File
@@ -1,45 +1,35 @@
#!/usr/bin/env python3
from __future__ import annotations
import glob
import hashlib
import json
import os
import glob
import sys
import json
import hashlib
IGNORED_PREFIXES = []
def find_fingerprints_and_check_dirs(base_dir):
all_fingerprints = set(
glob.glob(
os.path.join(base_dir, "**", "test*", "**", "*.fingerprint"), recursive=True
)
)
all_fingerprints = set(glob.glob(os.path.join(base_dir, '**', 'test*', '**', '*.fingerprint'), recursive=True))
all_fingerprints = {
os.path.relpath(fp)
for fp in all_fingerprints
if not any(fp.startswith(prefix) for prefix in IGNORED_PREFIXES)
}
all_fingerprints = {os.path.relpath(fp) for fp in all_fingerprints
if not any(fp.startswith(prefix) for prefix in IGNORED_PREFIXES)}
if not all_fingerprints:
show("No .fingerprint files or cache directories found.")
exit(1)
orphan_fingerprints = []
empty_content = []
missing_content = []
valid_paths = set()
fingerprint_contents = []
for fingerprint in all_fingerprints:
path = fingerprint.replace(".fingerprint", "")
path = fingerprint.replace('.fingerprint', '')
if not os.path.exists(path):
# paired content path is entirely missing — the .fingerprint is likely
# leftover from a moved/deleted source (testdata trees are git-ignored,
# so they persist locally across rename refactors)
orphan_fingerprints.append(fingerprint)
missing_content.append(path)
continue
if not os.path.isdir(path):
@@ -49,13 +39,13 @@ def find_fingerprints_and_check_dirs(base_dir):
if os.listdir(path):
valid_paths.add(path)
else:
empty_content.append(path)
missing_content.append(path)
with open(fingerprint, "r") as f:
with open(fingerprint, 'r') as f:
content = f.read().strip()
fingerprint_contents.append((fingerprint, content))
return sorted(valid_paths), empty_content, orphan_fingerprints, fingerprint_contents
return sorted(valid_paths), missing_content, fingerprint_contents
def parse_fingerprint_contents(fingerprint_content):
@@ -69,9 +59,7 @@ def parse_fingerprint_contents(fingerprint_content):
def calculate_sha256(fingerprint_contents):
sorted_fingerprint_contents = sorted(fingerprint_contents, key=lambda x: x[0])
concatenated_contents = "".join(
content for _, content in sorted_fingerprint_contents
)
concatenated_contents = ''.join(content for _, content in sorted_fingerprint_contents)
sha256_hash = hashlib.sha256(concatenated_contents.encode()).hexdigest()
@@ -80,7 +68,7 @@ def calculate_sha256(fingerprint_contents):
def calculate_file_sha256(file_path):
sha256_hash = hashlib.sha256()
with open(file_path, "rb") as f:
with open(file_path, 'rb') as f:
for byte_block in iter(lambda: f.read(4096), b""):
sha256_hash.update(byte_block)
return sha256_hash.hexdigest()
@@ -91,28 +79,17 @@ def show(*s: str):
def main(file_path: str | None):
base_dir = "."
valid_paths, empty_content, orphan_fingerprints, fingerprint_contents = (
find_fingerprints_and_check_dirs(base_dir)
)
base_dir = '.'
valid_paths, missing_content, fingerprint_contents = find_fingerprints_and_check_dirs(base_dir)
if empty_content:
show(
"The following paths exist but are empty, and have corresponding .fingerprint files:"
)
for path in sorted(empty_content):
if missing_content:
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.
if orphan_fingerprints:
show(
"The following .fingerprint files reference paths that no longer exist "
"(likely leftover from a moved/deleted cataloger — safe to delete, "
"or run `task prune-orphan-fingerprints`):"
)
for fp in sorted(orphan_fingerprints):
show(f"- {fp}")
# show("Please ensure these paths exist and have content if they are directories.")
# exit(1)
sha256_hash = calculate_sha256(fingerprint_contents)
@@ -124,24 +101,30 @@ def main(file_path: str | None):
file_digest = calculate_file_sha256(fingerprint_file)
# Parse the fingerprint file to get the digest/path tuples
with open(fingerprint_file, "r") as f:
with open(fingerprint_file, 'r') as f:
fingerprint_content = f.read().strip()
input_map = parse_fingerprint_contents(fingerprint_content)
paths_with_digests.append(
{"path": path, "digest": file_digest, "input": input_map}
)
paths_with_digests.append({
"path": path,
"digest": file_digest,
"input": input_map
})
except Exception as e:
show(f"Error processing {fingerprint_file}: {e}")
raise e
output = {"digest": sha256_hash, "paths": paths_with_digests}
output = {
"digest": sha256_hash,
"paths": paths_with_digests
}
content = json.dumps(output, indent=2, sort_keys=True)
if file_path:
with open(file_path, "w") as f:
with open(file_path, 'w') as f:
f.write(content)
print(content)
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
set -eu
ORIGINAL_STATE_DIR=$(mktemp -d "TEMP-original-state-XXXXXXXXX")
TIDY_STATE_DIR=$(mktemp -d "TEMP-tidy-state-XXXXXXXXX")
trap "cp -p ${ORIGINAL_STATE_DIR}/* ./ && git update-index -q --refresh && rm -fR ${ORIGINAL_STATE_DIR} ${TIDY_STATE_DIR}" EXIT
# capturing original state of files...
cp go.mod go.sum "${ORIGINAL_STATE_DIR}"
# capturing state of go.mod and go.sum after running go mod tidy...
go mod tidy
cp go.mod go.sum "${TIDY_STATE_DIR}"
set +e
# detect difference between the git HEAD state and the go mod tidy state
DIFF_MOD=$(diff -u "${ORIGINAL_STATE_DIR}/go.mod" "${TIDY_STATE_DIR}/go.mod")
DIFF_SUM=$(diff -u "${ORIGINAL_STATE_DIR}/go.sum" "${TIDY_STATE_DIR}/go.sum")
if [[ -n "${DIFF_MOD}" || -n "${DIFF_SUM}" ]]; then
echo "go.mod diff:"
echo "${DIFF_MOD}"
echo "go.sum diff:"
echo "${DIFF_SUM}"
echo ""
printf "FAILED! go.mod and/or go.sum are NOT tidy; please run 'go mod tidy'.\n\n"
exit 1
fi
@@ -1,102 +0,0 @@
#!/usr/bin/env python3
"""
Remove orphan *.fingerprint files left behind by moved/deleted catalogers.
A fingerprint is considered orphaned when:
1. its paired content path (the fingerprint path with `.fingerprint` stripped)
does not exist, AND
2. the nearest ancestor `testdata/` directory has no `Makefile` claiming
responsibility for generating that path.
The second condition is the safety check: if there is a Makefile, the
fingerprint is "live" and might just be waiting for fixtures to be built —
leave it alone. Without a Makefile, nothing in-repo will ever regenerate
the content, so the fingerprint is dead weight that triggers spurious
"missing path" warnings.
Empty parent directories are also pruned after removing the fingerprint.
Use --dry-run to preview without deleting.
"""
from __future__ import annotations
import argparse
import glob
import os
import sys
def find_ancestor_testdata(path: str) -> str | None:
d = os.path.dirname(path)
while d and d not in (".", os.sep):
if os.path.basename(d) == "testdata":
return d
d = os.path.dirname(d)
return None
def is_orphan(fingerprint: str) -> bool:
paired = fingerprint[: -len(".fingerprint")]
if os.path.exists(paired):
return False
testdata_dir = find_ancestor_testdata(fingerprint)
if testdata_dir and os.path.isfile(os.path.join(testdata_dir, "Makefile")):
# a Makefile exists that may regenerate this — not safe to prune
return False
return True
def prune_empty_parents(start: str, stop_at: str = ".") -> list[str]:
removed = []
d = os.path.dirname(start)
stop_at = os.path.abspath(stop_at)
while d and os.path.abspath(d) != stop_at:
try:
if not os.listdir(d):
os.rmdir(d)
removed.append(d)
d = os.path.dirname(d)
else:
break
except OSError:
break
return removed
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--dry-run",
action="store_true",
help="Show what would be removed without deleting anything",
)
args = parser.parse_args()
all_fingerprints = glob.glob("**/test*/**/*.fingerprint", recursive=True)
orphans = sorted(fp for fp in all_fingerprints if is_orphan(fp))
if not orphans:
print("no orphan fingerprints found")
return 0
verb = "would remove" if args.dry_run else "removing"
print(f"{verb} {len(orphans)} orphan fingerprint(s):")
for fp in orphans:
print(f"- {fp}")
if args.dry_run:
continue
try:
os.remove(fp)
except OSError as e:
print(f" ! failed to remove: {e}", file=sys.stderr)
continue
for d in prune_empty_parents(fp):
print(f" (also removed empty dir {d})")
return 0
if __name__ == "__main__":
sys.exit(main())
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
set -eu
bold=$(tput bold)
normal=$(tput sgr0)
GH_CLI=.tool/gh
if ! [ -x "$(command -v $GH_CLI)" ]; then
echo "The GitHub CLI could not be found. run: make bootstrap"
exit 1
fi
$GH_CLI auth status
# set the default repo in cases where multiple remotes are defined
$GH_CLI repo set-default anchore/syft
export GITHUB_TOKEN="${GITHUB_TOKEN-"$($GH_CLI auth token)"}"
# we need all of the git state to determine the next version. Since tagging is done by
# the release pipeline it is possible to not have all of the tags from previous releases.
git fetch --tags
# populates the CHANGELOG.md and VERSION files
echo "${bold}Generating changelog...${normal}"
make changelog 2> /dev/null
NEXT_VERSION=$(cat VERSION)
if [[ "$NEXT_VERSION" == "" || "${NEXT_VERSION}" == "(Unreleased)" ]]; then
echo "Could not determine the next version to release. Exiting..."
exit 1
fi
while true; do
read -p "${bold}Do you want to trigger a release for version '${NEXT_VERSION}'?${normal} [y/n] " yn
case $yn in
[Yy]* ) echo; break;;
[Nn]* ) echo; echo "Cancelling release..."; exit;;
* ) echo "Please answer yes or no.";;
esac
done
echo "${bold}Kicking off release for ${NEXT_VERSION}${normal}..."
echo
$GH_CLI workflow run release.yaml -f version=${NEXT_VERSION}
echo
echo "${bold}Waiting for release to start...${normal}"
sleep 10
set +e
echo "${bold}Head to the release workflow to monitor the release:${normal} $($GH_CLI run list --workflow=release.yaml --limit=1 --json url --jq '.[].url')"
id=$($GH_CLI run list --workflow=release.yaml --limit=1 --json databaseId --jq '.[].databaseId')
$GH_CLI run watch $id --exit-status || (echo ; echo "${bold}Logs of failed step:${normal}" && GH_PAGER="" $GH_CLI run view $id --log-failed)
+76
View File
@@ -0,0 +1,76 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
name: "CodeQL Security Scan"
on:
push:
branches:
# only run when there are pushes to the main branch (not on PRs)
- main
schedule:
- cron: '0 0 * * 3'
permissions:
contents: read
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
strategy:
fail-fast: false
matrix:
# Override automatic language detection by changing the below list
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
language: ['go', 'python']
# Learn more...
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
persist-credentials: false
- name: Install Go
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 #v6.3.0
with:
go-version-file: go.mod
check-latest: true
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# 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@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
-21
View File
@@ -1,21 +0,0 @@
name: "CodeQL"
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
schedule:
- cron: '38 11 * * 3'
permissions: {}
jobs:
analyze:
name: Analyze
uses: anchore/workflows/.github/workflows/codeql.yaml@7212994dc8fc3a53fe9c8e766ab5b4ddd16ea3d4 # v0.8.0
permissions:
security-events: write
packages: read
actions: read
contents: read
@@ -0,0 +1,10 @@
name: Dependabot Automation
on:
pull_request:
permissions:
pull-requests: write
jobs:
run:
uses: anchore/workflows/.github/workflows/dependabot-automation.yaml@main
@@ -0,0 +1,62 @@
name: "Detect schema changes"
on:
# IMPORTANT! This workflow is triggered by the `pull_request_target` event
# which means that forked PRs will run with access secrets from the repo
# it's forked from (the "target" repo).
#
# For this reason we only NEVER checkout the code from the pull request
# (e.g. "ref: ${{ github.event.pull_request.head.sha }}") to prevent
# accidentally running potentially untrusted code.
#
# By default the checkout will be:
# - GITHUB_SHA: Last commit on the PR base branch
# - GITHUB_REF: PR base branch
#
# ...unlike a typical PR where:
# - GITHUB_SHA: Last merge commit on the GITHUB_REF branch
# - GITHUB_REF: PR merge branch refs/pull/:prNumber/merge
pull_request_target:
env:
# note: this is used within hashFiles() so must be within the GITHUB_WORKSPACE path (or will silently fail)
CI_COMMENT_FILE: .tmp/labeler-comment.txt
# needs to be any string to uniquely identify the comment on a PR across multiple runs
COMMENT_HEADER: "label-commentary"
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
repository: anchore/syft # IMPORTANT! An additional protection that this is checking out code from the expected repository
ref: main # IMPORTANT! It is CRITICAL that this only ever considers the code from main and NEVER EVER from a fork.
- run: python .github/scripts/labeler.py
env:
# note: this token has write access to the repo
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_PR_NUMBER: ${{ github.event.number }}
- name: Delete existing comment
if: ${{ hashFiles( env.CI_COMMENT_FILE ) == '' }}
uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 #v2.9.4
with:
header: ${{ env.COMMENT_HEADER }}
hide: true
hide_classify: "OUTDATED"
- name: Add comment
if: ${{ hashFiles( env.CI_COMMENT_FILE ) != '' }}
uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 #v2.9.4
with:
header: ${{ env.COMMENT_HEADER }}
path: ${{ env.CI_COMMENT_FILE }}
@@ -0,0 +1,18 @@
name: Add to OSS board
permissions:
contents: read
on:
issues:
types:
- opened
- reopened
- transferred
- labeled
jobs:
run:
uses: "anchore/workflows/.github/workflows/oss-project-board-add.yaml@main"
secrets:
token: ${{ secrets.OSS_PROJECT_GH_TOKEN }}
+150 -64
View File
@@ -1,11 +1,7 @@
name: "Release"
permissions: {}
# there should never be two releases in progress at the same time
concurrency:
group: release
cancel-in-progress: false
permissions:
contents: read
on:
workflow_dispatch:
@@ -21,61 +17,131 @@ on:
options:
- "all"
- "install-script-only"
skip-checks:
description: skip the check-gate (release even if checks haven't passed on main)
type: boolean
default: false
required: false
jobs:
version-available:
quality-gate:
environment: release
if: ${{ github.event.inputs.phase == 'all' }}
permissions:
contents: read # required for fetching tags
uses: anchore/workflows/.github/workflows/check-version-available.yaml@7212994dc8fc3a53fe9c8e766ab5b4ddd16ea3d4 # v0.8.0
with:
version: ${{ github.event.inputs.version }}
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
persist-credentials: false
check-gate:
if: ${{ github.event.inputs.phase == 'all' && !inputs.skip-checks }}
permissions:
contents: read
checks: read # required for getting the status of specific check names
uses: anchore/workflows/.github/workflows/check-gate.yaml@7212994dc8fc3a53fe9c8e766ab5b4ddd16ea3d4 # v0.8.0
with:
# these are checks that should be run on pull-request and merges to main.
# we do NOT want to kick off a release if these have not been verified on main.
# Please see the validations.yaml workflow for the names that should be used here.
checks: '["Acceptance tests (Linux)", "Acceptance tests (Mac)", "Build snapshot artifacts", "CLI tests (Linux)", "Integration tests", "Static analysis", "Unit tests"]'
- 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 }}
- name: Check if running on main
if: github.ref != 'refs/heads/main'
# we are using the following flag when running `cosign blob-verify` for checksum signature verification:
# --certificate-identity-regexp "https://github.com/anchore/.github/workflows/release.yaml@refs/heads/main"
# if we are not on the main branch, the signature will not be verifiable since the suffix requires the main branch
# at the time of when the OIDC token was issued on the Github Actions runner.
run: echo "This can only be run on the main branch otherwise releases produced will not be verifiable with cosign" && exit 1
- 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 }}
- name: Check static analysis results
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
id: static-analysis
with:
token: ${{ secrets.GITHUB_TOKEN }}
# This check name is defined as the github action job name (in .github/workflows/testing.yaml)
checkName: "Static analysis"
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Check unit test results
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
id: unit
with:
token: ${{ secrets.GITHUB_TOKEN }}
# This check name is defined as the github action job name (in .github/workflows/testing.yaml)
checkName: "Unit tests"
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Check integration test results
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
id: integration
with:
token: ${{ secrets.GITHUB_TOKEN }}
# This check name is defined as the github action job name (in .github/workflows/testing.yaml)
checkName: "Integration tests"
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Check acceptance test results (linux)
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
id: acceptance-linux
with:
token: ${{ secrets.GITHUB_TOKEN }}
# This check name is defined as the github action job name (in .github/workflows/testing.yaml)
checkName: "Acceptance tests (Linux)"
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Check acceptance test results (mac)
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
id: acceptance-mac
with:
token: ${{ secrets.GITHUB_TOKEN }}
# This check name is defined as the github action job name (in .github/workflows/testing.yaml)
checkName: "Acceptance tests (Mac)"
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Check cli test results (linux)
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
id: cli-linux
with:
token: ${{ secrets.GITHUB_TOKEN }}
# This check name is defined as the github action job name (in .github/workflows/testing.yaml)
checkName: "CLI tests (Linux)"
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- 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"
false
release:
needs: [check-gate, version-available]
# run even when check-gate is skipped, but never when version-available
# failed/was skipped, nor when check-gate failed or was cancelled. note:
# always() disables the implicit success() gate on ALL needs, so the
# version-available requirement must be re-asserted explicitly here.
if: >-
${{ always()
&& needs.version-available.result == 'success'
&& !contains(fromJSON('["failure", "cancelled"]'), needs.check-gate.result) }}
environment: 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
# volume: enlarge dist/ disk -- the default 40GB EBS root fills up during the multi-arch
# image + binary build and OOMs (runs out of disk); size it well above peak usage.
# provisioned throughput/iops above the gp3 free baseline (125mbs/3000iops) approximate
# the tmpfs IO speed on disk, for ~1c/run.
# note: tmpfs intentionally omitted -- a RAM-backed dist/ never touches the enlarged
# volume and competes with the build's memory peak, so keep dist/ on disk.
runs-on: runs-on=${{ github.run_id }}/cpu=16+32/ram=32+128/family=c5+c6+c7+c8/spot=false/extras=s3-cache/volume=120gb:gp3:500mbs:4000iops
# 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
permissions:
contents: write # required for creating the GitHub release and pushing the version tag
packages: write # required for publishing release artifacts to GitHub packages
id-token: write # required for keyless signing (cosign/sigstore OIDC)
contents: write
packages: write
# required for goreleaser signs section with cosign
id-token: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
fetch-depth: 0
persist-credentials: true
@@ -83,30 +149,32 @@ jobs:
- name: Bootstrap environment
uses: ./.github/actions/bootstrap
# dockers_v2 builds multi-platform manifests in a single buildx invocation, which requires a
# docker-container driver builder (the default builder on the runner cannot build multi-platform).
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- name: Login to Docker Hub
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f #v4.6.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 #v4.0.0
with:
username: ${{ secrets.ANCHOREOSSWRITE_DH_USERNAME }}
password: ${{ secrets.ANCHOREOSSWRITE_DH_PAT }}
- name: Login to GitHub Container Registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f #v4.6.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 #v4.0.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Tag release
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 push origin --tags
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ github.event.inputs.version }}
- name: Build & publish release artifacts
run: make ci-release
env:
# for pushing tags (does not inherit workflow permissions)
TAG_TOKEN: ${{ secrets.TAG_TOKEN }}
RELEASE_VERSION: ${{ github.event.inputs.version }}
# for mac signing and notarization...
QUILL_SIGN_P12: ${{ secrets.ANCHORE_APPLE_DEVELOPER_ID_CERT_CHAIN }}
QUILL_SIGN_PASSWORD: ${{ secrets.ANCHORE_APPLE_DEVELOPER_ID_CERT_PASS }}
@@ -118,18 +186,36 @@ jobs:
# for updating brew formula in anchore/homebrew-syft
GITHUB_BREW_TOKEN: ${{ secrets.ANCHOREOPS_GITHUB_OSS_WRITE_TOKEN }}
- uses: anchore/sbom-action@3ad7283483fc7af8ff2b4ea19663c2d5ca935e26 #v0.24.2
- uses: anchore/sbom-action@17ae1740179002c89186b61233e0f892c3118b11 #v0.23.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
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 }}`
if: ${{ success() }}
release-install-script:
needs: [release]
needs: [ release ]
if: ${{ always() && (needs.release.result == 'success' || github.event.inputs.phase == 'install-script-only') }}
permissions:
contents: read # required for the reusable workflow to check out the repo and publish the install script
uses: anchore/workflows/.github/workflows/release-install-script.yaml@7212994dc8fc3a53fe9c8e766ab5b4ddd16ea3d4 # v0.8.0
uses: "anchore/workflows/.github/workflows/release-install-script.yaml@main"
with:
tag: ${{ github.event.inputs.version }}
secrets:
@@ -0,0 +1,15 @@
name: "Manage Awaiting Response Label"
on:
issue_comment:
types: [created]
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 }}
@@ -0,0 +1,48 @@
name: "Test fixture cache: publish"
on:
workflow_dispatch:
schedule:
# run nightly at 4AM UTC
- cron: "0 4 * * *"
permissions:
contents: read
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
permissions:
packages: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
persist-credentials: false
- name: Bootstrap environment
uses: ./.github/actions/bootstrap
with:
# we want to rebuild the cache with no previous state
download-test-fixture-cache: false
- name: Run all tests
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 }}
- name: Publish test fixture cache
run: make upload-test-fixture-cache
+10 -6
View File
@@ -1,8 +1,10 @@
name: "Validate GitHub Actions"
on:
workflow_dispatch:
pull_request:
paths:
- '.github/workflows/**'
- '.github/actions/**'
push:
branches:
- main
@@ -10,7 +12,8 @@ on:
- '.github/workflows/**'
- '.github/actions/**'
permissions: {}
permissions:
contents: read
jobs:
zizmor:
@@ -20,13 +23,14 @@ jobs:
contents: read
security-events: write # for uploading SARIF results
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: "Run zizmor"
uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4
uses: zizmorcore/zizmor-action@0dce2577a4760a2749d8cfb7a84b7d5585ebcb7d # v0.5.0
with:
# there is a pass/fail gate as a repo ruleset (if there is no ruleset configured then the action will pass by default)
advanced-security: true
config: .github/zizmor.yml
# Disable SARIF upload so the step is a simple pass/fail gate
advanced-security: false
inputs: .github
+29 -83
View File
@@ -13,7 +13,8 @@ on:
branches:
- main
permissions: {}
permissions:
contents: read
jobs:
Static-Analysis:
@@ -21,17 +22,11 @@ jobs:
name: "Static analysis"
# runs-on.com: memory & general purpose instances for testing
# spot enabled: ok to interrupt non-production workloads
# tmpfs intentionally omitted -- it RAM-backs /home/runner, /tmp and /var/lib/docker at
# 100% of instance memory, starving `make unit` of headroom to link with -race.
# volume: the caches and docker images tmpfs used to hold. gp3 baseline throughput is
# enough; a 4 vCPU instance can't pull much more than that anyway.
# note: s3-cache intentionally omitted -- PR runs are untrusted and must not write to the
# shared cache backend that the trusted release workflow reads from (cache poisoning).
runs-on: &test-runner "runs-on=${{ github.run_id }}/cpu=4+8/ram=16+32/family=m6+m7+m8+c6+c7+c8/spot=price-capacity-optimized/volume=100gb"
permissions:
contents: read
# 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"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
persist-credentials: false
@@ -49,29 +44,9 @@ jobs:
Unit-Test:
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
name: "Unit tests"
runs-on: ubuntu-latest
needs: [Unit-Test-Linux, Unit-Test-Windows]
# a skipped job satisfies a required check, so this job must always run and fail explicitly
# unless every dependency succeeded (otherwise a failing PR can still be auto-merged)
if: ${{ always() }}
steps:
- name: Require all unit test jobs to succeed
env:
NEEDS: ${{ toJSON(needs) }}
run: |
echo "$NEEDS" | jq -e 'length > 0 and all(.[]; .result == "success")' > /dev/null || {
echo "$NEEDS" | jq -r 'to_entries[] | "\(.key): \(.value.result)"'
exit 1
}
echo All unit tests passed!
Unit-Test-Linux:
name: "Unit tests (Linux)"
runs-on: *test-runner
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
persist-credentials: false
@@ -86,35 +61,12 @@ jobs:
- name: Check for capability drift
run: make check-capability-drift
Unit-Test-Windows:
name: "Unit tests (Windows)"
runs-on: windows-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
with:
persist-credentials: false
# go-make's setup only (not ./.github/actions/bootstrap): no apt, binny tools, or fixture cache are needed
- name: Setup go + go-make tooling
uses: anchore/go-make/.github/actions/setup@fa5421b3bf24c9d18f11a736c5eb4172f2b7d683 # v0.8.1
with:
# keep in sync with the go-version default in ./.github/actions/bootstrap
go-version: "1.26.2"
# the full unit suite leans on linux docker fixtures, so this runs the windows-relevant subset
- name: Run unit tests
run: make unit:windows
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
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
persist-credentials: false
@@ -134,20 +86,14 @@ jobs:
# runs-on.com: compute instances for parallel builds
# spot disabled: reliability for build workflows (used for releases too)
# goreleaser uses parallelism of 12, so we need more CPUs
# tmpfs intentionally omitted -- a RAM-backed dist/ competes with the goreleaser
# build's memory peak (parallel binaries + multi-arch image assembly) and OOMs the
# instance; keep dist/ on disk.
# note: s3-cache intentionally omitted -- PR runs are untrusted and must not write to the
# shared cache backend that the trusted release workflow reads from (cache poisoning).
# volume: enlarge dist/ disk -- the default 40GB EBS root fills up during the multi-arch
# image + binary build and OOMs (runs out of disk); size it well above peak usage.
# provisioned throughput/iops above the gp3 free baseline (125mbs/3000iops) approximate
# the tmpfs IO speed on disk, for ~1c/run.
runs-on: "runs-on=${{ github.run_id }}/cpu=16+32/ram=32+128/family=c5+c6+c7+c8/spot=false/volume=120gb:gp3:500mbs:4000iops"
permissions:
contents: read
# 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"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
# 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
@@ -163,7 +109,7 @@ jobs:
run: make snapshot-smoke-test
- name: Upload snapshot artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7.0.0
with:
name: snapshot
path: snapshot/
@@ -174,10 +120,11 @@ jobs:
name: "Acceptance tests (Linux)"
needs: [Build-Snapshot-Artifacts]
runs-on: *test-runner
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
# 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
@@ -187,7 +134,7 @@ jobs:
download-test-fixture-cache: true
- name: Download snapshot artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c #v8.0.1
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 #v8.0.0
with:
name: snapshot
path: snapshot
@@ -215,13 +162,11 @@ jobs:
needs: [Build-Snapshot-Artifacts]
# note: macos runners aren't supported yet for runs-on managed runners.
runs-on: macos-latest
permissions:
contents: read
steps:
- name: Install Cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
persist-credentials: false
@@ -233,7 +178,7 @@ jobs:
download-test-fixture-cache: true
- name: Download snapshot artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c #v8.0.1
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 #v8.0.0
with:
name: snapshot
path: snapshot
@@ -252,10 +197,11 @@ jobs:
name: "CLI tests (Linux)"
needs: [Build-Snapshot-Artifacts]
runs-on: *test-runner
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
# 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
@@ -265,7 +211,7 @@ jobs:
download-test-fixture-cache: true
- name: Download snapshot artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c #v8.0.1
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 #v8.0.0
with:
name: snapshot
path: snapshot
+5
View File
@@ -1,4 +1,9 @@
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,
+2 -5
View File
@@ -3,8 +3,6 @@ go.work
go.work.sum
.tool-versions
.python-version
.mise.toml
.env
# app configuration
/.syft.yaml
@@ -20,11 +18,8 @@ bin/
/.tool
/.task
/generate
/spec
/specs
mise.toml
.make/.make
.conductor
# changelog generation
CHANGELOG.md
@@ -81,3 +76,5 @@ cosign.pub
__pycache__/
*.py[cod]
*$py.class
+3 -26
View File
@@ -7,7 +7,6 @@ linters:
- asciicheck
- bodyclose
- copyloopvar
- depguard
- dogsled
- dupl
- errcheck
@@ -30,20 +29,9 @@ linters:
- unused
- whitespace
settings:
depguard:
rules:
yaml:
deny:
# gopkg.in/yaml.v3 is unmaintained and panics on some malformed input (see #5327)
- pkg: gopkg.in/yaml.v3
desc: use go.yaml.in/yaml/v3 instead
funlen:
lines: 70
statements: 50
goconst:
# the default of 3 turns every repeated literal in a lookup table or a test fixture into a finding.
# 5 keeps the ones worth hoisting without flagging data that reads better written out.
min-occurrences: 5
gocritic:
enabled-checks:
- deferInLoop
@@ -66,12 +54,6 @@ linters:
- linters:
- revive
text: "var-naming: avoid package names that conflict"
# cpegenerate is almost entirely lookup tables mapping artifact names to vendors, products, and
# maven group ids. The repeated strings are the data itself, so hoisting them to constants would
# replace a readable table with indirection for no gain.
- linters:
- goconst
path: syft/pkg/cataloger/internal/cpegenerate/
paths:
- third_party$
- builtin$
@@ -79,6 +61,7 @@ linters:
# do not enable...
# - deadcode # The owner seems to have abandoned the linter. Replaced by "unused".
# - depguard # We don't have a configuration for this yet
# - goprintffuncname # does not catch all cases and there are exceptions
# - nakedret # does not catch all cases and should not fail a build
# - gochecknoglobals
@@ -87,6 +70,7 @@ linters:
# - godot
# - godox
# - goerr113
# - goimports # we're using gosimports now instead to account for extra whitespaces (see https://github.com/golang/go/issues/20818)
# - golint # deprecated
# - gomnd # this is too aggressive
# - interfacer # this is a good idea, but is no longer supported and is prone to false positives
@@ -112,18 +96,11 @@ issues:
formatters:
enable:
- gci
- gofmt
- goimports
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
settings:
gci:
# See https://golangci-lint.run/docs/formatters/configuration/#gci
sections:
- standard # Standard section: captures all standard packages.
- default # Default section: contains all imports that could not be matched to another section type.
- prefix(github.com/anchore)
+247 -80
View File
@@ -14,7 +14,7 @@ builds:
dir: ./cmd/syft
binary: syft
goos: [linux]
goarch: [amd64, arm64, ppc64le, riscv64, s390x]
goarch: [amd64, arm64, ppc64le, s390x]
mod_timestamp: &build-timestamp '{{ .CommitTimestamp }}'
ldflags: &build-ldflags |
-w
@@ -73,88 +73,252 @@ brews:
description: *description
license: "Apache License 2.0"
dockers_v2:
# production images (scratch base, root)
- id: production
dockers:
# production images...
- image_templates:
- anchore/syft:{{.Tag}}-amd64
- ghcr.io/anchore/syft:{{.Tag}}-amd64
goarch: amd64
dockerfile: Dockerfile
ids: &docker-ids
- linux-build
images: &docker-images
- anchore/syft
- ghcr.io/anchore/syft
platforms: &docker-platforms
- linux/amd64
- linux/arm64
- linux/ppc64le
- linux/riscv64
- linux/s390x
labels: &docker-labels
"org.opencontainers.image.created": "{{.Date}}"
"org.opencontainers.image.title": "syft"
"org.opencontainers.image.description": "CLI tool and library for generating a Software Bill of Materials from container images and filesystems"
"org.opencontainers.image.source": "{{.GitURL}}"
"org.opencontainers.image.revision": "{{.FullCommit}}"
"org.opencontainers.image.vendor": "Anchore, Inc."
"org.opencontainers.image.version": "{{.Version}}"
"org.opencontainers.image.licenses": "Apache-2.0"
"io.artifacthub.package.readme-url": "https://raw.githubusercontent.com/anchore/syft/main/README.md"
"io.artifacthub.package.logo-url": "https://user-images.githubusercontent.com/5199289/136844524-1527b09f-c5cb-4aa9-be54-5aa92a6086c1.png"
"io.artifacthub.package.license": "Apache-2.0"
tags:
- latest
- "{{.Tag}}"
# DEBIAN_VERSION 13 (trixie) is the first distroless release with a riscv64 base image
# and is a superset of debian 12 for the other targeted arches; docker_v2 builds all
# platforms in a single buildx invocation, so build_args cannot vary per platform.
build_args: &docker-build-args
DEBIAN_VERSION: "13"
# disable provenance attestations to keep the manifest free of unknown/unknown entries
flags: &docker-flags
- "--provenance=false"
# SBOMs are produced separately for the archives (see the sboms section)
sbom: &docker-sbom "false"
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}}"
# nonroot images
- id: nonroot
- image_templates:
- anchore/syft:{{.Tag}}-arm64v8
- ghcr.io/anchore/syft:{{.Tag}}-arm64v8
goarch: arm64
dockerfile: Dockerfile
use: buildx
build_flag_templates:
- "--platform=linux/arm64/v8"
- "--build-arg=BUILD_DATE={{.Date}}"
- "--build-arg=BUILD_VERSION={{.Version}}"
- "--build-arg=VCS_REF={{.FullCommit}}"
- "--build-arg=VCS_URL={{.GitURL}}"
- image_templates:
- anchore/syft:{{.Tag}}-ppc64le
- ghcr.io/anchore/syft:{{.Tag}}-ppc64le
goarch: ppc64le
dockerfile: Dockerfile
use: buildx
build_flag_templates:
- "--platform=linux/ppc64le"
- "--build-arg=BUILD_DATE={{.Date}}"
- "--build-arg=BUILD_VERSION={{.Version}}"
- "--build-arg=VCS_REF={{.FullCommit}}"
- "--build-arg=VCS_URL={{.GitURL}}"
- image_templates:
- anchore/syft:{{.Tag}}-s390x
- ghcr.io/anchore/syft:{{.Tag}}-s390x
goarch: s390x
dockerfile: Dockerfile
use: buildx
build_flag_templates:
- "--platform=linux/s390x"
- "--build-arg=BUILD_DATE={{.Date}}"
- "--build-arg=BUILD_VERSION={{.Version}}"
- "--build-arg=VCS_REF={{.FullCommit}}"
- "--build-arg=VCS_URL={{.GitURL}}"
# nonroot images...
- image_templates:
- anchore/syft:{{.Tag}}-nonroot-amd64
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-amd64
goarch: amd64
dockerfile: Dockerfile.nonroot
ids: *docker-ids
images: *docker-images
platforms: *docker-platforms
labels: *docker-labels
tags:
- nonroot
- "{{.Tag}}-nonroot"
build_args: *docker-build-args
flags: *docker-flags
sbom: *docker-sbom
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}}"
# debug images (root)
- id: debug
- 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
ids: *docker-ids
images: *docker-images
platforms: *docker-platforms
labels: *docker-labels
tags:
- debug
- "{{.Tag}}-debug"
build_args: *docker-build-args
flags: *docker-flags
sbom: *docker-sbom
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}}"
# debug-nonroot images
- id: debug-nonroot
dockerfile: Dockerfile.debug-nonroot
ids: *docker-ids
images: *docker-images
platforms: *docker-platforms
labels: *docker-labels
tags:
- debug-nonroot
- "{{.Tag}}-debug-nonroot"
build_args: *docker-build-args
flags: *docker-flags
sbom: *docker-sbom
- 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}}-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}}-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-arm64v8
- ghcr.io/anchore/syft:{{.Tag}}-debug-ppc64le
- ghcr.io/anchore/syft:{{.Tag}}-debug-s390x
- name_template: anchore/syft:{{.Tag}}-debug
image_templates:
- anchore/syft:{{.Tag}}-debug-amd64
- anchore/syft:{{.Tag}}-debug-arm64v8
- anchore/syft:{{.Tag}}-debug-ppc64le
- anchore/syft:{{.Tag}}-debug-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
sboms:
- artifacts: archive
@@ -167,13 +331,16 @@ sboms:
- "--output"
- "json=$document"
# the signature, certificate, and transparency log proof all land in a single sigstore bundle.
signs:
- cmd: .tool/cosign
signature: "${artifact}.sigstore.json"
signature: "${artifact}.sig"
certificate: "${artifact}.pem"
args:
- "sign-blob"
- "--bundle=${signature}"
- "--use-signing-config=false"
- "--oidc-issuer=https://token.actions.githubusercontent.com"
- "--output-certificate=${certificate}"
- "--output-signature=${signature}"
- "${artifact}"
- "--yes"
artifacts: checksum
-12
View File
@@ -1,12 +0,0 @@
module github.com/anchore/syft/.make
go 1.25.8
require github.com/anchore/go-make v0.8.1
require (
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
golang.org/x/mod v0.40.0 // indirect
golang.org/x/sys v0.47.0 // indirect
)
-10
View File
@@ -1,10 +0,0 @@
github.com/anchore/go-make v0.8.1 h1:GAhPXR+9qVQhFB1pfVlHFOzxAt3dTDTUIA7Tp7nx9tg=
github.com/anchore/go-make v0.8.1/go.mod h1:xALbVsPsDoJnJxdkjPlxOLZV7GbAdT5Pv3gd0kP1Lz0=
github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs=
golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
-213
View File
@@ -1,213 +0,0 @@
package main
import (
"os"
"path/filepath"
"runtime"
"strconv"
. "github.com/anchore/go-make"
"github.com/anchore/go-make/config"
"github.com/anchore/go-make/file"
"github.com/anchore/go-make/lang"
"github.com/anchore/go-make/run"
"github.com/anchore/go-make/tasks/golint"
"github.com/anchore/go-make/tasks/goreleaser"
"github.com/anchore/go-make/tasks/gotask"
"github.com/anchore/go-make/tasks/gotest"
)
func main() {
Makefile(
// shared anchore tasks
golint.Tasks(),
goreleaser.Tasks(),
// unit tests: exclude packages under any test/ directory (matches the syft
// Taskfile's prior `grep -v` against test paths). Coverage threshold of 62%
// preserves the prior coverage gate that used to live in scripts/coverage.py.
gotest.Tasks(
gotest.Name("unit"),
gotest.ExcludeGlob("**/test/**"),
gotest.CoverageThreshold(62),
race(),
),
// integration tests: run `go test` directly instead of via gotest.Tasks(), which
// has no way to set a timeout. The suite is a single package of ~36 sequential
// tests over 18 docker fixture images, so with a cold fixture cache (every image
// built + saved inline, under -race) it runs well past `go test`'s default 10m
// timeout -- which is exactly the case when regenerating the fixture cache from
// scratch. -count=1 keeps the go test cache from short-circuiting a run whose
// side effect (the built fixtures) is the thing we're after.
//
// The race-detector smoke against a real image stays bundled here (RunsOn
// integration) so `make integration` behaves like the Taskfile version did.
Task{
Name: "integration",
Description: "run integration tests",
RunsOn: lang.List("test"),
Run: func() {
raceFlag := ""
if raceEnabled() {
raceFlag = " -race"
}
Run(
"go test -count=1 -timeout=30m"+raceFlag+" ./cmd/syft/internal/test/integration/...",
run.Env("GODEBUG", "dontfreezetheworld=1"),
)
},
},
Task{
Name: "integration:race-smoke",
Description: "exercise the CLI with the race detector",
RunsOn: lang.List("integration"),
Run: func() {
if !raceEnabled() {
Log("race detector disabled (RACE=false); skipping race smoke")
return
}
Run("go run -race cmd/syft/main.go anchore/test_images:grype-quality-dotnet-69f15d2")
},
},
// cli tests: native go-make Task. Runs SYFT_BINARY_LOCATION at an *absolute*
// path to the snapshot binary. Builds the snapshot only when the binary is
// missing rather than depending on the snapshot task unconditionally: in
// validations.yaml CI we download a pre-built snapshot artifact, so the binary
// already exists and rebuilding would both burn ~10m and clobber it. When
// `make test` runs cold (e.g. the release pipeline) or locally with no
// snapshot, we build a single-target snapshot (current OS/arch only) since
// that's all the CLI tests need.
Task{
Name: "cli",
Description: "Run CLI tests",
RunsOn: lang.List("test"),
Run: func() {
bin := snapshotBinPath()
if !file.Exists(bin) {
Log("snapshot binary not found at %s; building single-target snapshot", bin)
Run("make snapshot:single-target")
}
Log("testing binary: %s", bin)
Run(
"go test -count=1 -timeout=15m -v ./test/cli",
run.Env("SYFT_BINARY_LOCATION", bin),
)
},
},
// windows unit tests: deliberately not hooked into "test" (no RunsOn). The full unit suite leans on
// linux docker fixture images and posix-only assumptions, so this covers the packages that own
// host path handling (where windows bugs live) and skips the tests that need docker image fixtures.
// ponytail: curated package list, widen it as more of the suite is made windows-safe.
Task{
Name: "unit:windows",
Description: "run the windows-relevant subset of unit tests",
Run: func() {
Run("go test -count=1"+
` -skip "Image|Squash|AllLayers|MixFileTypes|GivenCoordinates"`+
" ./syft/internal/windows/... ./syft/internal/fileresolver/... ./syft/source/directorysource/... ./syft/source/filesource/..."+
" ./syft/file ./syft/file/cataloger/filedigest/... ./syft/file/cataloger/filecontent/...",
run.Stdout(os.Stderr),
)
},
},
// default validation pipeline (replaces Taskfile `default`/`pr-validations`/`validations`).
Task{
Name: "default",
Description: "Run all validation tasks",
Dependencies: Deps("static-analysis", "test", "install-test"),
},
// --- everything else is implemented in Taskfile.yaml. gotask.Tasks()
// discovers every (non-internal) Taskfile task — including the namespaced
// `generate:cpe-index:*` tasks from the included task.d file — and surfaces
// them as first-class go-make tasks (with their canonical names and `desc:`)
// that forward to `task <name>`. Descriptions live in Taskfile.yaml as the
// single source of truth; no per-task wrapping needed here.
gotask.Tasks(),
// gotask.Tasks() discovers canonical task names only, not Taskfile aliases,
// so re-expose `refresh-fixtures`'s `fixtures` alias for manual use.
Task{
Name: "fixtures",
Description: "Clear and fetch all test fixture cache (alias of refresh-fixtures)",
Dependencies: Deps("refresh-fixtures"),
},
// gotask.Tasks() can't attach RunsOn labels, so wire the syft-specific
// Taskfile tasks into go-make's native phases here. These thin hooks have
// no body and no description (hidden from `make help`); they only pull the
// discovered tasks in when the labeled phase runs.
Task{
Name: "static-analysis:syft",
RunsOn: lang.List("static-analysis"),
Dependencies: Deps("check-json-schema-drift", "check-binary-fixture-size"),
},
Task{
Name: "test:syft",
RunsOn: lang.List("test"),
Dependencies: Deps("validate-cyclonedx-schema", "test-utils", "check-docker-cache"),
},
// refresh-fixtures hooks into "unit" so `make unit` triggers the stale-cache
// detection + download just like `task unit` did on main (its
// `deps: [tmpdir, fixtures]` is what kept the fixture cache fresh).
Task{
Name: "unit:syft",
RunsOn: lang.List("unit"),
Dependencies: Deps("refresh-fixtures"),
},
Task{
Name: "clean:syft",
RunsOn: lang.List("clean"),
Dependencies: Deps(
"clean-snapshot",
"clean-docker-cache",
"clean-oras-cache",
"clean-cache",
"clean-test-observations",
),
},
)
}
// raceEnabled is the single switch for the race detector across every test suite.
// Unset it and we keep go-make's behavior (on in CI, off locally and on windows);
// set RACE=false to turn it off everywhere -- worth doing when rebuilding the test
// fixture cache from scratch, where every suite is dominated by building docker
// fixtures and the race detector only adds wall clock. RACE=true forces it on.
func raceEnabled() bool {
if enabled, err := strconv.ParseBool(config.Env("RACE", "")); err == nil {
return enabled
}
return config.CI && !config.Windows
}
// race applies raceEnabled() to a gotest suite. gotest exposes no functional option
// for the race detector, but gotest.Config.Race is exported.
func race() gotest.Option {
return func(c *gotest.Config) {
c.Race = raceEnabled()
}
}
// snapshotBinPath replicates the SNAPSHOT_BIN computation from the prior Taskfile:
// <repoRoot>/snapshot/<os>-build_<os>_<arch>/syft, where arch maps amd64->amd64_v1
// and arm64->arm64_v8.0 to match goreleaser's per-target output directory naming.
// Returns an absolute path: the cli tests' getSyftBinaryLocation contract requires
// SYFT_BINARY_LOCATION to be absolute because subtests run with cmd.Dir = t.TempDir().
func snapshotBinPath() string {
osName := runtime.GOOS
var arch string
switch runtime.GOARCH {
case "amd64":
arch = "amd64_v1"
case "arm64":
arch = "arm64_v8.0"
default:
arch = runtime.GOARCH
}
return filepath.Join(RootDir(), "snapshot", osName+"-build_"+osName+"_"+arch, "syft")
}
-49
View File
@@ -1,49 +0,0 @@
package main
import (
"os"
"testing"
"github.com/anchore/go-make/config"
"github.com/anchore/go-make/tasks/gotest"
)
func TestRaceEnabled(t *testing.T) {
tests := []struct {
name string
env string // "" means RACE is unset
ci bool
want bool
}{
{name: "default in CI", ci: true, want: true},
{name: "default locally", ci: false, want: false},
{name: "explicitly off in CI", env: "false", ci: true, want: false},
{name: "explicitly on locally", env: "true", ci: false, want: true},
{name: "unparseable falls back to default", env: "yes-please", ci: true, want: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// always go through t.Setenv (it registers the restore) so an ambient
// RACE from the caller's environment can't leak into the unset cases
t.Setenv("RACE", tt.env)
if tt.env == "" {
os.Unsetenv("RACE")
}
orig := config.CI
config.CI = tt.ci
t.Cleanup(func() { config.CI = orig })
if got := raceEnabled(); got != tt.want {
t.Errorf("raceEnabled() = %v, want %v", got, tt.want)
}
// the gotest suites get the same answer through the functional option
var cfg gotest.Config
race()(&cfg)
if cfg.Race != tt.want {
t.Errorf("race() set Race = %v, want %v", cfg.Race, tt.want)
}
})
}
}
+19 -4
View File
@@ -1,5 +1,4 @@
ARG DEBIAN_VERSION=13
FROM gcr.io/distroless/static-debian${DEBIAN_VERSION}:latest AS build
FROM gcr.io/distroless/static-debian12:latest AS build
FROM scratch
# needed for version check HTTPS request
@@ -8,7 +7,23 @@ COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certifica
# create the /tmp dir, which is needed for image content cache
WORKDIR /tmp
ARG TARGETPLATFORM
COPY ${TARGETPLATFORM}/syft /
COPY syft /
ARG BUILD_DATE
ARG BUILD_VERSION
ARG VCS_REF
ARG VCS_URL
LABEL org.opencontainers.image.created=$BUILD_DATE
LABEL org.opencontainers.image.title="syft"
LABEL org.opencontainers.image.description="CLI tool and library for generating a Software Bill of Materials from container images and filesystems"
LABEL org.opencontainers.image.source=$VCS_URL
LABEL org.opencontainers.image.revision=$VCS_REF
LABEL org.opencontainers.image.vendor="Anchore, Inc."
LABEL org.opencontainers.image.version=$BUILD_VERSION
LABEL org.opencontainers.image.licenses="Apache-2.0"
LABEL io.artifacthub.package.readme-url="https://raw.githubusercontent.com/anchore/syft/main/README.md"
LABEL io.artifacthub.package.logo-url="https://user-images.githubusercontent.com/5199289/136844524-1527b09f-c5cb-4aa9-be54-5aa92a6086c1.png"
LABEL io.artifacthub.package.license="Apache-2.0"
ENTRYPOINT ["/syft"]
+21 -4
View File
@@ -1,10 +1,27 @@
ARG DEBIAN_VERSION=13
FROM gcr.io/distroless/static-debian${DEBIAN_VERSION}:debug
FROM gcr.io/distroless/static-debian12:debug-nonroot
# create the /tmp dir, which is needed for image content cache
WORKDIR /tmp
ARG TARGETPLATFORM
COPY ${TARGETPLATFORM}/syft /
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"]
-12
View File
@@ -1,12 +0,0 @@
ARG DEBIAN_VERSION=13
FROM gcr.io/distroless/static-debian${DEBIAN_VERSION}:debug-nonroot
# create the /tmp dir, which is needed for image content cache
WORKDIR /tmp
ARG TARGETPLATFORM
COPY ${TARGETPLATFORM}/syft /
USER nonroot
ENTRYPOINT ["/syft"]
+19 -4
View File
@@ -1,12 +1,27 @@
ARG DEBIAN_VERSION=13
FROM gcr.io/distroless/static-debian${DEBIAN_VERSION}:nonroot
FROM gcr.io/distroless/static-debian12:nonroot
# create the /tmp dir, which is needed for image content cache
WORKDIR /tmp
ARG TARGETPLATFORM
COPY ${TARGETPLATFORM}/syft /
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"]
+41 -13
View File
@@ -1,18 +1,46 @@
# `test` and `snapshot` have matching directory names in this repo, so make would
# refuse to run them without an explicit .PHONY (Nothing to be done for ...).
.PHONY: test snapshot
test:
@go run -C .make . test
OWNER = anchore
PROJECT = syft
snapshot:
@go run -C .make . snapshot
TOOL_DIR = .tool
BINNY = $(TOOL_DIR)/binny
TASK = $(TOOL_DIR)/task
.PHONY: *
.DEFAULT_GOAL: make-default
.DEFAULT_GOAL := make-default
make-default:
@go run -C .make .
## Bootstrapping targets #################################
.DEFAULT:
# 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)
# note: we need to assume that binny and task have not already been installed
.PHONY: task
$(TASK) task: $(BINNY)
@$(BINNY) install task -q
.PHONY: ci-bootstrap-go
ci-bootstrap-go:
go mod download
# this is a bootstrapping catch-all, where if the target doesn't exist, we'll ensure the tools are installed and then try again
%:
@go run -C .make . $@
@make --silent $(TASK)
@$(TASK) $@
## Shim targets #################################
.PHONY: make-default
make-default: $(TASK)
@# run the default task in the taskfile
@$(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 ','")
.PHONY: $(TASKS)
$(TASKS): $(TASK)
@$(TASK) $@
help: $(TASK)
@$(TASK) -l
+2 -1
View File
@@ -8,9 +8,10 @@
<p align="center">
&nbsp;<a href="https://github.com/anchore/syft/actions/workflows/validations.yaml" target="_blank"><img alt="Validations" src="https://github.com/anchore/syft/actions/workflows/validations.yaml/badge.svg"></a>&nbsp;
&nbsp;<a href="https://goreportcard.com/report/github.com/anchore/syft" target="_blank"><img alt="Go Report Card" src="https://goreportcard.com/badge/github.com/anchore/syft"></a>&nbsp;
&nbsp;<a href="https://github.com/anchore/syft/releases/latest" target="_blank"><img alt="GitHub release" src="https://img.shields.io/github/release/anchore/syft.svg"></a>&nbsp;
&nbsp;<a href="https://github.com/anchore/syft" target="_blank"><img alt="GitHub go.mod Go version" src="https://img.shields.io/github/go-mod/go-version/anchore/syft.svg"></a>&nbsp;
&nbsp;<a href="https://github.com/anchore/syft/blob/main/LICENSE" target="_blank"><img alt="License: Apache-2.0" src="https://img.shields.io/badge/License-Apache%202.0-blue.svg"></a>&nbsp;
&nbsp;<a href="" target="_blank"><img alt="License: Apache-2.0" src="https://img.shields.io/badge/License-Apache%202.0-blue.svg"></a>&nbsp;
&nbsp;<a href="https://anchore.com/discourse" target="_blank"><img alt="Join our Discourse" src="https://img.shields.io/badge/Discourse-Join-blue?logo=discourse"/></a>&nbsp;
&nbsp;<a rel="me" href="https://fosstodon.org/@syft"><img alt="Follow on Mastodon" src="https://img.shields.io/badge/Mastodon-Follow-blue?logoColor=white&logo=mastodon"/></a>&nbsp;
</p>
-20
View File
@@ -14,23 +14,3 @@ 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/).
## Trust Boundary
Syft is a tool to scan content and product an SBOM. Syft is not a tool designed to scan malicious content. Detecting and properly reporting on purposely malicious artifacts is outside the scope of Syft's expected operating environment.
There are many possible ways for malicious content to cause Syft to become confused or fail to include results in an SBOM. We do not consider this to be a security vulnerability.
**Examples**
- Removing or altering a package lock file
- Removing or altering an RPM or DEB database
- A malicious archive that Syft will skip but the runtime may not
- Self modifying systems that change state when running
We consider the security trust boundary for Syft to be anything that causes problems for the overall system running Syft, or Syft operating in a way that is dangerous to itself, the system, or the operator.
**Examples**
- Filling up temp space permanently
- Syft executing arbitrary code when scanning an artifact
- Syft leaking secrets from the environment or configuration files into logs or SBOMs
- Syft operating outside of the expected artifact or directory (directory traversal)
+264 -58
View File
@@ -1,10 +1,5 @@
version: "3"
# NOTE: most generic tasks (static-analysis, format, lint, unit, snapshot, release,
# changelog, ci-release, etc.) are now provided natively by anchore/go-make and
# defined in .make/main.go. This file holds the syft-specific tasks, which go-make
# auto-discovers via gotask.Tasks() and surfaces as `make <name>` — keep
# descriptions (`desc:`) populated so they show up in `make help`.
version: "3"
includes:
generate:cpe-index: ./task.d/generate/cpe-index.yaml
@@ -15,8 +10,7 @@ vars:
# v1: when fixtures were located at test-fixtures dirs
# v2: migration to testdata dirs
CACHE_REPO: oss-cache
CACHE_IMAGE: ghcr.io/{{ .OWNER }}/{{ .CACHE_REPO }}/syft-test-fixture-cache:v2
CACHE_IMAGE: ghcr.io/{{ .OWNER }}/{{ .PROJECT }}/test-fixture-cache:v2
# static file dirs
TOOL_DIR: .tool
@@ -30,7 +24,11 @@ vars:
YQ: "{{ .TOOL_DIR }}/yq"
TASK: "{{ .TOOL_DIR }}/task"
# used for snapshot bin discovery in compare/install tasks
# used for changelog generation
CHANGELOG: CHANGELOG.md
NEXT_VERSION: VERSION
# used for snapshot builds
OS:
sh: uname -s | tr '[:upper:]' '[:lower:]'
ARCH:
@@ -39,16 +37,15 @@ vars:
PROJECT_ROOT:
sh: echo $PWD
# docker platform suffix goreleaser appends to snapshot image tags (e.g. latest-amd64).
# snapshot builds load per-arch images rather than a multi-arch manifest, so the smoke
# test must run the host-native tag to avoid emulation (and red-local/green-CI splits).
DOCKER_ARCH:
sh: go env GOARCH
# note: the snapshot dir must be a relative path starting with ./
# e.g. when installing snapshot debs from a local path, ./ forces the deb to be installed in the current working directory instead of referencing a package name
SNAPSHOT_DIR: ./snapshot
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"
BUILD_CMD: "{{ .TOOL_DIR }}/goreleaser build --config {{ .TMP_DIR }}/goreleaser.yaml --clean --snapshot --single-target"
RELEASE_CMD: "{{ .TOOL_DIR }}/goreleaser release --clean --release-notes {{ .CHANGELOG }}"
VERSION:
sh: git describe --dirty --always --tags
# used for install and acceptance testing
COMPARE_DIR: ./test/compare
@@ -59,10 +56,43 @@ env:
tasks:
## Bootstrap (internal helpers used by other Taskfile tasks) ###############
## High-level tasks #################################
default:
desc: Run all validation tasks
aliases:
- pr-validations
- validations
cmds:
- task: static-analysis
- task: test
- task: install-test
static-analysis:
desc: Run all static analysis tasks
cmds:
- task: check-go-mod-tidy
- task: check-licenses
- task: lint
- task: check-json-schema-drift
- task: check-binary-fixture-size
test:
desc: Run all levels of test
cmds:
- task: unit
- task: integration
- task: validate-cyclonedx-schema
- task: test-utils
- task: snapshot
- task: cli
- task: check-docker-cache
## Bootstrap tasks #################################
binny:
internal: true
# desc: Get the binny tool
generates:
- "{{ .TOOL_DIR }}/binny"
status:
@@ -71,8 +101,10 @@ tasks:
silent: true
tools:
internal: true
desc: Install all tools needed for CI and local development
deps: [binny]
aliases:
- bootstrap
generates:
- ".binny.yaml"
- "{{ .TOOL_DIR }}/*"
@@ -81,14 +113,79 @@ tasks:
cmd: "{{ .TOOL_DIR }}/binny install -v"
silent: true
update-tools:
desc: Update pinned versions of all tools to their latest available versions
deps: [binny]
generates:
- ".binny.yaml"
- "{{ .TOOL_DIR }}/*"
cmd: "{{ .TOOL_DIR }}/binny update -v"
silent: true
list-tools:
desc: List all tools needed for CI and local development
deps: [binny]
cmd: "{{ .TOOL_DIR }}/binny list"
silent: true
list-tool-updates:
desc: List all tools that are not up to date relative to the binny config
deps: [binny]
cmd: "{{ .TOOL_DIR }}/binny list --updates"
silent: true
tmpdir:
internal: true
silent: true
generates:
- "{{ .TMP_DIR }}"
cmd: "mkdir -p {{ .TMP_DIR }}"
## Static analysis extras #################################################
## Static analysis tasks #################################
format:
desc: Auto-format all source code
deps: [tools]
cmds:
- gofmt -w -s .
- "{{ .TOOL_DIR }}/gosimports -local github.com/anchore -w ."
- go mod tidy
lint-fix:
desc: Auto-format all source code + run golangci lint fixers
deps: [tools]
cmds:
- task: format
- "{{ .TOOL_DIR }}/golangci-lint run --tests=false --fix"
lint:
desc: Run gofmt + golangci lint checks
vars:
BAD_FMT_FILES:
sh: gofmt -l -s .
BAD_FILE_NAMES:
sh: "find . | grep -e ':' || true"
deps: [tools]
cmds:
# ensure there are no go fmt differences
- cmd: 'test -z "{{ .BAD_FMT_FILES }}" || (echo "files with gofmt issues: [{{ .BAD_FMT_FILES }}]"; exit 1)'
silent: true
# ensure there are no files with ":" in it (a known back case in the go ecosystem)
- cmd: 'test -z "{{ .BAD_FILE_NAMES }}" || (echo "files with bad names: [{{ .BAD_FILE_NAMES }}]"; exit 1)'
silent: true
# run linting
- "{{ .TOOL_DIR }}/golangci-lint run --tests=false"
check-licenses:
# desc: Ensure transitive dependencies are compliant with the current license policy
deps: [tools]
cmd: "{{ .TOOL_DIR }}/bouncer check ./..."
check-go-mod-tidy:
# desc: Ensure go.mod and go.sum are up to date
cmds:
- cmd: .github/scripts/go-mod-tidy-check.sh && echo "go.mod and go.sum are tidy!"
silent: true
check-json-schema-drift:
desc: Ensure there is no drift between the JSON schema and the code
@@ -105,8 +202,8 @@ tasks:
cmds:
- .github/scripts/check_binary_fixture_size.sh syft/pkg/cataloger/binary/testdata/classifiers/snippets
## Test extras ############################################################
## Testing tasks #################################
update-format-golden-files:
desc: "Update golden (i.e. snapshot) files used by unit tests"
cmds:
@@ -116,35 +213,60 @@ tasks:
- go test ./syft/format/cyclonedxjson -update-cyclonedx-json
- go test ./syft/format/syftjson -update-json
unit:
desc: Run unit tests
deps:
- tmpdir
- fixtures
vars:
TEST_PKGS:
sh: "go list ./... | grep -v {{ .OWNER }}/{{ .PROJECT }}/test | grep -v {{ .OWNER }}/{{ .PROJECT }}/cmd/syft/internal/test | tr '\n' ' '"
# 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
integration:
desc: Run integration tests
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"
validate-cyclonedx-schema:
desc: Validate that Syft produces valid CycloneDX documents
cmds:
- "cd schema/cyclonedx && make"
cli:
desc: Run CLI tests
deps: [tools]
cmds:
- cmd: "echo 'testing binary: {{ .SNAPSHOT_BIN }}'"
silent: true
- cmd: "test -f {{ .SNAPSHOT_BIN }} || (find {{ .SNAPSHOT_DIR }} && echo '\nno snapshot found for {{ .SNAPSHOT_BIN }}' && false)"
silent: true
- "go test -count=1 -timeout=15m -v ./test/cli"
env:
SYFT_BINARY_LOCATION: "{{ .SNAPSHOT_BIN }}"
test-utils:
desc: Run tests for pipeline utils
cmds:
- cmd: .github/scripts/labeler_test.py
- cmd: go test -C .make ./...
snapshot-smoke-test:
desc: Run a smoke test on the snapshot builds + docker images
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 --pull=never --rm anchore/syft:latest-{{ .DOCKER_ARCH }} version
- docker run --pull=never --rm anchore/syft:latest-{{ .DOCKER_ARCH }} scan alpine:latest
## Test-fixture-related targets ###########################################
## Test-fixture-related targets #################################
fingerprints:
desc: Generate fingerprints for all non-docker test fixtures
desc: Generate fingerprints for all non-docker test fixture
silent: true
deps: [tmpdir]
# this will look for `testdata/Makefile` and invoke the `fingerprint` target to calculate all cache input fingerprint files
generates:
- '**/testdata/**/*.fingerprint'
@@ -158,10 +280,16 @@ tasks:
echo -e "${YELLOW}creating fingerprint files for non-docker fixtures...${RESET}"
for dir in $(find . -type d -name 'testdata'); do
if [ -f "$dir/Makefile" ]; then
# for debugging...
#echo -e "${YELLOW}• calculating fingerprints in $dir... ${RESET}"
(make -C "$dir" fingerprint)
fi
done
# for debugging...
# echo -e "generated all fixture fingerprints"
- .github/scripts/fingerprint_docker_fixtures.py
- |
# if DOWNLOAD_TEST_FIXTURE_CACHE is set to 'false', then we don't need to calculate the fingerprint for the cache
@@ -296,14 +424,13 @@ tasks:
done
oras_command+=" {{ .CACHE_PATHS_FILE }}"
oras_command+=" --annotation org.opencontainers.image.source=https://github.com/{{ .OWNER }}/{{ .CACHE_REPO }}"
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
show-test-image-cache:
desc: Print the on-disk + docker daemon state of the stereoscope fixture image cache
silent: true
cmds:
- "echo 'Docker daemon cache:'"
@@ -324,36 +451,28 @@ tasks:
exit 1
fi
## install.sh testing targets #############################################
## install.sh testing targets #################################
install-test:
desc: Run install.sh test suite (delegates to test/install/Makefile)
cmds:
- "cd test/install && make"
install-test-cache-save:
desc: Save the install.sh test image cache (delegates to test/install/Makefile)
cmds:
- "cd test/install && make save"
install-test-cache-load:
desc: Load the install.sh test image cache (delegates to test/install/Makefile)
cmds:
- "cd test/install && make load"
install-test-ci-mac:
desc: Run install.sh CI test suite on macOS (delegates to test/install/Makefile)
cmds:
- "cd test/install && make ci-test-mac"
## Compare-test targets ###################################################
generate-compare-file:
desc: Generate the acceptance comparison reference JSON for the current compare image
cmd: "go run ./cmd/syft {{ .COMPARE_TEST_IMAGE }} -o json > {{ .COMPARE_DIR }}/testdata/acceptance-{{ .COMPARE_TEST_IMAGE }}.json"
compare-mac:
desc: Run macOS install + acceptance comparison against the snapshot build
deps: [tmpdir]
cmd: |
{{ .COMPARE_DIR }}/mac.sh \
@@ -363,13 +482,11 @@ tasks:
{{ .TMP_DIR }}
compare-linux:
desc: Run Linux install + acceptance comparison (deb + rpm) against the snapshot build
cmds:
- task: compare-test-deb-package-install
- task: compare-test-rpm-package-install
compare-test-deb-package-install:
desc: Run Linux .deb install + acceptance comparison against the snapshot build
deps: [tmpdir]
cmd: |
{{ .COMPARE_DIR }}/deb.sh \
@@ -379,7 +496,6 @@ tasks:
{{ .TMP_DIR }}
compare-test-rpm-package-install:
desc: Run Linux .rpm install + acceptance comparison against the snapshot build
deps: [tmpdir]
cmd: |
{{ .COMPARE_DIR }}/rpm.sh \
@@ -389,7 +505,7 @@ tasks:
{{ .TMP_DIR }}
## Code and data generation targets ######################################
## Code and data generation targets #################################
generate:
desc: Add data generation tasks
@@ -439,7 +555,104 @@ tasks:
- "SYFT_ENABLE_COMPLETENESS_TESTS=true go test -p 1 ./internal/capabilities/... -count=1"
## Cleanup targets ########################################################
## Build-related targets #################################
build:
desc: Build the project
deps: [tools, tmpdir]
generates:
- "{{ .PROJECT }}"
cmds:
- silent: true
cmd: |
echo "dist: {{ .SNAPSHOT_DIR }}" > {{ .TMP_DIR }}/goreleaser.yaml
cat .goreleaser.yaml >> {{ .TMP_DIR }}/goreleaser.yaml
- "{{ .BUILD_CMD }}"
snapshot:
desc: Create a snapshot release
aliases:
- build
deps: [tools, tmpdir]
sources:
- cmd/**/*.go
- syft/**/*.go
- internal/**/*.go
method: checksum
generates:
- "{{ .SNAPSHOT_BIN }}"
cmds:
- silent: true
cmd: |
echo "dist: {{ .SNAPSHOT_DIR }}" > {{ .TMP_DIR }}/goreleaser.yaml
cat .goreleaser.yaml >> {{ .TMP_DIR }}/goreleaser.yaml
- "{{ .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]
generates:
- "{{ .CHANGELOG }}"
- "{{ .NEXT_VERSION }}"
cmds:
- "{{ .TOOL_DIR }}/chronicle -vv -n --version-file {{ .NEXT_VERSION }} > {{ .CHANGELOG }}"
- "{{ .TOOL_DIR }}/glow -w 0 {{ .CHANGELOG }}"
## Release targets #################################
release:
desc: Create a release
interactive: true
deps: [tools]
cmds:
- cmd: .github/scripts/trigger-release.sh
silent: true
## CI-only targets #################################
ci-check:
# desc: "[CI only] Are you in CI?"
cmds:
- cmd: .github/scripts/ci-check.sh
silent: true
ci-release:
# desc: "[CI only] Create a release"
deps: [tools]
cmds:
- task: ci-check
- "{{ .TOOL_DIR }}/chronicle -vvv > CHANGELOG.md"
- cmd: "cat CHANGELOG.md"
silent: true
- "{{ .RELEASE_CMD }}"
## Cleanup targets #################################
clean:
desc: Remove all cache files and old builds
cmds:
- task: clean-snapshot
- task: clean-cache
- task: clean-test-observations
- task: clean-docker-cache
- task: clean-oras-cache
clean-snapshot:
desc: Remove any snapshot builds
@@ -461,7 +674,6 @@ tasks:
desc: Remove all image docker tar cache, images from the docker daemon, and ephemeral test fixtures
cmds:
- task: clean-docker-cache
- task: prune-orphan-fingerprints
- |
BOLD='\033[1m'
YELLOW='\033[0;33m'
@@ -477,12 +689,6 @@ tasks:
echo -e "${BOLD}Deleted all ephemeral test fixtures${RESET}"
- rm -f {{ .LAST_CACHE_PULL_FILE }} {{ .CACHE_PATHS_FILE }}
prune-orphan-fingerprints:
desc: Remove *.fingerprint files left behind by moved/deleted catalogers
silent: true
cmds:
- .github/scripts/prune_orphan_fingerprints.py
clean-test-observations:
desc: Remove all test observations (i.e. testdata/test-observations.json)
cmds:
@@ -1,6 +1,6 @@
[TestHandler_handleAttestationStarted/attesting_in_progress/task_line - 1]
⠋ Creating a thing running a thing
⠋ Creating a thing running a thing
---
[TestHandler_handleAttestationStarted/attesting_in_progress/log - 1]
@@ -11,7 +11,7 @@
---
[TestHandler_handleAttestationStarted/attesting_complete/task_line - 1]
✔ Created a thing running a thing
✔ Created a thing running a thing
---
[TestHandler_handleAttestationStarted/attesting_complete/log - 1]
@@ -1,8 +1,8 @@
[TestHandler_handleFetchImage/fetch_image_in_progress - 1]
⠋ Loading image ━━━━━━━━━━━━━━━━━━━━ [current] the-image
⠋ Loading image ━━━━━━━━━━━━━━━━━━━━ [current] the-image
---
[TestHandler_handleFetchImage/fetch_image_complete - 1]
✔ Loaded image the-image
✔ Loaded image the-image
---
@@ -1,8 +1,8 @@
[TestHandler_handleFileIndexingStarted/cataloging_in_progress - 1]
⠋ Indexing file system ━━━━━━━━━━━━━━━━━━━━ [current] /some/path
⠋ Indexing file system ━━━━━━━━━━━━━━━━━━━━ [current] /some/path
---
[TestHandler_handleFileIndexingStarted/cataloging_complete - 1]
✔ Indexed file system /some/path
✔ Indexed file system /some/path
---
@@ -1,8 +1,8 @@
[TestHandler_handlePullSourceStarted/snap_download_in_progress - 1]
⠋ Downloading snap file... ━━━━━━━━━━━━━━━━━━━━ example-app_1.0_amd64.snap
⠋ Downloading snap file... ━━━━━━━━━━━━━━━━━━━━ example-app_1.0_amd64.snap
---
[TestHandler_handlePullSourceStarted/snap_download_complete - 1]
✔ Snap downloaded successfully example-app_1.0_amd64.snap
✔ Snap downloaded successfully example-app_1.0_amd64.snap
---
@@ -1,8 +1,8 @@
[TestHandler_handleReadImage/read_image_in_progress - 1]
⠋ Parsing image ━━━━━━━━━━━━━━━━━━━━ id
⠋ Parsing image ━━━━━━━━━━━━━━━━━━━━ id
---
[TestHandler_handleReadImage/read_image_complete - 1]
✔ Parsed image id
✔ Parsed image id
---
-5
View File
@@ -158,11 +158,6 @@ func (l *backgroundLineReader) read(reader io.Reader, stage *progress.Stage) {
l.lock.Unlock()
}
if err := s.Err(); err != nil {
log.WithFields("error", err).Debug("unable to read attestation output")
// keep draining so the writer never blocks on a full pipe
_, _ = io.Copy(io.Discard, reader)
}
}
func (l backgroundLineReader) Lines() []string {
+1 -1
View File
@@ -59,7 +59,7 @@ func Attest(app clio.Application) *cobra.Command {
Use: "attest --output [FORMAT] <IMAGE>",
Short: "Generate an SBOM as an attestation for the given [SOURCE] container image",
Long: "Generate a packaged-based Software Bill Of Materials (SBOM) from a container image as the predicate of an in-toto attestation that will be uploaded to the image registry",
Example: internal.Tprintf(attestHelp, map[string]any{
Example: internal.Tprintf(attestHelp, map[string]interface{}{
"appName": id.Name,
"command": "attest",
}),
+17 -25
View File
@@ -5,7 +5,6 @@ import (
"encoding/json"
"fmt"
"os"
"slices"
"sort"
"strings"
@@ -60,7 +59,6 @@ type (
Packages []detectorPackageInfo `json:"packages,omitempty"`
Comment string `json:"comment,omitempty"`
PackageTypes []string `json:"package_types,omitempty"`
PURLTypes []string `json:"purl_types,omitempty"`
JSONSchemaTypes []string `json:"json_schema_types,omitempty"`
Capabilities capabilities.CapabilitySet `json:"capabilities,omitempty"`
}
@@ -236,7 +234,7 @@ func renderCatalogerInfoJSON(doc *capabilities.Document, catalogers []capabiliti
// if no parsers, use detectors instead
if len(info.Patterns) == 0 {
info.Capabilities = cat.Capabilities
info.Patterns = convertDetectorsToPatterns(cat.Detectors, cat.PackageTypes, cat.PURLTypes, cat.JSONSchemaTypes)
info.Patterns = convertDetectorsToPatterns(cat.Detectors, cat.PackageTypes, cat.JSONSchemaTypes)
}
info.Config = getConfigInfoFromDocument(doc, cat.Config)
@@ -250,7 +248,12 @@ func renderCatalogerInfoJSON(doc *capabilities.Document, catalogers []capabiliti
// isDeprecatedCataloger checks if a cataloger is deprecated based on its selectors
func isDeprecatedCataloger(selectors []string) bool {
return slices.Contains(selectors, "deprecated")
for _, selector := range selectors {
if selector == "deprecated" {
return true
}
}
return false
}
// convertDetectorPackages converts detector package info to the JSON output format
@@ -279,7 +282,6 @@ func convertParsersToPatterns(parsers []capabilities.Parser) []patternInfo {
Packages: convertDetectorPackages(parser.Detector.Packages),
Comment: parser.Detector.Comment,
PackageTypes: parser.PackageTypes,
PURLTypes: parser.PURLTypes,
JSONSchemaTypes: parser.JSONSchemaTypes,
Capabilities: parser.Capabilities,
})
@@ -288,7 +290,7 @@ func convertParsersToPatterns(parsers []capabilities.Parser) []patternInfo {
}
// convertDetectorsToPatterns converts detector entries to pattern info for JSON output (for non-parser catalogers)
func convertDetectorsToPatterns(detectors []capabilities.Detector, packageTypes, purlTypes, jsonSchemaTypes []string) []patternInfo {
func convertDetectorsToPatterns(detectors []capabilities.Detector, packageTypes, jsonSchemaTypes []string) []patternInfo {
var patterns []patternInfo
for _, det := range detectors {
patterns = append(patterns, patternInfo{
@@ -298,7 +300,6 @@ func convertDetectorsToPatterns(detectors []capabilities.Detector, packageTypes,
Packages: convertDetectorPackages(det.Packages),
Comment: det.Comment,
PackageTypes: packageTypes,
PURLTypes: purlTypes,
JSONSchemaTypes: jsonSchemaTypes,
})
}
@@ -349,14 +350,14 @@ func renderCatalogerInfoTable(_ *capabilities.Document, catalogers []capabilitie
})),
tablewriter.WithConfig(tablewriter.Config{
Row: tw.CellConfig{
Merging: tw.CellMerging{Mode: tw.MergeHierarchical},
Alignment: tw.CellAlignment{Global: tw.AlignLeft},
Formatting: tw.CellFormatting{MergeMode: tw.MergeHierarchical},
Alignment: tw.CellAlignment{Global: tw.AlignLeft},
},
}),
)
// set headers
table.Header("ECOSYSTEM", "CATALOGER", "CRITERIA", "PURL", "LICENSE", "NODES", "EDGES", "KINDS", "LISTING", "DIGESTS", "HASH")
table.Header("ECOSYSTEM", "CATALOGER", "CRITERIA", "LICENSE", "NODES", "EDGES", "KINDS", "LISTING", "DIGESTS", "HASH")
// build rows for each cataloger
var data [][]string
@@ -370,13 +371,13 @@ func renderCatalogerInfoTable(_ *capabilities.Document, catalogers []capabilitie
// generic catalogers: one row per parser
for _, parser := range cat.Parsers {
criteria := formatCriteria([]capabilities.Detector{parser.Detector})
row := buildTableRowFromCapabilities(ecosystem, cat.Name, criteria, parser.PURLTypes, parser.Capabilities)
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.PURLTypes, cat.Capabilities)
row := buildTableRowFromCapabilities(ecosystem, cat.Name, criteria, cat.Capabilities)
data = append(data, row)
}
}
@@ -388,7 +389,7 @@ func renderCatalogerInfoTable(_ *capabilities.Document, catalogers []capabilitie
}
// buildTableRowFromCapabilities builds a table row from capability values
func buildTableRowFromCapabilities(ecosystem, name, criteria string, purlTypes []string, caps capabilities.CapabilitySet) []string {
func buildTableRowFromCapabilities(ecosystem, name, criteria string, caps capabilities.CapabilitySet) []string {
// extract capability default values
license := extractBoolCapability(caps, "license")
nodes := extractNodesCapability(caps)
@@ -402,7 +403,6 @@ func buildTableRowFromCapabilities(ecosystem, name, criteria string, purlTypes [
ecosystem,
name,
criteria,
formatPURLTypes(purlTypes),
license,
nodes,
edges,
@@ -413,14 +413,6 @@ func buildTableRowFromCapabilities(ecosystem, name, criteria string, purlTypes [
}
}
// formatPURLTypes renders the PURL types as a comma-separated list, or a placeholder when empty
func formatPURLTypes(purlTypes []string) string {
if len(purlTypes) == 0 {
return noStyle.Render("·")
}
return strings.Join(purlTypes, ", ")
}
// extractBoolCapability extracts a boolean capability value and formats it
func extractBoolCapability(caps capabilities.CapabilitySet, name string) string {
for _, cap := range caps {
@@ -460,7 +452,7 @@ func extractArrayCapability(caps capabilities.CapabilitySet, name string) string
if len(v) > 0 {
return strings.Join(v, ", ")
}
case []any:
case []interface{}:
if len(v) > 0 {
strs := make([]string, 0, len(v))
for _, item := range v {
@@ -483,7 +475,7 @@ func extractNodesCapability(caps capabilities.CapabilitySet) string {
switch v := cap.Default.(type) {
case []string:
return formatDepthStringArray(v)
case []any:
case []interface{}:
return formatDepthInterfaceArray(v)
}
return noStyle.Render("·")
@@ -504,7 +496,7 @@ func formatDepthStringArray(v []string) string {
}
// formatDepthInterfaceArray formats a []interface{} dependency depth value
func formatDepthInterfaceArray(v []any) string {
func formatDepthInterfaceArray(v []interface{}) string {
if len(v) == 0 {
return noStyle.Render("·")
}
@@ -280,15 +280,12 @@ func Test_catalogerInfoReport(t *testing.T) {
Detectors: []capabilities.Detector{
{Method: capabilities.GlobDetection, Criteria: []string{"**/*.test"}},
},
PURLTypes: []string{"npm"},
},
},
assertions: func(t *testing.T, got string) {
assert.Contains(t, got, "test-cataloger")
assert.Contains(t, got, "ECOSYSTEM")
assert.Contains(t, got, "CATALOGER")
assert.Contains(t, got, "PURL")
assert.Contains(t, got, "npm")
},
},
{
@@ -301,17 +298,12 @@ func Test_catalogerInfoReport(t *testing.T) {
Ecosystem: "test",
Type: "custom",
Selectors: []string{"test", "custom"},
Detectors: []capabilities.Detector{
{Method: capabilities.GlobDetection, Criteria: []string{"**/*.test"}},
},
PURLTypes: []string{"npm"},
},
},
assertions: func(t *testing.T, got string) {
assert.Contains(t, got, `"name":"test-cataloger"`)
assert.Contains(t, got, `"ecosystem":"test"`)
assert.Contains(t, got, `"type":"custom"`)
assert.Contains(t, got, `"purl_types":["npm"]`)
},
},
{
+18 -231
View File
@@ -1,34 +1,24 @@
package commands
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
"github.com/anchore/clio"
"github.com/anchore/go-homedir"
"github.com/anchore/syft/cmd/syft/internal/options"
"github.com/anchore/syft/cmd/syft/internal/ui"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/spillbuf"
"github.com/anchore/syft/internal/tmpdir"
"github.com/anchore/syft/syft/format"
"github.com/anchore/syft/syft/format/syftjson"
"github.com/anchore/syft/syft/sbom"
)
const (
convertExample = ` {{.appName}} {{.command}} img.syft.json -o spdx-json convert a syft SBOM to spdx-json, output goes to stdout
{{.appName}} {{.command}} img.syft.json -o cyclonedx-json=img.cdx.json convert a syft SBOM to CycloneDX, output is written to the file "img.cdx.json"
{{.appName}} {{.command}} - -o spdx-json convert an SBOM from STDIN to spdx-json
SYFT_CONVERT_PASSTHROUGH_EXACT_FORMAT=true {{.appName}} {{.command}} img.syft.json -o syft-json=out.syft.json copy the input to "out.syft.json" unchanged if it is already syft-json at the current schema version
`
)
@@ -36,7 +26,6 @@ type ConvertOptions struct {
options.Config `yaml:",inline" mapstructure:",squash"`
options.Output `yaml:",inline" mapstructure:",squash"`
options.UpdateCheck `yaml:",inline" mapstructure:",squash"`
Convert options.Convert `yaml:"convert" json:"convert" mapstructure:"convert"`
}
func Convert(app clio.Application) *cobra.Command {
@@ -45,24 +34,23 @@ func Convert(app clio.Application) *cobra.Command {
opts := &ConvertOptions{
UpdateCheck: options.DefaultUpdateCheck(),
Output: options.DefaultOutput(),
Convert: options.DefaultConvert(),
}
return app.SetupCommand(&cobra.Command{
Use: "convert [SOURCE-SBOM] -o [FORMAT]",
Short: "Convert between SBOM formats",
Long: "[Experimental] Convert SBOM files to, and from, SPDX, CycloneDX and Syft's format. For more info about data loss between formats see https://github.com/anchore/syft/wiki/format-conversion",
Example: internal.Tprintf(convertExample, map[string]any{
Example: internal.Tprintf(convertExample, map[string]interface{}{
"appName": id.Name,
"command": "convert",
}),
Args: validateConvertArgs,
PreRunE: applicationUpdateCheck(id, &opts.UpdateCheck),
RunE: func(cmd *cobra.Command, args []string) error {
RunE: func(_ *cobra.Command, args []string) error {
restoreStdout := ui.CaptureStdoutToTraceLog()
defer restoreStdout()
return RunConvert(cmd.Context(), opts, args[0])
return RunConvert(opts, args[0])
},
}, opts)
}
@@ -71,63 +59,30 @@ func validateConvertArgs(cmd *cobra.Command, args []string) error {
return validateArgs(cmd, args, "an SBOM argument is required")
}
func RunConvert(ctx context.Context, opts *ConvertOptions, userInput string) error {
func RunConvert(opts *ConvertOptions, userInput string) error {
log.Warn("convert is an experimental feature, run `syft convert -h` for help")
// a temp dir is only needed when STDIN is a pipe and has to be spilled to disk; it is created lazily on
// first use (under the user's TMPDIR), so a file input never touches it. Mirrors syft.CreateSBOM, which
// also honors a TempDir already attached to the context.
td := tmpdir.FromContext(ctx)
if td == nil {
_, td = tmpdir.Root(ctx, "syft-convert")
defer func() {
_ = td.Cleanup()
}()
}
reader, inputInfo, err := openConvertInput(userInput, td)
writer, err := opts.SBOMWriter()
if err != nil {
return err
}
defer func() {
_ = reader.Close()
}()
outputs := opts.Outputs
var unchanged []string
if opts.Convert.PassthroughExactFormat {
log.Warn("convert.passthrough-exact-format is enabled: syft-json input that already exactly matches a requested output format and schema version will be copied to that output unchanged (this does not apply to any other input format)")
var reader io.ReadSeekCloser
unchanged, outputs, err = partitionOutputsBySourceFormat(opts.Output, reader)
if userInput == "-" {
// though os.Stdin is an os.File, it does not support seeking
// you will get errors such as "seek /dev/stdin: illegal seek".
// We need to buffer what we read.
reader = internal.NewBufferedSeeker(os.Stdin)
} else {
f, err := os.Open(userInput)
if err != nil {
return err
return fmt.Errorf("failed to open SBOM file: %w", err)
}
}
// resolve (and validate) every output that needs converting before writing anything, so that
// a bad -o value fails the command without leaving partial results behind
var writer sbom.Writer
if len(outputs) > 0 {
outputOpts := opts.Output
outputOpts.Outputs = outputs
writer, err = outputOpts.SBOMWriter()
if err != nil {
return err
}
}
for _, output := range unchanged {
if err := writeUnchanged(output, opts.LegacyFile, inputInfo, reader); err != nil {
return err
}
}
if writer == nil {
return nil
}
if _, err := reader.Seek(0, io.SeekStart); err != nil {
return fmt.Errorf("unable to seek to start of SBOM: %w", err)
defer func() {
_ = f.Close()
}()
reader = f
}
s, _, _, err := format.Decode(reader)
@@ -145,171 +100,3 @@ func RunConvert(ctx context.Context, opts *ConvertOptions, userInput string) err
return nil
}
// openConvertInput opens the SBOM document at the given path, or STDIN when "-". The returned reader is seekable so
// the document can be identified, copied, and decoded in turn without being loaded into memory. The returned
// os.FileInfo describes the file the document is being read from, so that writing back over it can be recognized;
// it is nil when the input is not a file (a pipe or a terminal on STDIN).
func openConvertInput(userInput string, td *tmpdir.TempDir) (io.ReadSeekCloser, os.FileInfo, error) {
if userInput == "-" {
return openStdin(td)
}
f, err := os.Open(userInput)
if err != nil {
return nil, nil, fmt.Errorf("failed to open SBOM file: %w", err)
}
info, err := f.Stat()
if err != nil {
// the document is readable, we just cannot recognize it as an output destination
log.WithFields("error", err, "path", userInput).Debug("unable to stat SBOM file")
}
return f, info, nil
}
// openStdin returns a seekable view of STDIN. When STDIN is a regular file (such as a shell redirect) it is read in
// place. A pipe or terminal cannot seek (you will get errors such as "seek /dev/stdin: illegal seek"), so its
// contents are drained into a spill buffer: a small head stays in memory and everything beyond it goes to a temp
// file under td, so an arbitrarily large piped SBOM never has to be held in memory to be made seekable.
func openStdin(td *tmpdir.TempDir) (io.ReadSeekCloser, os.FileInfo, error) {
if info, err := os.Stdin.Stat(); err == nil && info.Mode().IsRegular() {
if start, err := os.Stdin.Seek(0, io.SeekCurrent); err == nil {
// a redirect reads the file in place, so it is just as much an input file as a path argument is
return readSeekNopCloser{io.NewSectionReader(os.Stdin, start, info.Size()-start)}, info, nil
}
}
buf := spillbuf.New(td)
if _, err := io.Copy(io.NewOffsetWriter(buf, 0), os.Stdin); err != nil {
_ = buf.Close()
return nil, nil, fmt.Errorf("failed to read SBOM from STDIN: %w", err)
}
return &spilledReader{
SectionReader: io.NewSectionReader(buf, 0, buf.Size()),
buf: buf,
}, nil, nil
}
type readSeekNopCloser struct {
io.ReadSeeker
}
func (readSeekNopCloser) Close() error {
return nil
}
// spilledReader is a seekable view over a spill buffer; closing it releases the buffer's memory and spill file.
type spilledReader struct {
*io.SectionReader
buf *spillbuf.Buffer
}
func (r *spilledReader) Close() error {
return r.buf.Close()
}
// partitionOutputsBySourceFormat splits the requested outputs (in "<format>[@<version>][=<path>]" form) into those
// that exactly match the format and schema version of a syft-json input document (which can be written out
// unchanged) and those that still require conversion. Input in any other format is always converted. Outputs that
// cannot be resolved to an encoder are left for the writer to report on.
func partitionOutputsBySourceFormat(output options.Output, reader io.ReadSeeker) (unchanged []string, toConvert []string, err error) {
if _, err := reader.Seek(0, io.SeekStart); err != nil {
return nil, nil, fmt.Errorf("unable to seek to start of SBOM: %w", err)
}
id, version := format.Identify(reader)
if id == "" || version == "" {
// let decoding surface the appropriate error
return nil, output.Outputs, nil
}
if id != syftjson.ID {
log.WithFields("format", id, "version", version).Debug("input is not syft-json, passthrough does not apply")
return nil, output.Outputs, nil
}
encoders, err := output.Encoders()
if err != nil {
return nil, nil, err
}
collection := format.NewEncoderCollection(encoders...)
for _, out := range output.Outputs {
name, _, _ := strings.Cut(strings.TrimSpace(out), "=")
enc := collection.GetByString(name)
if enc != nil && enc.ID() == id && enc.Version() == version {
log.WithFields("format", id, "version", version, "output", out).Warn("input already exactly matches the requested output format and schema version, copying it unchanged instead of converting it")
unchanged = append(unchanged, out)
continue
}
toConvert = append(toConvert, out)
}
return unchanged, toConvert, nil
}
// writeUnchanged copies the SBOM document as-is to the destination described by the given "<format>[=<path>]"
// output value, falling back to the (deprecated) --file path, and finally to the report bus (STDOUT). When the
// destination is the input file itself there is nothing to do.
func writeUnchanged(output string, defaultFile string, inputInfo os.FileInfo, reader io.ReadSeeker) error {
if _, err := reader.Seek(0, io.SeekStart); err != nil {
return fmt.Errorf("unable to seek to start of SBOM: %w", err)
}
_, path, _ := strings.Cut(strings.TrimSpace(output), "=")
if path == "" {
path = defaultFile
}
if path == "" {
// the report bus only carries strings, so STDOUT output must be materialized in memory
content, err := io.ReadAll(reader) //nolint:gocritic // the whole SBOM is the report; there is no meaningful bound
if err != nil {
return fmt.Errorf("unable to read SBOM: %w", err)
}
bus.Report(string(content))
return nil
}
expandedPath, err := homedir.Expand(path)
if err != nil {
log.Warnf("could not expand given writer output path=%q: %w", path, err)
expandedPath = path
}
if isSameFile(inputInfo, expandedPath) {
log.WithFields("path", expandedPath).Info("output is the input file, leaving it as-is")
return nil
}
if err := os.MkdirAll(filepath.Dir(expandedPath), 0755); err != nil {
return fmt.Errorf("unable to create report directory: %w", err)
}
f, err := os.OpenFile(expandedPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return fmt.Errorf("unable to create report file: %w", err)
}
if _, err := io.Copy(f, reader); err != nil {
_ = f.Close()
return fmt.Errorf("unable to write report file: %w", err)
}
return f.Close()
}
// isSameFile reports whether the given path names the file the input is being read from. Comparing the input's
// os.FileInfo rather than its path covers a shell redirect onto STDIN, which has no path to compare.
func isSameFile(inputInfo os.FileInfo, path string) bool {
if inputInfo == nil {
return false
}
pathInfo, err := os.Stat(path)
if err != nil {
return false
}
return os.SameFile(inputInfo, pathInfo)
}
-448
View File
@@ -1,448 +0,0 @@
package commands
import (
"bytes"
"context"
"fmt"
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/wagoodman/go-partybus"
"github.com/anchore/syft/cmd/syft/internal/options"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/spillbuf"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/format"
"github.com/anchore/syft/syft/format/cyclonedxjson"
"github.com/anchore/syft/syft/format/spdxjson"
"github.com/anchore/syft/syft/format/syftjson"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
func convertTestSBOM() sbom.SBOM {
catalog := pkg.NewCollection()
catalog.Add(pkg.Package{
Name: "musl-utils",
Version: "1.2.3-r4",
Type: pkg.ApkPkg,
FoundBy: "apk-db-cataloger",
Locations: file.NewLocationSet(
file.NewLocation("/lib/apk/db/installed"),
),
PURL: "pkg:apk/alpine/musl-utils@1.2.3-r4",
})
return sbom.SBOM{
Artifacts: sbom.Artifacts{
Packages: catalog,
},
Source: source.Description{
ID: "some-id",
Name: "some-dir",
Version: "latest",
Metadata: source.DirectoryMetadata{
Path: "/some/path",
},
},
Descriptor: sbom.Descriptor{
Name: "syft",
Version: "v0.0.0-test",
},
}
}
func mustConvertEncoder(enc sbom.FormatEncoder, err error) sbom.FormatEncoder {
if err != nil {
panic(err)
}
return enc
}
func encodeConvertTestSBOM(t *testing.T, enc sbom.FormatEncoder) []byte {
buf := &bytes.Buffer{}
require.NoError(t, enc.Encode(buf, convertTestSBOM()))
return buf.Bytes()
}
// prettySyftJSON returns the test SBOM as pretty-printed syft-json. Pretty input makes a verbatim copy
// distinguishable from a re-encode, since re-encoding always produces compact output here.
func prettySyftJSON(t *testing.T) []byte {
cfg := syftjson.DefaultEncoderConfig()
cfg.Pretty = true
return encodeConvertTestSBOM(t, mustConvertEncoder(syftjson.NewFormatEncoderWithConfig(cfg)))
}
func Test_partitionOutputsBySourceFormat(t *testing.T) {
syftEncoder := mustConvertEncoder(syftjson.NewFormatEncoderWithConfig(syftjson.DefaultEncoderConfig()))
syftVersion := syftEncoder.Version()
syftContent := encodeConvertTestSBOM(t, syftEncoder)
// the same document claiming an older schema version
olderSyftContent := bytes.ReplaceAll(syftContent, []byte(`"version":"`+syftVersion+`"`), []byte(`"version":"16.0.0"`))
require.NotEqual(t, syftContent, olderSyftContent)
cdxContent := encodeConvertTestSBOM(t, mustConvertEncoder(cyclonedxjson.NewFormatEncoderWithConfig(cyclonedxjson.DefaultEncoderConfig())))
tests := []struct {
name string
content []byte
outputs []string
wantUnchanged []string
wantToConvert []string
}{
{
name: "syft-json matches the bare format name",
content: syftContent,
outputs: []string{"syft-json"},
wantUnchanged: []string{"syft-json"},
},
{
name: "syft-json matches its alias",
content: syftContent,
outputs: []string{"json"},
wantUnchanged: []string{"json"},
},
{
name: "syft-json matches an explicit version and file path",
content: syftContent,
outputs: []string{"syft-json@" + syftVersion + "=out/some.syft.json"},
wantUnchanged: []string{"syft-json@" + syftVersion + "=out/some.syft.json"},
},
{
name: "syft-json at an older schema version requires conversion",
content: olderSyftContent,
outputs: []string{"syft-json"},
wantToConvert: []string{"syft-json"},
},
{
name: "a different output format requires conversion",
content: syftContent,
outputs: []string{"spdx-json"},
wantToConvert: []string{"spdx-json"},
},
{
name: "mixed outputs are partitioned",
content: syftContent,
outputs: []string{"syft-json=a.json", "spdx-json=b.json", "cyclonedx-xml"},
wantUnchanged: []string{"syft-json=a.json"},
wantToConvert: []string{"spdx-json=b.json", "cyclonedx-xml"},
},
{
name: "unknown format is left for the writer to report",
content: syftContent,
outputs: []string{"bogus"},
wantToConvert: []string{"bogus"},
},
{
name: "non syft-json input is always converted, even when it matches the output",
content: cdxContent,
outputs: []string{"cyclonedx-json"},
wantToConvert: []string{"cyclonedx-json"},
},
{
name: "unidentifiable content is left for the decoder to report",
content: []byte("definitely not an sbom"),
outputs: []string{"syft-json"},
wantToConvert: []string{"syft-json"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
output := options.DefaultOutput()
output.Outputs = tt.outputs
unchanged, toConvert, err := partitionOutputsBySourceFormat(output, bytes.NewReader(tt.content))
require.NoError(t, err)
assert.Equal(t, tt.wantUnchanged, unchanged)
assert.Equal(t, tt.wantToConvert, toConvert)
})
}
}
func Test_RunConvert_passthroughExactFormat(t *testing.T) {
syftContent := prettySyftJSON(t)
newOpts := func(passthrough bool, outputs ...string) *ConvertOptions {
opts := &ConvertOptions{
Output: options.DefaultOutput(),
Convert: options.Convert{
PassthroughExactFormat: passthrough,
},
}
opts.Outputs = outputs
return opts
}
writeInput := func(t *testing.T, content []byte) string {
input := filepath.Join(t.TempDir(), "input.json")
require.NoError(t, os.WriteFile(input, content, 0600))
return input
}
t.Run("matching output is copied unchanged while others are converted", func(t *testing.T) {
dir := t.TempDir()
syftOut := filepath.Join(dir, "out.syft.json")
spdxOut := filepath.Join(dir, "out.spdx.json")
opts := newOpts(true, "syft-json="+syftOut, "spdx-json="+spdxOut)
require.NoError(t, RunConvert(context.Background(), opts, writeInput(t, syftContent)))
got, err := os.ReadFile(syftOut)
require.NoError(t, err)
assert.Equal(t, syftContent, got)
spdx, err := os.ReadFile(spdxOut)
require.NoError(t, err)
id, _ := format.Identify(bytes.NewReader(spdx))
assert.Equal(t, spdxjson.ID, id)
})
t.Run("STDIN input is copied unchanged while others are converted", func(t *testing.T) {
dir := t.TempDir()
syftOut := filepath.Join(dir, "out.syft.json")
spdxOut := filepath.Join(dir, "out.spdx.json")
stdin, err := os.Open(writeInput(t, syftContent))
require.NoError(t, err)
defer stdin.Close()
originalStdin := os.Stdin
os.Stdin = stdin
t.Cleanup(func() { os.Stdin = originalStdin })
opts := newOpts(true, "syft-json="+syftOut, "spdx-json="+spdxOut)
require.NoError(t, RunConvert(context.Background(), opts, "-"))
got, err := os.ReadFile(syftOut)
require.NoError(t, err)
assert.Equal(t, syftContent, got)
spdx, err := os.ReadFile(spdxOut)
require.NoError(t, err)
id, _ := format.Identify(bytes.NewReader(spdx))
assert.Equal(t, spdxjson.ID, id)
})
t.Run("piped STDIN input is copied unchanged while others are converted", func(t *testing.T) {
dir := t.TempDir()
syftOut := filepath.Join(dir, "out.syft.json")
spdxOut := filepath.Join(dir, "out.spdx.json")
pipeReader, pipeWriter, err := os.Pipe()
require.NoError(t, err)
defer pipeReader.Close()
go func() {
defer pipeWriter.Close()
_, _ = pipeWriter.Write(syftContent)
}()
originalStdin := os.Stdin
os.Stdin = pipeReader
t.Cleanup(func() { os.Stdin = originalStdin })
opts := newOpts(true, "syft-json="+syftOut, "spdx-json="+spdxOut)
require.NoError(t, RunConvert(context.Background(), opts, "-"))
got, err := os.ReadFile(syftOut)
require.NoError(t, err)
assert.Equal(t, syftContent, got)
spdx, err := os.ReadFile(spdxOut)
require.NoError(t, err)
id, _ := format.Identify(bytes.NewReader(spdx))
assert.Equal(t, spdxjson.ID, id)
})
t.Run("without the option the matching output is re-encoded", func(t *testing.T) {
syftOut := filepath.Join(t.TempDir(), "out.syft.json")
opts := newOpts(false, "syft-json="+syftOut)
require.NoError(t, RunConvert(context.Background(), opts, writeInput(t, syftContent)))
got, err := os.ReadFile(syftOut)
require.NoError(t, err)
id, _ := format.Identify(bytes.NewReader(got))
assert.Equal(t, syftjson.ID, id)
// the input was pretty-printed and the encoder is not configured to be, so a re-encode cannot reproduce it
assert.NotEqual(t, syftContent, got)
})
t.Run("non syft-json input is re-encoded even when it matches the output", func(t *testing.T) {
cdxContent := encodeConvertTestSBOM(t, mustConvertEncoder(cyclonedxjson.NewFormatEncoderWithConfig(cyclonedxjson.DefaultEncoderConfig())))
cdxOut := filepath.Join(t.TempDir(), "out.cdx.json")
opts := newOpts(true, "cyclonedx-json="+cdxOut)
require.NoError(t, RunConvert(context.Background(), opts, writeInput(t, cdxContent)))
got, err := os.ReadFile(cdxOut)
require.NoError(t, err)
id, _ := format.Identify(bytes.NewReader(got))
assert.Equal(t, cyclonedxjson.ID, id)
// a fresh serial number is minted on every encode, so a round trip never reproduces the input
assert.NotEqual(t, cdxContent, got)
})
t.Run("the deprecated --file path is honored", func(t *testing.T) {
syftOut := filepath.Join(t.TempDir(), "out.syft.json")
opts := newOpts(true, "syft-json")
opts.LegacyFile = syftOut
require.NoError(t, RunConvert(context.Background(), opts, writeInput(t, syftContent)))
got, err := os.ReadFile(syftOut)
require.NoError(t, err)
assert.Equal(t, syftContent, got)
})
t.Run("writing over the input file leaves it intact", func(t *testing.T) {
input := writeInput(t, syftContent)
opts := newOpts(true, "syft-json="+input)
require.NoError(t, RunConvert(context.Background(), opts, input))
got, err := os.ReadFile(input)
require.NoError(t, err)
assert.Equal(t, syftContent, got)
})
t.Run("writing over a redirected STDIN input leaves it intact", func(t *testing.T) {
input := writeInput(t, syftContent)
stdin, err := os.Open(input)
require.NoError(t, err)
defer stdin.Close()
originalStdin := os.Stdin
os.Stdin = stdin
t.Cleanup(func() { os.Stdin = originalStdin })
opts := newOpts(true, "syft-json="+input)
require.NoError(t, RunConvert(context.Background(), opts, "-"))
got, err := os.ReadFile(input)
require.NoError(t, err)
assert.Equal(t, syftContent, got)
})
t.Run("an invalid output fails before anything is written", func(t *testing.T) {
syftOut := filepath.Join(t.TempDir(), "out.syft.json")
opts := newOpts(true, "syft-json="+syftOut, "bogus")
require.Error(t, RunConvert(context.Background(), opts, writeInput(t, syftContent)))
assert.NoFileExists(t, syftOut)
})
t.Run("a missing input file is reported", func(t *testing.T) {
syftOut := filepath.Join(t.TempDir(), "out.syft.json")
opts := newOpts(true, "syft-json="+syftOut)
err := RunConvert(context.Background(), opts, filepath.Join(t.TempDir(), "does-not-exist.json"))
require.ErrorContains(t, err, "failed to open SBOM file")
assert.NoFileExists(t, syftOut)
})
t.Run("unrecognized input is left to the decoder to reject", func(t *testing.T) {
syftOut := filepath.Join(t.TempDir(), "out.syft.json")
opts := newOpts(true, "syft-json="+syftOut)
err := RunConvert(context.Background(), opts, writeInput(t, []byte("definitely not an sbom")))
require.ErrorContains(t, err, "failed to decode SBOM")
})
}
// largeSyftJSON returns a pretty-printed syft-json document of at least minBytes, built from many small packages.
func largeSyftJSON(t *testing.T, minBytes int64) []byte {
catalog := pkg.NewCollection()
for i := 0; i < int(minBytes/100); i++ {
catalog.Add(pkg.Package{
Name: fmt.Sprintf("pkg-%d", i),
Version: "1.0.0",
Type: pkg.ApkPkg,
PURL: fmt.Sprintf("pkg:apk/alpine/pkg-%d@1.0.0", i),
})
}
s := convertTestSBOM()
s.Artifacts.Packages = catalog
cfg := syftjson.DefaultEncoderConfig()
cfg.Pretty = true
enc := mustConvertEncoder(syftjson.NewFormatEncoderWithConfig(cfg))
buf := &bytes.Buffer{}
require.NoError(t, enc.Encode(buf, s))
require.Greater(t, int64(buf.Len()), minBytes)
return buf.Bytes()
}
func Test_RunConvert_pipedInputLargerThanMemoryLimitSpillsToDisk(t *testing.T) {
// a piped document bigger than the spill buffer's in-memory tier is written to a temp file under TMPDIR;
// the conversion must identify, copy and clean up from there rather than holding the document in memory
dir := t.TempDir()
syftOut := filepath.Join(dir, "out.syft.json")
spillRoot := filepath.Join(dir, "spill")
require.NoError(t, os.Mkdir(spillRoot, 0o755))
t.Setenv("TMPDIR", spillRoot)
content := largeSyftJSON(t, 2*spillbuf.DefaultMemLimit)
pipeReader, pipeWriter, err := os.Pipe()
require.NoError(t, err)
defer pipeReader.Close()
go func() {
defer pipeWriter.Close()
_, _ = pipeWriter.Write(content)
}()
originalStdin := os.Stdin
os.Stdin = pipeReader
t.Cleanup(func() { os.Stdin = originalStdin })
opts := &ConvertOptions{
Output: options.DefaultOutput(),
Convert: options.Convert{
PassthroughExactFormat: true,
},
}
opts.Outputs = []string{"syft-json=" + syftOut}
require.NoError(t, RunConvert(context.Background(), opts, "-"))
got, err := os.ReadFile(syftOut)
require.NoError(t, err)
assert.Equal(t, content, got)
entries, err := os.ReadDir(spillRoot)
require.NoError(t, err)
assert.Empty(t, entries, "the spill directory must be removed once the conversion finishes")
}
func Test_writeUnchanged_stdout(t *testing.T) {
// with no output path the document is published on the report bus, which is how the CLI writes to STDOUT
b := partybus.NewBus()
subscription := b.Subscribe()
bus.Set(b)
t.Cleanup(func() {
bus.Set(nil)
})
content := []byte(`{"some":"document"}`)
require.NoError(t, writeUnchanged("syft-json", "", nil, bytes.NewReader(content)))
select {
case e := <-subscription.Events():
assert.Equal(t, event.CLIReport, e.Type)
assert.Equal(t, string(content), e.Value)
case <-time.After(time.Second):
t.Fatal("no report was published")
}
}
+1 -1
View File
@@ -18,7 +18,7 @@ func Packages(app clio.Application, scanCmd *cobra.Command) *cobra.Command {
Short: scanCmd.Short,
Long: scanCmd.Long,
Args: scanCmd.Args,
Example: internal.Tprintf(scanHelp, map[string]any{
Example: internal.Tprintf(scanHelp, map[string]interface{}{
"appName": id.Name,
"command": "packages",
}),
+6 -6
View File
@@ -29,7 +29,7 @@ func Test_filterExpressionErrors_expressionErrorsHelp(t *testing.T) {
{
name: "single non-expression error is retained",
err: errors.New("foo"),
wantErr: func(t assert.TestingT, err error, i ...any) bool {
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
return assert.Equal(t, "foo", err.Error())
},
wantHelp: "",
@@ -42,7 +42,7 @@ func Test_filterExpressionErrors_expressionErrorsHelp(t *testing.T) {
err = multierror.Append(err, errors.New("bar"))
return err
}(),
wantErr: func(t assert.TestingT, err error, i ...any) bool {
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
// note: this is the default formatting from the hashicorp multierror object
expected := `2 errors occurred:
* foo
@@ -64,7 +64,7 @@ func Test_filterExpressionErrors_expressionErrorsHelp(t *testing.T) {
err = multierror.Append(err, errors.New("last"))
return err
}(),
wantErr: func(t assert.TestingT, err error, i ...any) bool {
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
expected := `5 errors occurred:
* foo
* invalid expression: "foo": tags are not allowed with this operation (must use exact names)
@@ -103,7 +103,7 @@ func Test_filterExpressionErrors_expressionErrorsHelp(t *testing.T) {
return fmt.Errorf("top: %w", fmt.Errorf("middle: %w", err))
}(),
wantErr: func(t assert.TestingT, err error, i ...any) bool {
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
expected := `top: middle: 4 errors occurred:
* foo: bar: last
* invalid expression: "foo": tags are not allowed with this operation (must use exact names)
@@ -142,7 +142,7 @@ func Test_filterExpressionErrors_expressionErrorsHelp(t *testing.T) {
// note we wrap the top error in a chain
return fmt.Errorf("top: %w", fmt.Errorf("middle: %w", err))
}(),
wantErr: func(t assert.TestingT, err error, i ...any) bool {
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
expected := `top: middle: 4 errors occurred:
* foo: bar: last
* invalid expression: "foo": tags are not allowed with this operation (must use exact names)
@@ -178,7 +178,7 @@ func Test_filterExpressionErrors_expressionErrorsHelp(t *testing.T) {
return err
}(),
wantErr: func(t assert.TestingT, err error, i ...any) bool {
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
// note: the errors are removed and the help text shows the enriched error help
expected := `2 errors occurred:
* invalid expression: "foo": tags are not allowed with this operation (must use exact names)
+1 -7
View File
@@ -7,7 +7,6 @@ import (
"os"
"reflect"
"strings"
"time"
"github.com/hashicorp/go-multierror"
"github.com/spf13/cobra"
@@ -93,7 +92,7 @@ func Scan(app clio.Application) *cobra.Command {
Use: "scan [SOURCE]",
Short: "Generate an SBOM",
Long: "Generate a packaged-based Software Bill Of Materials (SBOM) from container images and filesystems",
Example: internal.Tprintf(scanHelp, map[string]any{
Example: internal.Tprintf(scanHelp, map[string]interface{}{
"appName": id.Name,
"command": "scan",
}),
@@ -172,11 +171,6 @@ func validateArgs(cmd *cobra.Command, args []string, err string) error {
}
func runScan(ctx context.Context, id clio.Identification, opts *scanOptions, userInput string) error {
scanStart := time.Now()
defer func() {
log.Infof("scan completed in %s", time.Since(scanStart).Round(time.Millisecond))
}()
writer, err := opts.SBOMWriter()
if err != nil {
return err
+1 -1
View File
@@ -60,7 +60,7 @@ func Test_scanOptions_validateLegacyOptionsNotUsed(t *testing.T) {
}
func assertErrorContains(contains string) assert.ErrorAssertionFunc {
return func(t assert.TestingT, err error, i ...any) bool {
return func(t assert.TestingT, err error, i ...interface{}) bool {
return assert.ErrorContains(t, err, contains, i...)
}
}
+6 -15
View File
@@ -2,7 +2,6 @@ package options
import (
"fmt"
"slices"
"sort"
"strings"
@@ -20,7 +19,6 @@ import (
"github.com/anchore/syft/syft/file/cataloger/executable"
"github.com/anchore/syft/syft/file/cataloger/filecontent"
"github.com/anchore/syft/syft/pkg/cataloger/binary"
"github.com/anchore/syft/syft/pkg/cataloger/cpp"
"github.com/anchore/syft/syft/pkg/cataloger/dotnet"
"github.com/anchore/syft/syft/pkg/cataloger/golang"
"github.com/anchore/syft/syft/pkg/cataloger/java"
@@ -45,7 +43,6 @@ type Catalog struct {
Enrich []string `yaml:"enrich" json:"enrich" mapstructure:"enrich"`
// ecosystem-specific cataloger configuration
Cpp cppConfig `yaml:"cpp" json:"cpp" mapstructure:"cpp"`
Dotnet dotnetConfig `yaml:"dotnet" json:"dotnet" mapstructure:"dotnet"`
Golang golangConfig `yaml:"golang" json:"golang" mapstructure:"golang"`
Java javaConfig `yaml:"java" json:"java" mapstructure:"java"`
@@ -82,7 +79,6 @@ func DefaultCatalog() Catalog {
JavaScript: defaultJavaScriptConfig(),
Python: defaultPythonConfig(),
Nix: defaultNixConfig(),
Cpp: defaultCppConfig(),
Dotnet: defaultDotnetConfig(),
Golang: defaultGolangConfig(),
Java: defaultJavaConfig(),
@@ -175,15 +171,11 @@ func (cfg Catalog) ToPackagesConfig() pkgcataloging.Config {
}
return pkgcataloging.Config{
Binary: binary.DefaultClassifierCatalogerConfig(),
Cpp: cpp.DefaultCatalogerConfig().
WithVcpkgAllowGitClone(*multiLevelOption(false, enrichmentEnabled(cfg.Enrich, task.Cpp, task.Vcpkg), cfg.Cpp.VcpkgAllowGitClone)),
Dotnet: dotnet.DefaultCatalogerConfig().
WithDepPackagesMustHaveDLL(cfg.Dotnet.DepPackagesMustHaveDLL).
WithDepPackagesMustClaimDLL(cfg.Dotnet.DepPackagesMustClaimDLL).
WithPropagateDLLClaimsToParents(cfg.Dotnet.PropagateDLLClaimsToParents).
WithRelaxDLLClaimsWhenBundlingDetected(cfg.Dotnet.RelaxDLLClaimsWhenBundlingDetected).
WithExcludeProjectReferences(cfg.Dotnet.ExcludeProjectReferences),
WithRelaxDLLClaimsWhenBundlingDetected(cfg.Dotnet.RelaxDLLClaimsWhenBundlingDetected),
Golang: golang.DefaultCatalogerConfig().
WithSearchLocalModCacheLicenses(*multiLevelOption(false, enrichmentEnabled(cfg.Enrich, task.Go, task.Golang), cfg.Golang.SearchLocalModCacheLicenses)).
WithLocalModCacheDir(cfg.Golang.LocalModCacheDir).
@@ -198,9 +190,7 @@ func (cfg Catalog) ToPackagesConfig() pkgcataloging.Config {
WithFromBuildSettings(cfg.Golang.MainModuleVersion.FromBuildSettings).
WithFromLDFlags(cfg.Golang.MainModuleVersion.FromLDFlags),
).
WithUsePackagesLib(*multiLevelOption(true, enrichmentEnabled(cfg.Enrich, task.Go, task.Golang), cfg.Golang.UsePackagesLib)).
WithCaptureSymbols(cfg.Golang.CaptureSymbols).
WithCaptureSymbolsModules(cfg.Golang.CaptureSymbolsModules),
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)).
@@ -308,7 +298,6 @@ var publicisedEnrichmentOptions = []string{
task.Java,
task.JavaScript,
task.Python,
task.Vcpkg,
}
func enrichmentEnabled(enrichDirectives []string, features ...string) *bool {
@@ -324,8 +313,10 @@ func enrichmentEnabled(enrichDirectives []string, features ...string) *bool {
directive = directive[1:]
enable = false
}
if slices.Contains(features, directive) {
return &enable
for _, feature := range features {
if directive == feature {
return &enable
}
}
}
return nil
-27
View File
@@ -1,27 +0,0 @@
package options
import (
"github.com/anchore/clio"
)
var _ clio.FieldDescriber = (*Convert)(nil)
// Convert holds the options specific to the convert command. These are deliberately configuration-only (config file
// or environment) rather than flags: they change how the command behaves for every invocation in a pipeline and are
// not expected to vary from one call to the next.
type Convert struct {
PassthroughExactFormat bool `yaml:"passthrough-exact-format" json:"passthrough-exact-format" mapstructure:"passthrough-exact-format"`
}
func DefaultConvert() Convert {
return Convert{
PassthroughExactFormat: false,
}
}
func (o *Convert) DescribeFields(descriptions clio.FieldDescriptionSet) {
descriptions.Add(&o.PassthroughExactFormat, `when a syft-json input SBOM already exactly matches the requested output format and schema version, copy it to that output unchanged instead of decoding and re-encoding it.
Note: this only applies to syft-json input, and only when the document's schema version is identical to the schema version this version of syft produces. Any other input is always converted.
Note: output written this way is a byte-for-byte copy of the input, so format options (such as 'format.pretty' or 'format.json.legacy') are not applied to it.
Note: to keep memory use minimal on large documents, give the input as a file path (or a shell redirect) and write the output with '-o <format>=<path>'. Piped input, or output to STDOUT, requires the whole document to be held in memory.`)
}
-30
View File
@@ -1,30 +0,0 @@
package options
import (
"github.com/anchore/clio"
"github.com/anchore/syft/syft/pkg/cataloger/cpp"
)
type cppConfig struct {
VcpkgAllowGitClone *bool `yaml:"vcpkg-allow-git-clone" json:"vcpkg-allow-git-clone" mapstructure:"vcpkg-allow-git-clone"`
}
func defaultCppConfig() cppConfig {
// reference the cataloger default so capability generation can associate this config with the cpp
// ecosystem (it discovers ecosystem configs by their cataloger import). the value itself stays nil:
// nil defaults to false (no network), and leaving it unset lets --enrich opt in. cloning requires a
// network connection, which must be opt-in.
_ = cpp.DefaultCatalogerConfig()
return cppConfig{
VcpkgAllowGitClone: nil,
}
}
var _ interface {
clio.FieldDescriber
} = (*cppConfig)(nil)
func (o *cppConfig) DescribeFields(descriptions clio.FieldDescriptionSet) {
descriptions.Add(&o.VcpkgAllowGitClone, `enables Syft to use clone remote repositories for vcpkg custom git registries.
(also useful if the builtin vcpkg registry is not cloned locally)`)
}
@@ -13,10 +13,12 @@ func DefaultFormatSPDXJSON() FormatSPDXJSON {
}
func (o FormatSPDXJSON) config(v string) spdxjson.EncoderConfig {
c := spdxjson.DefaultEncoderConfig()
c.Version = v
var pretty bool
if o.Pretty != nil {
c.Pretty = *o.Pretty
pretty = *o.Pretty
}
return spdxjson.EncoderConfig{
Version: v,
Pretty: pretty,
}
return c
}
@@ -2,8 +2,6 @@ package options
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestFormatSPDXJSON_buildConfig(t *testing.T) {
@@ -13,7 +11,5 @@ func TestFormatSPDXJSON_buildConfig(t *testing.T) {
ft = setAllToNonZero(t, ft).(*FormatSPDXJSON)
subject := ft.config("Version")
require.Equal(t, "Version", subject.Version)
require.Equal(t, "2.3", subject.DefaultVersion)
require.True(t, subject.Pretty)
assertExpectedValue(t, subject)
}
+2 -41
View File
@@ -4,8 +4,6 @@ import (
"strings"
"github.com/anchore/clio"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/cataloging"
"github.com/anchore/syft/syft/pkg/cataloger/golang"
)
@@ -19,13 +17,10 @@ type golangConfig struct {
NoProxy string `json:"no-proxy" yaml:"no-proxy" mapstructure:"no-proxy"`
MainModuleVersion golangMainModuleVersionConfig `json:"main-module-version" yaml:"main-module-version" mapstructure:"main-module-version"`
UsePackagesLib *bool `json:"use-packages-lib" yaml:"use-packages-lib" mapstructure:"use-packages-lib"`
CaptureSymbols cataloging.SymbolScope `json:"capture-symbols" yaml:"capture-symbols" mapstructure:"capture-symbols"`
CaptureSymbolsModules []string `json:"capture-symbols-modules" yaml:"capture-symbols-modules" mapstructure:"capture-symbols-modules"`
}
var _ interface {
clio.FieldDescriber
clio.PostLoader
} = (*golangConfig)(nil)
func (o *golangConfig) DescribeFields(descriptions clio.FieldDescriptionSet) {
@@ -44,44 +39,12 @@ if unset this defaults to $GONOPROXY`)
always show (devel) as the version. Use these options to control heuristics to guess
a more accurate version from the binary.`)
descriptions.Add(&o.UsePackagesLib, `use the golang.org/x/tools/go/packages library, which executes golang tooling found on the path in addition to potential network access to get the most accurate results`)
// note: descriptions must be static string literals; the app config discovery that generates the
// capability docs reads them straight out of the AST
descriptions.Add(&o.CaptureSymbols, `capture function symbols from the binary symbol table (pclntab). valid values are:
"none" (disabled), "stdlib" (only the synthetic stdlib package), "extended-stdlib" (stdlib plus every
module under golang.org/x/), and "all" (all module packages plus stdlib)`)
descriptions.Add(&o.CaptureSymbolsModules, `glob patterns matched against go module paths (e.g. github.com/klauspost/**) that should have symbols
captured in addition to whatever capture-symbols selects. ** crosses path separators, * does not.
a trailing major version suffix is ignored when matching, so github.com/foo/* covers github.com/foo/bar/v2;
spelling a suffix out in the pattern selects only that major version.
this can only widen the selection, never narrow it, and is inert when capture-symbols is none`)
descriptions.Add(&o.MainModuleVersion.FromLDFlags, `look for LD flags that appear to be setting a version (e.g. -X main.version=1.0.0)`)
descriptions.Add(&o.MainModuleVersion.FromBuildSettings, `use the build settings (e.g. vcs.version & vcs.time) to craft a v0 pseudo version
descriptions.Add(&o.MainModuleVersion.FromBuildSettings, `use the build settings (e.g. vcs.version & vcs.time) to craft a v0 pseudo version
(e.g. v0.0.0-20220308212642-53e6d0aaf6fb) when a more accurate version cannot be found otherwise`)
descriptions.Add(&o.MainModuleVersion.FromContents, `search for semver-like strings in the binary contents`)
}
func (o *golangConfig) PostLoad() error {
raw := strings.TrimSpace(string(o.CaptureSymbols))
o.CaptureSymbols = o.CaptureSymbols.Parse()
// an unrecognized value still resolves to "none", but say so rather than silently capturing nothing.
// stay quiet for unset and an explicit "none", which is the default and would otherwise warn on nearly
// every scan.
if o.CaptureSymbols == cataloging.SymbolScopeNone && raw != "" && !strings.EqualFold(raw, string(cataloging.SymbolScopeNone)) {
log.Warnf("unknown golang.capture-symbols value %q, defaulting to %q (valid values: none, stdlib, extended-stdlib, all)", raw, cataloging.SymbolScopeNone)
}
// trim only, deliberately not Flatten: viper already splits a comma-separated scalar (env var or a bare
// yaml string) into a slice before this point, and there is no CLI flag feeding this key. The one thing
// Flatten would add is splitting commas *inside* a list entry, which silently breaks doublestar brace
// alternation like github.com/{foo,bar}/**. Viper's split does not trim, so that part is still needed.
for i, pattern := range o.CaptureSymbolsModules {
o.CaptureSymbolsModules[i] = strings.TrimSpace(pattern)
}
return nil
}
type golangMainModuleVersionConfig struct {
FromLDFlags bool `json:"from-ld-flags" yaml:"from-ld-flags" mapstructure:"from-ld-flags"`
FromContents bool `json:"from-contents" yaml:"from-contents" mapstructure:"from-contents"`
@@ -103,8 +66,6 @@ func defaultGolangConfig() golangConfig {
FromContents: def.MainModuleVersion.FromContents,
FromBuildSettings: def.MainModuleVersion.FromBuildSettings,
},
UsePackagesLib: nil, // this defaults to true, which is the API default
CaptureSymbols: def.CaptureSymbols,
CaptureSymbolsModules: def.CaptureSymbolsModules,
UsePackagesLib: nil, // this defaults to true, which is the API default
}
}
-95
View File
@@ -1,95 +0,0 @@
package options
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/anchore/syft/syft/cataloging"
)
func Test_golangConfig_PostLoad(t *testing.T) {
tests := []struct {
name string
cfg golangConfig
expected cataloging.SymbolScope
expectedModules []string
wantErr assert.ErrorAssertionFunc
}{
{
name: "normalize all",
cfg: golangConfig{CaptureSymbols: "all"},
expected: cataloging.SymbolScopeAll,
},
{
name: "normalize stdlib",
cfg: golangConfig{CaptureSymbols: "stdlib"},
expected: cataloging.SymbolScopeStdlib,
},
{
name: "normalize extended-stdlib",
cfg: golangConfig{CaptureSymbols: " Extended-Stdlib "},
expected: cataloging.SymbolScopeExtendedStdlib,
},
{
name: "module patterns keep embedded commas",
cfg: golangConfig{
CaptureSymbols: "stdlib",
// brace alternation contains a comma; splitting on it would corrupt the pattern
CaptureSymbolsModules: []string{"github.com/{foo,bar}/**", "golang.org/x/**"},
},
expected: cataloging.SymbolScopeStdlib,
expectedModules: []string{"github.com/{foo,bar}/**", "golang.org/x/**"},
},
{
// viper splits a comma-separated scalar (env var or bare yaml string) but does not trim,
// so a leading space would otherwise survive into a pattern that silently matches nothing
name: "module patterns are trimmed",
cfg: golangConfig{
CaptureSymbols: "stdlib",
CaptureSymbolsModules: []string{"golang.org/x/**", " github.com/foo/** "},
},
expected: cataloging.SymbolScopeStdlib,
expectedModules: []string{"golang.org/x/**", "github.com/foo/**"},
},
{
name: "empty defaults to none",
cfg: golangConfig{CaptureSymbols: ""},
expected: cataloging.SymbolScopeNone,
},
{
name: "invalid value defaults to none",
cfg: golangConfig{CaptureSymbols: "stdlbi"},
expected: cataloging.SymbolScopeNone,
},
{
name: "boolean spellings default to none",
cfg: golangConfig{CaptureSymbols: "true"},
expected: cataloging.SymbolScopeNone,
},
{
name: "explicit none resolves to none",
cfg: golangConfig{CaptureSymbols: "none"},
expected: cataloging.SymbolScopeNone,
},
{
name: "explicit none is not case sensitive",
cfg: golangConfig{CaptureSymbols: " NONE "},
expected: cataloging.SymbolScopeNone,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.wantErr == nil {
tt.wantErr = assert.NoError
}
err := tt.cfg.PostLoad()
tt.wantErr(t, err)
if err != nil {
return
}
assert.Equal(t, tt.expected, tt.cfg.CaptureSymbols)
assert.Equal(t, tt.expectedModules, tt.cfg.CaptureSymbolsModules)
})
}
}
+1 -1
View File
@@ -32,7 +32,7 @@ func Test_MakeSBOMWriter(t *testing.T) {
{
name: "unknown format",
outputs: []string{"unknown"},
wantErr: func(t assert.TestingT, err error, bla ...any) bool {
wantErr: func(t assert.TestingT, err error, bla ...interface{}) bool {
return assert.ErrorContains(t, err, `unsupported output format "unknown", supported formats are:`)
},
},
@@ -524,12 +524,4 @@ var commonTestCases = []testCase{
"kong": "3.7.0-0",
},
},
{
name: "find cpan distribution",
pkgType: pkg.CpanPkg,
pkgLanguage: pkg.Perl,
pkgInfo: map[string]string{
"URI": "5.35",
},
},
}
@@ -2,7 +2,6 @@ package integration
import (
"context"
"slices"
"strings"
"testing"
@@ -89,8 +88,6 @@ func TestPkgCoverageImage(t *testing.T) {
definedPkgs.Remove(string(pkg.PhpPeclPkg)) // we have coverage for pear instead
definedPkgs.Remove(string(pkg.CondaPkg))
definedPkgs.Remove(string(pkg.ModelPkg))
definedPkgs.Remove(string(pkg.VcpkgPkg))
definedPkgs.Remove(string(pkg.AppleAppBundlePkg))
var cases []testCase
cases = append(cases, commonTestCases...)
@@ -166,8 +163,6 @@ func TestPkgCoverageDirectory(t *testing.T) {
definedPkgs.Remove(string(pkg.CondaPkg))
definedPkgs.Remove(string(pkg.PhpPeclPkg)) // this is covered as pear packages
definedPkgs.Remove(string(pkg.ModelPkg))
definedPkgs.Remove(string(pkg.VcpkgPkg))
definedPkgs.Remove(string(pkg.AppleAppBundlePkg))
// for directory scans we should not expect to see any of the following package types
definedPkgs.Remove(string(pkg.KbPkg))
@@ -199,8 +194,11 @@ func assertPackages(t *testing.T, sbom sbom.SBOM, test testCase, observedLanguag
}
var foundLang bool
if slices.Contains(strings.Split(test.pkgLanguage.String(), ","), actualPkg.Language.String()) {
foundLang = true
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)
@@ -1,147 +0,0 @@
package integration
import (
"context"
"encoding/json"
"fmt"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/source"
)
// multiplatformImage is busybox:1.38.0 pinned by index digest, since docker hub periodically rebuilds and
// re-pushes official image tags, which changes the per-platform digests asserted below.
const multiplatformImage = "docker.io/library/busybox@sha256:fd7dc98638c8e305f4dc34e979f1c0fdfdcaeb0fbf8fcff77ae834b6da3d7e6e"
// TestMultiPlatformOCIImageSelection verifies that syft can load a single platform out of a
// multi-platform OCI image on disk (both --from oci-dir and --from oci-archive) by selecting the
// image that matches the requested platform.
func TestMultiPlatformOCIImageSelection(t *testing.T) {
// Per-platform image config digests within the multi-platform index, obtained from the OCI layout
// (see anchore/stereoscope integration tests: TestPlatformSelectionWithOciLocalSources).
expectedDigest := map[string]string{
"arm64": "sha256:d2482869a6b838d3b4c9cad0c8085c06277909482771eb525d298fc3a7d07927",
"s390x": "sha256:e55a0fa55032cceac2d983f4b4a25b28e42303b46a2f1915fc0fa45051e6ca70",
"amd64": "sha256:aaef90e065235eb0b2d9938be85d1b81add2902cf89affe2cdfa19be33476ac0",
"ppc64le": "sha256:8158829b015d1c5285d63d54d37e95998ef0af4177e3f416678decbcaff3170c",
}
// syft --from source tag -> stereoscope OCI source used to prepare the local fixture
sources := map[string]image.Source{
"oci-dir": image.OciDirectorySource,
"oci-archive": image.OciTarballSource,
}
for from, imageSource := range sources {
t.Run(from, func(t *testing.T) {
localPath := imagetest.PrepareMultiplatformFixtureImage(t, imageSource, multiplatformImage)
for _, arch := range []string{"amd64", "arm64", "s390x", "ppc64le"} {
t.Run(fmt.Sprintf("linux/%s", arch), func(t *testing.T) {
platform, err := image.NewPlatform("linux/" + arch)
require.NoError(t, err)
src, err := syft.GetSource(
context.Background(),
localPath,
syft.DefaultGetSourceConfig().WithSources(from).WithPlatform(platform),
)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, src.Close())
})
meta, ok := src.Describe().Metadata.(source.ImageMetadata)
require.True(t, ok, "expected image metadata, got %T", src.Describe().Metadata)
// The raw config of the selected image must match the requested platform...
assertConfigPlatform(t, meta.RawConfig, "linux", arch)
assert.Equal(t, "linux", meta.OS)
assert.Equal(t, arch, meta.Architecture)
// ...and it must be the exact per-platform image from the multi-platform index.
assert.Equal(t, expectedDigest[arch], meta.ID)
})
}
})
}
}
// TestMultiPlatformOCIImageSelection_UnavailablePlatform verifies that requesting a platform not present
// in the multi-platform OCI image results in an error rather than silently selecting the wrong image.
func TestMultiPlatformOCIImageSelection_UnavailablePlatform(t *testing.T) {
// windows/amd64 is not present in this linux-only multi-platform image
platform, err := image.NewPlatform("windows/amd64")
require.NoError(t, err)
sources := map[string]image.Source{
"oci-dir": image.OciDirectorySource,
"oci-archive": image.OciTarballSource,
}
for from, imageSource := range sources {
t.Run(from, func(t *testing.T) {
localPath := imagetest.PrepareMultiplatformFixtureImage(t, imageSource, multiplatformImage)
_, err := syft.GetSource(
context.Background(),
localPath,
syft.DefaultGetSourceConfig().WithSources(from).WithPlatform(platform),
)
require.ErrorContains(t, err, "windows/amd64")
})
}
}
// TestMultiPlatformOCIImageSelection_DefaultPlatform verifies that not specifying a platform results
// in the current platform being selected.
func TestMultiPlatformOCIImageSelection_DefaultPlatform(t *testing.T) {
sources := map[string]image.Source{
"oci-dir": image.OciDirectorySource,
"oci-archive": image.OciTarballSource,
}
for from, imageSource := range sources {
t.Run(from, func(t *testing.T) {
localPath := imagetest.PrepareMultiplatformFixtureImage(t, imageSource, multiplatformImage)
src, err := syft.GetSource(
context.Background(),
localPath,
syft.DefaultGetSourceConfig().WithSources(from),
)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, src.Close())
})
meta, ok := src.Describe().Metadata.(source.ImageMetadata)
require.True(t, ok, "expected image metadata, got %T", src.Describe().Metadata)
// The raw config of the selected image must match the current platform
assertConfigPlatform(t, meta.RawConfig, "linux", runtime.GOARCH)
assert.Equal(t, "linux", meta.OS)
assert.Equal(t, runtime.GOARCH, meta.Architecture)
})
}
}
// assertConfigPlatform asserts the os/architecture recorded in a raw OCI image config document.
func assertConfigPlatform(t *testing.T, rawConfig []byte, os, architecture string) {
t.Helper()
require.NotEmpty(t, rawConfig)
var cfg struct {
OS string `json:"os"`
Architecture string `json:"architecture"`
}
require.NoError(t, json.Unmarshal(rawConfig, &cfg))
assert.Equal(t, os, cfg.OS)
assert.Equal(t, architecture, cfg.Architecture)
}
@@ -329,7 +329,7 @@ func packageCatalogerExports(t *testing.T) map[string]exportTokenSet {
}
exportsPerPackage[pkg].Add(exportToken{
Name: decl.Name.Name,
Type: reflect.TypeFor[*ast.FuncType]().String(),
Type: reflect.TypeOf(decl.Type).String(),
SignatureSize: len(decl.Type.Params.List),
ReturnTypeNames: returnTypes,
})
@@ -1,64 +0,0 @@
package integration
import (
"context"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/source"
)
// TestSinglePlatformOCIImage covers the single-image (non-index) OCI layouts, where syft used to report
// empty os/architecture metadata and silently accept any --platform. Both now come from the image config.
func TestSinglePlatformOCIImage(t *testing.T) {
// a platform this fixture is never built for, so the mismatch is not host dependent
unavailablePlatform, err := image.NewPlatform("linux/ppc64le")
require.NoError(t, err)
sources := map[string]image.Source{
"oci-dir": image.OciDirectorySource,
"oci-archive": image.OciTarballSource,
}
for from, imageSource := range sources {
t.Run(from, func(t *testing.T) {
localPath := strings.TrimPrefix(imagetest.PrepareFixtureImage(t, imageSource, "image-distro-id"), string(imageSource)+":")
t.Run("reports the platform from the image config", func(t *testing.T) {
src, err := syft.GetSource(
context.Background(),
localPath,
syft.DefaultGetSourceConfig().WithSources(from),
)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, src.Close())
})
meta, ok := src.Describe().Metadata.(source.ImageMetadata)
require.True(t, ok, "expected image metadata, got %T", src.Describe().Metadata)
// the fixture is built for whatever the local docker daemon runs, so assert against the
// config rather than a fixed platform
assert.NotEmpty(t, meta.OS)
assert.NotEmpty(t, meta.Architecture)
assertConfigPlatform(t, meta.RawConfig, meta.OS, meta.Architecture)
})
t.Run("rejects a platform the image does not match", func(t *testing.T) {
_, err := syft.GetSource(
context.Background(),
localPath,
syft.DefaultGetSourceConfig().WithSources(from).WithPlatform(unavailablePlatform),
)
require.ErrorContains(t, err, "linux/ppc64le")
})
})
}
}
@@ -7,6 +7,4 @@
!image-pkg-coverage/pkgs/java/*.hpi
**/go.sum
!image-go-bin-arch-coverage/go.sum
cache/
!image-go-bin-arch-coverage/go.sum
@@ -8,6 +8,8 @@ RUN mkdir tmp
WORKDIR /usr/share/jenkins/tmp
RUN apt-get update 2>&1 > /dev/null && apt-get install -y less zip 2>&1 > /dev/null
RUN unzip ../jenkins.war 2>&1 > /dev/null
RUN rm -rf ./META-INF/MANIFEST.MF ./WEB-INF ./jsbundles ./scripts ./css
@@ -16,8 +18,7 @@ WORKDIR /usr/share/jenkins
RUN rm -rf jenkins.war
# use jar (from the JDK in the base image) to rezip -- M keeps it manifest-less, which is the point of this fixture
RUN cd ./tmp && jar cfM ../jenkins.war . && cd ..
RUN cd ./tmp && zip -r ../jenkins.war . && cd ..
RUN rm -rf ./tmp
@@ -16,7 +16,7 @@ RUN unxz /lib/modules/6.0.7-301.fc37.x86_64/kernel/drivers/tty/ttynull.ko.xz
# dotnet pkg coverage
# https://nuget.info/packages/DocuSign.eSign.dll/6.8.0
# https://github.com/docusign/docusign-esign-csharp-client/blob/master/LICENSE
RUN curl -fLO https://www.nuget.org/api/v2/package/DocuSign.eSign.dll/6.8.0
RUN curl -LO https://www.nuget.org/api/v2/package/DocuSign.eSign.dll/6.8.0
RUN unzip 6.8.0
RUN chmod 600 lib/net462/DocuSign.eSign.dll
RUN rm 6.8.0
@@ -1,9 +0,0 @@
{
"abstract": "Uniform Resource Identifiers (absolute and relative)",
"license": ["perl_5"],
"meta-spec": {"version": 2},
"name": "URI",
"prereqs": {"runtime": {"requires": {"Carp": "0", "perl": "5.008001"}}},
"release_status": "stable",
"version": "5.35"
}
@@ -1,11 +0,0 @@
{
"dist": "URI-5.35",
"name": "URI",
"pathname": "O/OA/OALDERS/URI-5.35.tar.gz",
"provides": {
"URI": {"file": "lib/URI.pm", "version": "5.35"},
"URI::Escape": {"file": "lib/URI/Escape.pm", "version": "5.35"}
},
"target": "URI",
"version": "5.35"
}
+12 -12
View File
@@ -36,33 +36,33 @@ func Test_logWriter(t *testing.T) {
type bufferLogger struct{ values []any }
func (l *bufferLogger) Tracef(_ string, _ ...any) {}
func (l *bufferLogger) Tracef(_ string, _ ...interface{}) {}
func (l *bufferLogger) Debugf(_ string, _ ...any) {}
func (l *bufferLogger) Debugf(_ string, _ ...interface{}) {}
func (l *bufferLogger) Infof(_ string, _ ...any) {}
func (l *bufferLogger) Infof(_ string, _ ...interface{}) {}
func (l *bufferLogger) Warnf(_ string, _ ...any) {}
func (l *bufferLogger) Warnf(_ string, _ ...interface{}) {}
func (l *bufferLogger) Errorf(_ string, _ ...any) {}
func (l *bufferLogger) Errorf(_ string, _ ...interface{}) {}
func (l *bufferLogger) Trace(vals ...any) {
func (l *bufferLogger) Trace(vals ...interface{}) {
l.values = append(l.values, vals...)
}
func (l *bufferLogger) Debug(_ ...any) {}
func (l *bufferLogger) Debug(_ ...interface{}) {}
func (l *bufferLogger) Info(_ ...any) {}
func (l *bufferLogger) Info(_ ...interface{}) {}
func (l *bufferLogger) Warn(vals ...any) {
func (l *bufferLogger) Warn(vals ...interface{}) {
l.values = append(l.values, vals...)
}
func (l *bufferLogger) Error(_ ...any) {}
func (l *bufferLogger) Error(_ ...interface{}) {}
func (l *bufferLogger) WithFields(_ ...any) logger.MessageLogger { return l }
func (l *bufferLogger) WithFields(_ ...interface{}) logger.MessageLogger { return l }
func (l *bufferLogger) Nested(_ ...any) logger.Logger { return l }
func (l *bufferLogger) Nested(_ ...interface{}) logger.Logger { return l }
func (l *bufferLogger) SetOutput(_ io.Writer) {}
+2 -46
View File
@@ -1,7 +1,6 @@
package ui
import (
"bytes"
"fmt"
"io"
"os"
@@ -9,7 +8,6 @@ import (
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/wagoodman/go-partybus"
"github.com/anchore/bubbly"
@@ -54,7 +52,7 @@ func New(out io.Writer, quiet bool, handlers ...bubbly.EventHandler) *UI {
func (m *UI) Setup(subscription partybus.Unsubscribable) error {
// we still want to collect log messages, however, we also the logger shouldn't write to the screen directly
if logWrapper, ok := log.Get().(logger.Controller); ok {
logWrapper.SetOutput(&logFooter{w: m.frame.(*frame.Frame).Footer()})
logWrapper.SetOutput(m.frame.(*frame.Frame).Footer())
}
m.subscription = subscription
@@ -102,7 +100,7 @@ func (m *UI) Teardown(force bool) error {
})
// it may be tempting to use Kill() however it has been found that this can cause the terminal to be left in
// a bad state (where Ctrl+C and other control characters no longer work for future processes in that terminal).
// a bad state (where Ctrl+C and other control characters no longer works for future processes in that terminal).
m.program.Quit()
_ = runWithTimeout(250*time.Millisecond, func() error {
@@ -189,48 +187,6 @@ func (m UI) View() string {
return m.frame.View()
}
var (
logTitleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("8")) // dark grey
// the first log line gets a branch pointing at it from the title, the rest are indented to match
logBranch = logTitleStyle.Render("└─▶") + " "
logIndent = " "
)
// logFooter decorates log entries drawn below the TUI with a one-time title (written when the first entry arrives)
// and a branch from the title to the first log line. Writes are serialized by the logger.
type logFooter struct {
w io.Writer
started bool
}
func (l *logFooter) Write(p []byte) (int, error) {
var buf bytes.Buffer
first := !l.started
if first {
buf.WriteString(logTitleStyle.Render("Logs (non-fatal, see -v for more):") + "\n")
l.started = true
}
for i, line := range bytes.SplitAfter(p, []byte("\n")) {
if len(line) == 0 {
continue
}
if first && i == 0 {
buf.WriteString(logBranch)
} else {
buf.WriteString(logIndent)
}
buf.Write(line)
}
if _, err := l.w.Write(buf.Bytes()); err != nil {
return 0, err
}
return len(p), nil
}
// IsTerminal marks the footer as terminal-bound (it is drawn by the TUI) so the logger keeps colors. NO_COLOR is
// still honored by the formatter.
func (*logFooter) IsTerminal() bool { return true }
func runWithTimeout(timeout time.Duration, fn func() error) (err error) {
c := make(chan struct{}, 1)
go func() {
+1 -3
View File
@@ -5,10 +5,8 @@ This directory contains examples of how to use the Syft API.
- `create_simple_sbom`: Create a simple SBOM from scratch
- `create_custom_sbom`: Create an SBOM using as much custom configuration as possible, including a custom cataloger implementation
- `decode_sbom`: Take an existing SBOM file (of arbitrary format) and decode it into a Syft SBOM object
- `select_catalogers`: Example demonstrating how to select specific catalogers to run
- `source_detection`: Shows how to detect what to catalog automatically from a user string (e.g. container image vs directory)
- `source_from_image`: Construct a source from only a container image
- `source_from_registry`: Construct a source directly from a container registry
- `source_from_image`: Construct a source from a only a container image
You can run any of these examples from this directory with:
+184 -200
View File
@@ -1,73 +1,67 @@
module github.com/anchore/syft
go 1.26.8
go 1.25.8
require (
github.com/BurntSushi/toml v1.6.0
github.com/CycloneDX/cyclonedx-go v0.12.0
github.com/Masterminds/semver/v3 v3.5.0
github.com/CycloneDX/cyclonedx-go v0.10.0
github.com/Masterminds/semver/v3 v3.4.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.2.2
github.com/anchore/clio v0.1.1
github.com/anchore/fangs v0.1.1
github.com/anchore/go-collections v0.1.1
github.com/anchore/go-homedir v0.1.1
github.com/anchore/go-logger v0.2.0
github.com/anchore/go-macholibre v0.1.1
github.com/anchore/go-rpmdb v0.2.0
github.com/anchore/go-sync v0.1.2
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/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-version v1.2.2-0.20200701162849-18adb9c92b9b
github.com/anchore/packageurl-go v0.2.0
github.com/anchore/stereoscope v0.3.3
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115
github.com/anchore/stereoscope v0.1.22
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.10.2
github.com/cespare/xxhash/v2 v2.3.0
github.com/bmatcuk/doublestar/v4 v4.10.0
github.com/charmbracelet/bubbles v1.0.0
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0
github.com/dave/jennifer v1.7.1
github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da
github.com/diskfs/go-diskfs v1.9.4
github.com/diskfs/go-diskfs v1.7.0
github.com/distribution/reference v0.6.0
github.com/dustin/go-humanize v1.1.0
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.7.0
github.com/gkampitakis/go-snaps v0.5.23
github.com/go-git/go-billy/v5 v5.9.1
github.com/go-git/go-git/v5 v5.19.2
github.com/github/go-spdx/v2 v2.4.0
github.com/gkampitakis/go-snaps v0.5.20
github.com/go-git/go-billy/v5 v5.8.0
github.com/go-git/go-git/v5 v5.17.0
github.com/go-test/deep v1.1.1
github.com/go-viper/mapstructure/v2 v2.5.0
github.com/goccy/go-yaml v1.19.2
github.com/gohugoio/hashstructure v0.6.0
github.com/google/go-cmp v0.7.0
github.com/google/go-containerregistry v0.22.1
github.com/google/go-containerregistry v0.21.2
github.com/google/licensecheck v0.3.1
github.com/google/uuid v1.6.0
github.com/gookit/color v1.6.1
github.com/gpustack/gguf-parser-go v0.26.3
github.com/gookit/color v1.6.0
github.com/hashicorp/go-cleanhttp v0.5.2
github.com/hashicorp/go-getter v1.8.9
github.com/hashicorp/go-getter v1.8.5
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/hcl/v2 v2.25.0
github.com/hashicorp/hcl/v2 v2.24.0
github.com/iancoleman/strcase v0.3.0
github.com/invopop/jsonschema v0.14.0
github.com/jedib0t/go-pretty/v6 v6.8.3
github.com/invopop/jsonschema v0.13.0
github.com/jedib0t/go-pretty/v6 v6.7.8
github.com/jinzhu/copier v0.4.0
github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953
github.com/klauspost/compress v1.20.0
github.com/magiconair/properties v1.18.12
github.com/magiconair/properties v1.8.10
github.com/mholt/archives v0.1.5
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.5
github.com/olekukonko/tablewriter v1.1.4
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
@@ -79,11 +73,10 @@ require (
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.6.0-rc4
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.12.1
github.com/ulikunitz/xz v0.5.17
github.com/stretchr/testify v1.11.1
github.com/vbatts/go-mtree v0.7.0
github.com/vifraa/gopom v1.0.0
github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651
@@ -91,263 +84,254 @@ require (
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.5
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f
golang.org/x/mod v0.41.0
golang.org/x/net v0.59.0
golang.org/x/time v0.16.0
golang.org/x/tools v0.50.0
modernc.org/sqlite v1.59.0
go.yaml.in/yaml/v3 v3.0.4
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546
golang.org/x/mod v0.34.0
golang.org/x/net v0.52.0
modernc.org/sqlite v1.46.1
)
require (
github.com/pb33f/ordered-map/v2 v2.3.1
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd
howett.net/plist v1.0.1
)
require (
cel.dev/expr v0.25.2 // indirect
cloud.google.com/go v0.123.0 // indirect
cloud.google.com/go/auth v0.23.2 // indirect
cloud.google.com/go/auth v0.18.1 // 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.12.0 // indirect
cloud.google.com/go/monitoring v1.30.0 // indirect
cloud.google.com/go/storage v1.65.1 // indirect
cyphar.com/go-pathrs v0.2.5 // indirect
cloud.google.com/go/iam v1.5.3 // indirect
cloud.google.com/go/storage v1.60.0 // indirect
dario.cat/mergo v1.0.2 // indirect
github.com/DataDog/zstd v1.5.5 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.57.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.57.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 // indirect
github.com/Microsoft/hcsshim v0.15.0-rc.4 // indirect
github.com/ProtonMail/go-crypto v1.4.1 // 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.4.0 // indirect
github.com/STARRY-S/zip v0.2.3 // indirect
github.com/VividCortex/ewma v1.2.0 // indirect
github.com/agext/levenshtein v1.2.1 // indirect
github.com/anchore/go-lzo v0.1.1 // indirect
github.com/anchore/go-struct-converter v0.2.0-rc2 // 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/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/apparentlymart/go-textseg/v17 v17.0.1 // indirect
github.com/aquasecurity/go-version v0.0.1 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aws/aws-sdk-go-v2 v1.44.0 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.20 // indirect
github.com/aws/aws-sdk-go-v2/config v1.32.40 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.39 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.40 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.40 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.40 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.41 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.10.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.40 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.41 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.108.0 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.6.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.34.0 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.39.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.46.0 // indirect
github.com/aws/smithy-go v1.28.1 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/bahlo/generic-list-go v0.2.0 // 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/buger/jsonparser v1.1.2 // indirect
github.com/charmbracelet/colorprofile v0.4.3 // indirect
github.com/charmbracelet/colorprofile v0.4.1 // indirect
github.com/charmbracelet/harmonica v0.2.0 // indirect
github.com/charmbracelet/x/ansi v0.11.6 // indirect
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/cloudflare/circl v1.6.3 // indirect
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
github.com/containerd/cgroups/v3 v3.1.3 // indirect
github.com/containerd/containerd/api v1.12.0 // indirect
github.com/containerd/containerd/v2 v2.4.1 // indirect
github.com/containerd/continuity v0.5.0 // 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/containerd/fifo v1.1.0 // indirect
github.com/containerd/log v0.2.0 // indirect
github.com/containerd/log/otel v0.1.0 // indirect
github.com/containerd/platforms v1.0.0-rc.5 // indirect
github.com/containerd/plugin 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.2 // indirect
github.com/containerd/ttrpc v1.2.9 // indirect
github.com/containerd/typeurl/v2 v2.3.0 // indirect
github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect
github.com/containers/ocicrypt v1.3.2 // indirect
github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect
github.com/cyphar/filepath-securejoin v0.7.0 // indirect
github.com/docker/cli v29.8.1+incompatible // indirect
github.com/docker/docker-credential-helpers v0.9.8 // indirect
github.com/docker/go-connections v0.8.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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/docker/cli v29.3.0+incompatible // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker-credential-helpers v0.9.5 // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/fatih/color v1.19.0 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/felixge/fgprof v0.9.5 // indirect
github.com/felixge/httpsnoop v1.1.0 // indirect
github.com/fsnotify/fsnotify v1.10.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.15 // indirect
github.com/gkampitakis/ciinfo v0.3.4 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/gkampitakis/ciinfo v0.3.2 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-jose/go-jose/v4 v4.1.5 // indirect
github.com/go-logr/logr v1.4.4 // indirect
github.com/go-logr/logr v1.4.3 // 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/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/go-intervals v0.0.2 // indirect
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 // 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.20 // indirect
github.com/googleapis/gax-go/v2 v2.24.0 // indirect
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.74 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-version v1.9.0 // indirect
github.com/hashicorp/go-version v1.8.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/henvic/httpretty v0.1.4 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/klauspost/compress v1.18.4 // 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/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/maruel/natural v1.3.0 // indirect
github.com/maruel/natural v1.1.1 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.24 // 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.27 // indirect
github.com/mattn/go-sqlite3 v1.14.48 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/miekg/pkcs11 v1.1.2 // indirect
github.com/mikelolasagasti/xz v1.0.1 // indirect
github.com/minio/minlz v1.0.1 // indirect
github.com/mistifyio/go-zfs/v4 v4.0.0 // 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/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/moby/api v1.56.0 // indirect
github.com/moby/moby/client v0.6.0 // indirect
github.com/moby/sys/capability v0.4.0 // 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.1 // indirect
github.com/moby/sys/userns v0.2.1 // 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/moby/sys/user v0.4.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/nwaples/rardecode/v2 v2.4.1 // indirect
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
github.com/olekukonko/errors v1.2.0 // indirect
github.com/olekukonko/ll v0.1.6 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/opencontainers/runtime-spec v1.3.0 // indirect
github.com/opencontainers/selinux v1.15.1 // indirect
github.com/opencontainers/selinux v1.13.1 // indirect
github.com/pborman/indent v1.2.1 // indirect
github.com/pelletier/go-toml/v2 v2.4.3 // indirect
github.com/pierrec/lz4/v4 v4.1.26 // indirect
github.com/piprate/json-gold v0.7.0 // indirect
github.com/pjbgf/sha1cd v0.6.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // 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.12 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
github.com/proglottis/gpgme v0.1.6 // 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.11.0 // indirect
github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/sahilm/fuzzy v0.1.1 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.11.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/sigstore/fulcio v1.8.7 // indirect
github.com/sigstore/protobuf-specs v0.5.1 // indirect
github.com/sigstore/sigstore v1.10.8 // indirect
github.com/sirupsen/logrus v1.10.2 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
github.com/smallnest/ringbuffer v0.0.0-20241116012123-461381446e3d // indirect
github.com/smallstep/pkcs7 v0.2.1 // indirect
github.com/sorairolake/lzip-go v0.3.8 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/spf13/viper v1.21.0 // indirect
github.com/spiffe/go-spiffe/v2 v2.7.0 // indirect
github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 // indirect
github.com/stretchr/objx v0.5.3 // indirect
github.com/spf13/viper v1.20.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/sylabs/sif/v2 v2.24.1 // indirect
github.com/sylabs/sif/v2 v2.24.0 // indirect
github.com/sylabs/squashfs v1.0.6 // indirect
github.com/tchap/go-patricia/v2 v2.3.3 // indirect
github.com/therootcompany/xz v1.0.1 // indirect
github.com/tidwall/gjson v1.19.0 // 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/vbatts/tar-split v0.12.3 // indirect
github.com/vbauerster/cupwriter v0.0.4 // indirect
github.com/vbauerster/mpb/v8 v8.14.0 // indirect
github.com/ulikunitz/xz v0.5.15
github.com/vbatts/tar-split v0.12.2 // 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/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
github.com/zclconf/go-cty v1.19.0 // indirect
github.com/zclconf/go-cty v1.16.3 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.44.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.71.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0 // indirect
go.opentelemetry.io/otel v1.46.0 // indirect
go.opentelemetry.io/otel/metric v1.46.0 // indirect
go.opentelemetry.io/otel/sdk v1.46.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.46.0 // indirect
go.opentelemetry.io/otel/trace v1.46.0 // indirect
go.podman.io/image/v5 v5.41.2 // indirect
go.podman.io/storage v1.64.1 // indirect
go.yaml.in/yaml/v4 v4.0.0-rc.2 // 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.40.0 // indirect
go.opentelemetry.io/otel/metric v1.40.0 // indirect
go.opentelemetry.io/otel/trace v1.40.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.57.0 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sync v0.23.0 // indirect
golang.org/x/sys v0.48.0 // indirect
golang.org/x/term v0.46.0 // indirect
golang.org/x/text v0.42.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/term v0.41.0 // indirect
golang.org/x/text v0.35.0 // indirect
golang.org/x/time v0.15.0
golang.org/x/tools v0.43.0
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
gonum.org/v1/gonum v0.17.0 // indirect
google.golang.org/api v0.294.0 // indirect
google.golang.org/genproto v0.0.0-20260715232425-e75dac1f907d // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260825221802-da73d73af1c5 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 // indirect
google.golang.org/grpc v1.83.2 // indirect
google.golang.org/protobuf v1.36.12 // indirect
google.golang.org/api v0.267.0 // indirect
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect
google.golang.org/grpc v1.79.3 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
modernc.org/libc v1.75.7 // indirect
gopkg.in/yaml.v3 v3.0.1
modernc.org/libc v1.67.6 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.12.1 // indirect
modernc.org/memory v1.11.0 // indirect
)
require (
github.com/acobaugh/osrelease v0.1.0
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
github.com/cespare/xxhash/v2 v2.3.0
github.com/gpustack/gguf-parser-go v0.24.0
github.com/wk8/go-ordered-map/v2 v2.1.8
)
require (
cel.dev/expr v0.25.1 // indirect
cloud.google.com/go/monitoring v1.24.3 // indirect
cyphar.com/go-pathrs v0.2.1 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect
github.com/aws/aws-sdk-go-v2 v1.41.2 // 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.10 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.10 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 // 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.17 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 // indirect
github.com/aws/smithy-go v1.24.1 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.2 // indirect
github.com/clipperhouse/displaywidth v0.10.0 // indirect
github.com/clipperhouse/uax29/v2 v2.6.0 // indirect
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // 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.36.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // 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/moby/moby/api v1.54.0 // indirect
github.com/moby/moby/client v0.3.0 // 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.2.0 // indirect
github.com/olekukonko/ll v0.1.6 // 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.6.0 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect
go.opentelemetry.io/otel/sdk v1.40.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect
gonum.org/v1/gonum v0.16.0 // indirect
)
retract (
+337 -435
View File
File diff suppressed because it is too large Load Diff
+27 -58
View File
@@ -8,6 +8,7 @@ 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}
PROGRAM_ARGS=$@
# signature verification options
@@ -18,8 +19,6 @@ VERIFY_SIGN=false
VERIFY_SIGN_SUPPORTED_VERSION=v0.104.0
# this is the earliest tag in the repo where the -v flag was introduced to this install.sh script
VERIFY_SIGN_FLAG_VERSION=v1.6.0
# releases at or after this version are signed with a sigstore bundle (.sigstore.json); earlier releases ship a separate .sig and .pem
VERIFY_SIGN_BUNDLE_VERSION=v1.53.0
# do not change the name of this parameter (this must always be backwards compatible)
DOWNLOAD_TAG_INSTALL_SCRIPT=${DOWNLOAD_TAG_INSTALL_SCRIPT:-true}
@@ -36,8 +35,7 @@ is_command() (
)
echo_stderr() (
IFS=" "
printf "%b\n" "$*" 1>&2
echo "$@" 1>&2
)
_logp=2
@@ -77,12 +75,12 @@ init_colors
log_tag() (
case $1 in
0) printf "%b" "${RED}${BOLD}[error]${RESET}" ;;
1) printf "%b" "${RED}[warn]${RESET}" ;;
2) printf "%b" "[info]${RESET}" ;;
3) printf "%b" "${BLUE}[debug]${RESET}" ;;
4) printf "%b" "${PURPLE}[trace]${RESET}" ;;
*) printf "[%s]" "$1" ;;
0) echo "${RED}${BOLD}[error]${RESET}" ;;
1) echo "${RED}[warn]${RESET}" ;;
2) echo "[info]${RESET}" ;;
3) echo "${BLUE}[debug]${RESET}" ;;
4) echo "${PURPLE}[trace]${RESET}" ;;
*) echo "[$1]" ;;
esac
)
@@ -156,7 +154,6 @@ uname_arch_check() (
mipsle) return 0 ;;
mips64) return 0 ;;
mips64le) return 0 ;;
riscv64) return 0 ;;
s390x) return 0 ;;
amd64p32) return 0 ;;
esac
@@ -387,8 +384,8 @@ download_github_release_checksums_files() (
complete_url=$(github_release_asset_url "${download_url}" "${name}" "${version}" "${filename}")
output_path="${output_dir}/${complete_filename}"
http_download "${output_path}" "${complete_url}" "" || return 1
asset_file_exists "${output_path}" || return 1
http_download "${output_path}" "${complete_url}" ""
asset_file_exists "${output_path}"
log_trace "download_github_release_checksums_files() returned '${output_path}' for file '${complete_filename}'"
@@ -586,31 +583,28 @@ download_and_install_asset() (
install_asset "${asset_filepath}" "${install_path}" "${binary}"
)
# verify_sign [checksums-file-path] [verification-material-flags...]
# verify_sign [checksums-file-path] [certificate-reference] [signature-reference] [version]
#
# attempts verify the signature of the checksums file from the release workflow in Github Actions run against the main branch.
# the verification material is either "--bundle <path>" or (for older releases) "--certificate <ref> --signature <ref>".
#
verify_sign() {
checksums_file=$1
shift
cert_reference=$2
sig_reference=$3
log_trace "verifying artifact ${checksums_file}"
log_trace "verifying artifact $1"
log_file=$(mktemp)
${COSIGN_BINARY} \
verify-blob "$checksums_file" \
"$@" \
--certificate "$cert_reference" \
--signature "$sig_reference" \
--certificate-identity "https://github.com/${OWNER}/${REPO}/.github/workflows/release.yaml@refs/heads/main" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" > "${log_file}" 2>&1
if [ $? -ne 0 ]; then
log_err "$(cat "${log_file}")"
# cosign older than v2.4.2 can't read the bundle's verification material and reports a missing cert
if [ "$1" = "--bundle" ] && grep -q "bundle does not contain cert" "${log_file}"; then
log_err "note: releases >= ${VERIFY_SIGN_BUNDLE_VERSION} are verified with a sigstore bundle, which requires cosign v2.5.0 or newer (check '${COSIGN_BINARY} version')"
fi
rm -f "${log_file}"
return 1
fi
@@ -634,10 +628,7 @@ download_asset() (
log_trace "download_asset(url=${download_url}, destination=${destination}, name=${name}, os=${os}, arch=${arch}, version=${version}, format=${format})"
if ! checksums_filepath=$(download_github_release_checksums "${download_url}" "${name}" "${version}" "${destination}"); then
log_err "unable to download checksums file"
return 1
fi
checksums_filepath=$(download_github_release_checksums "${download_url}" "${name}" "${version}" "${destination}")
log_trace "checksums content:\n$(cat ${checksums_filepath})"
@@ -649,25 +640,13 @@ download_asset() (
fi
if [ "$VERIFY_SIGN" = true ]; then
if compare_semver "${version}" "${VERIFY_SIGN_BUNDLE_VERSION}"; then
if ! checksums_bundle_filepath=$(download_github_release_checksums_files "${download_url}" "${name}" "${version}" "${destination}" "checksums.txt.sigstore.json"); then
log_err "unable to download checksums signature bundle"
return 1
fi
log_trace "checksums bundle: ${checksums_bundle_filepath}"
checksum_sig_file_url=$(github_release_checksums_sig_url "${download_url}" "${name}" "${version}")
log_trace "checksums signature url: ${checksum_sig_file_url}"
set -- --bundle "${checksums_bundle_filepath}"
else
checksum_sig_file_url=$(github_release_checksums_sig_url "${download_url}" "${name}" "${version}")
log_trace "checksums signature url: ${checksum_sig_file_url}"
checksums_cert_file_url=$(github_release_checksums_cert_url "${download_url}" "${name}" "${version}")
log_trace "checksums certificate url: ${checksums_cert_file_url}"
checksums_cert_file_url=$(github_release_checksums_cert_url "${download_url}" "${name}" "${version}")
log_trace "checksums certificate url: ${checksums_cert_file_url}"
set -- --certificate "${checksums_cert_file_url}" --signature "${checksum_sig_file_url}"
fi
if ! verify_sign "${checksums_filepath}" "$@"; then
if ! verify_sign "${checksums_filepath}" "${checksums_cert_file_url}" "${checksum_sig_file_url}"; then
log_err "signature verification failed"
return 1
fi
@@ -676,15 +655,9 @@ download_asset() (
asset_url="${download_url}/${asset_filename}"
asset_filepath="${destination}/${asset_filename}"
if ! http_download "${asset_filepath}" "${asset_url}" ""; then
log_err "unable to download asset '${asset_url}'"
return 1
fi
http_download "${asset_filepath}" "${asset_url}" ""
# this is what ties the asset to the (optionally signature verified) checksums file, so a mismatch must stop the install
if ! hash_sha256_verify "${asset_filepath}" "${checksums_filepath}"; then
return 1
fi
hash_sha256_verify "${asset_filepath}" "${checksums_filepath}"
log_trace "download_asset_by_checksums_file() returned '${asset_filepath}'"
@@ -790,7 +763,7 @@ prep_signature_verification() {
fi
}
parse_install_args() {
main() (
# parse arguments
# note: never change default install directory (this must always be backwards compatible)
@@ -830,11 +803,6 @@ EOF
set +u
tag=$1
set -u
}
main() (
parse_install_args "$@"
if [ -z "${tag}" ]; then
log_info "checking github for the current release tag"
@@ -842,6 +810,7 @@ main() (
else
log_info "checking github for release tag='${tag}'"
fi
set -u
if ! tag=$(get_release_tag "${OWNER}" "${REPO}" "${tag}"); then
log_err "unable to find tag='${tag}'"
@@ -872,7 +841,7 @@ main() (
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 -- "$@"
echo "${install_script}" | sh -s -- ${PROGRAM_ARGS}
exit $?
fi
-6
View File
@@ -2,8 +2,6 @@
# This file is partially auto-generated. Run 'go generate ./internal/capabilities' to regenerate.
application: # AUTO-GENERATED - application-level config keys
- key: cpp.vcpkg-allow-git-clone
description: enables Syft to use clone remote repositories for vcpkg custom git registries. (also useful if the builtin vcpkg registry is not cloned locally)
- key: dotnet.dep-packages-must-claim-dll
description: only keep dep.json packages which have a runtime/resource DLL claimed in the deps.json targets section (but not necessarily found on disk). The package is also included if any child package claims a DLL, even if the package itself does not claim a DLL.
- key: dotnet.dep-packages-must-have-dll
@@ -14,10 +12,6 @@ application: # AUTO-GENERATED - application-level config keys
description: treat DLL claims or on-disk evidence for child packages as DLL claims or on-disk evidence for any parent package
- key: dotnet.relax-dll-claims-when-bundling-detected
description: show all packages from the deps.json if bundling tooling is present as a dependency (e.g. ILRepack)
- key: golang.capture-symbols
description: 'capture function symbols from the binary symbol table (pclntab). valid values are: "none" (disabled), "stdlib" (only the synthetic stdlib package), "extended-stdlib" (stdlib plus every module under golang.org/x/), and "all" (all module packages plus stdlib)'
- key: golang.capture-symbols-modules
description: glob patterns matched against go module paths (e.g. github.com/klauspost/**) that should have symbols captured in addition to whatever capture-symbols selects. ** crosses path separators, * does not. a trailing major version suffix is ignored when matching, so github.com/foo/* covers github.com/foo/bar/v2; spelling a suffix out in the pattern selects only that major version. this can only widen the selection, never narrow it, and is inert when capture-symbols is none
- key: golang.local-mod-cache-dir
description: specify an explicit go mod cache directory, if unset this defaults to $GOPATH/pkg/mod or $HOME/go/pkg/mod
- key: golang.local-vendor-dir
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"sort"
"github.com/scylladb/go-set/strset"
"go.yaml.in/yaml/v3"
"gopkg.in/yaml.v3"
"github.com/anchore/syft/internal/task"
)
+5 -5
View File
@@ -5,8 +5,8 @@ import "reflect"
// EvaluateCapabilities evaluates a capability set against a given configuration
// and returns the effective capability values as a flat map.
// Example: {"license": false, "dependency.depth": ["direct", "indirect"]}
func EvaluateCapabilities(caps CapabilitySet, config map[string]any) map[string]any {
result := make(map[string]any)
func EvaluateCapabilities(caps CapabilitySet, config map[string]interface{}) map[string]interface{} {
result := make(map[string]interface{})
for _, capField := range caps {
result[capField.Name] = EvaluateField(capField, config)
}
@@ -16,7 +16,7 @@ func EvaluateCapabilities(caps CapabilitySet, config map[string]any) map[string]
// EvaluateField evaluates a single capability field against a configuration.
// Conditions are evaluated in order, and the first matching condition's value is returned.
// If no conditions match, the default value is returned.
func EvaluateField(capField CapabilityField, config map[string]any) any {
func EvaluateField(capField CapabilityField, config map[string]interface{}) interface{} {
// check conditions in order (first match wins)
for _, cond := range capField.Conditions {
if ConditionMatches(cond.When, config) {
@@ -30,7 +30,7 @@ func EvaluateField(capField CapabilityField, config map[string]any) any {
// ConditionMatches checks if a condition's when clause matches the given configuration.
// All fields in the when clause must match the config (AND logic).
// Returns true if all key-value pairs in when match the config.
func ConditionMatches(when map[string]any, config map[string]any) bool {
func ConditionMatches(when map[string]interface{}, config map[string]interface{}) bool {
// all fields in when must match config (AND logic)
for key, expectedValue := range when {
actualValue, exists := config[key]
@@ -46,7 +46,7 @@ func ConditionMatches(when map[string]any, config map[string]any) bool {
// valuesEqual compares two values for equality, handling different types appropriately.
// Uses reflect.DeepEqual for complex types like slices and maps.
func valuesEqual(a, b any) bool {
func valuesEqual(a, b interface{}) bool {
// handle nil cases
if a == nil && b == nil {
return true
+52 -52
View File
@@ -10,8 +10,8 @@ import (
func Test_valuesEqual(t *testing.T) {
tests := []struct {
name string
a any
b any
a interface{}
b interface{}
want bool
}{
{
@@ -123,47 +123,47 @@ func Test_valuesEqual(t *testing.T) {
func TestConditionMatches(t *testing.T) {
tests := []struct {
name string
when map[string]any
config map[string]any
when map[string]interface{}
config map[string]interface{}
want bool
}{
{
name: "empty when clause matches anything",
when: map[string]any{},
config: map[string]any{"key": "value"},
when: map[string]interface{}{},
config: map[string]interface{}{"key": "value"},
want: true,
},
{
name: "empty when clause with empty config",
when: map[string]any{},
config: map[string]any{},
when: map[string]interface{}{},
config: map[string]interface{}{},
want: true,
},
{
name: "single key match",
when: map[string]any{"SearchLocalModCacheLicenses": true},
config: map[string]any{"SearchLocalModCacheLicenses": true},
when: map[string]interface{}{"SearchLocalModCacheLicenses": true},
config: map[string]interface{}{"SearchLocalModCacheLicenses": true},
want: true,
},
{
name: "single key mismatch",
when: map[string]any{"SearchLocalModCacheLicenses": true},
config: map[string]any{"SearchLocalModCacheLicenses": false},
when: map[string]interface{}{"SearchLocalModCacheLicenses": true},
config: map[string]interface{}{"SearchLocalModCacheLicenses": false},
want: false,
},
{
name: "key missing from config",
when: map[string]any{"SearchLocalModCacheLicenses": true},
config: map[string]any{},
when: map[string]interface{}{"SearchLocalModCacheLicenses": true},
config: map[string]interface{}{},
want: false,
},
{
name: "multiple keys all match",
when: map[string]any{
when: map[string]interface{}{
"SearchLocalModCacheLicenses": true,
"UseNetwork": true,
},
config: map[string]any{
config: map[string]interface{}{
"SearchLocalModCacheLicenses": true,
"UseNetwork": true,
"ExtraKey": "ignored",
@@ -172,11 +172,11 @@ func TestConditionMatches(t *testing.T) {
},
{
name: "multiple keys one mismatch",
when: map[string]any{
when: map[string]interface{}{
"SearchLocalModCacheLicenses": true,
"UseNetwork": true,
},
config: map[string]any{
config: map[string]interface{}{
"SearchLocalModCacheLicenses": true,
"UseNetwork": false,
},
@@ -184,31 +184,31 @@ func TestConditionMatches(t *testing.T) {
},
{
name: "multiple keys one missing",
when: map[string]any{
when: map[string]interface{}{
"SearchLocalModCacheLicenses": true,
"UseNetwork": true,
},
config: map[string]any{
config: map[string]interface{}{
"SearchLocalModCacheLicenses": true,
},
want: false,
},
{
name: "string value match",
when: map[string]any{"mode": "fast"},
config: map[string]any{"mode": "fast"},
when: map[string]interface{}{"mode": "fast"},
config: map[string]interface{}{"mode": "fast"},
want: true,
},
{
name: "slice value match",
when: map[string]any{"formats": []string{"json", "yaml"}},
config: map[string]any{"formats": []string{"json", "yaml"}},
when: map[string]interface{}{"formats": []string{"json", "yaml"}},
config: map[string]interface{}{"formats": []string{"json", "yaml"}},
want: true,
},
{
name: "slice value mismatch",
when: map[string]any{"formats": []string{"json", "yaml"}},
config: map[string]any{"formats": []string{"json", "xml"}},
when: map[string]interface{}{"formats": []string{"json", "yaml"}},
config: map[string]interface{}{"formats": []string{"json", "xml"}},
want: false,
},
}
@@ -225,8 +225,8 @@ func TestEvaluateField(t *testing.T) {
tests := []struct {
name string
capField CapabilityField
config map[string]any
want any
config map[string]interface{}
want interface{}
}{
{
name: "no conditions returns default",
@@ -235,7 +235,7 @@ func TestEvaluateField(t *testing.T) {
Default: false,
Conditions: nil,
},
config: map[string]any{},
config: map[string]interface{}{},
want: false,
},
{
@@ -245,7 +245,7 @@ func TestEvaluateField(t *testing.T) {
Default: false,
Conditions: []CapabilityCondition{},
},
config: map[string]any{},
config: map[string]interface{}{},
want: false,
},
{
@@ -260,7 +260,7 @@ func TestEvaluateField(t *testing.T) {
},
},
},
config: map[string]any{"SearchLocalModCacheLicenses": true},
config: map[string]interface{}{"SearchLocalModCacheLicenses": true},
want: true,
},
{
@@ -275,7 +275,7 @@ func TestEvaluateField(t *testing.T) {
},
},
},
config: map[string]any{"SearchLocalModCacheLicenses": false},
config: map[string]interface{}{"SearchLocalModCacheLicenses": false},
want: false,
},
{
@@ -294,7 +294,7 @@ func TestEvaluateField(t *testing.T) {
},
},
},
config: map[string]any{
config: map[string]interface{}{
"SearchLocalModCacheLicenses": true,
"SearchRemoteLicenses": true,
},
@@ -316,7 +316,7 @@ func TestEvaluateField(t *testing.T) {
},
},
},
config: map[string]any{
config: map[string]interface{}{
"SearchLocalModCacheLicenses": false,
"SearchRemoteLicenses": true,
},
@@ -338,7 +338,7 @@ func TestEvaluateField(t *testing.T) {
},
},
},
config: map[string]any{
config: map[string]interface{}{
"SearchLocalModCacheLicenses": false,
"SearchRemoteLicenses": false,
},
@@ -351,7 +351,7 @@ func TestEvaluateField(t *testing.T) {
Default: []string{"direct", "indirect"},
Conditions: nil,
},
config: map[string]any{},
config: map[string]interface{}{},
want: []string{"direct", "indirect"},
},
{
@@ -369,7 +369,7 @@ func TestEvaluateField(t *testing.T) {
},
},
},
config: map[string]any{
config: map[string]interface{}{
"EnableFeatureA": true,
"EnableFeatureB": true,
},
@@ -390,7 +390,7 @@ func TestEvaluateField(t *testing.T) {
},
},
},
config: map[string]any{
config: map[string]interface{}{
"EnableFeatureA": true,
"EnableFeatureB": false,
},
@@ -412,14 +412,14 @@ func TestEvaluateCapabilities(t *testing.T) {
tests := []struct {
name string
caps CapabilitySet
config map[string]any
want map[string]any
config map[string]interface{}
want map[string]interface{}
}{
{
name: "empty capability set",
caps: CapabilitySet{},
config: map[string]any{},
want: map[string]any{},
config: map[string]interface{}{},
want: map[string]interface{}{},
},
{
name: "single capability no conditions",
@@ -429,8 +429,8 @@ func TestEvaluateCapabilities(t *testing.T) {
Default: false,
},
},
config: map[string]any{},
want: map[string]any{
config: map[string]interface{}{},
want: map[string]interface{}{
"license": false,
},
},
@@ -448,8 +448,8 @@ func TestEvaluateCapabilities(t *testing.T) {
},
},
},
config: map[string]any{"SearchLocalModCacheLicenses": true},
want: map[string]any{
config: map[string]interface{}{"SearchLocalModCacheLicenses": true},
want: map[string]interface{}{
"license": true,
},
},
@@ -475,8 +475,8 @@ func TestEvaluateCapabilities(t *testing.T) {
Default: "flat",
},
},
config: map[string]any{"SearchLocalModCacheLicenses": true},
want: map[string]any{
config: map[string]interface{}{"SearchLocalModCacheLicenses": true},
want: map[string]interface{}{
"license": true,
"dependency.depth": []string{"direct", "indirect"},
"dependency.edges": "flat",
@@ -512,11 +512,11 @@ func TestEvaluateCapabilities(t *testing.T) {
Default: false,
},
},
config: map[string]any{
config: map[string]interface{}{
"SearchLocalModCacheLicenses": false,
"SearchRemoteLicenses": true,
},
want: map[string]any{
want: map[string]interface{}{
"license": true,
"dependency.depth": []string{"direct", "indirect"},
"dependency.edges": "flat",
@@ -526,10 +526,10 @@ func TestEvaluateCapabilities(t *testing.T) {
{
name: "nil capability set",
caps: nil,
config: map[string]any{
config: map[string]interface{}{
"anything": true,
},
want: map[string]any{},
want: map[string]interface{}{},
},
}
@@ -15,9 +15,9 @@ import (
// AppConfigField represents an application-level configuration field for catalogers
type AppConfigField struct {
Key string // e.g., "golang.search-local-mod-cache-licenses"
Description string // extracted from DescribeFields() method
DefaultValue any // extracted from Default*() functions
Key string // e.g., "golang.search-local-mod-cache-licenses"
Description string // extracted from DescribeFields() method
DefaultValue interface{} // extracted from Default*() functions
}
// extractEcosystemConfigFieldsFromCatalog parses catalog.go and extracts the ecosystem-specific
@@ -390,7 +390,7 @@ func extractDescriptionsFromDescribeFields(f *ast.File) map[string]string {
}
// extractNestedAppConfigs handles nested config structs like golang.MainModuleVersion
func extractNestedAppConfigs(f *ast.File, parentKey, parentFieldName string, fieldType ast.Expr, descriptions map[string]string, defaults map[string]any) []AppConfigField {
func extractNestedAppConfigs(f *ast.File, parentKey, parentFieldName string, fieldType ast.Expr, descriptions map[string]string, defaults map[string]interface{}) []AppConfigField {
var configs []AppConfigField
// find the nested struct type
@@ -449,8 +449,8 @@ func extractNestedAppConfigs(f *ast.File, parentKey, parentFieldName string, fie
description := descriptions[nestedPath]
// try to get default value from nested defaults
var defaultValue any
if nestedDefaults, ok := defaults[parentFieldName].(map[string]any); ok {
var defaultValue interface{}
if nestedDefaults, ok := defaults[parentFieldName].(map[string]interface{}); ok {
defaultValue = nestedDefaults[fieldName]
}
@@ -465,8 +465,8 @@ func extractNestedAppConfigs(f *ast.File, parentKey, parentFieldName string, fie
}
// extractAppDefaultValues extracts default values from the default*Config function
func extractAppDefaultValues(f *ast.File) map[string]any {
defaults := make(map[string]any)
func extractAppDefaultValues(f *ast.File) map[string]interface{} {
defaults := make(map[string]interface{})
for _, decl := range f.Decls {
funcDecl, ok := decl.(*ast.FuncDecl)
@@ -518,7 +518,7 @@ func extractAppDefaultValues(f *ast.File) map[string]any {
}
// extractAppValue extracts a Go value from an AST expression
func extractAppValue(expr ast.Expr) any {
func extractAppValue(expr ast.Expr) interface{} {
switch v := expr.(type) {
case *ast.BasicLit:
// string, int, bool literals
@@ -543,7 +543,7 @@ func extractAppValue(expr ast.Expr) any {
}
case *ast.CompositeLit:
// nested struct literal
nested := make(map[string]any)
nested := make(map[string]interface{})
for _, elt := range v.Elts {
kvExpr, ok := elt.(*ast.KeyValueExpr)
if !ok {
@@ -340,7 +340,7 @@ func TestExtractAppValue(t *testing.T) {
tests := []struct {
name string
src string
want any
want interface{}
}{
{
name: "string literal",
@@ -406,7 +406,7 @@ func TestExtractAppValue_NestedStruct(t *testing.T) {
got := extractAppValue(compositeLit)
// verify it's a map with the expected values
gotMap, ok := got.(map[string]any)
gotMap, ok := got.(map[string]interface{})
require.True(t, ok)
require.Equal(t, "value", gotMap["Field1"])
require.Equal(t, true, gotMap["Field2"])
@@ -567,8 +567,8 @@ func resolveImportedConstant(pkgName, constName string, ctx *parseContext) strin
// resolveImportPath converts an import path to a file system path
func resolveImportPath(importPath, repoRoot string) string {
// for github.com/anchore/syft/... imports, convert to repo-relative path
if after, ok := strings.CutPrefix(importPath, "github.com/anchore/syft/"); ok {
relPath := after
if strings.HasPrefix(importPath, "github.com/anchore/syft/") {
relPath := strings.TrimPrefix(importPath, "github.com/anchore/syft/")
return filepath.Join(repoRoot, relPath)
}
+5 -10
View File
@@ -1,4 +1,4 @@
// this file handles YAML file reading and writing with comment preservation, using go.yaml.in/yaml/v3's node tree to maintain all existing comments during regeneration.
// this file handles YAML file reading and writing with comment preservation, using gopkg.in/yaml.v3's node tree to maintain all existing comments during regeneration.
package main
import (
@@ -6,7 +6,7 @@ import (
"os"
"regexp"
"go.yaml.in/yaml/v3"
"gopkg.in/yaml.v3"
"github.com/anchore/syft/internal/capabilities"
"github.com/anchore/syft/internal/capabilities/internal"
@@ -214,7 +214,7 @@ func mapCatalogerToEcosystem(cat capabilities.CatalogerEntry) string {
}
// updateNodeTreeEcosystem updates an existing ecosystem YAML node tree
func updateNodeTreeEcosystem(rootNode *yaml.Node, doc any) error {
func updateNodeTreeEcosystem(rootNode *yaml.Node, doc interface{}) error {
var newNode yaml.Node
if err := newNode.Encode(doc); err != nil {
return err
@@ -242,7 +242,7 @@ func updateNodeTreeEcosystem(rootNode *yaml.Node, doc any) error {
}
// updateNodeTreeAppConfig updates appconfig YAML node tree
func updateNodeTreeAppConfig(rootNode *yaml.Node, doc any) error {
func updateNodeTreeAppConfig(rootNode *yaml.Node, doc interface{}) error {
return updateNodeTreeEcosystem(rootNode, doc)
}
@@ -297,11 +297,6 @@ func addCatalogerFieldComment(keyNode, valueNode *yaml.Node, catalogerName strin
if keyNode.LineComment == "" {
keyNode.LineComment = autoGeneratedComment
}
case "purl_types":
// cataloger-level purl_types (for custom catalogers) are AUTO-GENERATED
if keyNode.LineComment == "" {
keyNode.LineComment = autoGeneratedComment
}
case "json_schema_types":
// json_schema_types are AUTO-GENERATED
if keyNode.LineComment == "" {
@@ -372,7 +367,7 @@ func addParserComments(parsersNode *yaml.Node) {
valueNode := parserNode.Content[i+1]
switch keyNode.Value {
case "parser_function", "metadata_types", "package_types", "purl_types", "json_schema_types":
case "parser_function", "metadata_types", "package_types", "json_schema_types":
// add AUTO-GENERATED comment to these fields
if keyNode.LineComment == "" {
keyNode.LineComment = autoGeneratedComment
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"testing"
"github.com/stretchr/testify/require"
"go.yaml.in/yaml/v3"
"gopkg.in/yaml.v3"
)
func TestFindFieldValue(t *testing.T) {
+2 -57
View File
@@ -3,7 +3,6 @@ package main
import (
"fmt"
"sort"
"strings"
"github.com/scylladb/go-set/strset"
@@ -11,7 +10,6 @@ import (
"github.com/anchore/syft/internal/capabilities"
"github.com/anchore/syft/internal/capabilities/internal"
"github.com/anchore/syft/internal/packagemetadata"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/binary"
)
@@ -194,9 +192,6 @@ func RegenerateCapabilities(catalogerDir string, repoRoot string) (*Statistics,
formatOrphans(orphans))
}
// 5a. Derive PURL types from the discovered package types (purely a function of pkg.Type)
derivePURLTypes(updated)
// 6. Write back to YAML files with comments, preserving existing node trees
fmt.Print(" → Writing updated capabilities files...")
if err := saveCapabilities(catalogerDir, repoRoot, updated, existingNodes); err != nil {
@@ -432,39 +427,6 @@ func (e *EnrichmentData) GetMetadataTypes(catalogerName string) ([]string, bool)
return types, ok
}
// derivePURLTypes populates the PURLTypes field on every cataloger and parser by mapping each
// observed package type to its canonical PURL type via pkg.Type.PackageURLType. this is a pure
// function of the already-discovered package_types, so it needs no additional test observations.
func derivePURLTypes(doc *capabilities.Document) {
for i := range doc.Catalogers {
cat := &doc.Catalogers[i]
cat.PURLTypes = purlTypesFromPackageTypes(cat.PackageTypes)
for j := range cat.Parsers {
cat.Parsers[j].PURLTypes = purlTypesFromPackageTypes(cat.Parsers[j].PackageTypes)
}
}
}
// purlTypesFromPackageTypes maps syft package type strings (e.g. "go-module") to their PURL types
// (e.g. "golang"), returning a deduplicated, sorted slice. package types with no PURL mapping are skipped.
func purlTypesFromPackageTypes(packageTypes []string) []string {
if len(packageTypes) == 0 {
return nil
}
set := strset.New()
for _, pt := range packageTypes {
if purlType := pkg.Type(pt).PackageURLType(); purlType != "" {
set.Add(purlType)
}
}
if set.IsEmpty() {
return nil
}
result := set.List()
sort.Strings(result)
return result
}
// GetPackageTypes returns the package types for the given cataloger name
func (e *EnrichmentData) GetPackageTypes(catalogerName string) ([]string, bool) {
types, ok := e.packageTypes[catalogerName]
@@ -502,19 +464,6 @@ func convertToJSONSchemaTypesFromMetadata(metadataTypes []string) []string {
return result
}
// packageTypeConstName renders a package type using its go constant name, which is what the detector
// "type" field reports (e.g. "BinaryPkg").
// a switch rather than a derivation, since pkg.Type values don't reliably map back to their constant
// names (e.g. "java-archive" is JavaPkg); add an arm when a classifier declares a new type.
func packageTypeConstName(t pkg.Type) string {
switch t {
case pkg.CpanPkg:
return "CpanPkg"
default:
return "BinaryPkg"
}
}
// EnrichWithBinaryClassifier enriches an entry with binary classifier detectors if it's the binary-classifier-cataloger
func (e *EnrichmentData) EnrichWithBinaryClassifier(catalogerName string, entry *capabilities.CatalogerEntry) {
// special handling for binary-classifier-cataloger: auto-generate one detector per classifier
@@ -536,20 +485,16 @@ func (e *EnrichmentData) EnrichWithBinaryClassifier(catalogerName string, entry
Name: classifier.Package,
PURL: purlStr,
CPEs: cpeStrings,
Type: packageTypeConstName(classifier.PackageType()),
Type: "BinaryPkg",
},
}
for _, o := range binaryClassifierOverrides[classifier.Class] {
cpeStrings := make([]string, len(o.CPEs))
for i, c := range o.CPEs {
cpeStrings[i] = c.Attributes.BindToFmtString()
}
packages = append(packages, capabilities.DetectorPackageInfo{
Class: o.Class,
Name: o.Package,
PURL: stripPURLVersion(o.PURL),
CPEs: cpeStrings,
CPEs: o.CPEs,
Type: "BinaryPkg",
})
}
@@ -552,48 +552,3 @@ func TestConvertToJSONSchemaTypesFromMetadata(t *testing.T) {
})
}
}
func TestPURLTypesFromPackageTypes(t *testing.T) {
tests := []struct {
name string
packageTypes []string
want []string
}{
{
name: "nil slice returns nil",
packageTypes: nil,
want: nil,
},
{
name: "single package type maps to purl type",
packageTypes: []string{"go-module"},
want: []string{"golang"},
},
{
name: "multiple package types are sorted",
packageTypes: []string{"npm", "go-module"},
want: []string{"golang", "npm"},
},
{
// java and jenkins-plugin both map to maven, so the result is deduplicated
name: "distinct package types collapsing to one purl type are deduplicated",
packageTypes: []string{"java-archive", "jenkins-plugin"},
want: []string{"maven"},
},
{
// unknown package types have no purl mapping and are skipped
name: "package type without a purl mapping is skipped",
packageTypes: []string{"UnknownPackage"},
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := purlTypesFromPackageTypes(tt.packageTypes)
if diff := cmp.Diff(tt.want, got); diff != "" {
t.Errorf("purlTypesFromPackageTypes() mismatch (-want +got):\n%s", diff)
}
})
}
}
+3 -52
View File
@@ -10,7 +10,7 @@ type binaryClassifierOverride struct {
Class string
Package string
PURL string
CPEs []cpe.CPE
CPEs []string
}
var binaryClassifierOverrides = map[string][]binaryClassifierOverride{
@@ -96,61 +96,12 @@ var binaryClassifierOverrides = map[string][]binaryClassifierOverride{
CPEs: singleCPE("cpe:2.3:a:oracle:jdk:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource),
},
},
"openssl-binary": {
{
Class: "openssl-binary-aws-lc",
Package: "aws-lc",
PURL: mustPURL("pkg:generic/aws-lc@version"),
CPEs: singleCPE("cpe:2.3:a:amazon:aws_libcrypto:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource),
},
{
Class: "openssl-binary",
Package: "openssl",
PURL: mustPURL("pkg:generic/openssl@version"),
CPEs: singleCPE("cpe:2.3:a:openssl:openssl:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource),
},
},
"mysqld-binary": {
{
Class: "mysqld-mysql-cluster-legacy-binary",
Package: "mysql-cluster",
PURL: mustPURL("pkg:generic/mysql-cluster@version"),
CPEs: []cpe.CPE{
cpe.Must("cpe:2.3:a:oracle:mysql_cluster:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource),
},
},
{
Class: "mysqld-mysql-cluster-binary",
Package: "mysql-cluster",
PURL: mustPURL("pkg:generic/mysql-cluster@version"),
CPEs: []cpe.CPE{
cpe.Must("cpe:2.3:a:oracle:mysql:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource),
cpe.Must("cpe:2.3:a:oracle:mysql_server:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource),
cpe.Must("cpe:2.3:a:oracle:mysql_cluster:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource),
},
},
{
Class: "mysqld-mysql-server-binary",
Package: "mysql-server",
PURL: mustPURL("pkg:generic/mysql-server@version"),
CPEs: []cpe.CPE{
cpe.Must("cpe:2.3:a:oracle:mysql:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource),
cpe.Must("cpe:2.3:a:oracle:mysql_server:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource),
},
},
},
}
func mustPURL(purl string) string {
return purl
}
func singleCPE(cpeString string, source ...cpe.Source) []cpe.CPE {
src := cpe.GeneratedSource
if len(source) > 0 {
src = source[0]
}
return []cpe.CPE{
cpe.Must(cpeString, src),
}
func singleCPE(cpeString string, _ ...any) []string {
return []string{cpeString}
}
@@ -5,7 +5,7 @@ import (
"os"
"path/filepath"
"go.yaml.in/yaml/v3"
"gopkg.in/yaml.v3"
"github.com/anchore/syft/internal/capabilities"
)
@@ -30,7 +30,7 @@ const requireParserObservations = false
// metadataTypeCoverageExceptions lists metadata types that are allowed to not be represented in any cataloger
var metadataTypeCoverageExceptions = strset.New(
reflect.TypeFor[pkg.MicrosoftKbPatch]().Name(),
reflect.TypeOf(pkg.MicrosoftKbPatch{}).Name(),
)
// packageTypeCoverageExceptions lists package types that are allowed to not be represented in any cataloger
@@ -408,7 +408,7 @@ func TestCapabilityValueTypes(t *testing.T) {
}
// validateCapabilityValueType checks if a value matches the expected type for a capability field
func validateCapabilityValueType(fieldPath string, value any) error {
func validateCapabilityValueType(fieldPath string, value interface{}) error {
if value == nil {
return nil // nil is acceptable
}
@@ -428,7 +428,7 @@ func validateCapabilityValueType(fieldPath string, value any) error {
switch v := value.(type) {
case []string:
// ok
case []any:
case []interface{}:
// check each element is a string
for i, elem := range v {
if _, ok := elem.(string); !ok {
@@ -829,6 +829,7 @@ func TestCapabilityEvidenceFieldReferences(t *testing.T) {
// validate each evidence reference
for _, ref := range allReferences {
ref := ref // capture for subtest
// create test name
testName := ref.catalogerName
@@ -988,6 +989,7 @@ func validateCapabilitiesFilled(t *testing.T, catalogers []capabilities.Cataloge
checkCompletenessTestsEnabled(t)
for _, c := range catalogers {
c := c // capture loop variable for subtest
t.Run(c.Name, func(t *testing.T) {
if c.Type == "generic" {
@@ -995,6 +997,7 @@ func validateCapabilitiesFilled(t *testing.T, catalogers []capabilities.Cataloge
require.NotEmpty(t, c.Parsers, "generic cataloger must have at least one parser")
for _, p := range c.Parsers {
p := p // capture loop variable for subtest
t.Run(p.ParserFunction, func(t *testing.T) {
require.NotEmpty(t, p.Capabilities, "parser must have at least one capability field defined")
@@ -1117,6 +1120,7 @@ func TestCatalogerStructure(t *testing.T) {
require.NoError(t, err)
for _, c := range catalogerEntries {
c := c // capture loop variable for subtest
t.Run(c.Name, func(t *testing.T) {
// ecosystem must always be set (it's MANUAL)
-2
View File
@@ -82,7 +82,6 @@ type CatalogerEntry struct {
Detectors []Detector `yaml:"detectors,omitempty" json:"detectors,omitempty"` // AUTO-GENERATED - detection methods (only for type=custom)
MetadataTypes []string `yaml:"metadata_types,omitempty" json:"metadata_types,omitempty"` // AUTO-GENERATED - pkg metadata types emitted (only for type=custom)
PackageTypes []string `yaml:"package_types,omitempty" json:"package_types,omitempty"` // AUTO-GENERATED - package types emitted (only for type=custom)
PURLTypes []string `yaml:"purl_types,omitempty" json:"purl_types,omitempty"` // AUTO-GENERATED - PURL types derived from package_types (only for type=custom)
JSONSchemaTypes []string `yaml:"json_schema_types,omitempty" json:"json_schema_types,omitempty"` // AUTO-GENERATED - JSON schema type names (UpperCamelCase)
Capabilities CapabilitySet `yaml:"capabilities,omitempty" json:"capabilities,omitempty"` // MANUAL - config-driven capability definitions (only for type=custom)
}
@@ -93,7 +92,6 @@ type Parser struct {
Detector Detector `yaml:"detector" json:"detector"` // AUTO-GENERATED - how artifacts are detected
MetadataTypes []string `yaml:"metadata_types,omitempty" json:"metadata_types,omitempty"` // AUTO-GENERATED - pkg metadata types emitted by this parser
PackageTypes []string `yaml:"package_types,omitempty" json:"package_types,omitempty"` // AUTO-GENERATED - package types emitted by this parser
PURLTypes []string `yaml:"purl_types,omitempty" json:"purl_types,omitempty"` // AUTO-GENERATED - PURL types derived from package_types
JSONSchemaTypes []string `yaml:"json_schema_types,omitempty" json:"json_schema_types,omitempty"` // AUTO-GENERATED - JSON schema type names (UpperCamelCase)
Capabilities CapabilitySet `yaml:"capabilities,omitempty" json:"capabilities,omitempty"` // MANUAL - config-driven capability definitions
}
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
"go.yaml.in/yaml/v3"
"gopkg.in/yaml.v3"
)
func TestConfigSchemaExtensions(t *testing.T) {
+2 -9
View File
@@ -3,19 +3,12 @@ package internal
const (
// JSONSchemaVersion is the current schema version output by the JSON encoder
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
JSONSchemaVersion = "16.1.11"
JSONSchemaVersion = "16.1.3"
// Changelog
// 16.1.0 - reformulated the python pdm fields (added "URL" and removed the unused "path" field).
// 16.1.1 - correct elf package osCpe field according to the document of systemd (also add appCpe field)
// 16.1.2 - placeholder for 16.1.2 changelog
// 16.1.3 - add GGUFFileParts to GGUFFileHeader metadata
// 16.1.4 - add BunLockEntry metadata type for bun.lock support
// 16.1.5 - add DenoLockEntry and DenoRemoteLockEntry metadata types for deno.lock support
// 16.1.6 - add Dependencies to ElixirMixLockEntry metadata
// 16.1.7 - add AppleAppBundleEntry metadata type for the apple app bundle cataloger
// 16.1.8 - add VcpkgManifest metadata type for vcpkg manifest support
// 16.1.9 - add Symbols (grouped by owning package import path) to GolangBinaryBuildinfoEntry metadata
// 16.1.10 - add SafeTensorsModelInfo metadata type for the safetensors AI model cataloger
// 16.1.11 - add CpanDistribution and CpanUnpackedRelease metadata types for the perl catalogers
)
+1 -13
View File
@@ -12,20 +12,8 @@ const perFileReadLimit = 2 * GB
// protect against decompression bomb attacks.
func safeCopy(writer io.Writer, reader io.Reader) error {
numBytes, err := io.Copy(writer, io.LimitReader(reader, perFileReadLimit))
if numBytes >= perFileReadLimit {
if numBytes >= perFileReadLimit || errors.Is(err, io.EOF) {
return fmt.Errorf("zip read limit hit (potential decompression bomb attack)")
}
// Propagate decompression / read errors up to the caller. io.Copy
// on the happy path returns (n, nil); the only way err is non-nil
// here is that the underlying reader surfaced a real failure
// ("flate: corrupt input before offset X" on a mangled ZIP entry,
// a network-backed reader erroring mid-stream, etc.). The previous
// implementation dropped that error and the caller stored a
// partial / empty buffer as a "successful" extract, which silently
// downgraded Java cataloger output and caused SBOM scanners to
// miss known CVEs (#4806).
if err != nil && !errors.Is(err, io.EOF) {
return fmt.Errorf("failed to read archive entry: %w", err)
}
return nil
}
-64
View File
@@ -1,64 +0,0 @@
package file
import (
"bytes"
"errors"
"io"
"strings"
"testing"
)
// errReader returns a deterministic non-EOF error after emitting some
// bytes, mimicking what compress/flate does when it hits a corrupt
// stream mid-entry.
type errReader struct {
data []byte
err error
off int
}
func (r *errReader) Read(p []byte) (int, error) {
if r.off >= len(r.data) {
return 0, r.err
}
n := copy(p, r.data[r.off:])
r.off += n
return n, nil
}
func TestSafeCopy(t *testing.T) {
t.Run("clean copy returns nil", func(t *testing.T) {
var buf bytes.Buffer
if err := safeCopy(&buf, strings.NewReader("hello")); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got := buf.String(); got != "hello" {
t.Fatalf("unexpected buffer contents: %q", got)
}
})
t.Run("propagates decompression error", func(t *testing.T) {
// #4806: safeCopy used to drop non-EOF errors, so the caller
// would persist a partial buffer as a successful extract and
// downstream catalogers silently read empty manifests.
sentinel := errors.New("flate: corrupt input before offset 42")
var buf bytes.Buffer
err := safeCopy(&buf, &errReader{data: []byte("partial"), err: sentinel})
if err == nil {
t.Fatalf("expected error to be returned, got nil")
}
if !errors.Is(err, sentinel) {
t.Fatalf("error does not wrap sentinel: %v", err)
}
})
t.Run("EOF is not treated as an error", func(t *testing.T) {
// The old code had a dead io.EOF branch that labelled clean
// reads as decompression bombs; keep the happy path clean.
var buf bytes.Buffer
err := safeCopy(&buf, io.LimitReader(strings.NewReader("abc"), 3))
if err != nil {
t.Fatalf("unexpected error on clean copy: %v", err)
}
})
}
+2 -2
View File
@@ -151,11 +151,11 @@ func TestGetter_GetToDir_CertConcerns(t *testing.T) {
}
}
func assertUnknownAuthorityError(t assert.TestingT, err error, _ ...any) bool {
func assertUnknownAuthorityError(t assert.TestingT, err error, _ ...interface{}) bool {
return assert.ErrorAs(t, err, &x509.UnknownAuthorityError{})
}
func assertErrNonArchiveSource(t assert.TestingT, err error, _ ...any) bool {
func assertErrNonArchiveSource(t assert.TestingT, err error, _ ...interface{}) bool {
return assert.ErrorIs(t, err, ErrNonArchiveSource)
}
-66
View File
@@ -1,66 +0,0 @@
package file
import "io"
// ReaderSize reports the number of bytes behind r. The bool is false when that cannot be determined, and
// callers must branch on it rather than treating the zero as a size.
//
// Binary parsers read sizes out of the files they are parsing and allocate against them. Those sizes are
// 32- and 64-bit fields under the control of whoever produced the file, so a truncated or hostile input
// can declare gigabytes that are not there. The read would fail afterwards either way; what matters is
// refusing before the allocation, and that needs a real byte count to weigh the claim against.
//
// The bool is the whole point of the signature. An earlier version returned 0 for every failure mode, and
// a caller that forgot to special-case it got a bound of "nothing is allowed" or, more often, silently
// fell back to an unbounded path. Making the caller name the failure keeps that from happening by
// omission.
//
// Both *bytes.Reader and *io.SectionReader answer directly. A file handle only has Seek, so fall back to
// a save-and-restore seek, which leaves the caller's cursor where it found it. That fallback is not
// atomic: it moves the cursor and puts it back, so a reader being read concurrently can observe the
// intermediate position.
//
// Neither answer is taken on faith, because a reader can report a length it cannot deliver: an
// *io.SectionReader answers with the length it was constructed with, and debug/elf and friends construct
// theirs with a nominal one, so io.NewSectionReader(r, 0, 1<<63-1) would otherwise report a bound that
// permits everything as if it had been measured. The last byte is read back to confirm the count, which is
// one ReadAt whatever the size, and the read is what the caller was going to weigh anyway.
//
// A wrapper that embeds io.ReaderAt as an interface promotes only ReadAt, so it answers no size here
// however sizable the reader underneath it is. A type meant to be bounded needs to forward Size() itself.
func ReaderSize(r io.ReaderAt) (int64, bool) {
size, ok := reportedSize(r)
if !ok {
return 0, false
}
// ReadAt may return io.EOF alongside a full read, so the count is what says the byte is there
var last [1]byte
if n, _ := r.ReadAt(last[:], size-1); n != len(last) {
return 0, false
}
return size, true
}
// reportedSize is what the reader says about itself, before ReaderSize checks whether it can back it up.
func reportedSize(r io.ReaderAt) (int64, bool) {
if sr, ok := r.(interface{ Size() int64 }); ok {
size := sr.Size()
return size, size > 0
}
s, ok := r.(io.Seeker)
if !ok {
return 0, false
}
cur, err := s.Seek(0, io.SeekCurrent)
if err != nil {
return 0, false
}
size, err := s.Seek(0, io.SeekEnd)
if err != nil {
return 0, false
}
if _, err := s.Seek(cur, io.SeekStart); err != nil {
return 0, false
}
return size, size > 0
}
-140
View File
@@ -1,140 +0,0 @@
package file
import (
"bytes"
"io"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestReaderSize(t *testing.T) {
tests := []struct {
name string
r io.ReaderAt
want int64
wantOK bool
}{
{
name: "bytes.Reader answers directly",
r: bytes.NewReader(make([]byte, 1234)),
want: 1234,
wantOK: true,
},
{
// an empty reader is not a size to bound against, so it reports not-ok along with everything
// else that cannot be measured
name: "empty bytes.Reader",
r: bytes.NewReader(nil),
want: 0,
},
{
name: "SectionReader over a real length",
r: io.NewSectionReader(bytes.NewReader(make([]byte, 500)), 100, 300),
want: 300,
wantOK: true,
},
{
name: "seek-only reader falls back to seeking",
r: seekOnly{bytes.NewReader(make([]byte, 4096))},
want: 4096,
wantOK: true,
},
{
name: "reader with neither Size nor Seek cannot be measured",
r: readAtOnly{bytes.NewReader(make([]byte, 4096))},
want: 0,
},
{
// the shape debug/elf, debug/pe and debug/macho build internally. Taking this at its word hands
// back a bound that permits everything, reported as if it had been measured.
name: "SectionReader over a nominal length reports what it can deliver",
r: io.NewSectionReader(bytes.NewReader(make([]byte, 500)), 0, 1<<63-1),
want: 0,
},
{
// a Size() a type simply got wrong is the same failure: the bytes are what decides
name: "a size the reader cannot back up is refused",
r: overstatedSize{ReaderAt: bytes.NewReader(make([]byte, 10)), size: 1 << 30},
want: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
size, ok := ReaderSize(tt.r)
assert.Equal(t, tt.want, size)
assert.Equal(t, tt.wantOK, ok)
})
}
}
func TestReaderSize_FileHandle(t *testing.T) {
path := filepath.Join(t.TempDir(), "f.bin")
require.NoError(t, os.WriteFile(path, make([]byte, 7777), 0o600))
f, err := os.Open(path)
require.NoError(t, err)
defer f.Close()
// the cursor has to come back where it was found: the caller may be mid-read, and an *os.File is the
// one input here that carries a shared position
_, err = f.Seek(101, io.SeekStart)
require.NoError(t, err)
size, ok := ReaderSize(f)
assert.True(t, ok)
assert.Equal(t, int64(7777), size)
at, err := f.Seek(0, io.SeekCurrent)
require.NoError(t, err)
assert.Equal(t, int64(101), at, "measuring the file must not move the caller's cursor")
}
// TestReaderSize_WrapperMustForwardSize pins the shape that made every bound expressed against this
// helper go silently inert. A type that embeds io.ReaderAt as an interface promotes only ReadAt, so it
// answers 0 here however sizable the reader underneath is, and a caller that treats 0 as "fall back to
// unbounded" then stops bounding anything. Forwarding Size() is what fixes it, and this is the assertion
// that a new wrapper has to satisfy.
func TestReaderSize_WrapperMustForwardSize(t *testing.T) {
inner := bytes.NewReader(make([]byte, 2048))
size, ok := ReaderSize(embedsReaderAt{inner})
assert.False(t, ok, "a wrapper that only embeds the interface cannot be measured")
assert.Equal(t, int64(0), size)
size, ok = ReaderSize(forwardsSize{inner})
assert.True(t, ok, "a wrapper meant to be bounded has to forward Size itself")
assert.Equal(t, int64(2048), size)
}
type seekOnly struct{ inner *bytes.Reader }
func (s seekOnly) ReadAt(p []byte, off int64) (int, error) { return s.inner.ReadAt(p, off) }
func (s seekOnly) Seek(off int64, whence int) (int64, error) {
return s.inner.Seek(off, whence)
}
type readAtOnly struct{ inner *bytes.Reader }
func (r readAtOnly) ReadAt(p []byte, off int64) (int, error) { return r.inner.ReadAt(p, off) }
type embedsReaderAt struct{ io.ReaderAt }
type forwardsSize struct{ io.ReaderAt }
func (f forwardsSize) Size() int64 {
size, _ := ReaderSize(f.ReaderAt)
return size
}
// overstatedSize answers with a size larger than the bytes behind it, the way a wrapper that returns a
// declared length rather than a measured one would.
type overstatedSize struct {
io.ReaderAt
size int64
}
func (o overstatedSize) Size() int64 { return o.size }
+4 -34
View File
@@ -5,7 +5,6 @@ import (
"io/fs"
"os"
"path/filepath"
"strings"
"github.com/diskfs/go-diskfs/filesystem"
)
@@ -15,7 +14,8 @@ type WalkDiskDirFunc func(fsys filesystem.FileSystem, path string, d os.FileInfo
// WalkDiskDir walks the file tree within the go-diskfs filesystem at root, calling fn for each file or directory in the tree, including root.
// This is meant to mimic the behavior of fs.WalkDir in the standard library.
func WalkDiskDir(fsys filesystem.FileSystem, root string, fn WalkDiskDirFunc) error {
infos, err := readDiskDir(fsys, root)
infos, err := fsys.ReadDir(root)
if err != nil {
return err
}
@@ -51,7 +51,7 @@ func walkDiskDir(fsys filesystem.FileSystem, name string, d os.FileInfo, walkDir
isDir := d != nil && d.IsDir()
if d == nil {
_, err := readDiskDir(fsys, name)
_, err := fsys.ReadDir(name)
if err != nil {
return nil
}
@@ -62,7 +62,7 @@ func walkDiskDir(fsys filesystem.FileSystem, name string, d os.FileInfo, walkDir
return nil
}
dirs, err := readDiskDir(fsys, name)
dirs, err := fsys.ReadDir(name)
if err != nil {
err = walkDirFn(fsys, name, d, err)
if err != nil {
@@ -87,33 +87,3 @@ func walkDiskDir(fsys filesystem.FileSystem, name string, d os.FileInfo, walkDir
}
return nil
}
// readDiskDir reads the directory entries at the given path from a go-diskfs filesystem.
// go-diskfs returns fs.DirEntry values; these are resolved to os.FileInfo so callers have
// access to the full file metadata (mode, size, modification time).
func readDiskDir(fsys filesystem.FileSystem, p string) ([]os.FileInfo, error) {
entries, err := fsys.ReadDir(ToFSPath(p))
if err != nil {
return nil, err
}
infos := make([]os.FileInfo, 0, len(entries))
for _, entry := range entries {
info, err := entry.Info()
if err != nil {
return nil, err
}
infos = append(infos, info)
}
return infos, nil
}
// ToFSPath converts an absolute ("/"-rooted) path into an io/fs-valid path as required by
// go-diskfs, where the root is "." and other paths carry no leading slash (see io/fs.ValidPath).
func ToFSPath(p string) string {
p = strings.TrimPrefix(p, "/")
if p == "" {
return "."
}
return p
}
+2 -2
View File
@@ -43,10 +43,10 @@ func createTestFS(t *testing.T) filesystem.FileSystem {
for _, tf := range testFiles {
if tf.isDir {
err := fsys.Mkdir(ToFSPath(tf.path))
err := fsys.Mkdir(tf.path)
require.NoError(t, err)
} else {
f, err := fsys.OpenFile(ToFSPath(tf.path), os.O_CREATE|os.O_RDWR)
f, err := fsys.OpenFile(tf.path, os.O_CREATE|os.O_RDWR)
require.NoError(t, err)
_, err = f.Write([]byte(tf.content))
require.NoError(t, err)
+1
View File
@@ -1,4 +1,5 @@
//go:build !windows
// +build !windows
package file

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