mirror of
https://github.com/anchore/syft.git
synced 2026-10-12 06:07:20 +02:00
Compare commits
38
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7c51e5c82 | ||
|
|
414fb2f8ad | ||
|
|
a32b8d7fc6 | ||
|
|
11c0b1c234 | ||
|
|
e0e1c4ba0a | ||
|
|
bf3cd9ed3b | ||
|
|
ad2843bf50 | ||
|
|
0bb94099b0 | ||
|
|
97d0108bd5 | ||
|
|
878df69330 | ||
|
|
0fe13888d5 | ||
|
|
8e39ca6dfc | ||
|
|
935b885ba2 | ||
|
|
cdad5e767a | ||
|
|
c5d15d1d6c | ||
|
|
03b7938fbf | ||
|
|
4c77783461 | ||
|
|
c6ce1de928 | ||
|
|
3046d43a8a | ||
|
|
df582e8463 | ||
|
|
3eab5932e5 | ||
|
|
0bc31f4e27 | ||
|
|
22f3a29fd7 | ||
|
|
969b5f1764 | ||
|
|
1b6c2470b0 | ||
|
|
ec802dfc80 | ||
|
|
8845c938ce | ||
|
|
308dc6f9b8 | ||
|
|
42dd04699f | ||
|
|
c816c73341 | ||
|
|
297ece6904 | ||
|
|
5602c80edb | ||
|
|
fe271e89f6 | ||
|
|
0409eef615 | ||
|
|
bdda3481c7 | ||
|
|
6f4fb61a1c | ||
|
|
313d9212b6 | ||
|
|
fb2b54a6dc |
+1
-1
@@ -90,7 +90,7 @@ tools:
|
||||
|
||||
- name: gh
|
||||
version:
|
||||
want: v2.42.0
|
||||
want: v2.42.1
|
||||
method: github-release
|
||||
with:
|
||||
repo: cli/cli
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
uses: ./.github/actions/bootstrap
|
||||
|
||||
- name: Restore base benchmark result
|
||||
uses: actions/cache@e12d46a63a90f2fae62d114769bbf2a179198b5c #v3.3.3
|
||||
uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 #v4.0.0
|
||||
with:
|
||||
path: test/results/benchmark-main.txt
|
||||
# use base sha for PR or new commit hash for main push in benchmark result key
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
OUTPUT="${OUTPUT//$'\r'/'%0D'}" # URL encode all '\r' characters
|
||||
echo "result=$OUTPUT" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/upload-artifact@1eb3cb2b3e0f29609092a73eb033bb759a334595 # v4.1.0
|
||||
- uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0
|
||||
with:
|
||||
name: benchmark-test-results
|
||||
path: test/results/**/*
|
||||
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@e5f05b81d5b6ff8cfa111c80c22c5fd02a384118 #v3.23.0
|
||||
uses: github/codeql-action/init@0b21cf2492b6b02c465a3e5d7c473717ad7721ba #v3.23.1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@e5f05b81d5b6ff8cfa111c80c22c5fd02a384118 #v3.23.0
|
||||
uses: github/codeql-action/autobuild@0b21cf2492b6b02c465a3e5d7c473717ad7721ba #v3.23.1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -70,4 +70,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@e5f05b81d5b6ff8cfa111c80c22c5fd02a384118 #v3.23.0
|
||||
uses: github/codeql-action/analyze@0b21cf2492b6b02c465a3e5d7c473717ad7721ba #v3.23.1
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
name: Dependabot Automation
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
run:
|
||||
uses: anchore/workflows/.github/workflows/dependabot-automation.yaml@main
|
||||
@@ -141,7 +141,7 @@ jobs:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.TOOLBOX_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.TOOLBOX_AWS_SECRET_ACCESS_KEY }}
|
||||
|
||||
- uses: anchore/sbom-action@c7f031d9249a826a082ea14c79d3b686a51d485a #v0.15.3
|
||||
- uses: anchore/sbom-action@24b0d5238516480139aa8bc6f92eeb7b54a9eb0a #v0.15.5
|
||||
continue-on-error: true
|
||||
with:
|
||||
artifact-name: sbom.spdx.json
|
||||
|
||||
@@ -37,31 +37,31 @@ jobs:
|
||||
uses: ./.github/actions/bootstrap
|
||||
|
||||
- name: Restore Java test-fixture cache
|
||||
uses: actions/cache@e12d46a63a90f2fae62d114769bbf2a179198b5c #v3.3.3
|
||||
uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 #v4.0.0
|
||||
with:
|
||||
path: syft/pkg/cataloger/java/test-fixtures/java-builds/packages
|
||||
key: ${{ runner.os }}-unit-java-cache-${{ hashFiles( 'syft/pkg/cataloger/java/test-fixtures/java-builds/cache.fingerprint' ) }}
|
||||
|
||||
- name: Restore RPM test-fixture cache
|
||||
uses: actions/cache@e12d46a63a90f2fae62d114769bbf2a179198b5c #v3.3.3
|
||||
uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 #v4.0.0
|
||||
with:
|
||||
path: syft/pkg/cataloger/redhat/test-fixtures/rpms
|
||||
key: ${{ runner.os }}-unit-rpm-cache-${{ hashFiles( 'syft/pkg/cataloger/redhat/test-fixtures/rpms.fingerprint' ) }}
|
||||
|
||||
- name: Restore go binary test-fixture cache
|
||||
uses: actions/cache@e12d46a63a90f2fae62d114769bbf2a179198b5c #v3.3.3
|
||||
uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 #v4.0.0
|
||||
with:
|
||||
path: syft/pkg/cataloger/golang/test-fixtures/archs/binaries
|
||||
key: ${{ runner.os }}-unit-go-binaries-cache-${{ hashFiles( 'syft/pkg/cataloger/golang/test-fixtures/archs/binaries.fingerprint' ) }}
|
||||
|
||||
- name: Restore binary cataloger test-fixture cache
|
||||
uses: actions/cache@e12d46a63a90f2fae62d114769bbf2a179198b5c #v3.3.3
|
||||
uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 #v4.0.0
|
||||
with:
|
||||
path: syft/pkg/cataloger/binary/test-fixtures/classifiers/bin
|
||||
key: ${{ runner.os }}-unit-binary-cataloger-cache-${{ hashFiles( 'syft/pkg/cataloger/binary/test-fixtures/cache.fingerprint' ) }}
|
||||
|
||||
- name: Restore Kernel test-fixture cache
|
||||
uses: actions/cache@e12d46a63a90f2fae62d114769bbf2a179198b5c #v3.3.3
|
||||
uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 #v4.0.0
|
||||
with:
|
||||
path: syft/pkg/cataloger/kernel/test-fixtures/cache
|
||||
key: ${{ runner.os }}-unit-kernel-cache-${{ hashFiles( 'syft/pkg/cataloger/kernel/test-fixtures/cache.fingerprint' ) }}
|
||||
@@ -84,10 +84,10 @@ jobs:
|
||||
run: make validate-cyclonedx-schema
|
||||
|
||||
- name: Restore integration test cache
|
||||
uses: actions/cache@e12d46a63a90f2fae62d114769bbf2a179198b5c #v3.3.3
|
||||
uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 #v4.0.0
|
||||
with:
|
||||
path: ${{ github.workspace }}/test/integration/test-fixtures/cache
|
||||
key: ${{ runner.os }}-integration-test-cache-${{ hashFiles('test/integration/test-fixtures/cache.fingerprint') }}
|
||||
path: ${{ github.workspace }}/cmd/syft/internal/test/integration/test-fixtures/cache
|
||||
key: ${{ runner.os }}-integration-test-cache-${{ hashFiles('/cmd/syft/internal/test/integration/test-fixtures/cache.fingerprint') }}
|
||||
|
||||
- name: Run integration tests
|
||||
run: make integration
|
||||
@@ -110,7 +110,7 @@ jobs:
|
||||
# why not use actions/upload-artifact? It is very slow (3 minutes to upload ~600MB of data, vs 10 seconds with this approach).
|
||||
# see https://github.com/actions/upload-artifact/issues/199 for more info
|
||||
- name: Upload snapshot artifacts
|
||||
uses: actions/cache/save@e12d46a63a90f2fae62d114769bbf2a179198b5c #v3.3.3
|
||||
uses: actions/cache/save@13aacd865c20de90d75de3b17ebe84f7a17d57d2 #v4.0.0
|
||||
with:
|
||||
# we need to preserve the snapshot data itself as well as the task data that confirms if the
|
||||
# snapshot build is stale or not. Otherwise the downstream jobs will attempt to rebuild the snapshot
|
||||
@@ -134,7 +134,7 @@ jobs:
|
||||
|
||||
- name: Download snapshot build
|
||||
id: snapshot-cache
|
||||
uses: actions/cache/restore@e12d46a63a90f2fae62d114769bbf2a179198b5c #v3.3.3
|
||||
uses: actions/cache/restore@13aacd865c20de90d75de3b17ebe84f7a17d57d2 #v4.0.0
|
||||
with:
|
||||
path: |
|
||||
snapshot
|
||||
@@ -152,7 +152,7 @@ jobs:
|
||||
|
||||
- name: Restore install.sh test image cache
|
||||
id: install-test-image-cache
|
||||
uses: actions/cache@e12d46a63a90f2fae62d114769bbf2a179198b5c #v3.3.3
|
||||
uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 #v4.0.0
|
||||
with:
|
||||
path: ${{ github.workspace }}/test/install/cache
|
||||
key: ${{ runner.os }}-install-test-image-cache-${{ hashFiles('test/install/cache.fingerprint') }}
|
||||
@@ -187,7 +187,7 @@ jobs:
|
||||
|
||||
- name: Download snapshot build
|
||||
id: snapshot-cache
|
||||
uses: actions/cache/restore@e12d46a63a90f2fae62d114769bbf2a179198b5c #v3.3.3
|
||||
uses: actions/cache/restore@13aacd865c20de90d75de3b17ebe84f7a17d57d2 #v4.0.0
|
||||
with:
|
||||
path: |
|
||||
snapshot
|
||||
@@ -202,7 +202,7 @@ jobs:
|
||||
|
||||
- name: Restore docker image cache for compare testing
|
||||
id: mac-compare-testing-cache
|
||||
uses: actions/cache@e12d46a63a90f2fae62d114769bbf2a179198b5c #v3.3.3
|
||||
uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 #v4.0.0
|
||||
with:
|
||||
path: image.tar
|
||||
key: ${{ runner.os }}-${{ hashFiles('test/compare/mac.sh') }}
|
||||
@@ -226,14 +226,14 @@ jobs:
|
||||
uses: ./.github/actions/bootstrap
|
||||
|
||||
- name: Restore CLI test-fixture cache
|
||||
uses: actions/cache@e12d46a63a90f2fae62d114769bbf2a179198b5c #v3.3.3
|
||||
uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 #v4.0.0
|
||||
with:
|
||||
path: ${{ github.workspace }}/test/cli/test-fixtures/cache
|
||||
key: ${{ runner.os }}-cli-test-cache-${{ hashFiles('test/cli/test-fixtures/cache.fingerprint') }}
|
||||
|
||||
- name: Download snapshot build
|
||||
id: snapshot-cache
|
||||
uses: actions/cache/restore@e12d46a63a90f2fae62d114769bbf2a179198b5c #v3.3.3
|
||||
uses: actions/cache/restore@13aacd865c20de90d75de3b17ebe84f7a17d57d2 #v4.0.0
|
||||
with:
|
||||
path: |
|
||||
snapshot
|
||||
|
||||
@@ -32,7 +32,6 @@ VERSION
|
||||
/test/results
|
||||
coverage.txt
|
||||
*.log
|
||||
test/integration/test-fixtures/**/go.sum
|
||||
|
||||
# probable archives
|
||||
.images
|
||||
|
||||
+1
-1
@@ -234,7 +234,7 @@ rough outline how that works:
|
||||
the `/test` directory. These tests should focus on correctness of functionality in depth. % test coverage metrics
|
||||
only considers unit tests and no other forms of testing.
|
||||
|
||||
- `integration`: located within `test/integration`, these tests focus on the behavior surfaced by the common library
|
||||
- `integration`: located within `cmd/syft/internal/test/integration`, these tests focus on the behavior surfaced by the common library
|
||||
entrypoints from the `syft` package and make light assertions about the results surfaced. Additionally, these tests
|
||||
tend to make diversity assertions for enum-like objects, ensuring that as enum values are added to a definition
|
||||
that integration tests will automatically fail if no test attempts to use that enum value. For more details see
|
||||
|
||||
@@ -502,19 +502,20 @@ Configuration options (example values are the default):
|
||||
|
||||
```yaml
|
||||
# the output format(s) of the SBOM report (options: syft-table, syft-text, syft-json, spdx-json, ...)
|
||||
# same as -o, --output, and SYFT_OUTPUT env var
|
||||
# to specify multiple output files in differing formats, use a list:
|
||||
# output:
|
||||
# - "syft-json=<syft-json-output-file>"
|
||||
# - "spdx-json=<spdx-json-output-file>"
|
||||
output: "syft-table"
|
||||
# SYFT_OUTPUT env var / -o, --output flags
|
||||
output:
|
||||
- "syft-table"
|
||||
|
||||
# suppress all output (except for the SBOM report)
|
||||
# same as -q ; SYFT_QUIET env var
|
||||
# SYFT_QUIET env var / -q flag
|
||||
quiet: false
|
||||
|
||||
# enable/disable checking for application updates on startup
|
||||
# same as SYFT_CHECK_FOR_APP_UPDATE env var
|
||||
# SYFT_CHECK_FOR_APP_UPDATE env var
|
||||
check-for-app-update: true
|
||||
|
||||
# maximum number of workers used to process the list of package catalogers in parallel
|
||||
@@ -524,15 +525,11 @@ parallelism: 1
|
||||
# exclude:
|
||||
# - "/etc/**"
|
||||
# - "./out/**/*.json"
|
||||
# same as --exclude ; SYFT_EXCLUDE env var
|
||||
# SYFT_EXCLUDE env var / --exclude flag
|
||||
exclude: []
|
||||
|
||||
# allows users to exclude synthetic binary packages from the sbom
|
||||
# these packages are removed if an overlap with a non-synthetic package is found
|
||||
exclude-binary-overlap-by-ownership: true
|
||||
|
||||
# os and/or architecture to use when referencing container images (e.g. "windows/armv6" or "arm64")
|
||||
# same as --platform; SYFT_PLATFORM env var
|
||||
# SYFT_PLATFORM env var / --platform flag
|
||||
platform: ""
|
||||
|
||||
# the search space to look for file and package data (options: all-layers, squashed)
|
||||
@@ -541,48 +538,24 @@ scope: "squashed"
|
||||
|
||||
# set the list of package catalogers to use when generating the SBOM
|
||||
# default = empty (cataloger set determined automatically by the source type [image or file/directory])
|
||||
# catalogers:
|
||||
# - alpm-db-cataloger
|
||||
# - apkdb-cataloger
|
||||
# - binary-cataloger
|
||||
# - cargo-auditable-binary-cataloger
|
||||
# - cocoapods-cataloger
|
||||
# - conan-cataloger
|
||||
# - dartlang-lock-cataloger
|
||||
# - dotnet-deps-cataloger
|
||||
# - dotnet-portable-executable-cataloger
|
||||
# - dpkg-db-cataloger
|
||||
# - elixir-mix-lock-cataloger
|
||||
# - erlang-rebar-lock-cataloger
|
||||
# - go-module-file-cataloger
|
||||
# - go-module-binary-cataloger
|
||||
# - graalvm-native-image-cataloger
|
||||
# - haskell-cataloger
|
||||
# - java-cataloger
|
||||
# - java-gradle-lockfile-cataloger
|
||||
# - java-pom-cataloger
|
||||
# - javascript-lock-cataloger
|
||||
# - javascript-package-cataloger
|
||||
# - linux-kernel-cataloger
|
||||
# - nix-store-cataloger
|
||||
# - php-composer-installed-cataloger
|
||||
# - php-composer-lock-cataloger
|
||||
# - portage-cataloger
|
||||
# - python-package-cataloger
|
||||
# - python-installed-package-cataloger
|
||||
# - rpm-db-cataloger
|
||||
# - rpm-archive-cataloger
|
||||
# - ruby-gemfile-cataloger
|
||||
# - ruby-installed-gemspec-cataloger
|
||||
# - rust-cargo-lock-cataloger
|
||||
# - sbom-cataloger
|
||||
# - spm-cataloger
|
||||
# Use `syft cataloger list` for a list of catalogers you can specify
|
||||
# DEPRECATED: please use default-catalogers and select-catalogers configuration options instead
|
||||
# SYFT_CATALOGERS env var / --catalogers flag
|
||||
catalogers:
|
||||
|
||||
# set the base set of catalogers to use (defaults to 'image' or 'directory' depending on the scan source)
|
||||
# SYFT_DEFAULT_CATALOGERS env var / --override-default-catalogers flag
|
||||
default-catalogers: []
|
||||
|
||||
# add, remove, and filter the catalogers to be used
|
||||
# SYFT_SELECT_CATALOGERS env var / --select-cataloger flag;
|
||||
select-catalogers: []
|
||||
|
||||
# all format configuration
|
||||
format:
|
||||
|
||||
# default value for all formats that support the "pretty" option (default is unset)
|
||||
# SYFT_FORMAT_PRETTY env var
|
||||
pretty:
|
||||
|
||||
# all syft-json format options
|
||||
@@ -590,6 +563,7 @@ format:
|
||||
|
||||
# include space indention and newlines (inherits default value from 'format.pretty' or 'false' if parent is unset)
|
||||
# note: inherits default value from 'format.pretty' or 'false' if parent is unset
|
||||
# SYFT_FORMAT_JSON_PRETTY env var
|
||||
pretty: false
|
||||
|
||||
# transform any syft-json output to conform to an approximation of the v11.0.1 schema. This includes:
|
||||
@@ -599,21 +573,30 @@ format:
|
||||
# that output might not strictly be json schema v11 compliant, however, for consumers that require time to port
|
||||
# over to the final syft 1.0 json output this option can be used to ease the transition.
|
||||
#
|
||||
# Note: long term support for this option is not guaranteed.
|
||||
# Note: long term support for this option is not guaranteed (it may change or break at any time).
|
||||
# SYFT_FORMAT_JSON_LEGACY env var
|
||||
legacy: false
|
||||
|
||||
# all template format options
|
||||
template:
|
||||
# path to the template file to use when rendering the output with the `template` output format.
|
||||
# Note that all template paths are based on the current syft-json schema.
|
||||
# same as -t ; SYFT_TEMPLATE_PATH env var
|
||||
# SYFT_FORMAT_TEMPLATE_PATH env var / -t flag
|
||||
path: ""
|
||||
|
||||
# if true, uses the go structs for the syft-json format for templating.
|
||||
# if false, uses the syft-json output for templating (which follows the syft JSON schema exactly).
|
||||
#
|
||||
# Note: long term support for this option is not guaranteed (it may change or break at any time).
|
||||
# SYFT_FORMAT_TEMPLATE_LEGACY env var
|
||||
legacy: false
|
||||
|
||||
# all spdx-json format options
|
||||
spdx-json:
|
||||
|
||||
# include space indention and newlines
|
||||
# note: inherits default value from 'format.pretty' or 'false' if parent is unset
|
||||
# SYFT_FORMAT_SPDX_JSON_PRETTY env var
|
||||
pretty: false
|
||||
|
||||
# all cyclonedx-json format options
|
||||
@@ -621,6 +604,7 @@ format:
|
||||
|
||||
# include space indention and newlines
|
||||
# note: inherits default value from 'format.pretty' or 'false' if parent is unset
|
||||
# SYFT_FORMAT_CYCLONEDX_JSON_PRETTY env var
|
||||
pretty: false
|
||||
|
||||
# all cyclonedx-xml format options
|
||||
@@ -628,6 +612,7 @@ format:
|
||||
|
||||
# include space indention
|
||||
# note: inherits default value from 'format.pretty' or 'false' if parent is unset
|
||||
# SYFT_FORMAT_CYCLONEDX_XML_PRETTY env var
|
||||
pretty: false
|
||||
|
||||
|
||||
@@ -636,16 +621,17 @@ file:
|
||||
metadata:
|
||||
# select which files should be captured by the file-metadata cataloger and included in the SBOM.
|
||||
# Options include:
|
||||
# - "all-files": capture all files from the search space
|
||||
# - "owned-files": capture only files owned by packages
|
||||
# - "unowned-files": capture only files not owned by packages
|
||||
# - "no-files", "": do not capture any files
|
||||
# - "all": capture all files from the search space
|
||||
# - "owned-by-package": capture only files owned by packages
|
||||
# - "none", "": do not capture any files
|
||||
# SYFT_FILE_METADATA_SELECTION env var
|
||||
selection: "owned-files"
|
||||
selection: "owned-by-package"
|
||||
|
||||
# the file digest algorithms to use when cataloging files (options: "md5", "sha1", "sha224", "sha256", "sha384", "sha512")
|
||||
# SYFT_FILE_METADATA_DIGESTS env var
|
||||
digests: ["sha256"]
|
||||
digests:
|
||||
- "sha256"
|
||||
- "sha1"
|
||||
|
||||
# capture the contents of select files in the SBOM
|
||||
content:
|
||||
@@ -674,6 +660,7 @@ package:
|
||||
|
||||
# allows users to exclude synthetic binary packages from the sbom
|
||||
# these packages are removed if an overlap with a non-synthetic package is found
|
||||
# SYFT_PACKAGE_EXCLUDE_BINARY_OVERLAP_BY_OWNERSHIP env var
|
||||
exclude-binary-overlap-by-ownership: true
|
||||
|
||||
|
||||
@@ -732,17 +719,23 @@ javascript:
|
||||
# configuration for the source that the SBOM is generated from (e.g. a file, directory, or container image)
|
||||
source:
|
||||
# alias name for the source
|
||||
# SYFT_SOURCE_NAME env var; --source-name flag
|
||||
# SYFT_SOURCE_NAME env var / --source-name flag
|
||||
name: ""
|
||||
|
||||
# alias version for the source
|
||||
# SYFT_SOURCE_VERSION env var; --source-version flag
|
||||
# SYFT_SOURCE_VERSION env var / --source-version flag
|
||||
version: ""
|
||||
|
||||
# options affecting the file source type
|
||||
|
||||
# base directory for scanning, no links will be followed above this directory, and all paths will be
|
||||
# reported relative to this directory
|
||||
# SYFT_SOURCE_BASE_PATH env var
|
||||
base-path: ''
|
||||
|
||||
# options affecting the file source type
|
||||
file:
|
||||
# the file digest algorithms to use on the scanned file (options: "md5", "sha1", "sha224", "sha256", "sha384", "sha512")
|
||||
digests: ["sha256"]
|
||||
digests:
|
||||
- "sha256"
|
||||
|
||||
image:
|
||||
|
||||
@@ -804,15 +797,15 @@ attest:
|
||||
|
||||
log:
|
||||
# use structured logging
|
||||
# same as SYFT_LOG_STRUCTURED env var
|
||||
# SYFT_LOG_STRUCTURED env var
|
||||
structured: false
|
||||
|
||||
# the log level; note: detailed logging suppress the ETUI
|
||||
# same as SYFT_LOG_LEVEL env var
|
||||
# SYFT_LOG_LEVEL env var
|
||||
level: "error"
|
||||
|
||||
# location to write the log file (default is not to have a log file)
|
||||
# same as SYFT_LOG_FILE env var
|
||||
# SYFT_LOG_FILE env var
|
||||
file: ""
|
||||
```
|
||||
|
||||
|
||||
+5
-5
@@ -185,7 +185,7 @@ tasks:
|
||||
- fixtures
|
||||
vars:
|
||||
TEST_PKGS:
|
||||
sh: "go list ./... | grep -v {{ .OWNER }}/{{ .PROJECT }}/test | tr '\n' ' '"
|
||||
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
|
||||
@@ -197,7 +197,7 @@ tasks:
|
||||
integration:
|
||||
desc: Run integration tests
|
||||
cmds:
|
||||
- "go test -v ./test/integration"
|
||||
- "go test -v ./cmd/syft/internal/test/integration"
|
||||
# exercise most of the CLI with the data race detector
|
||||
- "go run -race cmd/syft/main.go alpine:latest"
|
||||
|
||||
@@ -265,7 +265,7 @@ tasks:
|
||||
fingerprints:
|
||||
desc: Generate test fixture fingerprints
|
||||
generates:
|
||||
- test/integration/test-fixtures/cache.fingerprint
|
||||
- cmd/syft/internal/test/integration/test-fixtures/cache.fingerprint
|
||||
- syft/pkg/cataloger/binary/test-fixtures/cache.fingerprint
|
||||
- syft/pkg/cataloger/java/test-fixtures/java-builds/cache.fingerprint
|
||||
- syft/pkg/cataloger/golang/test-fixtures/archs/binaries.fingerprint
|
||||
@@ -275,7 +275,7 @@ tasks:
|
||||
- test/cli/test-fixtures/cache.fingerprint
|
||||
cmds:
|
||||
# for IMAGE integration test fixtures
|
||||
- "cd test/integration/test-fixtures && make cache.fingerprint"
|
||||
- "cd cmd/syft/internal/test/integration/test-fixtures && make cache.fingerprint"
|
||||
# for BINARY test fixtures
|
||||
- "cd syft/pkg/cataloger/binary/test-fixtures && make cache.fingerprint"
|
||||
# for JAVA BUILD test fixtures
|
||||
@@ -286,7 +286,7 @@ tasks:
|
||||
- "cd syft/pkg/cataloger/redhat/test-fixtures && make rpms.fingerprint"
|
||||
# for Kernel test fixtures
|
||||
- "cd syft/pkg/cataloger/kernel/test-fixtures && make cache.fingerprint"
|
||||
# for INSTALL integration test fixtures
|
||||
# for INSTALL test fixtures
|
||||
- "cd test/install && make cache.fingerprint"
|
||||
# for CLI test fixtures
|
||||
- "cd test/cli/test-fixtures && make cache.fingerprint"
|
||||
|
||||
+2
-1
@@ -9,8 +9,8 @@ import (
|
||||
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/stereoscope"
|
||||
"github.com/anchore/syft/cmd/syft/cli/commands"
|
||||
handler "github.com/anchore/syft/cmd/syft/cli/ui"
|
||||
"github.com/anchore/syft/cmd/syft/internal/commands"
|
||||
"github.com/anchore/syft/cmd/syft/internal/ui"
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
@@ -86,6 +86,7 @@ func create(id clio.Identification, out io.Writer) (clio.Application, *cobra.Com
|
||||
rootCmd.AddCommand(
|
||||
scanCmd,
|
||||
commands.Packages(app, scanCmd), // this is currently an alias for the scan command
|
||||
commands.Cataloger(app),
|
||||
commands.Attest(app),
|
||||
commands.Convert(app),
|
||||
clio.VersionCommand(id),
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/syft/syft/format/cyclonedxjson"
|
||||
"github.com/anchore/syft/syft/format/cyclonedxxml"
|
||||
"github.com/anchore/syft/syft/format/github"
|
||||
"github.com/anchore/syft/syft/format/spdxjson"
|
||||
"github.com/anchore/syft/syft/format/spdxtagvalue"
|
||||
"github.com/anchore/syft/syft/format/syftjson"
|
||||
"github.com/anchore/syft/syft/format/table"
|
||||
"github.com/anchore/syft/syft/format/template"
|
||||
"github.com/anchore/syft/syft/format/text"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
var _ clio.PostLoader = (*Format)(nil)
|
||||
|
||||
// Format contains all user configuration for output formatting.
|
||||
type Format struct {
|
||||
Pretty *bool `yaml:"pretty" json:"pretty" mapstructure:"pretty"`
|
||||
Template FormatTemplate `yaml:"template" json:"template" mapstructure:"template"`
|
||||
SyftJSON FormatSyftJSON `yaml:"json" json:"json" mapstructure:"json"`
|
||||
SPDXJSON FormatSPDXJSON `yaml:"spdx-json" json:"spdx-json" mapstructure:"spdx-json"`
|
||||
CyclonedxJSON FormatCyclonedxJSON `yaml:"cyclonedx-json" json:"cyclonedx-json" mapstructure:"cyclonedx-json"`
|
||||
CyclonedxXML FormatCyclonedxXML `yaml:"cyclonedx-xml" json:"cyclonedx-xml" mapstructure:"cyclonedx-xml"`
|
||||
}
|
||||
|
||||
func (o *Format) PostLoad() error {
|
||||
o.SyftJSON.Pretty = multiLevelOption[bool](false, o.Pretty, o.SyftJSON.Pretty)
|
||||
o.SPDXJSON.Pretty = multiLevelOption[bool](false, o.Pretty, o.SPDXJSON.Pretty)
|
||||
o.CyclonedxJSON.Pretty = multiLevelOption[bool](false, o.Pretty, o.CyclonedxJSON.Pretty)
|
||||
o.CyclonedxXML.Pretty = multiLevelOption[bool](false, o.Pretty, o.CyclonedxXML.Pretty)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func DefaultFormat() Format {
|
||||
return Format{
|
||||
Template: DefaultFormatTemplate(),
|
||||
SyftJSON: DefaultFormatJSON(),
|
||||
SPDXJSON: DefaultFormatSPDXJSON(),
|
||||
CyclonedxJSON: DefaultFormatCyclonedxJSON(),
|
||||
CyclonedxXML: DefaultFormatCyclonedxXML(),
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Format) Encoders() ([]sbom.FormatEncoder, error) {
|
||||
// setup all encoders based on the configuration
|
||||
var list encoderList
|
||||
|
||||
// in the future there will be application configuration options that can be used to set the default output format
|
||||
list.addWithErr(template.ID)(o.Template.formatEncoders())
|
||||
list.addWithErr(syftjson.ID)(o.SyftJSON.formatEncoders())
|
||||
list.add(table.ID)(table.NewFormatEncoder())
|
||||
list.add(text.ID)(text.NewFormatEncoder())
|
||||
list.add(github.ID)(github.NewFormatEncoder())
|
||||
list.addWithErr(cyclonedxxml.ID)(o.CyclonedxXML.formatEncoders())
|
||||
list.addWithErr(cyclonedxjson.ID)(o.CyclonedxJSON.formatEncoders())
|
||||
list.addWithErr(spdxjson.ID)(o.SPDXJSON.formatEncoders())
|
||||
list.addWithErr(spdxtagvalue.ID)(spdxTagValueEncoders())
|
||||
|
||||
return list.encoders, list.err
|
||||
}
|
||||
|
||||
// TODO: when application configuration is made for this format then this should be ported to the options object
|
||||
// that is created for that configuration (as done with the template output option)
|
||||
func spdxTagValueEncoders() ([]sbom.FormatEncoder, error) {
|
||||
var (
|
||||
encs []sbom.FormatEncoder
|
||||
errs error
|
||||
)
|
||||
for _, v := range spdxtagvalue.SupportedVersions() {
|
||||
enc, err := spdxtagvalue.NewFormatEncoderWithConfig(spdxtagvalue.EncoderConfig{Version: v})
|
||||
if err != nil {
|
||||
errs = multierror.Append(errs, err)
|
||||
} else {
|
||||
encs = append(encs, enc)
|
||||
}
|
||||
}
|
||||
return encs, errs
|
||||
}
|
||||
|
||||
type encoderList struct {
|
||||
encoders []sbom.FormatEncoder
|
||||
err error
|
||||
}
|
||||
|
||||
func (l *encoderList) addWithErr(name sbom.FormatID) func([]sbom.FormatEncoder, error) {
|
||||
return func(encs []sbom.FormatEncoder, err error) {
|
||||
if err != nil {
|
||||
l.err = multierror.Append(l.err, fmt.Errorf("unable to configure %q format encoder: %w", name, err))
|
||||
return
|
||||
}
|
||||
for _, enc := range encs {
|
||||
if enc == nil {
|
||||
l.err = multierror.Append(l.err, fmt.Errorf("unable to configure %q format encoder: nil encoder returned", name))
|
||||
continue
|
||||
}
|
||||
l.encoders = append(l.encoders, enc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *encoderList) add(name sbom.FormatID) func(...sbom.FormatEncoder) {
|
||||
return func(encs ...sbom.FormatEncoder) {
|
||||
for _, enc := range encs {
|
||||
if enc == nil {
|
||||
l.err = multierror.Append(l.err, fmt.Errorf("unable to configure %q format encoder: nil encoder returned", name))
|
||||
continue
|
||||
}
|
||||
l.encoders = append(l.encoders, enc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func multiLevelOption[T any](defaultValue T, option ...*T) *T {
|
||||
result := defaultValue
|
||||
for _, opt := range option {
|
||||
if opt != nil {
|
||||
result = *opt
|
||||
}
|
||||
}
|
||||
return &result
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/go-multierror"
|
||||
|
||||
"github.com/anchore/syft/syft/format/cyclonedxjson"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
type FormatCyclonedxJSON struct {
|
||||
Pretty *bool `yaml:"pretty" json:"pretty" mapstructure:"pretty"`
|
||||
}
|
||||
|
||||
func DefaultFormatCyclonedxJSON() FormatCyclonedxJSON {
|
||||
return FormatCyclonedxJSON{}
|
||||
}
|
||||
|
||||
func (o FormatCyclonedxJSON) formatEncoders() ([]sbom.FormatEncoder, error) {
|
||||
var (
|
||||
encs []sbom.FormatEncoder
|
||||
errs error
|
||||
)
|
||||
for _, v := range cyclonedxjson.SupportedVersions() {
|
||||
enc, err := cyclonedxjson.NewFormatEncoderWithConfig(o.buildConfig(v))
|
||||
if err != nil {
|
||||
errs = multierror.Append(errs, err)
|
||||
} else {
|
||||
encs = append(encs, enc)
|
||||
}
|
||||
}
|
||||
return encs, errs
|
||||
}
|
||||
|
||||
func (o FormatCyclonedxJSON) buildConfig(version string) cyclonedxjson.EncoderConfig {
|
||||
var pretty bool
|
||||
if o.Pretty != nil {
|
||||
pretty = *o.Pretty
|
||||
}
|
||||
return cyclonedxjson.EncoderConfig{
|
||||
Version: version,
|
||||
Pretty: pretty,
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/go-multierror"
|
||||
|
||||
"github.com/anchore/syft/syft/format/cyclonedxxml"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
type FormatCyclonedxXML struct {
|
||||
Pretty *bool `yaml:"pretty" json:"pretty" mapstructure:"pretty"`
|
||||
}
|
||||
|
||||
func DefaultFormatCyclonedxXML() FormatCyclonedxXML {
|
||||
return FormatCyclonedxXML{}
|
||||
}
|
||||
|
||||
func (o FormatCyclonedxXML) formatEncoders() ([]sbom.FormatEncoder, error) {
|
||||
var (
|
||||
encs []sbom.FormatEncoder
|
||||
errs error
|
||||
)
|
||||
for _, v := range cyclonedxxml.SupportedVersions() {
|
||||
enc, err := cyclonedxxml.NewFormatEncoderWithConfig(o.buildConfig(v))
|
||||
if err != nil {
|
||||
errs = multierror.Append(errs, err)
|
||||
} else {
|
||||
encs = append(encs, enc)
|
||||
}
|
||||
}
|
||||
return encs, errs
|
||||
}
|
||||
|
||||
func (o FormatCyclonedxXML) buildConfig(version string) cyclonedxxml.EncoderConfig {
|
||||
var pretty bool
|
||||
if o.Pretty != nil {
|
||||
pretty = *o.Pretty
|
||||
}
|
||||
return cyclonedxxml.EncoderConfig{
|
||||
Version: version,
|
||||
Pretty: pretty,
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/go-multierror"
|
||||
|
||||
"github.com/anchore/syft/syft/format/spdxjson"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
type FormatSPDXJSON struct {
|
||||
Pretty *bool `yaml:"pretty" json:"pretty" mapstructure:"pretty"`
|
||||
}
|
||||
|
||||
func DefaultFormatSPDXJSON() FormatSPDXJSON {
|
||||
return FormatSPDXJSON{}
|
||||
}
|
||||
|
||||
func (o FormatSPDXJSON) formatEncoders() ([]sbom.FormatEncoder, error) {
|
||||
var (
|
||||
encs []sbom.FormatEncoder
|
||||
errs error
|
||||
)
|
||||
for _, v := range spdxjson.SupportedVersions() {
|
||||
enc, err := spdxjson.NewFormatEncoderWithConfig(o.buildConfig(v))
|
||||
if err != nil {
|
||||
errs = multierror.Append(errs, err)
|
||||
} else {
|
||||
encs = append(encs, enc)
|
||||
}
|
||||
}
|
||||
return encs, errs
|
||||
}
|
||||
|
||||
func (o FormatSPDXJSON) buildConfig(v string) spdxjson.EncoderConfig {
|
||||
var pretty bool
|
||||
if o.Pretty != nil {
|
||||
pretty = *o.Pretty
|
||||
}
|
||||
return spdxjson.EncoderConfig{
|
||||
Version: v,
|
||||
Pretty: pretty,
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/syft/cmd/syft/cli/options"
|
||||
"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"
|
||||
@@ -30,7 +30,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
attestExample = ` {{.appName}} {{.command}} --output [FORMAT] alpine:latest defaults to using images from a Docker daemon. If Docker is not present, the image is pulled directly from the registry
|
||||
attestExample = ` {{.appName}} {{.command}} --output [FORMAT] alpine:latest defaults to using images from a Docker daemon. If Docker is not present, the image is pulled directly from the registry
|
||||
`
|
||||
attestSchemeHelp = "\n " + schemeHelpHeader + "\n" + imageSchemeHelp
|
||||
attestHelp = attestExample + attestSchemeHelp
|
||||
@@ -232,7 +232,7 @@ func attestCommand(sbomFilepath string, opts *attestOptions, userInput string) (
|
||||
}
|
||||
|
||||
func predicateType(outputName string) string {
|
||||
// Select Cosign predicate type based on defined output type
|
||||
// select the Cosign predicate type based on defined output type
|
||||
// As orientation, check: https://github.com/sigstore/cosign/blob/main/pkg/cosign/attestation/attestation.go
|
||||
switch strings.ToLower(outputName) {
|
||||
case "cyclonedx-json":
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/syft/cmd/syft/cli/options"
|
||||
"github.com/anchore/syft/cmd/syft/internal/options"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
@@ -0,0 +1,20 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/anchore/clio"
|
||||
)
|
||||
|
||||
func Cataloger(app clio.Application) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "cataloger",
|
||||
Short: "Show available catalogers and configuration",
|
||||
}
|
||||
|
||||
cmd.AddCommand(
|
||||
CatalogerList(app),
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/scylladb/go-set/strset"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/task"
|
||||
"github.com/anchore/syft/syft/cataloging/pkgcataloging"
|
||||
)
|
||||
|
||||
type catalogerListOptions struct {
|
||||
Output string `yaml:"output" json:"output" mapstructure:"output"`
|
||||
DefaultCatalogers []string `yaml:"default-catalogers" json:"default-catalogers" mapstructure:"default-catalogers"`
|
||||
SelectCatalogers []string `yaml:"select-catalogers" json:"select-catalogers" mapstructure:"select-catalogers"`
|
||||
ShowHidden bool `yaml:"show-hidden" json:"show-hidden" mapstructure:"show-hidden"`
|
||||
}
|
||||
|
||||
func (o *catalogerListOptions) AddFlags(flags clio.FlagSet) {
|
||||
flags.StringVarP(&o.Output, "output", "o", "format to output the cataloger list (available: table, json)")
|
||||
|
||||
flags.StringArrayVarP(&o.DefaultCatalogers, "override-default-catalogers", "", "override the default catalogers with an expression")
|
||||
|
||||
flags.StringArrayVarP(&o.SelectCatalogers, "select-catalogers", "", "select catalogers with an expression")
|
||||
|
||||
flags.BoolVarP(&o.ShowHidden, "show-hidden", "s", "show catalogers that have been de-selected")
|
||||
}
|
||||
|
||||
func defaultCatalogerListOptions() *catalogerListOptions {
|
||||
return &catalogerListOptions{
|
||||
DefaultCatalogers: []string{"all"},
|
||||
}
|
||||
}
|
||||
|
||||
func CatalogerList(app clio.Application) *cobra.Command {
|
||||
opts := defaultCatalogerListOptions()
|
||||
|
||||
return app.SetupCommand(&cobra.Command{
|
||||
Use: "list [OPTIONS]",
|
||||
Short: "List available catalogers",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runCatalogerList(opts)
|
||||
},
|
||||
}, opts)
|
||||
}
|
||||
|
||||
func runCatalogerList(opts *catalogerListOptions) error {
|
||||
factories := task.DefaultPackageTaskFactories()
|
||||
allTasks, err := factories.Tasks(task.DefaultCatalogingFactoryConfig())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create cataloger tasks: %w", err)
|
||||
}
|
||||
|
||||
report, err := catalogerListReport(opts, allTasks)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to generate cataloger list report: %w", err)
|
||||
}
|
||||
|
||||
bus.Report(report)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func catalogerListReport(opts *catalogerListOptions, allTasks []task.Task) (string, error) {
|
||||
selectedTasks, selectionEvidence, err := task.Select(allTasks,
|
||||
pkgcataloging.NewSelectionRequest().
|
||||
WithDefaults(opts.DefaultCatalogers...).
|
||||
WithExpression(opts.SelectCatalogers...),
|
||||
)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to select catalogers: %w", err)
|
||||
}
|
||||
var report string
|
||||
|
||||
switch opts.Output {
|
||||
case "json":
|
||||
report, err = renderCatalogerListJSON(selectedTasks, selectionEvidence, opts.DefaultCatalogers, opts.SelectCatalogers)
|
||||
case "table", "":
|
||||
if opts.ShowHidden {
|
||||
report = renderCatalogerListTable(allTasks, selectionEvidence, opts.DefaultCatalogers, opts.SelectCatalogers)
|
||||
} else {
|
||||
report = renderCatalogerListTable(selectedTasks, selectionEvidence, opts.DefaultCatalogers, opts.SelectCatalogers)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to render cataloger list: %w", err)
|
||||
}
|
||||
|
||||
return report, nil
|
||||
}
|
||||
|
||||
func renderCatalogerListJSON(tasks []task.Task, selection task.Selection, defaultSelections, selections []string) (string, error) {
|
||||
type node struct {
|
||||
Name string `json:"name"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
names, tagsByName := extractTaskInfo(tasks)
|
||||
|
||||
nodesByName := make(map[string]node)
|
||||
|
||||
for name := range tagsByName {
|
||||
tagsSelected := selection.TokensByTask[name].SelectedOn.List()
|
||||
|
||||
if len(tagsSelected) == 1 && tagsSelected[0] == "all" {
|
||||
tagsSelected = tagsByName[name]
|
||||
}
|
||||
|
||||
sort.Strings(tagsSelected)
|
||||
|
||||
if tagsSelected == nil {
|
||||
// ensure collections are not null
|
||||
tagsSelected = []string{}
|
||||
}
|
||||
|
||||
nodesByName[name] = node{
|
||||
Name: name,
|
||||
Tags: tagsSelected,
|
||||
}
|
||||
}
|
||||
|
||||
type document struct {
|
||||
DefaultSelection []string `json:"default"`
|
||||
Selection []string `json:"selection"`
|
||||
Catalogers []node `json:"catalogers"`
|
||||
}
|
||||
|
||||
if selections == nil {
|
||||
// ensure collections are not null
|
||||
selections = []string{}
|
||||
}
|
||||
|
||||
doc := document{
|
||||
DefaultSelection: defaultSelections,
|
||||
Selection: selections,
|
||||
}
|
||||
|
||||
for _, name := range names {
|
||||
doc.Catalogers = append(doc.Catalogers, nodesByName[name])
|
||||
}
|
||||
|
||||
by, err := json.Marshal(doc)
|
||||
|
||||
return string(by), err
|
||||
}
|
||||
|
||||
func renderCatalogerListTable(tasks []task.Task, selection task.Selection, defaultSelections, selections []string) string {
|
||||
t := table.NewWriter()
|
||||
t.SetStyle(table.StyleLight)
|
||||
t.AppendHeader(table.Row{"Cataloger", "Tags"})
|
||||
|
||||
names, tagsByName := extractTaskInfo(tasks)
|
||||
|
||||
rowsByName := make(map[string]table.Row)
|
||||
|
||||
for name, tags := range tagsByName {
|
||||
rowsByName[name] = formatRow(name, tags, selection)
|
||||
}
|
||||
|
||||
for _, name := range names {
|
||||
t.AppendRow(rowsByName[name])
|
||||
}
|
||||
|
||||
report := t.Render()
|
||||
|
||||
if len(selections) > 0 {
|
||||
header := "Selected by expressions:\n"
|
||||
for _, expr := range selections {
|
||||
header += fmt.Sprintf(" - %q\n", expr)
|
||||
}
|
||||
report = header + report
|
||||
}
|
||||
|
||||
if len(defaultSelections) > 0 {
|
||||
header := "Default selections:\n"
|
||||
for _, expr := range defaultSelections {
|
||||
header += fmt.Sprintf(" - %q\n", expr)
|
||||
}
|
||||
report = header + report
|
||||
}
|
||||
|
||||
return report
|
||||
}
|
||||
|
||||
func formatRow(name string, tags []string, selection task.Selection) table.Row {
|
||||
isIncluded := selection.Result.Has(name)
|
||||
var selections *task.TokenSelection
|
||||
if s, exists := selection.TokensByTask[name]; exists {
|
||||
selections = &s
|
||||
}
|
||||
|
||||
var formattedTags []string
|
||||
for _, tag := range tags {
|
||||
formattedTags = append(formattedTags, formatToken(tag, selections, isIncluded))
|
||||
}
|
||||
|
||||
var tagStr string
|
||||
if isIncluded {
|
||||
tagStr = strings.Join(formattedTags, ", ")
|
||||
} else {
|
||||
tagStr = strings.Join(formattedTags, grey.Render(", "))
|
||||
}
|
||||
|
||||
// TODO: selection should keep warnings (non-selections) in struct
|
||||
|
||||
return table.Row{
|
||||
formatToken(name, selections, isIncluded),
|
||||
tagStr,
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
green = lipgloss.NewStyle().Foreground(lipgloss.Color("10")) // hi green
|
||||
grey = lipgloss.NewStyle().Foreground(lipgloss.Color("8")) // dark grey
|
||||
red = lipgloss.NewStyle().Foreground(lipgloss.Color("9")) // high red
|
||||
)
|
||||
|
||||
func formatToken(token string, selection *task.TokenSelection, included bool) string {
|
||||
if included && selection != nil {
|
||||
// format all tokens in selection in green
|
||||
if selection.SelectedOn.Has(token) {
|
||||
return green.Render(token)
|
||||
}
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
// format all tokens in selection in red, all others in grey
|
||||
if selection != nil && selection.DeselectedOn.Has(token) {
|
||||
return red.Render(token)
|
||||
}
|
||||
|
||||
return grey.Render(token)
|
||||
}
|
||||
|
||||
func extractTaskInfo(tasks []task.Task) ([]string, map[string][]string) {
|
||||
tagsByName := make(map[string][]string)
|
||||
var names []string
|
||||
|
||||
for _, tsk := range tasks {
|
||||
var tags []string
|
||||
name := tsk.Name()
|
||||
|
||||
if s, ok := tsk.(task.Selector); ok {
|
||||
set := strset.New(s.Selectors()...)
|
||||
set.Remove(name)
|
||||
tags = set.List()
|
||||
sort.Strings(tags)
|
||||
}
|
||||
|
||||
tagsByName[name] = tags
|
||||
names = append(names, name)
|
||||
}
|
||||
|
||||
sort.Strings(names)
|
||||
|
||||
return names, tagsByName
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/scylladb/go-set/strset"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/internal/sbomsync"
|
||||
"github.com/anchore/syft/internal/task"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
)
|
||||
|
||||
var _ interface {
|
||||
task.Task
|
||||
task.Selector
|
||||
} = (*dummyTask)(nil)
|
||||
|
||||
type dummyTask struct {
|
||||
name string
|
||||
selectors []string
|
||||
}
|
||||
|
||||
func (d dummyTask) HasAllSelectors(s ...string) bool {
|
||||
return strset.New(d.selectors...).Has(s...)
|
||||
}
|
||||
|
||||
func (d dummyTask) Selectors() []string {
|
||||
return d.selectors
|
||||
}
|
||||
|
||||
func (d dummyTask) Name() string {
|
||||
return d.name
|
||||
}
|
||||
|
||||
func (d dummyTask) Execute(_ context.Context, _ file.Resolver, _ sbomsync.Builder) error {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func testTasks() []task.Task {
|
||||
return []task.Task{
|
||||
dummyTask{
|
||||
name: "task1",
|
||||
selectors: []string{"image", "a", "b", "1"},
|
||||
},
|
||||
dummyTask{
|
||||
name: "task2",
|
||||
selectors: []string{"image", "b", "c", "2"},
|
||||
},
|
||||
dummyTask{
|
||||
name: "task3",
|
||||
selectors: []string{"directory", "c", "d", "3"},
|
||||
},
|
||||
dummyTask{
|
||||
name: "task4",
|
||||
selectors: []string{"directory", "d", "e", "4"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func Test_catalogerListReport(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
options *catalogerListOptions
|
||||
want string
|
||||
wantErr require.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "no expressions, table",
|
||||
options: func() *catalogerListOptions {
|
||||
c := defaultCatalogerListOptions()
|
||||
c.Output = "table"
|
||||
return c
|
||||
}(),
|
||||
want: `
|
||||
Default selections:
|
||||
- "all"
|
||||
┌───────────┬────────────────────┐
|
||||
│ CATALOGER │ TAGS │
|
||||
├───────────┼────────────────────┤
|
||||
│ task1 │ 1, a, b, image │
|
||||
│ task2 │ 2, b, c, image │
|
||||
│ task3 │ 3, c, d, directory │
|
||||
│ task4 │ 4, d, directory, e │
|
||||
└───────────┴────────────────────┘
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "no expressions, json",
|
||||
options: func() *catalogerListOptions {
|
||||
c := defaultCatalogerListOptions()
|
||||
c.Output = "json"
|
||||
return c
|
||||
}(),
|
||||
want: `
|
||||
{"default":["all"],"selection":[],"catalogers":[{"name":"task1","tags":["1","a","b","image"]},{"name":"task2","tags":["2","b","c","image"]},{"name":"task3","tags":["3","c","d","directory"]},{"name":"task4","tags":["4","d","directory","e"]}]}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "no expressions, default selection, table",
|
||||
options: func() *catalogerListOptions {
|
||||
c := defaultCatalogerListOptions()
|
||||
c.Output = "table"
|
||||
c.DefaultCatalogers = []string{
|
||||
"image",
|
||||
}
|
||||
return c
|
||||
}(),
|
||||
want: `
|
||||
Default selections:
|
||||
- "image"
|
||||
┌───────────┬────────────────┐
|
||||
│ CATALOGER │ TAGS │
|
||||
├───────────┼────────────────┤
|
||||
│ task1 │ 1, a, b, image │
|
||||
│ task2 │ 2, b, c, image │
|
||||
└───────────┴────────────────┘
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "no expressions, default selection, json",
|
||||
options: func() *catalogerListOptions {
|
||||
c := defaultCatalogerListOptions()
|
||||
c.Output = "json"
|
||||
c.DefaultCatalogers = []string{
|
||||
"image",
|
||||
}
|
||||
return c
|
||||
}(),
|
||||
want: `
|
||||
{"default":["image"],"selection":[],"catalogers":[{"name":"task1","tags":["image"]},{"name":"task2","tags":["image"]}]}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "with expressions, default selection, table",
|
||||
options: func() *catalogerListOptions {
|
||||
c := defaultCatalogerListOptions()
|
||||
c.Output = "table"
|
||||
c.DefaultCatalogers = []string{
|
||||
"image",
|
||||
}
|
||||
c.SelectCatalogers = []string{
|
||||
"-directory",
|
||||
"+task3",
|
||||
"-c",
|
||||
"b",
|
||||
}
|
||||
return c
|
||||
}(),
|
||||
want: `
|
||||
Default selections:
|
||||
- "image"
|
||||
Selected by expressions:
|
||||
- "-directory"
|
||||
- "+task3"
|
||||
- "-c"
|
||||
- "b"
|
||||
┌───────────┬────────────────────┐
|
||||
│ CATALOGER │ TAGS │
|
||||
├───────────┼────────────────────┤
|
||||
│ task1 │ 1, a, b, image │
|
||||
│ task3 │ 3, c, d, directory │
|
||||
└───────────┴────────────────────┘
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "with expressions, default selection, json",
|
||||
options: func() *catalogerListOptions {
|
||||
c := defaultCatalogerListOptions()
|
||||
c.Output = "json"
|
||||
c.DefaultCatalogers = []string{
|
||||
"image",
|
||||
}
|
||||
c.SelectCatalogers = []string{
|
||||
"-directory",
|
||||
"+task3",
|
||||
"-c",
|
||||
"b",
|
||||
}
|
||||
return c
|
||||
}(),
|
||||
want: `
|
||||
{"default":["image"],"selection":["-directory","+task3","-c","b"],"catalogers":[{"name":"task1","tags":["b","image"]},{"name":"task3","tags":["task3"]}]}
|
||||
`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.wantErr == nil {
|
||||
tt.wantErr = require.NoError
|
||||
}
|
||||
|
||||
got, err := catalogerListReport(tt.options, testTasks())
|
||||
tt.wantErr(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(tt.want), strings.TrimSpace(got))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/syft/cmd/syft/cli/options"
|
||||
"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/log"
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/syft/cmd/syft/internal/ui"
|
||||
"github.com/anchore/syft/internal"
|
||||
)
|
||||
|
||||
func Packages(app clio.Application, scanCmd *cobra.Command) *cobra.Command {
|
||||
@@ -13,11 +14,14 @@ func Packages(app clio.Application, scanCmd *cobra.Command) *cobra.Command {
|
||||
opts := defaultScanOptions()
|
||||
|
||||
cmd := app.SetupCommand(&cobra.Command{
|
||||
Use: "packages [SOURCE]",
|
||||
Short: scanCmd.Short,
|
||||
Long: scanCmd.Long,
|
||||
Args: scanCmd.Args,
|
||||
Example: scanCmd.Example,
|
||||
Use: "packages [SOURCE]",
|
||||
Short: scanCmd.Short,
|
||||
Long: scanCmd.Long,
|
||||
Args: scanCmd.Args,
|
||||
Example: internal.Tprintf(scanHelp, map[string]interface{}{
|
||||
"appName": id.Name,
|
||||
"command": "packages",
|
||||
}),
|
||||
PreRunE: applicationUpdateCheck(id, &opts.UpdateCheck),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
restoreStdout := ui.CaptureStdoutToTraceLog()
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
"github.com/anchore/syft/cmd/syft/cli/options"
|
||||
"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"
|
||||
@@ -132,7 +132,7 @@ func (o *scanOptions) validateLegacyOptionsNotUsed() error {
|
||||
}
|
||||
|
||||
if legacy.ExcludeBinaryOverlapByOwnership != nil {
|
||||
return fmt.Errorf("the config file option 'exclude-binary-overlap-by-ownership' has been removed, please use 'relationships.exclude-binary-packages-with-file-ownership-overlap' instead")
|
||||
return fmt.Errorf("the config file option 'exclude-binary-overlap-by-ownership' has been removed, please use 'package.exclude-binary-overlap-by-ownership' instead")
|
||||
}
|
||||
|
||||
if legacy.BasePath != nil {
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/anchore/syft/cmd/syft/cli/options"
|
||||
"github.com/anchore/syft/cmd/syft/internal/options"
|
||||
)
|
||||
|
||||
func Test_scanOptions_validateLegacyOptionsNotUsed(t *testing.T) {
|
||||
@@ -29,7 +29,7 @@ func Test_scanOptions_validateLegacyOptionsNotUsed(t *testing.T) {
|
||||
{
|
||||
name: "config file with exclude-binary-overlap-by-ownership legacy option",
|
||||
cfg: "test-fixtures/scan-configs/with-exclude-binary-overlap-by-ownership.yaml",
|
||||
wantErr: assertErrorContains("relationships.exclude-binary-packages-with-file-ownership-overlap"),
|
||||
wantErr: assertErrorContains("package.exclude-binary-overlap-by-ownership"),
|
||||
},
|
||||
{
|
||||
name: "config file with file string legacy option",
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
|
||||
"github.com/anchore/clio"
|
||||
hashiVersion "github.com/anchore/go-version"
|
||||
"github.com/anchore/syft/cmd/syft/cli/options"
|
||||
"github.com/anchore/syft/cmd/syft/internal"
|
||||
"github.com/anchore/syft/cmd/syft/internal/options"
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
@@ -46,7 +46,6 @@ type Catalog struct {
|
||||
// configuration for the source (the subject being analyzed)
|
||||
Registry registryConfig `yaml:"registry" json:"registry" mapstructure:"registry"`
|
||||
Platform string `yaml:"platform" json:"platform" mapstructure:"platform"`
|
||||
Name string `yaml:"name" json:"name" mapstructure:"name"` // deprecated
|
||||
Source sourceConfig `yaml:"source" json:"source" mapstructure:"source"`
|
||||
Exclusions []string `yaml:"exclude" json:"exclude" mapstructure:"exclude"`
|
||||
}
|
||||
@@ -175,14 +174,6 @@ func (cfg *Catalog) AddFlags(flags clio.FlagSet) {
|
||||
flags.StringArrayVarP(&cfg.SelectCatalogers, "select-catalogers", "",
|
||||
"add, remove, and filter the catalogers to be used")
|
||||
|
||||
flags.StringVarP(&cfg.Source.Name, "name", "",
|
||||
"set the name of the target being analyzed")
|
||||
|
||||
if pfp, ok := flags.(fangs.PFlagSetProvider); ok {
|
||||
flagSet := pfp.PFlagSet()
|
||||
flagSet.Lookup("name").Deprecated = "use: source-name"
|
||||
}
|
||||
|
||||
flags.StringVarP(&cfg.Source.Name, "source-name", "",
|
||||
"set the name of the target being analyzed")
|
||||
|
||||
@@ -198,13 +189,6 @@ func (cfg *Catalog) DescribeFields(descriptions fangs.FieldDescriptionSet) {
|
||||
}
|
||||
|
||||
func (cfg *Catalog) PostLoad() error {
|
||||
if cfg.Name != "" {
|
||||
log.Warnf("name parameter is deprecated. please use: source-name. name will be removed in a future version")
|
||||
if cfg.Source.Name == "" {
|
||||
cfg.Source.Name = cfg.Name
|
||||
}
|
||||
}
|
||||
|
||||
usingLegacyCatalogers := len(cfg.Catalogers) > 0
|
||||
usingNewCatalogers := len(cfg.DefaultCatalogers) > 0 || len(cfg.SelectCatalogers) > 0
|
||||
|
||||
@@ -2,6 +2,9 @@ package options
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/scylladb/go-set/strset"
|
||||
|
||||
intFile "github.com/anchore/syft/internal/file"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
@@ -35,6 +38,10 @@ func defaultFileConfig() fileConfig {
|
||||
}
|
||||
|
||||
func (c *fileConfig) PostLoad() error {
|
||||
digests := strset.New(c.Metadata.Digests...).List()
|
||||
sort.Strings(digests)
|
||||
c.Metadata.Digests = digests
|
||||
|
||||
switch c.Metadata.Selection {
|
||||
case file.NoFilesSelection, file.FilesOwnedByPackageSelection, file.AllFilesSelection:
|
||||
return nil
|
||||
@@ -0,0 +1,56 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/anchore/syft/syft/file"
|
||||
)
|
||||
|
||||
func Test_fileConfig_PostLoad(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg fileConfig
|
||||
assert func(t *testing.T, cfg fileConfig)
|
||||
wantErr assert.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "deduplicate digests",
|
||||
cfg: fileConfig{
|
||||
Metadata: fileMetadata{
|
||||
Selection: file.NoFilesSelection,
|
||||
Digests: []string{"sha1", "sha1"},
|
||||
},
|
||||
},
|
||||
assert: func(t *testing.T, cfg fileConfig) {
|
||||
assert.Equal(t, []string{"sha1"}, cfg.Metadata.Digests)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "error on invalid selection",
|
||||
cfg: fileConfig{
|
||||
Metadata: fileMetadata{
|
||||
Selection: file.Selection("invalid"),
|
||||
},
|
||||
},
|
||||
wantErr: assert.Error,
|
||||
},
|
||||
{
|
||||
name: "error on empty selection",
|
||||
cfg: fileConfig{},
|
||||
wantErr: assert.Error,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.wantErr == nil {
|
||||
tt.wantErr = assert.NoError
|
||||
}
|
||||
tt.wantErr(t, tt.cfg.PostLoad())
|
||||
if tt.assert != nil {
|
||||
tt.assert(t, tt.cfg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/syft/syft/format"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
var _ clio.PostLoader = (*Format)(nil)
|
||||
|
||||
// Format contains all user configuration for output formatting.
|
||||
type Format struct {
|
||||
Pretty *bool `yaml:"pretty" json:"pretty" mapstructure:"pretty"`
|
||||
Template FormatTemplate `yaml:"template" json:"template" mapstructure:"template"`
|
||||
SyftJSON FormatSyftJSON `yaml:"json" json:"json" mapstructure:"json"`
|
||||
SPDXJSON FormatSPDXJSON `yaml:"spdx-json" json:"spdx-json" mapstructure:"spdx-json"`
|
||||
CyclonedxJSON FormatCyclonedxJSON `yaml:"cyclonedx-json" json:"cyclonedx-json" mapstructure:"cyclonedx-json"`
|
||||
CyclonedxXML FormatCyclonedxXML `yaml:"cyclonedx-xml" json:"cyclonedx-xml" mapstructure:"cyclonedx-xml"`
|
||||
}
|
||||
|
||||
func (o *Format) PostLoad() error {
|
||||
o.SyftJSON.Pretty = multiLevelOption[bool](false, o.Pretty, o.SyftJSON.Pretty)
|
||||
o.SPDXJSON.Pretty = multiLevelOption[bool](false, o.Pretty, o.SPDXJSON.Pretty)
|
||||
o.CyclonedxJSON.Pretty = multiLevelOption[bool](false, o.Pretty, o.CyclonedxJSON.Pretty)
|
||||
o.CyclonedxXML.Pretty = multiLevelOption[bool](false, o.Pretty, o.CyclonedxXML.Pretty)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func DefaultFormat() Format {
|
||||
return Format{
|
||||
Template: DefaultFormatTemplate(),
|
||||
SyftJSON: DefaultFormatJSON(),
|
||||
SPDXJSON: DefaultFormatSPDXJSON(),
|
||||
CyclonedxJSON: DefaultFormatCyclonedxJSON(),
|
||||
CyclonedxXML: DefaultFormatCyclonedxXML(),
|
||||
}
|
||||
}
|
||||
|
||||
func (o Format) Encoders() ([]sbom.FormatEncoder, error) {
|
||||
return format.EncodersConfig{
|
||||
Template: o.Template.config(),
|
||||
SyftJSON: o.SyftJSON.config(),
|
||||
SPDXJSON: o.SPDXJSON.config(format.AllVersions), // we support multiple versions, not just a single version
|
||||
CyclonedxJSON: o.CyclonedxJSON.config(format.AllVersions), // we support multiple versions, not just a single version
|
||||
CyclonedxXML: o.CyclonedxXML.config(format.AllVersions), // we support multiple versions, not just a single version
|
||||
}.Encoders()
|
||||
}
|
||||
|
||||
func multiLevelOption[T any](defaultValue T, option ...*T) *T {
|
||||
result := defaultValue
|
||||
for _, opt := range option {
|
||||
if opt != nil {
|
||||
result = *opt
|
||||
}
|
||||
}
|
||||
return &result
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/syft/format/cyclonedxjson"
|
||||
)
|
||||
|
||||
type FormatCyclonedxJSON struct {
|
||||
Pretty *bool `yaml:"pretty" json:"pretty" mapstructure:"pretty"`
|
||||
}
|
||||
|
||||
func DefaultFormatCyclonedxJSON() FormatCyclonedxJSON {
|
||||
return FormatCyclonedxJSON{}
|
||||
}
|
||||
|
||||
func (o FormatCyclonedxJSON) config(version string) cyclonedxjson.EncoderConfig {
|
||||
var pretty bool
|
||||
if o.Pretty != nil {
|
||||
pretty = *o.Pretty
|
||||
}
|
||||
return cyclonedxjson.EncoderConfig{
|
||||
Version: version,
|
||||
Pretty: pretty,
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -13,7 +13,7 @@ func TestFormatCyclonedxJSON_buildConfig(t *testing.T) {
|
||||
ft := &FormatCyclonedxJSON{}
|
||||
ft = setAllToNonZero(t, ft).(*FormatCyclonedxJSON)
|
||||
|
||||
subject := ft.buildConfig("Version")
|
||||
subject := ft.config("Version")
|
||||
assertExpectedValue(t, subject)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/syft/format/cyclonedxxml"
|
||||
)
|
||||
|
||||
type FormatCyclonedxXML struct {
|
||||
Pretty *bool `yaml:"pretty" json:"pretty" mapstructure:"pretty"`
|
||||
}
|
||||
|
||||
func DefaultFormatCyclonedxXML() FormatCyclonedxXML {
|
||||
return FormatCyclonedxXML{}
|
||||
}
|
||||
|
||||
func (o FormatCyclonedxXML) config(version string) cyclonedxxml.EncoderConfig {
|
||||
var pretty bool
|
||||
if o.Pretty != nil {
|
||||
pretty = *o.Pretty
|
||||
}
|
||||
return cyclonedxxml.EncoderConfig{
|
||||
Version: version,
|
||||
Pretty: pretty,
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -10,6 +10,6 @@ func TestFormatCyclonedxXML_buildConfig(t *testing.T) {
|
||||
ft := FormatCyclonedxXML{}
|
||||
ftp := setAllToNonZero(t, &ft).(*FormatCyclonedxXML)
|
||||
|
||||
subject := ftp.buildConfig("Version")
|
||||
subject := ftp.config("Version")
|
||||
assertExpectedValue(t, subject)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/syft/format/spdxjson"
|
||||
)
|
||||
|
||||
type FormatSPDXJSON struct {
|
||||
Pretty *bool `yaml:"pretty" json:"pretty" mapstructure:"pretty"`
|
||||
}
|
||||
|
||||
func DefaultFormatSPDXJSON() FormatSPDXJSON {
|
||||
return FormatSPDXJSON{}
|
||||
}
|
||||
|
||||
func (o FormatSPDXJSON) config(v string) spdxjson.EncoderConfig {
|
||||
var pretty bool
|
||||
if o.Pretty != nil {
|
||||
pretty = *o.Pretty
|
||||
}
|
||||
return spdxjson.EncoderConfig{
|
||||
Version: v,
|
||||
Pretty: pretty,
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -10,6 +10,6 @@ func TestFormatSPDXJSON_buildConfig(t *testing.T) {
|
||||
ft := &FormatSPDXJSON{}
|
||||
ft = setAllToNonZero(t, ft).(*FormatSPDXJSON)
|
||||
|
||||
subject := ft.buildConfig("Version")
|
||||
subject := ft.config("Version")
|
||||
assertExpectedValue(t, subject)
|
||||
}
|
||||
+1
-7
@@ -2,7 +2,6 @@ package options
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/syft/format/syftjson"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
type FormatSyftJSON struct {
|
||||
@@ -16,12 +15,7 @@ func DefaultFormatJSON() FormatSyftJSON {
|
||||
}
|
||||
}
|
||||
|
||||
func (o FormatSyftJSON) formatEncoders() ([]sbom.FormatEncoder, error) {
|
||||
enc, err := syftjson.NewFormatEncoderWithConfig(o.buildConfig())
|
||||
return []sbom.FormatEncoder{enc}, err
|
||||
}
|
||||
|
||||
func (o FormatSyftJSON) buildConfig() syftjson.EncoderConfig {
|
||||
func (o FormatSyftJSON) config() syftjson.EncoderConfig {
|
||||
var pretty bool
|
||||
if o.Pretty != nil {
|
||||
pretty = *o.Pretty
|
||||
+1
-1
@@ -10,6 +10,6 @@ func TestFormatSyftJSON_buildConfig(t *testing.T) {
|
||||
ft := &FormatSyftJSON{}
|
||||
ft = setAllToNonZero(t, ft).(*FormatSyftJSON)
|
||||
|
||||
subject := ft.buildConfig()
|
||||
subject := ft.config()
|
||||
assertExpectedValue(t, subject)
|
||||
}
|
||||
+5
-8
@@ -3,7 +3,6 @@ package options
|
||||
import (
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/syft/syft/format/template"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
var _ clio.FlagAdder = (*FormatTemplate)(nil)
|
||||
@@ -11,6 +10,7 @@ var _ clio.FlagAdder = (*FormatTemplate)(nil)
|
||||
type FormatTemplate struct {
|
||||
Enabled bool `yaml:"-" json:"-" mapstructure:"-"`
|
||||
Path string `yaml:"path" json:"path" mapstructure:"path"` // -t template file to use for output
|
||||
Legacy bool `yaml:"legacy" json:"legacy" mapstructure:"legacy"`
|
||||
}
|
||||
|
||||
func DefaultFormatTemplate() FormatTemplate {
|
||||
@@ -26,12 +26,9 @@ func (o *FormatTemplate) AddFlags(flags clio.FlagSet) {
|
||||
}
|
||||
}
|
||||
|
||||
func (o FormatTemplate) formatEncoders() ([]sbom.FormatEncoder, error) {
|
||||
if !o.Enabled {
|
||||
return nil, nil
|
||||
}
|
||||
enc, err := template.NewFormatEncoder(template.EncoderConfig{
|
||||
func (o FormatTemplate) config() template.EncoderConfig {
|
||||
return template.EncoderConfig{
|
||||
TemplatePath: o.Path,
|
||||
})
|
||||
return []sbom.FormatEncoder{enc}, err
|
||||
Legacy: o.Legacy,
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package options
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/scylladb/go-set/strset"
|
||||
@@ -34,6 +35,13 @@ func defaultSourceConfig() sourceConfig {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *fileSource) PostLoad() error {
|
||||
digests := strset.New(c.Digests...).List()
|
||||
sort.Strings(digests)
|
||||
c.Digests = digests
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c imageSource) PostLoad() error {
|
||||
return checkDefaultSourceValues(c.DefaultPullSource)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_fileSource_PostLoad(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg fileSource
|
||||
assert func(t *testing.T, cfg fileSource)
|
||||
wantErr assert.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "deduplicate digests",
|
||||
cfg: fileSource{
|
||||
Digests: []string{"sha1", "sha1"},
|
||||
},
|
||||
assert: func(t *testing.T, cfg fileSource) {
|
||||
assert.Equal(t, []string{"sha1"}, cfg.Digests)
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.wantErr == nil {
|
||||
tt.wantErr = assert.NoError
|
||||
}
|
||||
tt.wantErr(t, tt.cfg.PostLoad())
|
||||
if tt.assert != nil {
|
||||
tt.assert(t, tt.cfg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/cmd/syft/cli/options"
|
||||
"github.com/anchore/syft/cmd/syft/internal/options"
|
||||
"github.com/anchore/syft/syft/format"
|
||||
"github.com/anchore/syft/syft/format/cyclonedxjson"
|
||||
"github.com/anchore/syft/syft/format/cyclonedxxml"
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
|
||||
"github.com/anchore/clio"
|
||||
stereoscopeFile "github.com/anchore/stereoscope/pkg/file"
|
||||
"github.com/anchore/syft/cmd/syft/cli/options"
|
||||
"github.com/anchore/syft/cmd/syft/internal/options"
|
||||
"github.com/anchore/syft/syft/cataloging/filecataloging"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/file/cataloger/filecontent"
|
||||
@@ -0,0 +1,350 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/bmatcuk/doublestar/v4"
|
||||
"github.com/scylladb/go-set/strset"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_packageCatalogerExports(t *testing.T) {
|
||||
// sanity check that we are actually finding exports
|
||||
|
||||
exports := packageCatalogerExports(t)
|
||||
require.NotEmpty(t, exports)
|
||||
|
||||
expectAtLeast := map[string]*strset.Set{
|
||||
"golang": strset.New("NewGoModuleFileCataloger", "NewGoModuleBinaryCataloger", "CatalogerConfig", "DefaultCatalogerConfig"),
|
||||
}
|
||||
|
||||
for pkg, expected := range expectAtLeast {
|
||||
actual, ok := exports[pkg]
|
||||
require.True(t, ok, pkg)
|
||||
require.True(t, expected.IsSubset(actual.Names()), pkg)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_validatePackageCatalogerExport(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
export exportToken
|
||||
wantErr assert.ErrorAssertionFunc
|
||||
}{
|
||||
// valid...
|
||||
{
|
||||
name: "valid constructor",
|
||||
export: exportToken{
|
||||
Name: "NewFooCataloger",
|
||||
Type: "*ast.FuncType",
|
||||
SignatureSize: 1,
|
||||
ReturnTypeNames: []string{
|
||||
"pkg.Cataloger",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid default config",
|
||||
export: exportToken{
|
||||
Name: "DefaultFooConfig",
|
||||
Type: "*ast.FuncType",
|
||||
SignatureSize: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid config",
|
||||
export: exportToken{
|
||||
Name: "FooConfig",
|
||||
Type: "*ast.StructType",
|
||||
},
|
||||
},
|
||||
// invalid...
|
||||
{
|
||||
name: "constructor that returns a concrete type",
|
||||
export: exportToken{
|
||||
Name: "NewFooCataloger",
|
||||
Type: "*ast.FuncType",
|
||||
SignatureSize: 1,
|
||||
ReturnTypeNames: []string{
|
||||
"*generic.Cataloger",
|
||||
},
|
||||
},
|
||||
wantErr: assert.Error,
|
||||
},
|
||||
{
|
||||
name: "struct with constructor name",
|
||||
export: exportToken{
|
||||
Name: "NewFooCataloger",
|
||||
Type: "*ast.StructType",
|
||||
},
|
||||
wantErr: assert.Error,
|
||||
},
|
||||
{
|
||||
name: "struct with default config fn name",
|
||||
export: exportToken{
|
||||
Name: "DefaultFooConfig",
|
||||
Type: "*ast.StructType",
|
||||
},
|
||||
wantErr: assert.Error,
|
||||
},
|
||||
{
|
||||
name: "fn with struct name",
|
||||
export: exportToken{
|
||||
Name: "FooConfig",
|
||||
Type: "*ast.FuncType",
|
||||
},
|
||||
wantErr: assert.Error,
|
||||
},
|
||||
{
|
||||
name: "default config with parameters",
|
||||
export: exportToken{
|
||||
Name: "DefaultFooConfig",
|
||||
Type: "*ast.FuncType",
|
||||
SignatureSize: 1,
|
||||
},
|
||||
wantErr: assert.Error,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
if c.wantErr == nil {
|
||||
c.wantErr = assert.NoError
|
||||
}
|
||||
err := validatePackageCatalogerExport(t, "test", c.export)
|
||||
c.wantErr(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_PackageCatalogerConventions(t *testing.T) {
|
||||
// look at each package in syft/pkg/cataloger...
|
||||
// we want to make certain that only the following things are exported from the package:
|
||||
// - function matching New*Cataloger (e.g. NewAptCataloger)
|
||||
// - function matching Default*Config
|
||||
// - struct matching *Config
|
||||
//
|
||||
// anything else that is exported should result in the test failing.
|
||||
// note: this is meant to apply to things in static space, not methods on structs or within interfaces.
|
||||
//
|
||||
// this additionally ensures that:
|
||||
// - any config struct has a Default*Config function to pair with it.
|
||||
// - all cataloger constructors return pkg.Cataloger interface instead of a concrete type
|
||||
|
||||
exportsPerPackage := packageCatalogerExports(t)
|
||||
|
||||
//for debugging purposes...
|
||||
//for pkg, exports := range exportsPerPackage {
|
||||
// t.Log(pkg)
|
||||
// for _, export := range exports {
|
||||
// t.Logf(" %#v", export)
|
||||
// }
|
||||
//}
|
||||
|
||||
for pkg, exports := range exportsPerPackage {
|
||||
for _, export := range exports.List() {
|
||||
// assert the export name is valid...
|
||||
assert.NoError(t, validatePackageCatalogerExport(t, pkg, export))
|
||||
|
||||
// assert that config structs have a Default*Config functions to pair with them...
|
||||
if strings.Contains(export.Name, "Config") && !strings.Contains(export.Name, "Default") {
|
||||
// this is a config struct, make certain there is a pairing with a Default*Config function
|
||||
assert.True(t, exports.Has("Default"+export.Name), "cataloger config struct %q in pkg %q must have a 'Default%s' function", export.Name, pkg, export.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validatePackageCatalogerExport(t *testing.T, pkg string, export exportToken) error {
|
||||
|
||||
constructorMatches, err := doublestar.Match("New*Cataloger", export.Name)
|
||||
require.NoError(t, err)
|
||||
|
||||
defaultConfigMatches, err := doublestar.Match("Default*Config", export.Name)
|
||||
require.NoError(t, err)
|
||||
|
||||
configMatches, err := doublestar.Match("*Config", export.Name)
|
||||
require.NoError(t, err)
|
||||
|
||||
switch {
|
||||
case constructorMatches:
|
||||
if !export.isFunction() {
|
||||
return fmt.Errorf("constructor convention used for non-function in pkg=%q: %#v", pkg, export)
|
||||
}
|
||||
|
||||
returnTypes := strset.New(export.ReturnTypeNames...)
|
||||
if !returnTypes.Has("pkg.Cataloger") {
|
||||
return fmt.Errorf("constructor convention is to return pkg.Cataloger and not concrete types. pkg=%q constructor=%q types=%+v", pkg, export.Name, strings.Join(export.ReturnTypeNames, ","))
|
||||
}
|
||||
|
||||
case defaultConfigMatches:
|
||||
if !export.isFunction() {
|
||||
return fmt.Errorf("default config convention used for non-function in pkg=%q: %#v", pkg, export)
|
||||
}
|
||||
if export.SignatureSize != 0 {
|
||||
return fmt.Errorf("default config convention used for non-zero signature size in pkg=%q: %#v", pkg, export)
|
||||
}
|
||||
case configMatches:
|
||||
if !export.isStruct() {
|
||||
return fmt.Errorf("config convention used for non-struct in pkg=%q: %#v", pkg, export)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unexpected export in pkg=%q: %#v", pkg, export)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type exportToken struct {
|
||||
Name string
|
||||
Type string
|
||||
SignatureSize int
|
||||
ReturnTypeNames []string
|
||||
}
|
||||
|
||||
func (e exportToken) isFunction() bool {
|
||||
return strings.Contains(e.Type, "ast.FuncType")
|
||||
}
|
||||
|
||||
func (e exportToken) isStruct() bool {
|
||||
return strings.Contains(e.Type, "ast.StructType")
|
||||
}
|
||||
|
||||
type exportTokenSet map[string]exportToken
|
||||
|
||||
func (s exportTokenSet) Names() *strset.Set {
|
||||
set := strset.New()
|
||||
for k := range s {
|
||||
set.Add(k)
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
func (s exportTokenSet) Has(name string) bool {
|
||||
_, ok := s[name]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (s exportTokenSet) Add(tokens ...exportToken) {
|
||||
for _, t := range tokens {
|
||||
if _, ok := s[t.Name]; ok {
|
||||
panic("duplicate token name: " + t.Name)
|
||||
}
|
||||
s[t.Name] = t
|
||||
}
|
||||
}
|
||||
|
||||
func (s exportTokenSet) Remove(names ...string) {
|
||||
for _, name := range names {
|
||||
delete(s, name)
|
||||
}
|
||||
}
|
||||
|
||||
func (s exportTokenSet) List() []exportToken {
|
||||
var tokens []exportToken
|
||||
for _, t := range s {
|
||||
tokens = append(tokens, t)
|
||||
}
|
||||
return tokens
|
||||
}
|
||||
|
||||
func packageCatalogerExports(t *testing.T) map[string]exportTokenSet {
|
||||
t.Helper()
|
||||
|
||||
catalogerPath := filepath.Join(repoRoot(t), "syft", "pkg", "cataloger")
|
||||
|
||||
ignorePaths := []string{
|
||||
filepath.Join(catalogerPath, "common"),
|
||||
filepath.Join(catalogerPath, "generic"),
|
||||
}
|
||||
|
||||
exportsPerPackage := make(map[string]exportTokenSet)
|
||||
|
||||
err := filepath.Walk(catalogerPath, func(path string, info os.FileInfo, err error) error {
|
||||
require.NoError(t, err)
|
||||
|
||||
if info.IsDir() ||
|
||||
!strings.HasSuffix(info.Name(), ".go") ||
|
||||
strings.HasSuffix(info.Name(), "_test.go") ||
|
||||
strings.Contains(path, "test-fixtures") ||
|
||||
strings.Contains(path, "internal") {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, ignorePath := range ignorePaths {
|
||||
if strings.Contains(path, ignorePath) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
fset := token.NewFileSet()
|
||||
node, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
|
||||
require.NoError(t, err)
|
||||
|
||||
pkg := node.Name.Name
|
||||
for _, f := range node.Decls {
|
||||
switch decl := f.(type) {
|
||||
case *ast.GenDecl:
|
||||
for _, spec := range decl.Specs {
|
||||
switch spec := spec.(type) {
|
||||
case *ast.TypeSpec:
|
||||
if spec.Name.IsExported() {
|
||||
if _, ok := exportsPerPackage[pkg]; !ok {
|
||||
exportsPerPackage[pkg] = make(exportTokenSet)
|
||||
}
|
||||
exportsPerPackage[pkg].Add(exportToken{
|
||||
Name: spec.Name.Name,
|
||||
Type: reflect.TypeOf(spec.Type).String(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
case *ast.FuncDecl:
|
||||
if decl.Recv == nil && decl.Name.IsExported() {
|
||||
var returnTypes []string
|
||||
if decl.Type.Results != nil {
|
||||
for _, field := range decl.Type.Results.List {
|
||||
// TODO: there is probably a better way to extract the specific type name
|
||||
//ty := strings.Join(strings.Split(fmt.Sprint(field.Type), " "), ".")
|
||||
ty := types.ExprString(field.Type)
|
||||
|
||||
returnTypes = append(returnTypes, ty)
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := exportsPerPackage[pkg]; !ok {
|
||||
exportsPerPackage[pkg] = make(exportTokenSet)
|
||||
}
|
||||
exportsPerPackage[pkg].Add(exportToken{
|
||||
Name: decl.Name.Name,
|
||||
Type: reflect.TypeOf(decl.Type).String(),
|
||||
SignatureSize: len(decl.Type.Params.List),
|
||||
ReturnTypeNames: returnTypes,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
// remove exceptions
|
||||
// these are known violations to the common convention that are allowed.
|
||||
if vs, ok := exportsPerPackage["binary"]; ok {
|
||||
vs.Remove("Classifier", "EvidenceMatcher", "FileContentsVersionMatcher", "DefaultClassifiers")
|
||||
}
|
||||
|
||||
return exportsPerPackage
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/scylladb/go-set/strset"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/internal/task"
|
||||
"github.com/anchore/syft/syft/cataloging/pkgcataloging"
|
||||
)
|
||||
|
||||
func TestAllPackageCatalogersReachableInTasks(t *testing.T) {
|
||||
// we want to see if we can get a task for all package catalogers. This is a bit tricky since we
|
||||
// don't have a nice way to find all cataloger names in the codebase. Instead, we'll look at the
|
||||
// count of unique task names from the package task factory set and compare that with the known constructors
|
||||
// from a source analysis... they should match.
|
||||
|
||||
// additionally, at this time they should either have a "directory" or "image" tag as well. If there is no tag
|
||||
// on a cataloger task then the test should fail.
|
||||
|
||||
taskFactories := task.DefaultPackageTaskFactories()
|
||||
taskTagsByName := make(map[string][]string)
|
||||
for _, factory := range taskFactories {
|
||||
tsk := factory(task.DefaultCatalogingFactoryConfig())
|
||||
if taskTagsByName[tsk.Name()] != nil {
|
||||
t.Fatalf("duplicate task name: %q", tsk.Name())
|
||||
}
|
||||
|
||||
require.NotNil(t, tsk)
|
||||
if sel, ok := tsk.(task.Selector); ok {
|
||||
taskTagsByName[tsk.Name()] = sel.Selectors()
|
||||
} else {
|
||||
taskTagsByName[tsk.Name()] = []string{}
|
||||
}
|
||||
}
|
||||
|
||||
var constructorCount int
|
||||
constructorsPerPackage := getCatalogerConstructors(t)
|
||||
for _, constructors := range constructorsPerPackage {
|
||||
constructorCount += constructors.Size()
|
||||
}
|
||||
|
||||
assert.Equal(t, len(taskTagsByName), constructorCount, "mismatch in number of cataloger constructors and task names")
|
||||
|
||||
for taskName, tags := range taskTagsByName {
|
||||
if taskName == "sbom-cataloger" {
|
||||
continue // this is a special case
|
||||
}
|
||||
if !strset.New(tags...).HasAny(pkgcataloging.ImageTag, pkgcataloging.DirectoryTag) {
|
||||
t.Errorf("task %q is missing 'directory' or 'image' a tag", taskName)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestAllPackageCatalogersRepresentedInSource(t *testing.T) {
|
||||
// find all functions in syft/pkg/cataloger/** that either:
|
||||
// - match the name glob "New*Cataloger"
|
||||
// - are in cataloger.go and match the name glob "New*"
|
||||
//
|
||||
// Then:
|
||||
// - keep track of all packages with cataloger constructors
|
||||
// - keep track of all constructors
|
||||
constructorsPerPackage := getCatalogerConstructors(t)
|
||||
|
||||
// look at the source file in internal/task/package_tasks.go:
|
||||
// - ensure all go packages that have constructors are imported
|
||||
// - ensure there is a reference to all package constructors
|
||||
assertAllPackageCatalogersRepresented(t, constructorsPerPackage)
|
||||
}
|
||||
|
||||
func getCatalogerConstructors(t *testing.T) map[string]*strset.Set {
|
||||
t.Helper()
|
||||
root := repoRoot(t)
|
||||
catalogerPath := filepath.Join(root, "syft", "pkg", "cataloger")
|
||||
|
||||
constructorsPerPackage := make(map[string]*strset.Set)
|
||||
|
||||
err := filepath.Walk(catalogerPath, func(path string, info os.FileInfo, err error) error {
|
||||
require.NoError(t, err)
|
||||
|
||||
// ignore directories and test files...
|
||||
if info.IsDir() || strings.HasSuffix(info.Name(), "_test.go") {
|
||||
return nil
|
||||
}
|
||||
|
||||
partialResults := getConstructorsFromExpectedFile(t, path, info)
|
||||
|
||||
constructorsPerPackage = mergeConstructors(constructorsPerPackage, partialResults)
|
||||
|
||||
partialResults = getCatalogerConstructorsFromPackage(t, path, info)
|
||||
|
||||
constructorsPerPackage = mergeConstructors(constructorsPerPackage, partialResults)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
// remove some exceptions
|
||||
delete(constructorsPerPackage, "generic") // this is not an actual cataloger
|
||||
|
||||
return constructorsPerPackage
|
||||
}
|
||||
|
||||
func getConstructorsFromExpectedFile(t *testing.T, path string, info os.FileInfo) map[string][]string {
|
||||
constructorsPerPackage := make(map[string][]string)
|
||||
|
||||
if !strings.HasSuffix(info.Name(), "cataloger.go") && !strings.HasSuffix(info.Name(), "catalogers.go") {
|
||||
return nil
|
||||
}
|
||||
|
||||
fset := token.NewFileSet()
|
||||
node, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, f := range node.Decls {
|
||||
fn, ok := f.(*ast.FuncDecl)
|
||||
if !ok || fn.Recv != nil || !strings.HasPrefix(fn.Name.Name, "New") {
|
||||
continue
|
||||
}
|
||||
|
||||
pkg := node.Name.Name
|
||||
constructorsPerPackage[pkg] = append(constructorsPerPackage[pkg], fn.Name.Name)
|
||||
}
|
||||
|
||||
return constructorsPerPackage
|
||||
}
|
||||
|
||||
func getCatalogerConstructorsFromPackage(t *testing.T, path string, info os.FileInfo) map[string][]string {
|
||||
constructorsPerPackage := make(map[string][]string)
|
||||
|
||||
if info.IsDir() || !strings.HasSuffix(info.Name(), ".go") {
|
||||
return nil
|
||||
}
|
||||
|
||||
fset := token.NewFileSet()
|
||||
node, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, f := range node.Decls {
|
||||
fn, ok := f.(*ast.FuncDecl)
|
||||
if !ok || fn.Recv != nil || !strings.HasPrefix(fn.Name.Name, "New") || !strings.HasSuffix(fn.Name.Name, "Cataloger") {
|
||||
continue
|
||||
}
|
||||
|
||||
pkg := node.Name.Name
|
||||
constructorsPerPackage[pkg] = append(constructorsPerPackage[pkg], fn.Name.Name)
|
||||
}
|
||||
|
||||
return constructorsPerPackage
|
||||
}
|
||||
|
||||
func assertAllPackageCatalogersRepresented(t *testing.T, constructorsPerPackage map[string]*strset.Set) {
|
||||
t.Helper()
|
||||
|
||||
contents, err := os.ReadFile(filepath.Join(repoRoot(t), "internal", "task", "package_tasks.go"))
|
||||
require.NoError(t, err)
|
||||
|
||||
// ensure all packages (keys) are represented in the package_tasks.go file
|
||||
for pkg, constructors := range constructorsPerPackage {
|
||||
if !assert.True(t, bytes.Contains(contents, []byte(pkg)), "missing package %q", pkg) {
|
||||
continue
|
||||
}
|
||||
for _, constructor := range constructors.List() {
|
||||
assert.True(t, bytes.Contains(contents, []byte(constructor)), "missing constructor %q for package %q", constructor, pkg)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func repoRoot(t testing.TB) string {
|
||||
t.Helper()
|
||||
root, err := exec.Command("git", "rev-parse", "--show-toplevel").Output()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to find repo root dir: %+v", err)
|
||||
}
|
||||
absRepoRoot, err := filepath.Abs(strings.TrimSpace(string(root)))
|
||||
if err != nil {
|
||||
t.Fatal("unable to get abs path to repo root:", err)
|
||||
}
|
||||
return absRepoRoot
|
||||
}
|
||||
|
||||
func mergeConstructors(constructorsPerPackage map[string]*strset.Set, partialResults map[string][]string) map[string]*strset.Set {
|
||||
for pkg, constructors := range partialResults {
|
||||
if _, ok := constructorsPerPackage[pkg]; !ok {
|
||||
constructorsPerPackage[pkg] = strset.New()
|
||||
}
|
||||
constructorsPerPackage[pkg].Add(constructors...)
|
||||
}
|
||||
|
||||
return constructorsPerPackage
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/scylladb/go-set/strset"
|
||||
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
func TestRelationshipsUnique(t *testing.T) {
|
||||
// This test is to ensure that the relationships are deduplicated in the final SBOM.
|
||||
// It is not a test of the relationships themselves.
|
||||
// This test is a regression test for #syft/2509
|
||||
sbom, _ := catalogFixtureImage(t, "image-pkg-coverage", source.SquashedScope)
|
||||
observedRelationships := strset.New()
|
||||
|
||||
for _, rel := range sbom.Relationships {
|
||||
unique := fmt.Sprintf("%s:%s:%s", rel.From.ID(), rel.To.ID(), rel.Type)
|
||||
if observedRelationships.Has(unique) {
|
||||
t.Errorf("duplicate relationship found: %s", unique)
|
||||
}
|
||||
observedRelationships.Add(unique)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
func TestSbomCataloger(t *testing.T) {
|
||||
assertCount := func(t *testing.T, sbom sbom.SBOM, expectedGoModCatalogerPkgs int, expectedSbomCatalogerPkgs int) {
|
||||
actualSbomPkgs := 0
|
||||
actualGoModPkgs := 0
|
||||
|
||||
for p := range sbom.Artifacts.Packages.Enumerate(pkg.GoModulePkg) {
|
||||
if p.FoundBy == "go-module-file-cataloger" {
|
||||
actualGoModPkgs += 1
|
||||
} else if p.FoundBy == "sbom-cataloger" {
|
||||
actualSbomPkgs += 1
|
||||
}
|
||||
}
|
||||
|
||||
if actualGoModPkgs != expectedGoModCatalogerPkgs {
|
||||
t.Errorf("unexpected number of packages from go mod cataloger: %d != %d", expectedGoModCatalogerPkgs, actualGoModPkgs)
|
||||
}
|
||||
if actualSbomPkgs != expectedSbomCatalogerPkgs {
|
||||
t.Errorf("unexpected number of packages from sbom cataloger: %d != %d", expectedSbomCatalogerPkgs, actualSbomPkgs)
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("default catalogers", func(t *testing.T) {
|
||||
sbom, _ := catalogFixtureImage(t, "image-sbom-cataloger", source.SquashedScope, "+go-module-file-cataloger")
|
||||
|
||||
expectedSbomCatalogerPkgs := 0
|
||||
expectedGoModCatalogerPkgs := 2
|
||||
assertCount(t, sbom, expectedGoModCatalogerPkgs, expectedSbomCatalogerPkgs)
|
||||
})
|
||||
|
||||
// The image contains a go.mod file with 2 dependencies and an spdx json sbom.
|
||||
// The go.mod file contains 2 dependencies, and the sbom includes a go dependency
|
||||
// that overlaps with the go.mod
|
||||
t.Run("with sbom cataloger", func(t *testing.T) {
|
||||
sbom, _ := catalogFixtureImage(t, "image-sbom-cataloger", source.SquashedScope, "+go-module-file-cataloger", "+sbom-cataloger")
|
||||
|
||||
expectedSbomCatalogerPkgs := 1
|
||||
expectedGoModCatalogerPkgs := 2
|
||||
assertCount(t, sbom, expectedGoModCatalogerPkgs, expectedSbomCatalogerPkgs)
|
||||
})
|
||||
}
|
||||
+3
@@ -5,3 +5,6 @@
|
||||
# functionality), committing it seems like an acceptable exception.
|
||||
!image-pkg-coverage/pkgs/java/*.jar
|
||||
!image-pkg-coverage/pkgs/java/*.hpi
|
||||
|
||||
**/go.sum
|
||||
!image-go-bin-arch-coverage/go.sum
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user