mirror of
https://github.com/anchore/syft.git
synced 2026-10-13 22:57:19 +02:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfde0974b0 | ||
|
|
9f047fdf11 |
+104
-7
@@ -1,10 +1,67 @@
|
||||
# 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:
|
||||
## internal tools ############################################################################
|
||||
|
||||
# we want to use a pinned version of binny to manage the toolchain (so binny manages itself!)
|
||||
- name: binny
|
||||
version:
|
||||
want: v0.13.0
|
||||
method: github-release
|
||||
with:
|
||||
repo: anchore/binny
|
||||
|
||||
# used to produce SBOMs during release
|
||||
- name: syft
|
||||
version:
|
||||
want: v1.42.3
|
||||
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 at release to generate the changelog
|
||||
- name: chronicle
|
||||
version:
|
||||
want: v0.8.0
|
||||
method: github-release
|
||||
with:
|
||||
repo: anchore/chronicle
|
||||
|
||||
## external tools ############################################################################
|
||||
|
||||
# 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 +70,58 @@ tools:
|
||||
with:
|
||||
repo: neilpa/yajsv
|
||||
|
||||
# used to release all artifacts
|
||||
- name: goreleaser
|
||||
version:
|
||||
want: v2.15.2
|
||||
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 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.89.0
|
||||
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.5
|
||||
method: github-release
|
||||
with:
|
||||
repo: mikefarah/yq
|
||||
|
||||
@@ -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
|
||||
@@ -1,45 +1,55 @@
|
||||
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)"
|
||||
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"
|
||||
|
||||
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@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
if: inputs.go-version != ''
|
||||
with:
|
||||
go-version: ${{ inputs.go-version }}
|
||||
cache-key-prefix: ${{ inputs.cache-key-prefix }}
|
||||
cache-enabled: ${{ inputs.cache-enabled }}
|
||||
|
||||
- name: Install binny-managed tools
|
||||
check-latest: true
|
||||
- name: Restore tool cache
|
||||
if: inputs.tools == 'true'
|
||||
id: tool-cache
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
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 != ''
|
||||
shell: bash
|
||||
@@ -47,34 +57,13 @@ 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[@]}"
|
||||
|
||||
# ORAS cache: restore-only on non-default branches / forks
|
||||
DEBIAN_FRONTEND=noninteractive sudo apt update && sudo -E apt install -y "${packages[@]}"
|
||||
- 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
|
||||
if: inputs.download-test-fixture-cache == 'true'
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
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
|
||||
with:
|
||||
path: ${{ github.workspace }}/.tmp/oras-cache
|
||||
key: ${{ inputs.cache-key-prefix }}-oras-cache
|
||||
|
||||
- name: Download test fixture cache
|
||||
if: inputs.download-test-fixture-cache == 'true'
|
||||
shell: bash
|
||||
|
||||
+26
-6
@@ -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,9 +28,14 @@ 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:
|
||||
@@ -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"
|
||||
|
||||
Executable
+11
@@ -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
|
||||
Executable
+36
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
Executable
+30
@@ -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())
|
||||
Executable
+57
@@ -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)
|
||||
@@ -13,7 +13,7 @@ permissions: {}
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
uses: anchore/workflows/.github/workflows/codeql.yaml@7212994dc8fc3a53fe9c8e766ab5b4ddd16ea3d4 # v0.8.0
|
||||
uses: anchore/workflows/.github/workflows/codeql.yaml@e8cee3a5916cebb68cda68b54c180f43394c1910 # v0.5.0
|
||||
permissions:
|
||||
security-events: write
|
||||
packages: read
|
||||
|
||||
@@ -21,61 +21,42 @@ 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:
|
||||
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
|
||||
uses: anchore/workflows/.github/workflows/check-version-available.yaml@8b2b1caf40e03933c6807e03b99e883e2ceb5ac8 # v0.4.0
|
||||
with:
|
||||
version: ${{ github.event.inputs.version }}
|
||||
|
||||
check-gate:
|
||||
if: ${{ github.event.inputs.phase == 'all' && !inputs.skip-checks }}
|
||||
if: ${{ github.event.inputs.phase == 'all' }}
|
||||
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
|
||||
checks: read # required for getting the status of specific check names
|
||||
uses: anchore/workflows/.github/workflows/check-gate.yaml@8b2b1caf40e03933c6807e03b99e883e2ceb5ac8 # v0.4.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)", "Containers-storage tests (Podman)", "Containers-storage tests (Buildah)", "Integration tests", "Static analysis", "Unit tests"]'
|
||||
checks: '["Acceptance tests (Linux)", "Acceptance tests (Mac)", "Build snapshot artifacts", "CLI tests (Linux)", "Integration tests", "Static analysis", "Unit tests"]'
|
||||
|
||||
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: [ check-gate, version-available ]
|
||||
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 +64,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@4907a6ddec9925e35a0a9e82d7399ccc52663121 #v4.1.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@4907a6ddec9925e35a0a9e82d7399ccc52663121 #v4.1.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 +101,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@e22c389904149dbc22b58101806040fa8d37a610 #v0.24.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@45a88b9581bfab2566dc881e2cd66d334e621e2c #v3.0.3
|
||||
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:
|
||||
|
||||
@@ -20,12 +20,12 @@ 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@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
|
||||
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
|
||||
|
||||
@@ -21,17 +21,13 @@ 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"
|
||||
# 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"
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -49,29 +45,11 @@ 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 +64,6 @@ 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): binny ships no windows release, so binny tools
|
||||
# are unavailable here (`make unit` skips the fixture refresh on windows)
|
||||
- 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 binny-free version of `make download-test-fixture-cache` (keep the oras version in sync with .binny.yaml)
|
||||
- name: Download test fixture cache
|
||||
shell: bash
|
||||
run: |
|
||||
curl -sSfL https://github.com/oras-project/oras/releases/download/v1.3.4/oras_1.3.4_windows_amd64.zip -o "$RUNNER_TEMP/oras.zip"
|
||||
unzip -q "$RUNNER_TEMP/oras.zip" oras.exe -d "$RUNNER_TEMP"
|
||||
"$RUNNER_TEMP/oras.exe" pull ghcr.io/anchore/oss-cache/syft-test-fixture-cache:v2
|
||||
|
||||
- name: Run unit tests
|
||||
run: make unit
|
||||
|
||||
Integration-Test:
|
||||
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
|
||||
name: "Integration tests"
|
||||
@@ -122,7 +71,7 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -142,20 +91,16 @@ 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"
|
||||
# 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"
|
||||
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@742bf56072eb4845a0f94b3394673e4903c90ff0 # v2.1.0
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -177,72 +122,6 @@ jobs:
|
||||
path: snapshot/
|
||||
retention-days: 30
|
||||
|
||||
Containers-Storage-Podman:
|
||||
name: "Containers-storage tests (Podman)"
|
||||
needs: [Build-Snapshot-Artifacts]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c #v8.0.1
|
||||
with:
|
||||
name: snapshot
|
||||
path: snapshot
|
||||
|
||||
# go-make's setup only (not ./.github/actions/bootstrap): the test only needs make + the snapshot binary
|
||||
- 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"
|
||||
|
||||
- name: Install Podman
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --no-install-recommends -y jq podman uidmap
|
||||
|
||||
- name: Scan image from Podman's local store
|
||||
run: make containers-storage-test
|
||||
env:
|
||||
BUILDER: podman
|
||||
|
||||
Containers-Storage-Buildah:
|
||||
name: "Containers-storage tests (Buildah)"
|
||||
needs: [Build-Snapshot-Artifacts]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c #v8.0.1
|
||||
with:
|
||||
name: snapshot
|
||||
path: snapshot
|
||||
|
||||
# go-make's setup only (not ./.github/actions/bootstrap): the test only needs make + the snapshot binary
|
||||
- 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"
|
||||
|
||||
- name: Install Buildah
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --no-install-recommends -y buildah jq uidmap
|
||||
|
||||
- name: Scan image from Buildah's local store
|
||||
run: make containers-storage-test
|
||||
env:
|
||||
BUILDER: buildah
|
||||
|
||||
Acceptance-Linux:
|
||||
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
|
||||
name: "Acceptance tests (Linux)"
|
||||
@@ -251,7 +130,10 @@ jobs:
|
||||
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@742bf56072eb4845a0f94b3394673e4903c90ff0 # v2.1.0
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -293,9 +175,9 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
|
||||
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
|
||||
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -329,7 +211,10 @@ jobs:
|
||||
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@742bf56072eb4845a0f94b3394673e4903c90ff0 # v2.1.0
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -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
@@ -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
-30
@@ -1,17 +1,12 @@
|
||||
version: "2"
|
||||
run:
|
||||
tests: false
|
||||
# keep in sync with buildTags in .make/main.go
|
||||
build-tags:
|
||||
- containers_image_openpgp
|
||||
- exclude_graphdriver_btrfs
|
||||
linters:
|
||||
default: none
|
||||
enable:
|
||||
- asciicheck
|
||||
- bodyclose
|
||||
- copyloopvar
|
||||
- depguard
|
||||
- dogsled
|
||||
- dupl
|
||||
- errcheck
|
||||
@@ -34,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
|
||||
@@ -70,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$
|
||||
@@ -83,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
|
||||
@@ -91,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
|
||||
@@ -116,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)
|
||||
|
||||
+300
-80
@@ -15,7 +15,6 @@ builds:
|
||||
binary: syft
|
||||
goos: [linux]
|
||||
goarch: [amd64, arm64, ppc64le, riscv64, s390x]
|
||||
tags: [containers_image_openpgp, exclude_graphdriver_btrfs]
|
||||
mod_timestamp: &build-timestamp '{{ .CommitTimestamp }}'
|
||||
ldflags: &build-ldflags |
|
||||
-w
|
||||
@@ -74,88 +73,306 @@ 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}}-riscv64
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-riscv64
|
||||
goarch: riscv64
|
||||
dockerfile: Dockerfile
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- "--platform=linux/riscv64"
|
||||
- "--build-arg=DEBIAN_VERSION=13"
|
||||
- "--build-arg=BUILD_DATE={{.Date}}"
|
||||
- "--build-arg=BUILD_VERSION={{.Version}}"
|
||||
- "--build-arg=VCS_REF={{.FullCommit}}"
|
||||
- "--build-arg=VCS_URL={{.GitURL}}"
|
||||
|
||||
- image_templates:
|
||||
- anchore/syft:{{.Tag}}-s390x
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-s390x
|
||||
goarch: s390x
|
||||
dockerfile: Dockerfile
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- "--platform=linux/s390x"
|
||||
- "--build-arg=BUILD_DATE={{.Date}}"
|
||||
- "--build-arg=BUILD_VERSION={{.Version}}"
|
||||
- "--build-arg=VCS_REF={{.FullCommit}}"
|
||||
- "--build-arg=VCS_URL={{.GitURL}}"
|
||||
|
||||
# nonroot images...
|
||||
- image_templates:
|
||||
- anchore/syft:{{.Tag}}-nonroot-amd64
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-amd64
|
||||
goarch: amd64
|
||||
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-riscv64
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-riscv64
|
||||
goarch: riscv64
|
||||
dockerfile: Dockerfile.nonroot
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- "--platform=linux/riscv64"
|
||||
- "--build-arg=DEBIAN_VERSION=13"
|
||||
- "--build-arg=BUILD_DATE={{.Date}}"
|
||||
- "--build-arg=BUILD_VERSION={{.Version}}"
|
||||
- "--build-arg=VCS_REF={{.FullCommit}}"
|
||||
- "--build-arg=VCS_URL={{.GitURL}}"
|
||||
|
||||
- image_templates:
|
||||
- anchore/syft:{{.Tag}}-nonroot-s390x
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-s390x
|
||||
goarch: s390x
|
||||
dockerfile: Dockerfile.nonroot
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- "--platform=linux/s390x"
|
||||
- "--build-arg=BUILD_DATE={{.Date}}"
|
||||
- "--build-arg=BUILD_VERSION={{.Version}}"
|
||||
- "--build-arg=VCS_REF={{.FullCommit}}"
|
||||
- "--build-arg=VCS_URL={{.GitURL}}"
|
||||
|
||||
# debug images...
|
||||
- image_templates:
|
||||
- anchore/syft:{{.Tag}}-debug-amd64
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug-amd64
|
||||
goarch: amd64
|
||||
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-riscv64
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug-riscv64
|
||||
goarch: riscv64
|
||||
dockerfile: Dockerfile.debug
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- "--platform=linux/riscv64"
|
||||
- "--build-arg=DEBIAN_VERSION=13"
|
||||
- "--build-arg=BUILD_DATE={{.Date}}"
|
||||
- "--build-arg=BUILD_VERSION={{.Version}}"
|
||||
- "--build-arg=VCS_REF={{.FullCommit}}"
|
||||
- "--build-arg=VCS_URL={{.GitURL}}"
|
||||
|
||||
- image_templates:
|
||||
- anchore/syft:{{.Tag}}-debug-s390x
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug-s390x
|
||||
goarch: s390x
|
||||
dockerfile: Dockerfile.debug
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- "--platform=linux/s390x"
|
||||
- "--build-arg=BUILD_DATE={{.Date}}"
|
||||
- "--build-arg=BUILD_VERSION={{.Version}}"
|
||||
- "--build-arg=VCS_REF={{.FullCommit}}"
|
||||
- "--build-arg=VCS_URL={{.GitURL}}"
|
||||
|
||||
docker_manifests:
|
||||
- name_template: anchore/syft:latest
|
||||
image_templates:
|
||||
- anchore/syft:{{.Tag}}-amd64
|
||||
- anchore/syft:{{.Tag}}-arm64v8
|
||||
- anchore/syft:{{.Tag}}-ppc64le
|
||||
- anchore/syft:{{.Tag}}-riscv64
|
||||
- anchore/syft:{{.Tag}}-s390x
|
||||
|
||||
- name_template: ghcr.io/anchore/syft:latest
|
||||
image_templates:
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-amd64
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-arm64v8
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-ppc64le
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-riscv64
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-s390x
|
||||
|
||||
- name_template: anchore/syft:{{.Tag}}
|
||||
image_templates:
|
||||
- anchore/syft:{{.Tag}}-amd64
|
||||
- anchore/syft:{{.Tag}}-arm64v8
|
||||
- anchore/syft:{{.Tag}}-ppc64le
|
||||
- anchore/syft:{{.Tag}}-riscv64
|
||||
- anchore/syft:{{.Tag}}-s390x
|
||||
|
||||
- name_template: ghcr.io/anchore/syft:{{.Tag}}
|
||||
image_templates:
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-amd64
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-arm64v8
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-ppc64le
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-riscv64
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-s390x
|
||||
|
||||
# nonroot images...
|
||||
- name_template: anchore/syft:nonroot
|
||||
image_templates:
|
||||
- anchore/syft:{{.Tag}}-nonroot-amd64
|
||||
- anchore/syft:{{.Tag}}-nonroot-arm64v8
|
||||
- anchore/syft:{{.Tag}}-nonroot-ppc64le
|
||||
- anchore/syft:{{.Tag}}-nonroot-riscv64
|
||||
- anchore/syft:{{.Tag}}-nonroot-s390x
|
||||
|
||||
- name_template: ghcr.io/anchore/syft:nonroot
|
||||
image_templates:
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-amd64
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-arm64v8
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-ppc64le
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-riscv64
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-s390x
|
||||
|
||||
- name_template: anchore/syft:{{.Tag}}-nonroot
|
||||
image_templates:
|
||||
- anchore/syft:{{.Tag}}-nonroot-amd64
|
||||
- anchore/syft:{{.Tag}}-nonroot-arm64v8
|
||||
- anchore/syft:{{.Tag}}-nonroot-ppc64le
|
||||
- anchore/syft:{{.Tag}}-nonroot-riscv64
|
||||
- anchore/syft:{{.Tag}}-nonroot-s390x
|
||||
|
||||
- name_template: ghcr.io/anchore/syft:{{.Tag}}-nonroot
|
||||
image_templates:
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-amd64
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-arm64v8
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-ppc64le
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-riscv64
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-s390x
|
||||
|
||||
# debug images...
|
||||
- name_template: anchore/syft:debug
|
||||
image_templates:
|
||||
- anchore/syft:{{.Tag}}-debug-amd64
|
||||
- anchore/syft:{{.Tag}}-debug-arm64v8
|
||||
- anchore/syft:{{.Tag}}-debug-ppc64le
|
||||
- anchore/syft:{{.Tag}}-debug-riscv64
|
||||
- anchore/syft:{{.Tag}}-debug-s390x
|
||||
|
||||
- name_template: ghcr.io/anchore/syft:debug
|
||||
image_templates:
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug-amd64
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug-arm64v8
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug-ppc64le
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug-riscv64
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug-s390x
|
||||
|
||||
- name_template: anchore/syft:{{.Tag}}-debug
|
||||
image_templates:
|
||||
- anchore/syft:{{.Tag}}-debug-amd64
|
||||
- anchore/syft:{{.Tag}}-debug-arm64v8
|
||||
- anchore/syft:{{.Tag}}-debug-ppc64le
|
||||
- anchore/syft:{{.Tag}}-debug-riscv64
|
||||
- anchore/syft:{{.Tag}}-debug-s390x
|
||||
|
||||
- name_template: ghcr.io/anchore/syft:{{.Tag}}-debug
|
||||
image_templates:
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug-amd64
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug-arm64v8
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug-ppc64le
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug-riscv64
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug-s390x
|
||||
|
||||
sboms:
|
||||
- artifacts: archive
|
||||
@@ -168,13 +385,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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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=
|
||||
-229
@@ -1,229 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"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"
|
||||
)
|
||||
|
||||
// buildTags must match the linux build in .goreleaser.yaml so tests and lint compile the same code that ships
|
||||
// (containers_image_openpgp compiles in stereoscope's real containers-storage provider instead of its stub).
|
||||
// exclude_graphdriver_btrfs drops the cgo-only btrfs driver, which needs libbtrfs headers whenever cgo is on (e.g.
|
||||
// under -race); release builds are CGO_ENABLED=0 and never include it anyway.
|
||||
const buildTags = "containers_image_openpgp,exclude_graphdriver_btrfs"
|
||||
|
||||
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),
|
||||
gotest.Tags(buildTags),
|
||||
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 -tags="+buildTags+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 -tags="+buildTags+" 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),
|
||||
)
|
||||
},
|
||||
},
|
||||
|
||||
// containers-storage tests: build a fixture image with BUILDER (podman or buildah) into an isolated local store
|
||||
// and scan it with the snapshot binary. Linux only, and not hooked into "test" since it needs the builder installed.
|
||||
Task{
|
||||
Name: "containers-storage-test",
|
||||
Description: "Run containers-storage tests (BUILDER=podman|buildah)",
|
||||
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")
|
||||
}
|
||||
Run(
|
||||
"bash test/containers-storage/source-test.sh "+config.Env("BUILDER", "podman"),
|
||||
run.Env("SYFT_BINARY_LOCATION", bin),
|
||||
)
|
||||
},
|
||||
},
|
||||
|
||||
// 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: unitFixtureDeps(),
|
||||
},
|
||||
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
|
||||
}
|
||||
|
||||
// unitFixtureDeps skips the fixture cache refresh on windows: it needs binny-managed tools (binny ships
|
||||
// no windows release) and linux docker, so fixture-backed tests there have to skip themselves instead.
|
||||
func unitFixtureDeps() []string {
|
||||
if config.Windows {
|
||||
return nil
|
||||
}
|
||||
return Deps("refresh-fixtures")
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
@@ -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
-3
@@ -1,4 +1,4 @@
|
||||
ARG DEBIAN_VERSION=13
|
||||
ARG DEBIAN_VERSION=12
|
||||
FROM gcr.io/distroless/static-debian${DEBIAN_VERSION}:latest AS build
|
||||
|
||||
FROM scratch
|
||||
@@ -8,7 +8,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"]
|
||||
|
||||
+22
-4
@@ -1,10 +1,28 @@
|
||||
ARG DEBIAN_VERSION=13
|
||||
FROM gcr.io/distroless/static-debian${DEBIAN_VERSION}:debug
|
||||
ARG DEBIAN_VERSION=12
|
||||
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 /
|
||||
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"]
|
||||
|
||||
@@ -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
-3
@@ -1,12 +1,28 @@
|
||||
ARG DEBIAN_VERSION=13
|
||||
ARG DEBIAN_VERSION=12
|
||||
FROM gcr.io/distroless/static-debian${DEBIAN_VERSION}: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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,9 +8,10 @@
|
||||
|
||||
<p align="center">
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<a href="" target="_blank"><img alt="License: Apache-2.0" src="https://img.shields.io/badge/License-Apache%202.0-blue.svg"></a>
|
||||
<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>
|
||||
<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>
|
||||
</p>
|
||||
@@ -61,18 +62,6 @@ syft <image> -o cyclonedx-json
|
||||
syft <image> -o spdx-json=./spdx.json -o cyclonedx-json=./cdx.json
|
||||
```
|
||||
|
||||
### Local containers-storage images
|
||||
|
||||
On Linux, syft can scan images from a local containers-storage store (e.g. built with Buildah or Podman) when asked explicitly; plain image references never look there:
|
||||
|
||||
```bash
|
||||
syft --from containers-storage localhost/myimage:latest
|
||||
|
||||
# rootless stores must be read from inside the builder's user namespace
|
||||
podman unshare syft --from containers-storage localhost/myimage:latest
|
||||
```
|
||||
|
||||
This is included in the Linux release binaries. When building from source, add `-tags containers_image_openpgp,exclude_graphdriver_btrfs`.
|
||||
|
||||
> [!TIP]
|
||||
> **Check out the [Getting Started guide](https://oss.anchore.com/docs/guides/sbom/getting-started/)** to explore all of the capabilities and features.
|
||||
|
||||
+262
-55
@@ -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
|
||||
@@ -30,7 +25,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 +38,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 +57,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 +102,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 +114,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 +203,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 +214,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 +281,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
|
||||
@@ -303,7 +432,6 @@ tasks:
|
||||
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 +452,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 +483,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 +497,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 +506,7 @@ tasks:
|
||||
{{ .TMP_DIR }}
|
||||
|
||||
|
||||
## Code and data generation targets ######################################
|
||||
## Code and data generation targets #################################
|
||||
|
||||
generate:
|
||||
desc: Add data generation tasks
|
||||
@@ -439,7 +556,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 +675,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 +690,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
|
||||
---
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -60,7 +60,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 +235,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)
|
||||
@@ -279,7 +278,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 +286,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 +296,6 @@ func convertDetectorsToPatterns(detectors []capabilities.Detector, packageTypes,
|
||||
Packages: convertDetectorPackages(det.Packages),
|
||||
Comment: det.Comment,
|
||||
PackageTypes: packageTypes,
|
||||
PURLTypes: purlTypes,
|
||||
JSONSchemaTypes: jsonSchemaTypes,
|
||||
})
|
||||
}
|
||||
@@ -349,14 +346,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 +367,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 +385,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 +399,6 @@ func buildTableRowFromCapabilities(ecosystem, name, criteria string, purlTypes [
|
||||
ecosystem,
|
||||
name,
|
||||
criteria,
|
||||
formatPURLTypes(purlTypes),
|
||||
license,
|
||||
nodes,
|
||||
edges,
|
||||
@@ -413,14 +409,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 {
|
||||
|
||||
@@ -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"]`)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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,7 +34,6 @@ func Convert(app clio.Application) *cobra.Command {
|
||||
opts := &ConvertOptions{
|
||||
UpdateCheck: options.DefaultUpdateCheck(),
|
||||
Output: options.DefaultOutput(),
|
||||
Convert: options.DefaultConvert(),
|
||||
}
|
||||
|
||||
return app.SetupCommand(&cobra.Command{
|
||||
@@ -58,11 +46,11 @@ func Convert(app clio.Application) *cobra.Command {
|
||||
}),
|
||||
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,67 +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
|
||||
}
|
||||
// output files are opened up front; release them if we bail out before writing
|
||||
if c, ok := writer.(io.Closer); ok {
|
||||
defer func() { _ = c.Close() }()
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -149,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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,9 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -173,19 +171,10 @@ 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
|
||||
}
|
||||
// output files are opened up front; release them if we bail out before writing
|
||||
if c, ok := writer.(io.Closer); ok {
|
||||
defer func() { _ = c.Close() }()
|
||||
}
|
||||
|
||||
sources := opts.From
|
||||
if len(sources) == 0 {
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -23,10 +22,9 @@ func Test_defaultDir(t *testing.T) {
|
||||
homeDir := filepath.Join(tmpDir, "fake-home")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
env map[string]string
|
||||
expected string
|
||||
skipOnWindows bool
|
||||
name string
|
||||
env map[string]string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "no-xdg",
|
||||
@@ -34,8 +32,6 @@ func Test_defaultDir(t *testing.T) {
|
||||
"HOME": homeDir,
|
||||
},
|
||||
expected: homeDir,
|
||||
// xdg falls back to LOCALAPPDATA on windows, not HOME
|
||||
skipOnWindows: true,
|
||||
},
|
||||
{
|
||||
name: "xdg-cache",
|
||||
@@ -63,9 +59,6 @@ func Test_defaultDir(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if test.skipOnWindows && runtime.GOOS == "windows" {
|
||||
t.Skip("posix-only cache dir fallback")
|
||||
}
|
||||
restoreCache(t)
|
||||
|
||||
for k, v := range test.env {
|
||||
|
||||
@@ -20,7 +20,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 +44,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 +80,6 @@ func DefaultCatalog() Catalog {
|
||||
JavaScript: defaultJavaScriptConfig(),
|
||||
Python: defaultPythonConfig(),
|
||||
Nix: defaultNixConfig(),
|
||||
Cpp: defaultCppConfig(),
|
||||
Dotnet: defaultDotnetConfig(),
|
||||
Golang: defaultGolangConfig(),
|
||||
Java: defaultJavaConfig(),
|
||||
@@ -175,15 +172,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 +191,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 +299,6 @@ var publicisedEnrichmentOptions = []string{
|
||||
task.Java,
|
||||
task.JavaScript,
|
||||
task.Python,
|
||||
task.Vcpkg,
|
||||
}
|
||||
|
||||
func enrichmentEnabled(enrichDirectives []string, features ...string) *bool {
|
||||
|
||||
@@ -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.`)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ var _ clio.PostLoader = (*imageSource)(nil)
|
||||
func (o *sourceConfig) DescribeFields(descriptions clio.FieldDescriptionSet) {
|
||||
descriptions.Add(&o.File.Digests, `the file digest algorithms to use on the scanned file (options: "md5", "sha1", "sha224", "sha256", "sha384", "sha512")`)
|
||||
descriptions.Add(&o.Image.DefaultPullSource, `allows users to specify which image source should be used to generate the sbom
|
||||
valid values are: registry, docker, podman, containers-storage`)
|
||||
valid values are: registry, docker, podman`)
|
||||
}
|
||||
|
||||
type imageSource struct {
|
||||
@@ -74,7 +74,7 @@ func (c *imageSource) PostLoad() error {
|
||||
return checkDefaultSourceValues(c.DefaultPullSource)
|
||||
}
|
||||
|
||||
var validDefaultSourceValues = []string{"registry", "docker", "podman", "containers-storage", ""}
|
||||
var validDefaultSourceValues = []string{"registry", "docker", "podman", ""}
|
||||
|
||||
func checkDefaultSourceValues(source string) error {
|
||||
validValues := strset.New(validDefaultSourceValues...)
|
||||
|
||||
@@ -2,11 +2,10 @@ package options
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
@@ -21,10 +20,7 @@ import (
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
var _ interface {
|
||||
io.Closer
|
||||
sbom.Writer
|
||||
} = (*sbomMultiWriter)(nil)
|
||||
var _ sbom.Writer = (*sbomMultiWriter)(nil)
|
||||
|
||||
var _ interface {
|
||||
io.Closer
|
||||
@@ -168,12 +164,6 @@ func newSBOMMultiWriter(options ...sbomWriterDescription) (_ *sbomMultiWriter, e
|
||||
}
|
||||
|
||||
out := &sbomMultiWriter{}
|
||||
defer func() {
|
||||
// don't leak the files already opened when a later output fails
|
||||
if err != nil {
|
||||
_ = out.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
for _, option := range options {
|
||||
switch len(option.Path) {
|
||||
@@ -182,8 +172,8 @@ func newSBOMMultiWriter(options ...sbomWriterDescription) (_ *sbomMultiWriter, e
|
||||
format: option.Format,
|
||||
})
|
||||
default:
|
||||
// create any missing subdirectories (option.Path is a host path, so this must be filepath)
|
||||
dir := filepath.Dir(option.Path)
|
||||
// create any missing subdirectories
|
||||
dir := path.Dir(option.Path)
|
||||
if dir != "" {
|
||||
s, err := os.Stat(dir)
|
||||
if err != nil {
|
||||
@@ -220,20 +210,6 @@ func (m *sbomMultiWriter) Write(s sbom.SBOM) (errs error) {
|
||||
return errs
|
||||
}
|
||||
|
||||
// Close releases any files held by the writers. Write already closes each file it writes to, so this matters
|
||||
// when the SBOM is never written (e.g. an error after the writer was created).
|
||||
func (m *sbomMultiWriter) Close() error {
|
||||
var errs error
|
||||
for _, w := range m.writers {
|
||||
if c, ok := w.(io.Closer); ok {
|
||||
if err := c.Close(); err != nil && !errors.Is(err, os.ErrClosed) {
|
||||
errs = multierror.Append(errs, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// sbomStreamWriter implements sbom.Writer for a given format and io.Writer, also providing a close function for cleanup
|
||||
type sbomStreamWriter struct {
|
||||
format sbom.FormatEncoder
|
||||
|
||||
@@ -202,9 +202,9 @@ func Test_newSBOMMultiWriter(t *testing.T) {
|
||||
if test.err {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = mw.Close() })
|
||||
|
||||
assert.Len(t, mw.writers, len(test.expected))
|
||||
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -89,8 +89,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 +164,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))
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
+3
-2
@@ -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
|
||||
|
||||
-9
@@ -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"
|
||||
}
|
||||
-11
@@ -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"
|
||||
}
|
||||
@@ -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
@@ -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:
|
||||
|
||||
|
||||
@@ -1,73 +1,73 @@
|
||||
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.2
|
||||
github.com/anchore/go-sync v0.1.2
|
||||
github.com/anchore/bubbly v0.2.0
|
||||
github.com/anchore/clio v0.1.0
|
||||
github.com/anchore/fangs v0.1.0
|
||||
github.com/anchore/go-collections v0.1.0
|
||||
github.com/anchore/go-homedir v0.1.0
|
||||
github.com/anchore/go-logger v0.1.0
|
||||
github.com/anchore/go-macholibre v0.1.0
|
||||
github.com/anchore/go-rpmdb v0.1.0
|
||||
github.com/anchore/go-sync v0.1.0
|
||||
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.4
|
||||
github.com/anchore/stereoscope v0.1.23
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
|
||||
github.com/aquasecurity/go-pep440-version v0.0.1
|
||||
github.com/bitnami/go-version v0.0.0-20250131085805-b1f57a8634ef
|
||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.2
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.0
|
||||
github.com/cespare/xxhash/v2 v2.3.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.21
|
||||
github.com/go-git/go-billy/v5 v5.9.0
|
||||
github.com/go-git/go-git/v5 v5.19.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.5
|
||||
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/gpustack/gguf-parser-go v0.24.0
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2
|
||||
github.com/hashicorp/go-getter v1.8.9
|
||||
github.com/hashicorp/go-getter v1.8.6
|
||||
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.1
|
||||
github.com/magiconair/properties v1.18.12
|
||||
github.com/klauspost/compress v1.18.5
|
||||
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,80 +79,75 @@ 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/ulikunitz/xz v0.5.15
|
||||
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
|
||||
github.com/wagoodman/go-progress v0.0.0-20260303201901-10176f79b2c0
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8
|
||||
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
|
||||
go.yaml.in/yaml/v3 v3.0.4
|
||||
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
|
||||
golang.org/x/mod v0.35.0
|
||||
golang.org/x/net v0.53.0
|
||||
golang.org/x/time v0.15.0
|
||||
golang.org/x/tools v0.44.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
modernc.org/sqlite v1.46.2
|
||||
)
|
||||
|
||||
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
|
||||
cel.dev/expr v0.25.1 // 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.2 // 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/monitoring v1.24.3 // indirect
|
||||
cloud.google.com/go/storage v1.61.3 // indirect
|
||||
cyphar.com/go-pathrs v0.2.1 // 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/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/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/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/aws/aws-sdk-go-v2 v1.41.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 // indirect
|
||||
github.com/aws/smithy-go v1.24.2 // 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
|
||||
@@ -161,62 +156,59 @@ require (
|
||||
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/clipperhouse/displaywidth v0.10.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.6.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/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
|
||||
github.com/containerd/cgroups/v3 v3.1.2 // indirect
|
||||
github.com/containerd/containerd/api v1.10.0 // indirect
|
||||
github.com/containerd/containerd/v2 v2.2.2 // 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.4 // indirect
|
||||
github.com/containerd/plugin v1.0.0 // 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.2+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.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/docker/cli v29.4.0+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/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.0 // 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-jose/go-jose/v4 v4.1.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/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.14 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
|
||||
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.72 // 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
|
||||
@@ -229,125 +221,109 @@ require (
|
||||
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/mailru/easyjson v0.7.7 // 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/moby/api v1.54.1 // indirect
|
||||
github.com/moby/moby/client 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/moby/sys/user v0.4.0 // indirect
|
||||
github.com/moby/sys/userns v0.1.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/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/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/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/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.22 // indirect
|
||||
github.com/pjbgf/sha1cd v0.6.0 // 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/pkg/xattr v0.4.9 // 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/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/sahilm/fuzzy v0.1.1 // indirect
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.11.1 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/sigstore/fulcio v1.8.7 // indirect
|
||||
github.com/sigstore/protobuf-specs v0.5.2 // indirect
|
||||
github.com/sigstore/sigstore v1.10.11 // 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/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/spiffe/go-spiffe/v2 v2.6.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.2 // 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/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/detectors/gcp v1.39.0 // 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.43.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
|
||||
golang.org/x/crypto v0.57.0 // indirect
|
||||
golang.org/x/crypto v0.50.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.43.0 // indirect
|
||||
golang.org/x/term v0.42.0 // indirect
|
||||
golang.org/x/text v0.36.0 // indirect
|
||||
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
|
||||
gonum.org/v1/gonum v0.16.0 // indirect
|
||||
google.golang.org/api v0.271.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-20260226221140-a57be14db171 // 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
|
||||
modernc.org/libc v1.70.0 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.12.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
)
|
||||
|
||||
retract (
|
||||
|
||||
+27
-57
@@ -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
|
||||
)
|
||||
|
||||
@@ -387,8 +385,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 +584,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 +629,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 +641,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 +656,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 +764,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 +804,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 +811,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 +842,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
|
||||
|
||||
|
||||
Vendored
-4
@@ -6,7 +6,6 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -96,9 +95,6 @@ func Test_makeDiskKey(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_errors(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("windows ignores unix permission bits on directories")
|
||||
}
|
||||
tmp := t.TempDir()
|
||||
cache := filepath.Join(tmp, "cache")
|
||||
// make a non-writable directory
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -212,7 +212,7 @@ func parseGenericCatalogerFunction(funcDecl *ast.FuncDecl, filePath, repoRoot st
|
||||
// extractPackageNameFromPath extracts the package name from a cataloger source file path
|
||||
// e.g., "syft/pkg/cataloger/swift/cataloger.go" -> "swift"
|
||||
func extractPackageNameFromPath(filePath string) string {
|
||||
parts := strings.Split(filepath.ToSlash(filePath), "/")
|
||||
parts := strings.Split(filePath, string(filepath.Separator))
|
||||
|
||||
// find the LAST occurrence of "cataloger" in the path
|
||||
// (to handle test fixtures with multiple "cataloger" segments)
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -338,7 +337,7 @@ func TestResolveImportPath(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := resolveImportPath(tt.importPath, testRepoRoot)
|
||||
require.Equal(t, filepath.FromSlash(tt.want), got)
|
||||
require.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ type binaryClassifierOverride struct {
|
||||
Class string
|
||||
Package string
|
||||
PURL string
|
||||
CPEs []cpe.CPE
|
||||
CPEs []string
|
||||
}
|
||||
|
||||
var binaryClassifierOverrides = map[string][]binaryClassifierOverride{
|
||||
@@ -110,47 +110,12 @@ var binaryClassifierOverrides = map[string][]binaryClassifierOverride{
|
||||
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"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,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
|
||||
}
|
||||
@@ -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,8 +4,7 @@ import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"path/filepath"
|
||||
|
||||
"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
|
||||
}
|
||||
@@ -25,8 +25,7 @@ func WalkDiskDir(fsys filesystem.FileSystem, root string, fn WalkDiskDirFunc) er
|
||||
}
|
||||
|
||||
for _, info := range infos {
|
||||
// squashfs paths are posix regardless of host os
|
||||
p := path.Join(root, info.Name())
|
||||
p := filepath.Join(root, info.Name())
|
||||
err = walkDiskDir(fsys, p, info, fn)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.SkipDir) {
|
||||
@@ -52,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
|
||||
}
|
||||
@@ -63,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 {
|
||||
@@ -75,7 +74,7 @@ func walkDiskDir(fsys filesystem.FileSystem, name string, d os.FileInfo, walkDir
|
||||
}
|
||||
|
||||
for _, d1 := range dirs {
|
||||
name1 := path.Join(name, d1.Name())
|
||||
name1 := filepath.Join(name, d1.Name())
|
||||
if err := walkDiskDir(fsys, name1, d1, walkDirFn); err != nil {
|
||||
if errors.Is(err, fs.SkipDir) {
|
||||
break
|
||||
@@ -88,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
|
||||
}
|
||||
|
||||
@@ -19,8 +19,6 @@ func createTestFS(t *testing.T) filesystem.FileSystem {
|
||||
filename := "test.squashfs"
|
||||
f, err := os.Create(filepath.Join(dir, filename))
|
||||
require.NoError(t, err)
|
||||
// windows can't remove the temp dir while the backing file is open
|
||||
t.Cleanup(func() { _ = f.Close() })
|
||||
|
||||
b := file.New(f, false)
|
||||
fsys, err := squashfs.Create(b, 0, 0, 4096)
|
||||
@@ -45,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)
|
||||
|
||||
@@ -84,8 +84,7 @@ func ExtractGlobsFromTarToUniqueTempFile(ctx context.Context, archivePath, dir s
|
||||
|
||||
func matchesAnyGlob(name string, globs ...string) bool {
|
||||
for _, glob := range globs {
|
||||
// tar entry names are always posix, so don't match with the host separator (PathMatch)
|
||||
if matches, err := doublestar.Match(glob, name); err == nil && matches {
|
||||
if matches, err := doublestar.PathMatch(glob, name); err == nil && matches {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/invopop/jsonschema"
|
||||
orderedmap "github.com/pb33f/ordered-map/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
orderedmap "github.com/wk8/go-ordered-map/v2"
|
||||
)
|
||||
|
||||
// TestCopyAliasFieldComments verifies that field comments from source types are correctly copied to alias types.
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
)
|
||||
|
||||
// maxScannedLineSize is the longest line NewLineScanner will return before Scan stops with bufio.ErrTooLong.
|
||||
// The buffer only grows to this size when a line needs it.
|
||||
const maxScannedLineSize = 1024 * 1024
|
||||
|
||||
// NewLineScanner returns a bufio.Scanner that accepts lines up to 1MB rather than the 64KB default, since
|
||||
// package metadata files carry long single-line fields (descriptions, license text, dependency lists). Callers
|
||||
// must still check Err() after scanning.
|
||||
func NewLineScanner(r io.Reader) *bufio.Scanner {
|
||||
s := bufio.NewScanner(r)
|
||||
s.Buffer(nil, maxScannedLineSize)
|
||||
return s
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewLineScanner(t *testing.T) {
|
||||
long := strings.Repeat("a", 100*1024)
|
||||
s := NewLineScanner(strings.NewReader(long + "\nb\n"))
|
||||
require.True(t, s.Scan())
|
||||
require.Equal(t, long, s.Text())
|
||||
require.True(t, s.Scan())
|
||||
require.Equal(t, "b", s.Text())
|
||||
require.False(t, s.Scan())
|
||||
require.NoError(t, s.Err())
|
||||
|
||||
s = NewLineScanner(strings.NewReader(strings.Repeat("a", maxScannedLineSize+1)))
|
||||
require.False(t, s.Scan())
|
||||
require.ErrorIs(t, s.Err(), bufio.ErrTooLong)
|
||||
}
|
||||
@@ -2,18 +2,14 @@ package os
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"slices"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/sbomsync"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/linux"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
@@ -23,11 +19,8 @@ func DetectFeatures(_ context.Context, resolver file.Resolver, builder sbomsync.
|
||||
return
|
||||
}
|
||||
|
||||
if err := findRhelFeatures(resolver, s.Artifacts.LinuxDistribution); err != nil {
|
||||
log.WithFields("error", err, "release", s.Artifacts.LinuxDistribution).Trace("error searching for extended support")
|
||||
}
|
||||
|
||||
if err := findUbuntuFeatures(resolver, s.Artifacts.LinuxDistribution, s.Artifacts.Packages); err != nil {
|
||||
err := findRhelFeatures(resolver, s.Artifacts.LinuxDistribution)
|
||||
if err != nil {
|
||||
log.WithFields("error", err, "release", s.Artifacts.LinuxDistribution).Trace("error searching for extended support")
|
||||
}
|
||||
})
|
||||
@@ -35,9 +28,6 @@ func DetectFeatures(_ context.Context, resolver file.Resolver, builder sbomsync.
|
||||
return nil
|
||||
}
|
||||
|
||||
// rhelEUSPatterns match a RHEL content manifest referencing an extended-update-support (EUS) repo.
|
||||
var rhelEUSPatterns = []*regexp.Regexp{regexp.MustCompile(`baseos-eus`), regexp.MustCompile(`baseos-eus|appstream-eus`)}
|
||||
|
||||
func findRhelFeatures(resolver file.Resolver, release *linux.Release) error {
|
||||
if release == nil || release.ID != "rhel" {
|
||||
return nil
|
||||
@@ -47,9 +37,9 @@ func findRhelFeatures(resolver file.Resolver, release *linux.Release) error {
|
||||
return fmt.Errorf("unable to find content manifests: %w", err)
|
||||
}
|
||||
for _, contentManifestFile := range contentManifestFiles {
|
||||
found, err := fileMatchesAny(resolver, contentManifestFile, rhelEUSPatterns...)
|
||||
found, err := hasRhelExtendedSupportInContentManifest(resolver, contentManifestFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read content manifest from %s: %w", contentManifestFile.RealPath, err)
|
||||
return fmt.Errorf("unable to read content manifest from %s: %w", contentManifestFile.String(), err)
|
||||
}
|
||||
if found {
|
||||
release.ExtendedSupport = true
|
||||
@@ -59,149 +49,13 @@ func findRhelFeatures(resolver file.Resolver, release *linux.Release) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ubuntu Pro (formerly Ubuntu Advantage) is Canonical's subscription that unlocks Expanded Security
|
||||
// Maintenance (ESM): security fixes past a package's standard support window. we detect the two base ESM
|
||||
// streams, esm-infra (the `main` repo) and esm-apps (`universe`), and collapse them into one ExtendedSupport
|
||||
// signal; the infra-vs-apps split is disambiguated by the downstream consumer, not by this boolean.
|
||||
//
|
||||
// the other Pro streams on the same host (esm.ubuntu.com/fips, /fips-updates, /realtime) are deliberately
|
||||
// excluded: they are separate compliance products with their own downstream channels, and a host can run
|
||||
// them with base ESM disabled, so folding them in would be unsound.
|
||||
//
|
||||
// detection is deliberately forgiving: apt/status evidence disappears when ESM is disabled, but an installed
|
||||
// +esmN package is durable proof ESM content is on disk. so ExtendedSupport means "ESM was or is in effect
|
||||
// for this host or its content", not "currently entitled" (mere eligibility, without evidence, is not enough).
|
||||
|
||||
// esmAptSourcePattern matches an uncommented apt source (classic or DEB822) or auth entry pointing at the
|
||||
// plain ESM streams esm.ubuntu.com/infra or /apps. it deliberately scopes to /(infra|apps) so the other
|
||||
// Pro streams served from the same host (/fips, /fips-updates, /realtime) are never folded into the base
|
||||
// esm channel. the [^#\n]* before the host ensures a leading `#` (comment) can never satisfy the match.
|
||||
var esmAptSourcePattern = regexp.MustCompile(`(?m)^[^#\n]*esm\.ubuntu\.com/(infra|apps)`)
|
||||
|
||||
// esmVersionPattern matches a dpkg version carrying a numbered ESM pocket suffix, e.g. `...+esm1` or
|
||||
// `...~esm2`. the trailing \d scopes this to the ESM pocket and excludes other Pro pockets (e.g. +fips).
|
||||
var esmVersionPattern = regexp.MustCompile(`[~+]esm\d`)
|
||||
|
||||
func findUbuntuFeatures(resolver file.Resolver, release *linux.Release, packages *pkg.Collection) error {
|
||||
if release == nil || release.ID != "ubuntu" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// an uncommented esm.ubuntu.com apt source or an active pro esm service both prove Pro/ESM was enabled.
|
||||
// these are the durable, file-based signals present on attached hosts. a read error in one signal must
|
||||
// not suppress the others, so errors are collected and detection continues to the next signal.
|
||||
var errs error
|
||||
for _, check := range []func() (bool, error){
|
||||
func() (bool, error) { return hasUbuntuESMAptEvidence(resolver) },
|
||||
func() (bool, error) { return hasActiveUbuntuESMService(resolver) },
|
||||
} {
|
||||
found, err := check()
|
||||
if err != nil {
|
||||
errs = errors.Join(errs, err)
|
||||
continue
|
||||
}
|
||||
if found {
|
||||
release.ExtendedSupport = true
|
||||
return errs
|
||||
}
|
||||
}
|
||||
|
||||
// fallback for images (typically not pro-attached): an installed package whose version carries an
|
||||
// +esm/~esm suffix was pulled from an ESM pocket, so ESM was in effect when it was installed.
|
||||
if hasInstalledESMPackage(packages) {
|
||||
release.ExtendedSupport = true
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// hasUbuntuESMAptEvidence reports whether any apt source (classic .list or DEB822 .sources) or auth entry
|
||||
// references esm.ubuntu.com via an uncommented line.
|
||||
func hasUbuntuESMAptEvidence(resolver file.Resolver) (bool, error) {
|
||||
// apt source files live in a directory (classic .list or DEB822 .sources), the credentials file is a fixed path.
|
||||
sourceLocations, err := resolver.FilesByGlob("/etc/apt/sources.list.d/*")
|
||||
func hasRhelExtendedSupportInContentManifest(resolver file.Resolver, contentManifestFile file.Location) (bool, error) {
|
||||
contents, err := resolver.FileContentsByLocation(contentManifestFile)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("unable to find apt esm sources: %w", err)
|
||||
}
|
||||
authLocations, err := resolver.FilesByPath("/etc/apt/auth.conf.d/90ubuntu-advantage")
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("unable to find apt esm auth: %w", err)
|
||||
return false, fmt.Errorf("unable to read content manifest from %s: %w", contentManifestFile.String(), err)
|
||||
}
|
||||
defer internal.CloseAndLogError(contents, "content-manifest")
|
||||
|
||||
// ponytail: a DEB822 source with `Enabled: no` would still match; the pro client removes/comments the
|
||||
// file on disable rather than toggling that key, so this is an accepted false-positive ceiling. the
|
||||
// auth.conf.d entry is deleted by the pro client on disable, so it is a durable positive-only signal.
|
||||
for _, location := range append(sourceLocations, authLocations...) {
|
||||
match, err := fileMatchesAny(resolver, location, esmAptSourcePattern)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if match {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// hasActiveUbuntuESMService reports whether the ubuntu-advantage/pro status cache shows an enabled esm service.
|
||||
func hasActiveUbuntuESMService(resolver file.Resolver) (bool, error) {
|
||||
locations, err := resolver.FilesByPath("/var/lib/ubuntu-advantage/status.json")
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("unable to find ubuntu-advantage status: %w", err)
|
||||
}
|
||||
for _, location := range locations {
|
||||
enabled, err := hasEnabledESMServiceInStatus(resolver, location)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if enabled {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func hasEnabledESMServiceInStatus(resolver file.Resolver, location file.Location) (bool, error) {
|
||||
contents, err := resolver.FileContentsByLocation(location)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("unable to read ubuntu-advantage status from %s: %w", location.RealPath, err)
|
||||
}
|
||||
defer internal.CloseAndLogError(contents, location.RealPath)
|
||||
|
||||
var status struct {
|
||||
Services []struct {
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
} `json:"services"`
|
||||
}
|
||||
if err := json.NewDecoder(contents).Decode(&status); err != nil {
|
||||
return false, fmt.Errorf("unable to parse ubuntu-advantage status from %s: %w", location.RealPath, err)
|
||||
}
|
||||
|
||||
// pro status values are enabled/disabled/n-a/warning/—; only "enabled" proves the stream is active.
|
||||
// warning/expired states are intentionally excluded here and left to the apt-source and installed-package signals.
|
||||
for _, svc := range status.Services {
|
||||
if (svc.Name == "esm-infra" || svc.Name == "esm-apps") && svc.Status == "enabled" {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// hasInstalledESMPackage reports whether any installed dpkg package version carries an ESM pocket suffix.
|
||||
func hasInstalledESMPackage(packages *pkg.Collection) bool {
|
||||
if packages == nil {
|
||||
return false
|
||||
}
|
||||
return slices.ContainsFunc(packages.Sorted(pkg.DebPkg), func(p pkg.Package) bool {
|
||||
return esmVersionPattern.MatchString(p.Version)
|
||||
})
|
||||
}
|
||||
|
||||
func fileMatchesAny(resolver file.Resolver, location file.Location, patterns ...*regexp.Regexp) (bool, error) {
|
||||
contents, err := resolver.FileContentsByLocation(location)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("unable to read %s: %w", location.RealPath, err)
|
||||
}
|
||||
defer internal.CloseAndLogError(contents, location.RealPath)
|
||||
patterns := []*regexp.Regexp{regexp.MustCompile(`baseos-eus`), regexp.MustCompile(`baseos-eus|appstream-eus`)}
|
||||
return internal.MatchAnyFromReader(contents, patterns...)
|
||||
}
|
||||
|
||||
@@ -2,13 +2,7 @@ package os_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
stdos "os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
@@ -16,11 +10,8 @@ import (
|
||||
"github.com/anchore/syft/internal/os"
|
||||
"github.com/anchore/syft/internal/sbomsync"
|
||||
"github.com/anchore/syft/internal/task"
|
||||
"github.com/anchore/syft/internal/testutils"
|
||||
"github.com/anchore/syft/syft"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/linux"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
@@ -85,7 +76,6 @@ func Test_EnvironmentTask(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
testutils.SkipWithoutLinuxContainers(t)
|
||||
tarPath := imagetest.GetFixtureImageTarPath(t, test.name)
|
||||
|
||||
// get the source
|
||||
@@ -109,237 +99,3 @@ func Test_EnvironmentTask(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_findUbuntuFeatures(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
dir string
|
||||
releaseID string
|
||||
packages []pkg.Package
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "esm apt source present",
|
||||
dir: "testdata/ubuntu_esm_apt",
|
||||
releaseID: "ubuntu",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "esm apt source in DEB822 .sources format",
|
||||
dir: "testdata/ubuntu_esm_deb822",
|
||||
releaseID: "ubuntu",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "esm evidence in apt auth.conf.d",
|
||||
dir: "testdata/ubuntu_esm_authconf",
|
||||
releaseID: "ubuntu",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "active esm service in ubuntu-advantage status",
|
||||
dir: "testdata/ubuntu_esm_status",
|
||||
releaseID: "ubuntu",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "only esm-infra enabled (esm-apps disabled)",
|
||||
dir: "testdata/ubuntu_esm_infra_only",
|
||||
releaseID: "ubuntu",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "malformed status.json does not panic or match",
|
||||
dir: "testdata/ubuntu_esm_malformed",
|
||||
releaseID: "ubuntu",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "installed +esm package version",
|
||||
dir: "testdata/ubuntu_plain",
|
||||
releaseID: "ubuntu",
|
||||
packages: []pkg.Package{
|
||||
{Name: "openssl", Version: "1.1.1f-1ubuntu2.19+esm1", Type: pkg.DebPkg},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "installed ~esm package version",
|
||||
dir: "testdata/ubuntu_plain",
|
||||
releaseID: "ubuntu",
|
||||
packages: []pkg.Package{
|
||||
{Name: "libcap2", Version: "1:2.32-1ubuntu0.1~esm1", Type: pkg.DebPkg},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "plain ubuntu with no esm evidence",
|
||||
dir: "testdata/ubuntu_plain",
|
||||
releaseID: "ubuntu",
|
||||
packages: []pkg.Package{
|
||||
{Name: "openssl", Version: "1.1.1f-1ubuntu2.19", Type: pkg.DebPkg},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "commented esm source and disabled esm service",
|
||||
dir: "testdata/ubuntu_esm_disabled",
|
||||
releaseID: "ubuntu",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "esm disabled but +esm package remains installed",
|
||||
dir: "testdata/ubuntu_esm_disabled",
|
||||
releaseID: "ubuntu",
|
||||
packages: []pkg.Package{
|
||||
{Name: "openssl", Version: "1.1.1f-1ubuntu2.19+esm1", Type: pkg.DebPkg},
|
||||
},
|
||||
expected: true, // durable proof: ESM content is on disk even though the channel is now off
|
||||
},
|
||||
{
|
||||
name: "fips-only pro host is not plain esm",
|
||||
dir: "testdata/ubuntu_fips_only",
|
||||
releaseID: "ubuntu",
|
||||
expected: false, // esm.ubuntu.com/fips must not be folded into the base esm channel
|
||||
},
|
||||
{
|
||||
name: "non-ubuntu is unaffected by esm evidence",
|
||||
dir: "testdata/ubuntu_esm_apt",
|
||||
releaseID: "debian",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "non-ubuntu is unaffected by esm package version",
|
||||
dir: "testdata/ubuntu_plain",
|
||||
releaseID: "debian",
|
||||
packages: []pkg.Package{
|
||||
{Name: "openssl", Version: "1.1.1f-1ubuntu2.19+esm1", Type: pkg.DebPkg},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
resolver := fixtureResolverForDir(t, test.dir)
|
||||
|
||||
s := sbom.SBOM{}
|
||||
s.Artifacts.LinuxDistribution = &linux.Release{ID: test.releaseID}
|
||||
s.Artifacts.Packages = pkg.NewCollection(test.packages...)
|
||||
|
||||
err := os.DetectFeatures(context.Background(), resolver, sbomsync.NewBuilder(&s))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, test.expected, s.Artifacts.LinuxDistribution.ExtendedSupport)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// returning early from Collection.Enumerate leaks its goroutine and read lock, so any later write blocks forever.
|
||||
func Test_findUbuntuFeatures_releasesPackageCollectionLock(t *testing.T) {
|
||||
resolver := fixtureResolverForDir(t, "testdata/ubuntu_plain")
|
||||
|
||||
s := sbom.SBOM{}
|
||||
s.Artifacts.LinuxDistribution = &linux.Release{ID: "ubuntu"}
|
||||
s.Artifacts.Packages = pkg.NewCollection(
|
||||
pkg.Package{Name: "openssl", Version: "1.1.1f-1ubuntu2.19+esm1", Type: pkg.DebPkg},
|
||||
pkg.Package{Name: "libcap2", Version: "1:2.32-1ubuntu0.1~esm1", Type: pkg.DebPkg},
|
||||
)
|
||||
|
||||
require.NoError(t, os.DetectFeatures(context.Background(), resolver, sbomsync.NewBuilder(&s)))
|
||||
require.True(t, s.Artifacts.LinuxDistribution.ExtendedSupport)
|
||||
|
||||
added := make(chan struct{})
|
||||
go func() {
|
||||
s.Artifacts.Packages.Add(pkg.Package{Name: "zlib1g", Version: "1:1.2.11.dfsg-2ubuntu1", Type: pkg.DebPkg})
|
||||
close(added)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-added:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("package collection is still read-locked after feature detection")
|
||||
}
|
||||
}
|
||||
|
||||
// a read error in the apt-source signal must not suppress the ubuntu-advantage status signal; the three
|
||||
// signals are meant to be independent fallbacks.
|
||||
func Test_findUbuntuFeatures_signalErrorDoesNotSuppressOthers(t *testing.T) {
|
||||
resolver := errOnGlobResolver{
|
||||
fixtureResolver: fixtureResolverForDir(t, "testdata/ubuntu_esm_status").(fixtureResolver),
|
||||
errOnGlob: "/etc/apt/sources.list.d/*",
|
||||
}
|
||||
|
||||
s := sbom.SBOM{}
|
||||
s.Artifacts.LinuxDistribution = &linux.Release{ID: "ubuntu"}
|
||||
s.Artifacts.Packages = pkg.NewCollection()
|
||||
|
||||
require.NoError(t, os.DetectFeatures(context.Background(), resolver, sbomsync.NewBuilder(&s)))
|
||||
|
||||
// the apt glob errors, but the status.json signal (esm-infra enabled) should still be consulted
|
||||
require.True(t, s.Artifacts.LinuxDistribution.ExtendedSupport)
|
||||
}
|
||||
|
||||
// errOnGlobResolver injects a failure for a specific glob to simulate an unreadable apt directory.
|
||||
type errOnGlobResolver struct {
|
||||
fixtureResolver
|
||||
errOnGlob string
|
||||
}
|
||||
|
||||
func (r errOnGlobResolver) FilesByGlob(patterns ...string) ([]file.Location, error) {
|
||||
if slices.Contains(patterns, r.errOnGlob) {
|
||||
return nil, stdos.ErrPermission
|
||||
}
|
||||
return r.fixtureResolver.FilesByGlob(patterns...)
|
||||
}
|
||||
|
||||
// fixtureResolver maps in-image logical paths to on-disk fixture files so DetectFeatures can be driven
|
||||
// without building a container image.
|
||||
type fixtureResolver struct {
|
||||
file.Resolver // unused methods; nil is fine since only the three below are called
|
||||
files map[string]string
|
||||
}
|
||||
|
||||
func (r fixtureResolver) FilesByGlob(patterns ...string) ([]file.Location, error) {
|
||||
var out []file.Location
|
||||
for logical := range r.files {
|
||||
for _, p := range patterns {
|
||||
if ok, _ := path.Match(p, logical); ok {
|
||||
out = append(out, file.NewLocation(logical))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r fixtureResolver) FilesByPath(paths ...string) ([]file.Location, error) {
|
||||
var out []file.Location
|
||||
for _, p := range paths {
|
||||
if _, ok := r.files[p]; ok {
|
||||
out = append(out, file.NewLocation(p))
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r fixtureResolver) FileContentsByLocation(l file.Location) (io.ReadCloser, error) {
|
||||
return stdos.Open(r.files[l.RealPath])
|
||||
}
|
||||
|
||||
func fixtureResolverForDir(t *testing.T, dir string) file.Resolver {
|
||||
t.Helper()
|
||||
files := map[string]string{}
|
||||
require.NoError(t, filepath.WalkDir(dir, func(p string, d stdos.DirEntry, err error) error {
|
||||
if err != nil || d.IsDir() {
|
||||
return err
|
||||
}
|
||||
rel, err := filepath.Rel(dir, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
files["/"+filepath.ToSlash(rel)] = p
|
||||
return nil
|
||||
}))
|
||||
return fixtureResolver{files: files}
|
||||
}
|
||||
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
# Written by ubuntu-advantage-tools
|
||||
deb https://esm.ubuntu.com/infra/ubuntu focal-infra-security main
|
||||
# deb-src https://esm.ubuntu.com/infra/ubuntu focal-infra-security main
|
||||
|
||||
deb https://esm.ubuntu.com/infra/ubuntu focal-infra-updates main
|
||||
# deb-src https://esm.ubuntu.com/infra/ubuntu focal-infra-updates main
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
machine esm.ubuntu.com/infra/ubuntu/ login bearer password DUMMYTOKENREDACTED # ubuntu-pro-client
|
||||
machine esm.ubuntu.com/apps/ubuntu/ login bearer password DUMMYTOKENREDACTED # ubuntu-pro-client
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
Types: deb
|
||||
URIs: https://esm.ubuntu.com/infra/ubuntu
|
||||
Suites: noble-infra-security noble-infra-updates
|
||||
Components: main
|
||||
Signed-By: /usr/share/keyrings/ubuntu-pro-esm-infra-keyring.gpg
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
# Written by ubuntu-advantage-tools
|
||||
# deb https://esm.ubuntu.com/infra/ubuntu focal-infra-security main
|
||||
# deb-src https://esm.ubuntu.com/infra/ubuntu focal-infra-security main
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"_schema_version": "0.1",
|
||||
"attached": true,
|
||||
"services": [
|
||||
{
|
||||
"name": "esm-apps",
|
||||
"available": "yes",
|
||||
"entitled": "yes",
|
||||
"status": "disabled"
|
||||
},
|
||||
{
|
||||
"name": "esm-infra",
|
||||
"available": "yes",
|
||||
"entitled": "yes",
|
||||
"status": "disabled"
|
||||
}
|
||||
]
|
||||
}
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"_schema_version": "0.1",
|
||||
"attached": true,
|
||||
"execution_status": "success",
|
||||
"services": [
|
||||
{
|
||||
"name": "esm-apps",
|
||||
"description": "Expanded Security Maintenance for Applications",
|
||||
"available": "yes",
|
||||
"entitled": "yes",
|
||||
"status": "disabled"
|
||||
},
|
||||
{
|
||||
"name": "esm-infra",
|
||||
"description": "Expanded Security Maintenance for Infrastructure",
|
||||
"available": "yes",
|
||||
"entitled": "yes",
|
||||
"status": "enabled"
|
||||
}
|
||||
]
|
||||
}
|
||||
-1
@@ -1 +0,0 @@
|
||||
{ this is not valid json
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"_doc": "Content provided in json response is currently considered Experimental and may change",
|
||||
"_schema_version": "0.1",
|
||||
"attached": true,
|
||||
"services": [
|
||||
{
|
||||
"name": "esm-apps",
|
||||
"description": "Expanded Security Maintenance for Applications",
|
||||
"available": "yes",
|
||||
"entitled": "yes",
|
||||
"status": "enabled"
|
||||
},
|
||||
{
|
||||
"name": "esm-infra",
|
||||
"description": "Expanded Security Maintenance for Infrastructure",
|
||||
"available": "yes",
|
||||
"entitled": "yes",
|
||||
"status": "enabled"
|
||||
},
|
||||
{
|
||||
"name": "fips",
|
||||
"description": "NIST-certified FIPS crypto packages",
|
||||
"available": "no",
|
||||
"entitled": "no",
|
||||
"status": "n/a"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
# Written by ubuntu-pro-client
|
||||
deb https://esm.ubuntu.com/fips/ubuntu focal main
|
||||
# deb-src https://esm.ubuntu.com/fips/ubuntu focal main
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"_schema_version": "0.1",
|
||||
"attached": true,
|
||||
"execution_status": "success",
|
||||
"services": [
|
||||
{
|
||||
"name": "esm-apps",
|
||||
"description": "Expanded Security Maintenance for Applications",
|
||||
"available": "yes",
|
||||
"entitled": "yes",
|
||||
"status": "disabled"
|
||||
},
|
||||
{
|
||||
"name": "esm-infra",
|
||||
"description": "Expanded Security Maintenance for Infrastructure",
|
||||
"available": "yes",
|
||||
"entitled": "yes",
|
||||
"status": "disabled"
|
||||
},
|
||||
{
|
||||
"name": "fips",
|
||||
"description": "NIST-certified FIPS crypto packages",
|
||||
"available": "yes",
|
||||
"entitled": "yes",
|
||||
"status": "enabled"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
Types: deb
|
||||
URIs: http://archive.ubuntu.com/ubuntu/
|
||||
Suites: noble noble-updates noble-backports
|
||||
Components: main restricted universe multiverse
|
||||
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user