mirror of
https://github.com/anchore/syft.git
synced 2026-10-15 07:37:20 +02:00
Compare commits
63
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 | ||
|
|
3de5e98db1 | ||
|
|
41601e60ca | ||
|
|
0748413d6b | ||
|
|
b0ab75fd89 | ||
|
|
1bffc6ef0c | ||
|
|
f9a862d268 | ||
|
|
aec53bc32d | ||
|
|
75fdb9e623 | ||
|
|
29c9857857 | ||
|
|
d1e4ecba42 | ||
|
|
c209d03fe8 | ||
|
|
fecfb2f939 | ||
|
|
e61aac0b53 | ||
|
|
85cc64f7e3 | ||
|
|
1af68683d0 | ||
|
|
1ca8ee2a8d | ||
|
|
7182f5b519 | ||
|
|
a16a4ad6c9 | ||
|
|
3174a17efb | ||
|
|
bf39456fbc | ||
|
|
c72d295719 | ||
|
|
4c20a74d2f | ||
|
|
7c67df397e | ||
|
|
0e5fb8e01f | ||
|
|
8ea2425c97 |
+2
-2
@@ -43,7 +43,7 @@ tools:
|
||||
|
||||
- name: goreleaser
|
||||
version:
|
||||
want: v1.22.1
|
||||
want: v1.23.0
|
||||
method: github-release
|
||||
with:
|
||||
repo: goreleaser/goreleaser
|
||||
@@ -90,7 +90,7 @@ tools:
|
||||
|
||||
- name: gh
|
||||
version:
|
||||
want: v2.40.1
|
||||
want: v2.42.1
|
||||
method: github-release
|
||||
with:
|
||||
repo: cli/cli
|
||||
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
#!/bin/bash
|
||||
|
||||
# current limit for fixture size
|
||||
size=1000
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "Usage: $0 <directory>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
directory="$1"
|
||||
|
||||
# Remove trailing slash using parameter expansion
|
||||
directory="${directory%/}"
|
||||
|
||||
if [ ! -d "$directory" ]; then
|
||||
echo "Directory not found: $directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
found_large_files=0
|
||||
while IFS= read -r -d '' file; do
|
||||
if [ $(wc -c < "$file") -gt $size ]; then
|
||||
echo "File $file is greater than ${size} bytes."
|
||||
found_large_files=1
|
||||
fi
|
||||
done < <(find "$directory" -type f -print0)
|
||||
|
||||
if [ "$found_large_files" -eq 1 ]; then
|
||||
echo "Script failed: Some files are greater than ${size} bytes."
|
||||
exit 1
|
||||
else
|
||||
echo "All files in $directory and its subdirectories are ${size} bytes or smaller. Check passed."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
uses: ./.github/actions/bootstrap
|
||||
|
||||
- name: Restore base benchmark result
|
||||
uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2
|
||||
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@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.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@012739e5082ff0c22ca6d6ab32e07c36df03c4a4 #v3.22.12
|
||||
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@012739e5082ff0c22ca6d6ab32e07c36df03c4a4 #v3.22.12
|
||||
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@012739e5082ff0c22ca6d6ab32e07c36df03c4a4 #v3.22.12
|
||||
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@5ecf649a417b8ae17dc8383dc32d46c03f2312df #v0.15.1
|
||||
- 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@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2
|
||||
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@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2
|
||||
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@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2
|
||||
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@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2
|
||||
uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 #v4.0.0
|
||||
with:
|
||||
path: syft/pkg/cataloger/binary/test-fixtures/classifiers/dynamic
|
||||
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@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2
|
||||
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@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2
|
||||
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@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2
|
||||
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@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2
|
||||
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@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2
|
||||
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@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2
|
||||
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@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2
|
||||
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@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2
|
||||
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@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2
|
||||
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
|
||||
|
||||
@@ -109,13 +109,13 @@ nix-shell -p syft
|
||||
|
||||
To generate an SBOM for a container image:
|
||||
|
||||
```
|
||||
```bash
|
||||
syft <image>
|
||||
```
|
||||
|
||||
The above output includes only software that is visible in the container (i.e., the squashed representation of the image). To include software from all image layers in the SBOM, regardless of its presence in the final image, provide `--scope all-layers`:
|
||||
|
||||
```
|
||||
```bash
|
||||
syft <image> --scope all-layers
|
||||
```
|
||||
|
||||
@@ -123,7 +123,7 @@ syft <image> --scope all-layers
|
||||
|
||||
Syft can generate an SBOM from a variety of sources:
|
||||
|
||||
```
|
||||
```bash
|
||||
# catalog a container image archive (from the result of `docker image save ...`, `podman save ...`, or `skopeo copy` commands)
|
||||
syft path/to/image.tar
|
||||
|
||||
@@ -155,69 +155,114 @@ If docker is not present, then the Podman daemon is attempted next, followed by
|
||||
|
||||
This default behavior can be overridden with the `default-image-pull-source` configuration option (See [Configuration](https://github.com/anchore/syft#configuration) for more details).
|
||||
|
||||
### Default Cataloger Configuration by scan type
|
||||
|
||||
Syft uses different default sets of catalogers depending on what it is scanning: a container image or a directory on disk. The default catalogers for an image scan assumes that package installation steps have already been completed. For example, Syft will identify Python packages that have egg or wheel metadata files under a site-packages directory, since this indicates software actually installed on an image.
|
||||
### File selection
|
||||
|
||||
However, if you are scanning a directory, Syft doesn't assume that all relevant software is installed, and will use catalogers that can identify declared dependencies that may not yet be installed on the final system: for example, dependencies listed in a Python requirements.txt.
|
||||
By default, Syft will catalog file details and digests for files that are owned by discovered packages. You can change this behavior by using the `SYFT_FILE_METADATA_SELECTION` environment variable or the `file.metadata.selection` configuration option. The options are:
|
||||
|
||||
You can override the list of enabled/disabled catalogers by using the "catalogers" keyword in the [Syft configuration file](https://github.com/anchore/syft#configuration).
|
||||
- `all`: capture all files from the search space
|
||||
- `owned-by-package`: capture only files owned by packages (default)
|
||||
- `none`: disable capturing any file information
|
||||
|
||||
##### Image Scanning:
|
||||
- alpmdb
|
||||
- apkdb
|
||||
- binary
|
||||
- dotnet-deps
|
||||
- dotnet-portable-executable
|
||||
- dpkgdb
|
||||
- go-module-binary
|
||||
- graalvm-native-image
|
||||
- java
|
||||
- javascript-package
|
||||
- linux-kernel
|
||||
- nix-store
|
||||
- php-composer-installed
|
||||
- portage
|
||||
- python-package
|
||||
- rpm-db
|
||||
- ruby-gemspec
|
||||
- sbom
|
||||
|
||||
##### Directory Scanning:
|
||||
- alpmdb
|
||||
- apkdb
|
||||
- binary
|
||||
- cocoapods
|
||||
- conan
|
||||
- dartlang-lock
|
||||
- dotnet-deps
|
||||
- dotnet-portable-executable
|
||||
- dpkgdb
|
||||
- elixir-mix-lock
|
||||
- erlang-rebar-lock
|
||||
- go-mod-file
|
||||
- go-module-binary
|
||||
- graalvm-native-image
|
||||
- haskell
|
||||
- java
|
||||
- java-gradle-lockfile
|
||||
- java-pom
|
||||
- javascript-lock
|
||||
- linux-kernel
|
||||
- nix-store
|
||||
- php-composer-lock
|
||||
- portage
|
||||
- python-index
|
||||
- python-package
|
||||
- rpm-db
|
||||
- rpm-file
|
||||
- ruby-gemfile
|
||||
- rust-cargo-lock
|
||||
- sbom
|
||||
- swift-package-manager
|
||||
### Package cataloger selection
|
||||
|
||||
#### Concepts
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Syft uses a different set of catalogers by default when scanning files directly than it does when scanning images
|
||||
|
||||
The catalogers for an image scan assumes that package installation steps have already been completed. For example, Syft will identify Python packages that have egg or wheel metadata files under a `site-packages` directory, since this is how the canonical tooling `pip` installs python packages.
|
||||
|
||||
The catalogers for a directory scan will look for installed software as well as declared dependencies that are not necessarily installed. For example, dependencies listed in a Python `requirements.txt`.
|
||||
|
||||
This default set of catalogers being dynamic is critical as this allows Syft to be used in a variety of contexts while still generating accurate SBOMs.
|
||||
Overriding the set of default catalogers is not recommended for most purposes, however, is possible if needed.
|
||||
|
||||
Catalogers can be referenced in two different ways:
|
||||
- *by name*: the exact cataloger name (e.g. `java-pom-cataloger` or `java-archive-cataloger`)
|
||||
- *by tag*: a tag that is associated with a cataloger (e.g. `java`)
|
||||
|
||||
Syft can take lists of references on the CLI or in the application configuration to define which catalogers to use.
|
||||
|
||||
You can **set** the list of catalogers explicitly to use with the `--override-default-catalogers` CLI flag, accepting a comma-separated list of cataloger names or tags.
|
||||
|
||||
You can also **add** to, **remove** from, or **sub-select** catalogers to use within the default set of catalogers by using the `--select-catalogers` CLI flag.
|
||||
- To **sub-select** catalogers simply provide a tag (e.g. `--select-catalogers TAG`). Catalogers will always be selected from the default set of catalogers (e.g. `--select-catalogers java,go` will select all the `java` catalogers in the default set and all the `go` catalogers in the default set).
|
||||
- To **add** a cataloger prefix the cataloger name with `+` (e.g. `--select-catalogers +NAME`). Added catalogers will _always be added_ regardless of removals, filtering, or other defaults.
|
||||
- To **remove** a cataloger prefix the cataloger name or tag with `-` (e.g. `--select-catalogers -NAME_OR_TAG`). Catalogers are removed from the set of default catalogers after processing any sub-selections.
|
||||
|
||||
These rules and the dynamic default cataloger sets approximates to the following logic:
|
||||
|
||||
```
|
||||
image_catalogers = all_catalogers AND catalogers_tagged("image")
|
||||
|
||||
directory_catalogers = all_catalogers AND catalogers_tagged("directory")
|
||||
|
||||
default_catalogers = image_catalogers OR directory_catalogers
|
||||
|
||||
sub_selected_catalogers = default_catalogers INTERSECT catalogers_tagged(TAG) [ UNION sub_selected_catalogers ... ]
|
||||
|
||||
base_catalogers = default_catalogers OR sub_selected_catalogers
|
||||
|
||||
final_set = (base_catalogers SUBTRACT removed_catalogers) UNION added_catalogers
|
||||
```
|
||||
|
||||
|
||||
#### Examples
|
||||
|
||||
Only scan for python related packages with catalogers appropriate for the source type (image or directory):
|
||||
```bash
|
||||
syft <some container image> --select-catalogers "python"
|
||||
# results in the following catalogers being used:
|
||||
# - python-installed-package-cataloger
|
||||
```
|
||||
|
||||
Same command, but the set of catalogers changes based on what is being analyzed (in this case a directory):
|
||||
```bash
|
||||
syft <a directory> --select-catalogers "python"
|
||||
# results in the following catalogers being used:
|
||||
# - python-installed-package-cataloger
|
||||
# - python-package-cataloger
|
||||
```
|
||||
|
||||
Use the default set of catalogers and add a cataloger to the set:
|
||||
```bash
|
||||
syft ... --catalogers "+sbom-cataloger"
|
||||
```
|
||||
|
||||
Use the default set of catalogers but remove any catalogers that deal with RPMs:
|
||||
```bash
|
||||
syft ... --catalogers "-rpm"
|
||||
```
|
||||
|
||||
Only scan with catalogers that:
|
||||
- are tagged with "go"
|
||||
- always use the sbom-cataloger
|
||||
- are appropriate for the source type (image or directory)
|
||||
|
||||
```bash
|
||||
syft <some container image> --select-catalogers "go,+sbom-cataloger"
|
||||
# results in the following catalogers being used:
|
||||
# - go-module-binary-cataloger
|
||||
# - sbom-cataloger
|
||||
```
|
||||
|
||||
Scan with all catalogers that deal with binary analysis, regardless of the source type:
|
||||
```bash
|
||||
syft ... --override-default-catalogers "binary"
|
||||
# results in the following catalogers being used:
|
||||
# - binary-cataloger
|
||||
# - cargo-auditable-binary-cataloger
|
||||
# - dotnet-portable-executable-cataloger
|
||||
# - go-module-binary-cataloger
|
||||
```
|
||||
|
||||
Only scan with the specific `go-module-binary-cataloger` and `go-module-file-cataloger` catalogers:
|
||||
```bash
|
||||
syft ... --override-default-catalogers "go-module-binary-cataloger,go-module-file-cataloger"
|
||||
```
|
||||
|
||||
##### Non Default:
|
||||
- cargo-auditable-binary
|
||||
|
||||
### Excluding file paths
|
||||
|
||||
@@ -457,87 +502,60 @@ 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
|
||||
|
||||
# same as --file; write output report to a file (default is to write to stdout)
|
||||
file: ""
|
||||
|
||||
# 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
|
||||
|
||||
# allows users to specify which image source should be used to generate the sbom
|
||||
# valid values are: registry, docker, podman
|
||||
default-image-pull-source: ""
|
||||
# maximum number of workers used to process the list of package catalogers in parallel
|
||||
parallelism: 1
|
||||
|
||||
# a list of globs to exclude from scanning. same as --exclude ; for example:
|
||||
# a list of globs to exclude from scanning, for example:
|
||||
# exclude:
|
||||
# - "/etc/**"
|
||||
# - "./out/**/*.json"
|
||||
# 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)
|
||||
# SYFT_SCOPE env var
|
||||
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
|
||||
@@ -545,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:
|
||||
@@ -554,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
|
||||
@@ -576,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
|
||||
@@ -583,12 +612,41 @@ 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
|
||||
|
||||
|
||||
file:
|
||||
|
||||
metadata:
|
||||
# select which files should be captured by the file-metadata cataloger and included in the SBOM.
|
||||
# Options include:
|
||||
# - "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-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"
|
||||
- "sha1"
|
||||
|
||||
# capture the contents of select files in the SBOM
|
||||
content:
|
||||
# skip searching a file entirely if it is above the given size (default = 1MB; unit = bytes)
|
||||
# SYFT_FILE_CONTENT_SKIP_FILES_ABOVE_SIZE env var
|
||||
skip-files-above-size: 1048576
|
||||
|
||||
# file globs for the cataloger to match on
|
||||
# SYFT_FILE_CONTENT_GLOBS env var
|
||||
globs: []
|
||||
|
||||
|
||||
# cataloging packages is exposed through the packages and power-user subcommands
|
||||
package:
|
||||
|
||||
|
||||
# search within archives that do contain a file index to search against (zip)
|
||||
# note: for now this only applies to the java package cataloger
|
||||
# SYFT_PACKAGE_SEARCH_INDEXED_ARCHIVES env var
|
||||
@@ -600,14 +658,11 @@ package:
|
||||
# SYFT_PACKAGE_SEARCH_UNINDEXED_ARCHIVES env var
|
||||
search-unindexed-archives: false
|
||||
|
||||
cataloger:
|
||||
# enable/disable cataloging of packages
|
||||
# SYFT_PACKAGE_CATALOGER_ENABLED env var
|
||||
enabled: true
|
||||
# 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
|
||||
|
||||
# the search space to look for packages (options: all-layers, squashed)
|
||||
# same as -s ; SYFT_PACKAGE_CATALOGER_SCOPE env var
|
||||
scope: "squashed"
|
||||
|
||||
golang:
|
||||
# search for go package licences in the GOPATH of the system running Syft, note that this is outside the
|
||||
@@ -657,58 +712,38 @@ python:
|
||||
guess-unpinned-requirements: false
|
||||
|
||||
javascript:
|
||||
search-remote-licenses: false
|
||||
npm-base-url: "https://registry.npmjs.org"
|
||||
search-remote-licenses: false
|
||||
npm-base-url: "https://registry.npmjs.org"
|
||||
|
||||
file-contents:
|
||||
cataloger:
|
||||
# enable/disable cataloging of file contents
|
||||
# SYFT_FILE_CONTENTS_CATALOGER_ENABLED env var
|
||||
enabled: true
|
||||
|
||||
# the search space to look for file contents (options: all-layers, squashed)
|
||||
# SYFT_FILE_CONTENTS_CATALOGER_SCOPE env var
|
||||
scope: "squashed"
|
||||
|
||||
# skip searching a file entirely if it is above the given size (default = 1MB; unit = bytes)
|
||||
# SYFT_FILE_CONTENTS_SKIP_FILES_ABOVE_SIZE env var
|
||||
skip-files-above-size: 1048576
|
||||
|
||||
# file globs for the cataloger to match on
|
||||
# SYFT_FILE_CONTENTS_GLOBS env var
|
||||
globs: []
|
||||
|
||||
file-metadata:
|
||||
cataloger:
|
||||
# enable/disable cataloging of file metadata
|
||||
# SYFT_FILE_METADATA_CATALOGER_ENABLED env var
|
||||
enabled: true
|
||||
|
||||
# the search space to look for file metadata (options: all-layers, squashed)
|
||||
# SYFT_FILE_METADATA_CATALOGER_SCOPE env var
|
||||
scope: "squashed"
|
||||
|
||||
# the file digest algorithms to use when cataloging files (options: "md5", "sha1", "sha224", "sha256", "sha384", "sha512")
|
||||
# SYFT_FILE_METADATA_DIGESTS env var
|
||||
digests: ["sha256"]
|
||||
|
||||
# maximum number of workers used to process the list of package catalogers in parallel
|
||||
parallelism: 1
|
||||
|
||||
# options that apply to all scan sources
|
||||
# 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:
|
||||
|
||||
# allows users to specify which image source should be used to generate the sbom
|
||||
# valid values are: registry, docker, podman
|
||||
# SYFT_SOURCE_IMAGE_DEFAULT_PULL_SOURCE env var
|
||||
default-pull-source: ""
|
||||
|
||||
|
||||
# options when pulling directly from a registry via the "registry:" or "containerd:" scheme
|
||||
registry:
|
||||
@@ -762,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: ""
|
||||
```
|
||||
|
||||
|
||||
+11
-5
@@ -54,6 +54,7 @@ tasks:
|
||||
- task: check-licenses
|
||||
- task: lint
|
||||
- task: check-json-schema-drift
|
||||
- task: check-binary-fixture-size
|
||||
|
||||
test:
|
||||
desc: Run all levels of test
|
||||
@@ -169,6 +170,11 @@ tasks:
|
||||
cmds:
|
||||
- .github/scripts/json-schema-drift-check.sh
|
||||
|
||||
check-binary-fixture-size:
|
||||
desc: Ensure that the binary test fixtures are not too large
|
||||
cmds:
|
||||
- .github/scripts/check_binary_fixture_size.sh syft/pkg/cataloger/binary/test-fixtures/classifiers/snippets
|
||||
|
||||
|
||||
## Testing tasks #################################
|
||||
|
||||
@@ -179,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
|
||||
@@ -191,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"
|
||||
|
||||
@@ -259,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
|
||||
@@ -269,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
|
||||
@@ -280,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"
|
||||
|
||||
+11
-7
@@ -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"
|
||||
@@ -77,22 +77,26 @@ func create(id clio.Identification, out io.Writer) (clio.Application, *cobra.Com
|
||||
|
||||
// since root is aliased as the packages cmd we need to construct this command first
|
||||
// we also need the command to have information about the `root` options because of this alias
|
||||
packagesCmd := commands.Packages(app)
|
||||
scanCmd := commands.Scan(app)
|
||||
|
||||
// rootCmd is currently an alias for the packages command
|
||||
rootCmd := commands.Root(app, packagesCmd)
|
||||
// root is currently an alias for the scan command
|
||||
rootCmd := commands.Root(app, scanCmd)
|
||||
|
||||
// add sub-commands
|
||||
rootCmd.AddCommand(
|
||||
packagesCmd,
|
||||
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),
|
||||
cranecmd.NewCmdAuthLogin(id.Name), // syft login uses the same command as crane
|
||||
)
|
||||
|
||||
// explicitly set Cobra output to the real stdout to write things like errors and help
|
||||
rootCmd.SetOut(out)
|
||||
// note: we would direct cobra to use our writer explicitly with rootCmd.SetOut(out) , however this causes
|
||||
// deprecation warnings to be shown to stdout via the writer instead of stderr. This is unfortunate since this
|
||||
// does not appear to be the correct behavior on cobra's part https://github.com/spf13/cobra/issues/1708 .
|
||||
// In the future this functionality should be restored.
|
||||
|
||||
return app, rootCmd
|
||||
}
|
||||
|
||||
@@ -1,255 +0,0 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
"github.com/anchore/syft/cmd/syft/cli/eventloop"
|
||||
"github.com/anchore/syft/cmd/syft/cli/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/file"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
const (
|
||||
packagesExample = ` {{.appName}} {{.command}} alpine:latest a summary of discovered packages
|
||||
{{.appName}} {{.command}} alpine:latest -o json show all possible cataloging details
|
||||
{{.appName}} {{.command}} alpine:latest -o cyclonedx show a CycloneDX formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -o cyclonedx-json show a CycloneDX JSON formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -o spdx show a SPDX 2.3 Tag-Value formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -o spdx@2.2 show a SPDX 2.2 Tag-Value formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -o spdx-json show a SPDX 2.3 JSON formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -o spdx-json@2.2 show a SPDX 2.2 JSON formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -vv show verbose debug information
|
||||
{{.appName}} {{.command}} alpine:latest -o template -t my_format.tmpl show a SBOM formatted according to given template file
|
||||
|
||||
Supports the following image sources:
|
||||
{{.appName}} {{.command}} yourrepo/yourimage:tag defaults to using images from a Docker daemon. If Docker is not present, the image is pulled directly from the registry.
|
||||
{{.appName}} {{.command}} path/to/a/file/or/dir a Docker tar, OCI tar, OCI directory, SIF container, or generic filesystem directory
|
||||
`
|
||||
|
||||
schemeHelpHeader = "You can also explicitly specify the scheme to use:"
|
||||
imageSchemeHelp = ` {{.appName}} {{.command}} docker:yourrepo/yourimage:tag explicitly use the Docker daemon
|
||||
{{.appName}} {{.command}} podman:yourrepo/yourimage:tag explicitly use the Podman daemon
|
||||
{{.appName}} {{.command}} registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required)
|
||||
{{.appName}} {{.command}} docker-archive:path/to/yourimage.tar use a tarball from disk for archives created from "docker save"
|
||||
{{.appName}} {{.command}} oci-archive:path/to/yourimage.tar use a tarball from disk for OCI archives (from Skopeo or otherwise)
|
||||
{{.appName}} {{.command}} oci-dir:path/to/yourimage read directly from a path on disk for OCI layout directories (from Skopeo or otherwise)
|
||||
{{.appName}} {{.command}} singularity:path/to/yourimage.sif read directly from a Singularity Image Format (SIF) container on disk
|
||||
`
|
||||
nonImageSchemeHelp = ` {{.appName}} {{.command}} dir:path/to/yourproject read directly from a path on disk (any directory)
|
||||
{{.appName}} {{.command}} file:path/to/yourproject/file read directly from a path on disk (any single file)
|
||||
`
|
||||
packagesSchemeHelp = "\n " + schemeHelpHeader + "\n" + imageSchemeHelp + nonImageSchemeHelp
|
||||
|
||||
packagesHelp = packagesExample + packagesSchemeHelp
|
||||
)
|
||||
|
||||
type packagesOptions struct {
|
||||
options.Config `yaml:",inline" mapstructure:",squash"`
|
||||
options.Output `yaml:",inline" mapstructure:",squash"`
|
||||
options.UpdateCheck `yaml:",inline" mapstructure:",squash"`
|
||||
options.Catalog `yaml:",inline" mapstructure:",squash"`
|
||||
}
|
||||
|
||||
func defaultPackagesOptions() *packagesOptions {
|
||||
return &packagesOptions{
|
||||
Output: options.DefaultOutput(),
|
||||
UpdateCheck: options.DefaultUpdateCheck(),
|
||||
Catalog: options.DefaultCatalog(),
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:dupl
|
||||
func Packages(app clio.Application) *cobra.Command {
|
||||
id := app.ID()
|
||||
|
||||
opts := defaultPackagesOptions()
|
||||
|
||||
return app.SetupCommand(&cobra.Command{
|
||||
Use: "packages [SOURCE]",
|
||||
Short: "Generate a package SBOM",
|
||||
Long: "Generate a packaged-based Software Bill Of Materials (SBOM) from container images and filesystems",
|
||||
Example: internal.Tprintf(packagesHelp, map[string]interface{}{
|
||||
"appName": id.Name,
|
||||
"command": "packages",
|
||||
}),
|
||||
Args: validatePackagesArgs,
|
||||
PreRunE: applicationUpdateCheck(id, &opts.UpdateCheck),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
restoreStdout := ui.CaptureStdoutToTraceLog()
|
||||
defer restoreStdout()
|
||||
|
||||
return runPackages(id, opts, args[0])
|
||||
},
|
||||
}, opts)
|
||||
}
|
||||
|
||||
func validatePackagesArgs(cmd *cobra.Command, args []string) error {
|
||||
return validateArgs(cmd, args, "an image/directory argument is required")
|
||||
}
|
||||
|
||||
func validateArgs(cmd *cobra.Command, args []string, error string) error {
|
||||
if len(args) == 0 {
|
||||
// in the case that no arguments are given we want to show the help text and return with a non-0 return code.
|
||||
if err := cmd.Help(); err != nil {
|
||||
return fmt.Errorf("unable to display help: %w", err)
|
||||
}
|
||||
return fmt.Errorf(error)
|
||||
}
|
||||
|
||||
return cobra.MaximumNArgs(1)(cmd, args)
|
||||
}
|
||||
|
||||
// nolint:funlen
|
||||
func runPackages(id clio.Identification, opts *packagesOptions, userInput string) error {
|
||||
writer, err := opts.SBOMWriter()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
src, err := getSource(&opts.Catalog, userInput)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if src != nil {
|
||||
if err := src.Close(); err != nil {
|
||||
log.Tracef("unable to close source: %+v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
s, err := generateSBOM(id, src, &opts.Catalog)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s == nil {
|
||||
return fmt.Errorf("no SBOM produced for %q", userInput)
|
||||
}
|
||||
|
||||
if err := writer.Write(*s); err != nil {
|
||||
return fmt.Errorf("failed to write SBOM: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getSource(opts *options.Catalog, userInput string, filters ...func(*source.Detection) error) (source.Source, error) {
|
||||
detection, err := source.Detect(
|
||||
userInput,
|
||||
source.DetectConfig{
|
||||
DefaultImageSource: opts.DefaultImagePullSource,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not deteremine source: %w", err)
|
||||
}
|
||||
|
||||
for _, filter := range filters {
|
||||
if err := filter(detection); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var platform *image.Platform
|
||||
|
||||
if opts.Platform != "" {
|
||||
platform, err = image.NewPlatform(opts.Platform)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid platform: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
hashers, err := file.Hashers(opts.Source.File.Digests...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid hash: %w", err)
|
||||
}
|
||||
|
||||
src, err := detection.NewSource(
|
||||
source.DetectionSourceConfig{
|
||||
Alias: source.Alias{
|
||||
Name: opts.Source.Name,
|
||||
Version: opts.Source.Version,
|
||||
},
|
||||
RegistryOptions: opts.Registry.ToOptions(),
|
||||
Platform: platform,
|
||||
Exclude: source.ExcludeConfig{
|
||||
Paths: opts.Exclusions,
|
||||
},
|
||||
DigestAlgorithms: hashers,
|
||||
BasePath: opts.BasePath,
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
if userInput == "power-user" {
|
||||
bus.Notify("Note: the 'power-user' command has been removed.")
|
||||
}
|
||||
return nil, fmt.Errorf("failed to construct source from user input %q: %w", userInput, err)
|
||||
}
|
||||
|
||||
return src, nil
|
||||
}
|
||||
|
||||
func generateSBOM(id clio.Identification, src source.Source, opts *options.Catalog) (*sbom.SBOM, error) {
|
||||
tasks, err := eventloop.Tasks(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s := sbom.SBOM{
|
||||
Source: src.Describe(),
|
||||
Descriptor: sbom.Descriptor{
|
||||
Name: id.Name,
|
||||
Version: id.Version,
|
||||
Configuration: opts,
|
||||
},
|
||||
}
|
||||
|
||||
err = buildRelationships(&s, src, tasks)
|
||||
|
||||
return &s, err
|
||||
}
|
||||
|
||||
func buildRelationships(s *sbom.SBOM, src source.Source, tasks []eventloop.Task) error {
|
||||
var errs error
|
||||
|
||||
var relationships []<-chan artifact.Relationship
|
||||
for _, task := range tasks {
|
||||
c := make(chan artifact.Relationship)
|
||||
relationships = append(relationships, c)
|
||||
go func(task eventloop.Task) {
|
||||
err := eventloop.RunTask(task, &s.Artifacts, src, c)
|
||||
if err != nil {
|
||||
errs = multierror.Append(errs, err)
|
||||
}
|
||||
}(task)
|
||||
}
|
||||
|
||||
s.Relationships = append(s.Relationships, mergeRelationships(relationships...)...)
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func mergeRelationships(cs ...<-chan artifact.Relationship) (relationships []artifact.Relationship) {
|
||||
for _, c := range cs {
|
||||
for n := range c {
|
||||
relationships = append(relationships, n)
|
||||
}
|
||||
}
|
||||
|
||||
return relationships
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
package eventloop
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/cmd/syft/cli/options"
|
||||
"github.com/anchore/syft/internal/file"
|
||||
"github.com/anchore/syft/syft"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/file/cataloger/filecontent"
|
||||
"github.com/anchore/syft/syft/file/cataloger/filedigest"
|
||||
"github.com/anchore/syft/syft/file/cataloger/filemetadata"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
type Task func(*sbom.Artifacts, source.Source) ([]artifact.Relationship, error)
|
||||
|
||||
func Tasks(opts *options.Catalog) ([]Task, error) {
|
||||
var tasks []Task
|
||||
|
||||
generators := []func(opts *options.Catalog) (Task, error){
|
||||
generateCatalogPackagesTask,
|
||||
generateCatalogFileMetadataTask,
|
||||
generateCatalogFileDigestsTask,
|
||||
generateCatalogContentsTask,
|
||||
}
|
||||
|
||||
for _, generator := range generators {
|
||||
task, err := generator(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if task != nil {
|
||||
tasks = append(tasks, task)
|
||||
}
|
||||
}
|
||||
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
func generateCatalogPackagesTask(opts *options.Catalog) (Task, error) {
|
||||
if !opts.Package.Cataloger.Enabled {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
task := func(results *sbom.Artifacts, src source.Source) ([]artifact.Relationship, error) {
|
||||
packageCatalog, relationships, theDistro, err := syft.CatalogPackages(src, opts.ToCatalogerConfig())
|
||||
|
||||
results.Packages = packageCatalog
|
||||
results.LinuxDistribution = theDistro
|
||||
|
||||
return relationships, err
|
||||
}
|
||||
|
||||
return task, nil
|
||||
}
|
||||
|
||||
func generateCatalogFileMetadataTask(opts *options.Catalog) (Task, error) {
|
||||
if !opts.FileMetadata.Cataloger.Enabled {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
metadataCataloger := filemetadata.NewCataloger()
|
||||
|
||||
task := func(results *sbom.Artifacts, src source.Source) ([]artifact.Relationship, error) {
|
||||
resolver, err := src.FileResolver(opts.FileMetadata.Cataloger.GetScope())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, err := metadataCataloger.Catalog(resolver)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results.FileMetadata = result
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return task, nil
|
||||
}
|
||||
|
||||
func generateCatalogFileDigestsTask(opts *options.Catalog) (Task, error) {
|
||||
if !opts.FileMetadata.Cataloger.Enabled {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
hashes, err := file.Hashers(opts.FileMetadata.Digests...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
digestsCataloger := filedigest.NewCataloger(hashes)
|
||||
|
||||
task := func(results *sbom.Artifacts, src source.Source) ([]artifact.Relationship, error) {
|
||||
resolver, err := src.FileResolver(opts.FileMetadata.Cataloger.GetScope())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, err := digestsCataloger.Catalog(resolver)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results.FileDigests = result
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return task, nil
|
||||
}
|
||||
|
||||
func generateCatalogContentsTask(opts *options.Catalog) (Task, error) {
|
||||
if !opts.FileContents.Cataloger.Enabled {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
contentsCataloger, err := filecontent.NewCataloger(opts.FileContents.Globs, opts.FileContents.SkipFilesAboveSize) //nolint:staticcheck
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
task := func(results *sbom.Artifacts, src source.Source) ([]artifact.Relationship, error) {
|
||||
resolver, err := src.FileResolver(opts.FileContents.Cataloger.GetScope())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, err := contentsCataloger.Catalog(resolver)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results.FileContents = result
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return task, nil
|
||||
}
|
||||
|
||||
func RunTask(t Task, a *sbom.Artifacts, src source.Source, c chan<- artifact.Relationship) error {
|
||||
defer close(c)
|
||||
|
||||
relationships, err := t(a, src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, relationship := range relationships {
|
||||
c <- relationship
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/iancoleman/strcase"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/scylladb/go-set/strset"
|
||||
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/fangs"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/cataloging"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger"
|
||||
golangCataloger "github.com/anchore/syft/syft/pkg/cataloger/golang"
|
||||
javaCataloger "github.com/anchore/syft/syft/pkg/cataloger/java"
|
||||
javascriptCataloger "github.com/anchore/syft/syft/pkg/cataloger/javascript"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/kernel"
|
||||
pythonCataloger "github.com/anchore/syft/syft/pkg/cataloger/python"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
type Catalog struct {
|
||||
Catalogers []string `yaml:"catalogers" json:"catalogers" mapstructure:"catalogers"`
|
||||
Package pkg `yaml:"package" json:"package" mapstructure:"package"`
|
||||
Golang golang `yaml:"golang" json:"golang" mapstructure:"golang"`
|
||||
Java java `yaml:"java" json:"java" mapstructure:"java"`
|
||||
Javascript javascript `yaml:"javascript" json:"javascript" mapstructure:"javascript"`
|
||||
LinuxKernel linuxKernel `yaml:"linux-kernel" json:"linux-kernel" mapstructure:"linux-kernel"`
|
||||
Python python `yaml:"python" json:"python" mapstructure:"python"`
|
||||
FileMetadata fileMetadata `yaml:"file-metadata" json:"file-metadata" mapstructure:"file-metadata"`
|
||||
FileContents fileContents `yaml:"file-contents" json:"file-contents" mapstructure:"file-contents"`
|
||||
Registry registry `yaml:"registry" json:"registry" mapstructure:"registry"`
|
||||
Exclusions []string `yaml:"exclude" json:"exclude" mapstructure:"exclude"`
|
||||
Platform string `yaml:"platform" json:"platform" mapstructure:"platform"`
|
||||
Name string `yaml:"name" json:"name" mapstructure:"name"`
|
||||
Source sourceCfg `yaml:"source" json:"source" mapstructure:"source"`
|
||||
Parallelism int `yaml:"parallelism" json:"parallelism" mapstructure:"parallelism"` // the number of catalog workers to run in parallel
|
||||
DefaultImagePullSource string `yaml:"default-image-pull-source" json:"default-image-pull-source" mapstructure:"default-image-pull-source"` // specify default image pull source
|
||||
BasePath string `yaml:"base-path" json:"base-path" mapstructure:"base-path"` // specify base path for all file paths
|
||||
ExcludeBinaryOverlapByOwnership bool `yaml:"exclude-binary-overlap-by-ownership" json:"exclude-binary-overlap-by-ownership" mapstructure:"exclude-binary-overlap-by-ownership"` // exclude synthetic binary packages owned by os package files
|
||||
}
|
||||
|
||||
var _ interface {
|
||||
clio.FlagAdder
|
||||
clio.PostLoader
|
||||
} = (*Catalog)(nil)
|
||||
|
||||
func DefaultCatalog() Catalog {
|
||||
return Catalog{
|
||||
Package: defaultPkg(),
|
||||
LinuxKernel: defaultLinuxKernel(),
|
||||
FileMetadata: defaultFileMetadata(),
|
||||
FileContents: defaultFileContents(),
|
||||
Source: defaultSourceCfg(),
|
||||
Parallelism: 1,
|
||||
ExcludeBinaryOverlapByOwnership: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (cfg *Catalog) AddFlags(flags clio.FlagSet) {
|
||||
var validScopeValues []string
|
||||
for _, scope := range source.AllScopes {
|
||||
validScopeValues = append(validScopeValues, strcase.ToDelimited(string(scope), '-'))
|
||||
}
|
||||
flags.StringVarP(&cfg.Package.Cataloger.Scope, "scope", "s",
|
||||
fmt.Sprintf("selection of layers to catalog, options=%v", validScopeValues))
|
||||
|
||||
flags.StringVarP(&cfg.Platform, "platform", "",
|
||||
"an optional platform specifier for container image sources (e.g. 'linux/arm64', 'linux/arm64/v8', 'arm64', 'linux')")
|
||||
|
||||
flags.StringArrayVarP(&cfg.Exclusions, "exclude", "",
|
||||
"exclude paths from being scanned using a glob expression")
|
||||
|
||||
flags.StringArrayVarP(&cfg.Catalogers, "catalogers", "",
|
||||
"enable one or more package catalogers")
|
||||
|
||||
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")
|
||||
|
||||
flags.StringVarP(&cfg.Source.Version, "source-version", "",
|
||||
"set the version of the target being analyzed")
|
||||
|
||||
flags.StringVarP(&cfg.BasePath, "base-path", "",
|
||||
"base directory for scanning, no links will be followed above this directory, and all paths will be reported relative to this directory")
|
||||
}
|
||||
|
||||
func (cfg *Catalog) PostLoad() error {
|
||||
// parse options on this struct
|
||||
var catalogers []string
|
||||
for _, c := range cfg.Catalogers {
|
||||
for _, f := range strings.Split(c, ",") {
|
||||
catalogers = append(catalogers, strings.TrimSpace(f))
|
||||
}
|
||||
}
|
||||
sort.Strings(catalogers)
|
||||
cfg.Catalogers = catalogers
|
||||
|
||||
if err := checkDefaultSourceValues(cfg.DefaultImagePullSource); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg Catalog) ToCatalogerConfig() cataloger.Config {
|
||||
return cataloger.Config{
|
||||
Search: cataloger.SearchConfig{
|
||||
IncludeIndexedArchives: cfg.Package.SearchIndexedArchives,
|
||||
IncludeUnindexedArchives: cfg.Package.SearchUnindexedArchives,
|
||||
Scope: cfg.Package.Cataloger.GetScope(),
|
||||
},
|
||||
Catalogers: cfg.Catalogers,
|
||||
Parallelism: cfg.Parallelism,
|
||||
Golang: golangCataloger.DefaultCatalogerConfig().
|
||||
WithSearchLocalModCacheLicenses(cfg.Golang.SearchLocalModCacheLicenses).
|
||||
WithLocalModCacheDir(cfg.Golang.LocalModCacheDir).
|
||||
WithSearchRemoteLicenses(cfg.Golang.SearchRemoteLicenses).
|
||||
WithProxy(cfg.Golang.Proxy).
|
||||
WithNoProxy(cfg.Golang.NoProxy),
|
||||
LinuxKernel: kernel.LinuxKernelCatalogerConfig{
|
||||
CatalogModules: cfg.LinuxKernel.CatalogModules,
|
||||
},
|
||||
Java: javaCataloger.DefaultArchiveCatalogerConfig().
|
||||
WithUseNetwork(cfg.Java.UseNetwork).
|
||||
WithMavenBaseURL(cfg.Java.MavenURL).
|
||||
WithArchiveTraversal(
|
||||
cataloging.ArchiveSearchConfig{
|
||||
IncludeIndexedArchives: cfg.Package.SearchIndexedArchives,
|
||||
IncludeUnindexedArchives: cfg.Package.SearchUnindexedArchives,
|
||||
},
|
||||
cfg.Java.MaxParentRecursiveDepth),
|
||||
Javascript: javascriptCataloger.DefaultCatalogerConfig().
|
||||
WithSearchRemoteLicenses(cfg.Javascript.SearchRemoteLicenses).
|
||||
WithNpmBaseURL(cfg.Javascript.NpmBaseURL),
|
||||
Python: pythonCataloger.CatalogerConfig{
|
||||
GuessUnpinnedRequirements: cfg.Python.GuessUnpinnedRequirements,
|
||||
},
|
||||
ExcludeBinaryOverlapByOwnership: cfg.ExcludeBinaryOverlapByOwnership,
|
||||
}
|
||||
}
|
||||
|
||||
var validDefaultSourceValues = []string{"registry", "docker", "podman", ""}
|
||||
|
||||
func checkDefaultSourceValues(source string) error {
|
||||
validValues := strset.New(validDefaultSourceValues...)
|
||||
if !validValues.Has(source) {
|
||||
validValuesString := strings.Join(validDefaultSourceValues, ", ")
|
||||
return fmt.Errorf("%s is not a valid default source; please use one of the following: %s''", source, validValuesString)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func expandFilePath(file string) (string, error) {
|
||||
if file != "" {
|
||||
expandedPath, err := homedir.Expand(file)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to expand file path=%q: %w", file, err)
|
||||
}
|
||||
file = expandedPath
|
||||
}
|
||||
return file, nil
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package options
|
||||
|
||||
// Config holds a reference to the specific config file that was used to load application configuration
|
||||
type Config struct {
|
||||
ConfigFile string `yaml:"config" json:"config" mapstructure:"config"`
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/internal/file"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
type fileContents struct {
|
||||
Cataloger scope `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"`
|
||||
SkipFilesAboveSize int64 `yaml:"skip-files-above-size" json:"skip-files-above-size" mapstructure:"skip-files-above-size"`
|
||||
Globs []string `yaml:"globs" json:"globs" mapstructure:"globs"`
|
||||
}
|
||||
|
||||
func defaultFileContents() fileContents {
|
||||
return fileContents{
|
||||
Cataloger: scope{
|
||||
Scope: source.SquashedScope.String(),
|
||||
},
|
||||
SkipFilesAboveSize: 1 * file.MB,
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
type fileMetadata struct {
|
||||
Cataloger scope `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"`
|
||||
Digests []string `yaml:"digests" json:"digests" mapstructure:"digests"`
|
||||
}
|
||||
|
||||
func defaultFileMetadata() fileMetadata {
|
||||
return fileMetadata{
|
||||
Cataloger: scope{
|
||||
Scope: source.SquashedScope.String(),
|
||||
},
|
||||
Digests: []string{"sha256"},
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/syft/pkg/cataloger"
|
||||
)
|
||||
|
||||
type pkg struct {
|
||||
Cataloger scope `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"`
|
||||
SearchUnindexedArchives bool `yaml:"search-unindexed-archives" json:"search-unindexed-archives" mapstructure:"search-unindexed-archives"`
|
||||
SearchIndexedArchives bool `yaml:"search-indexed-archives" json:"search-indexed-archives" mapstructure:"search-indexed-archives"`
|
||||
}
|
||||
|
||||
func defaultPkg() pkg {
|
||||
c := cataloger.DefaultSearchConfig()
|
||||
return pkg{
|
||||
SearchIndexedArchives: c.IncludeIndexedArchives,
|
||||
SearchUnindexedArchives: c.IncludeUnindexedArchives,
|
||||
Cataloger: scope{
|
||||
Enabled: true,
|
||||
Scope: c.Scope.String(),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
type scope struct {
|
||||
Enabled bool `yaml:"enabled" json:"enabled" mapstructure:"enabled"`
|
||||
Scope string `yaml:"scope" json:"scope" mapstructure:"scope"`
|
||||
}
|
||||
|
||||
var _ clio.PostLoader = (*scope)(nil)
|
||||
|
||||
func (opt *scope) PostLoad() error {
|
||||
s := opt.GetScope()
|
||||
if s == source.UnknownScope {
|
||||
return fmt.Errorf("bad scope value %v", opt.Scope)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (opt scope) GetScope() source.Scope {
|
||||
return source.ParseScope(opt.Scope)
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package options
|
||||
|
||||
type sourceCfg struct {
|
||||
Name string `json:"name" yaml:"name" mapstructure:"name"`
|
||||
Version string `json:"version" yaml:"version" mapstructure:"version"`
|
||||
File fileSource `json:"file" yaml:"file" mapstructure:"file"`
|
||||
}
|
||||
|
||||
type fileSource struct {
|
||||
Digests []string `json:"digests" yaml:"digests" mapstructure:"digests"`
|
||||
}
|
||||
|
||||
func defaultSourceCfg() sourceCfg {
|
||||
return sourceCfg{
|
||||
File: fileSource{
|
||||
Digests: []string{"sha256"},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
|
||||
[TestHandler_handleFileDigestsCatalogerStarted/cataloging_in_progress - 1]
|
||||
⠋ Cataloging file digests ━━━━━━━━━━━━━━━━━━━━ [current]
|
||||
---
|
||||
|
||||
[TestHandler_handleFileDigestsCatalogerStarted/cataloging_complete - 1]
|
||||
✔ Cataloged file digests
|
||||
---
|
||||
@@ -1,8 +0,0 @@
|
||||
|
||||
[TestHandler_handleFileMetadataCatalogerStarted/cataloging_in_progress - 1]
|
||||
⠋ Cataloging file metadata ━━━━━━━━━━━━━━━━━━━━ [current]
|
||||
---
|
||||
|
||||
[TestHandler_handleFileMetadataCatalogerStarted/cataloging_complete - 1]
|
||||
✔ Cataloged file metadata
|
||||
---
|
||||
@@ -1,16 +0,0 @@
|
||||
|
||||
[TestHandler_handlePackageCatalogerStarted/cataloging_in_progress - 1]
|
||||
⠋ Cataloging packages [50 packages]
|
||||
---
|
||||
|
||||
[TestHandler_handlePackageCatalogerStarted/cataloging_only_files_complete - 1]
|
||||
⠋ Cataloging packages [50 packages]
|
||||
---
|
||||
|
||||
[TestHandler_handlePackageCatalogerStarted/cataloging_only_packages_complete - 1]
|
||||
⠋ Cataloging packages [100 packages]
|
||||
---
|
||||
|
||||
[TestHandler_handlePackageCatalogerStarted/cataloging_complete - 1]
|
||||
✔ Cataloged packages [100 packages]
|
||||
---
|
||||
@@ -1,8 +0,0 @@
|
||||
|
||||
[TestHandler_handleSecretsCatalogerStarted/cataloging_in_progress - 1]
|
||||
⠋ Cataloging secrets ━━━━━━━━━━━━━━━━━━━━ [64 secrets]
|
||||
---
|
||||
|
||||
[TestHandler_handleSecretsCatalogerStarted/cataloging_complete - 1]
|
||||
✔ Cataloged secrets [64 secrets]
|
||||
---
|
||||
@@ -1,6 +1,7 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -12,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"
|
||||
@@ -29,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
|
||||
@@ -60,13 +61,13 @@ func Attest(app clio.Application) *cobra.Command {
|
||||
"appName": id.Name,
|
||||
"command": "attest",
|
||||
}),
|
||||
Args: validatePackagesArgs,
|
||||
Args: validateScanArgs,
|
||||
PreRunE: applicationUpdateCheck(id, &opts.UpdateCheck),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
restoreStdout := ui.CaptureStdoutToTraceLog()
|
||||
defer restoreStdout()
|
||||
|
||||
return runAttest(id, &opts, args[0])
|
||||
return runAttest(cmd.Context(), id, &opts, args[0])
|
||||
},
|
||||
}, &opts)
|
||||
}
|
||||
@@ -98,7 +99,7 @@ func defaultAttestOutputOptions() options.Output {
|
||||
}
|
||||
|
||||
//nolint:funlen
|
||||
func runAttest(id clio.Identification, opts *attestOptions, userInput string) error {
|
||||
func runAttest(ctx context.Context, id clio.Identification, opts *attestOptions, userInput string) error {
|
||||
// TODO: what other validation here besides binary name?
|
||||
if !commandExists(cosignBinName) {
|
||||
return fmt.Errorf("'syft attest' requires cosign to be installed, however it does not appear to be on PATH")
|
||||
@@ -111,7 +112,7 @@ func runAttest(id clio.Identification, opts *attestOptions, userInput string) er
|
||||
}
|
||||
defer os.Remove(f.Name())
|
||||
|
||||
s, err := generateSBOMForAttestation(id, &opts.Catalog, userInput)
|
||||
s, err := generateSBOMForAttestation(ctx, id, &opts.Catalog, userInput)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to build SBOM: %w", err)
|
||||
}
|
||||
@@ -231,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":
|
||||
@@ -245,7 +246,7 @@ func predicateType(outputName string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func generateSBOMForAttestation(id clio.Identification, opts *options.Catalog, userInput string) (*sbom.SBOM, error) {
|
||||
func generateSBOMForAttestation(ctx context.Context, id clio.Identification, opts *options.Catalog, userInput string) (*sbom.SBOM, error) {
|
||||
src, err := getSource(opts, userInput, onlyContainerImages)
|
||||
|
||||
if err != nil {
|
||||
@@ -260,7 +261,7 @@ func generateSBOMForAttestation(id clio.Identification, opts *options.Catalog, u
|
||||
}
|
||||
}()
|
||||
|
||||
s, err := generateSBOM(id, src, opts)
|
||||
s, err := generateSBOM(ctx, id, src, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
@@ -13,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"
|
||||
)
|
||||
@@ -258,7 +259,7 @@ func Test_buildSBOMForAttestation(t *testing.T) {
|
||||
if tt.wantErr == nil {
|
||||
tt.wantErr = require.NoError
|
||||
}
|
||||
_, err := generateSBOMForAttestation(tt.args.id, tt.args.opts, tt.args.userInput)
|
||||
_, err := generateSBOMForAttestation(context.Background(), tt.args.id, tt.args.opts, tt.args.userInput)
|
||||
tt.wantErr(t, err)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -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"
|
||||
@@ -0,0 +1,37 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"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 {
|
||||
id := app.ID()
|
||||
|
||||
opts := defaultScanOptions()
|
||||
|
||||
cmd := app.SetupCommand(&cobra.Command{
|
||||
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()
|
||||
defer restoreStdout()
|
||||
|
||||
return runScan(cmd.Context(), id, opts, args[0])
|
||||
},
|
||||
}, opts)
|
||||
|
||||
cmd.Deprecated = "use `syft scan` instead"
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/anchore/syft/internal/task"
|
||||
)
|
||||
|
||||
func Test_filterExpressionErrors_expressionErrorsHelp(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
wantExpErrs []task.ErrInvalidExpression
|
||||
wantErr assert.ErrorAssertionFunc
|
||||
wantHelp string
|
||||
}{
|
||||
{
|
||||
name: "no errors",
|
||||
err: nil,
|
||||
wantExpErrs: nil,
|
||||
wantErr: assert.NoError,
|
||||
wantHelp: "",
|
||||
},
|
||||
{
|
||||
name: "single non-expression error is retained",
|
||||
err: errors.New("foo"),
|
||||
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
|
||||
return assert.Equal(t, "foo", err.Error())
|
||||
},
|
||||
wantHelp: "",
|
||||
},
|
||||
{
|
||||
name: "multiple non-expression sibling errors are retained",
|
||||
err: func() error {
|
||||
var err error
|
||||
err = multierror.Append(err, errors.New("foo"))
|
||||
err = multierror.Append(err, errors.New("bar"))
|
||||
return err
|
||||
}(),
|
||||
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
|
||||
// note: this is the default formatting from the hashicorp multierror object
|
||||
expected := `2 errors occurred:
|
||||
* foo
|
||||
* bar
|
||||
|
||||
`
|
||||
return assert.Equal(t, expected, err.Error())
|
||||
},
|
||||
wantHelp: "",
|
||||
},
|
||||
{
|
||||
name: "has multiple expression errors (with sibling errors)",
|
||||
err: func() error {
|
||||
var err error
|
||||
err = multierror.Append(err, errors.New("foo"))
|
||||
err = multierror.Append(err, task.ErrInvalidExpression{Expression: "foo", Operation: task.AddOperation, Err: task.ErrTagsNotAllowed})
|
||||
err = multierror.Append(err, errors.New("bar"))
|
||||
err = multierror.Append(err, task.ErrInvalidExpression{Expression: "bar", Operation: task.SubSelectOperation, Err: task.ErrNamesNotAllowed})
|
||||
err = multierror.Append(err, errors.New("last"))
|
||||
return err
|
||||
}(),
|
||||
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
|
||||
expected := `5 errors occurred:
|
||||
* foo
|
||||
* invalid expression: "foo": tags are not allowed with this operation (must use exact names)
|
||||
* bar
|
||||
* invalid expression: "bar": names are not allowed with this operation (must use tags)
|
||||
* last
|
||||
|
||||
`
|
||||
return assert.Equal(t, expected, err.Error())
|
||||
},
|
||||
wantExpErrs: []task.ErrInvalidExpression{
|
||||
{Expression: "foo", Operation: task.AddOperation, Err: task.ErrTagsNotAllowed},
|
||||
{Expression: "bar", Operation: task.SubSelectOperation, Err: task.ErrNamesNotAllowed},
|
||||
},
|
||||
wantHelp: `Suggestions:
|
||||
|
||||
❖ Given expression "--select-catalogers foo"
|
||||
However, tags are not allowed with this operation (must use exact names).
|
||||
Adding groups of catalogers may result in surprising behavior (create inaccurate SBOMs).
|
||||
If you are certain this is what you want to do, use "--override-default-catalogers foo" instead.
|
||||
|
||||
❖ Given expression "--select-catalogers bar"
|
||||
However, names are not allowed with this operation (must use tags).
|
||||
It seems like you are intending to add a cataloger in addition to the default set.
|
||||
... Did you mean "--select-catalogers +bar" instead?
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "has multiple expression errors (with error chains and sibling errors)",
|
||||
err: func() error {
|
||||
var err error
|
||||
err = multierror.Append(err, fmt.Errorf("foo: %w", fmt.Errorf("bar: %w", errors.New("last"))))
|
||||
err = multierror.Append(err, task.ErrInvalidExpression{Expression: "foo", Operation: task.AddOperation, Err: task.ErrTagsNotAllowed})
|
||||
err = multierror.Append(err, task.ErrInvalidExpression{Expression: "bar", Operation: task.SubSelectOperation, Err: task.ErrNamesNotAllowed})
|
||||
err = multierror.Append(err, errors.New("bottom"))
|
||||
|
||||
return fmt.Errorf("top: %w", fmt.Errorf("middle: %w", err))
|
||||
}(),
|
||||
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
|
||||
expected := `top: middle: 4 errors occurred:
|
||||
* foo: bar: last
|
||||
* invalid expression: "foo": tags are not allowed with this operation (must use exact names)
|
||||
* invalid expression: "bar": names are not allowed with this operation (must use tags)
|
||||
* bottom
|
||||
|
||||
`
|
||||
return assert.Equal(t, expected, err.Error())
|
||||
},
|
||||
wantExpErrs: []task.ErrInvalidExpression{
|
||||
{Expression: "foo", Operation: task.AddOperation, Err: task.ErrTagsNotAllowed},
|
||||
{Expression: "bar", Operation: task.SubSelectOperation, Err: task.ErrNamesNotAllowed},
|
||||
},
|
||||
wantHelp: `Suggestions:
|
||||
|
||||
❖ Given expression "--select-catalogers foo"
|
||||
However, tags are not allowed with this operation (must use exact names).
|
||||
Adding groups of catalogers may result in surprising behavior (create inaccurate SBOMs).
|
||||
If you are certain this is what you want to do, use "--override-default-catalogers foo" instead.
|
||||
|
||||
❖ Given expression "--select-catalogers bar"
|
||||
However, names are not allowed with this operation (must use tags).
|
||||
It seems like you are intending to add a cataloger in addition to the default set.
|
||||
... Did you mean "--select-catalogers +bar" instead?
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "has multiple expression errors (with error chains and sibling errors)",
|
||||
err: func() error {
|
||||
var err error
|
||||
err = multierror.Append(err, fmt.Errorf("foo: %w", fmt.Errorf("bar: %w", errors.New("last"))))
|
||||
err = multierror.Append(err, task.ErrInvalidExpression{Expression: "foo", Operation: task.AddOperation, Err: task.ErrTagsNotAllowed})
|
||||
err = multierror.Append(err, task.ErrInvalidExpression{Expression: "bar", Operation: task.SubSelectOperation, Err: task.ErrNamesNotAllowed})
|
||||
err = multierror.Append(err, errors.New("bottom"))
|
||||
|
||||
// note we wrap the top error in a chain
|
||||
return fmt.Errorf("top: %w", fmt.Errorf("middle: %w", err))
|
||||
}(),
|
||||
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
|
||||
expected := `top: middle: 4 errors occurred:
|
||||
* foo: bar: last
|
||||
* invalid expression: "foo": tags are not allowed with this operation (must use exact names)
|
||||
* invalid expression: "bar": names are not allowed with this operation (must use tags)
|
||||
* bottom
|
||||
|
||||
`
|
||||
return assert.Equal(t, expected, err.Error())
|
||||
},
|
||||
wantExpErrs: []task.ErrInvalidExpression{
|
||||
{Expression: "foo", Operation: task.AddOperation, Err: task.ErrTagsNotAllowed},
|
||||
{Expression: "bar", Operation: task.SubSelectOperation, Err: task.ErrNamesNotAllowed},
|
||||
},
|
||||
wantHelp: `Suggestions:
|
||||
|
||||
❖ Given expression "--select-catalogers foo"
|
||||
However, tags are not allowed with this operation (must use exact names).
|
||||
Adding groups of catalogers may result in surprising behavior (create inaccurate SBOMs).
|
||||
If you are certain this is what you want to do, use "--override-default-catalogers foo" instead.
|
||||
|
||||
❖ Given expression "--select-catalogers bar"
|
||||
However, names are not allowed with this operation (must use tags).
|
||||
It seems like you are intending to add a cataloger in addition to the default set.
|
||||
... Did you mean "--select-catalogers +bar" instead?
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "preserve for any errors within ErrInvalidExpression types",
|
||||
err: func() error {
|
||||
var err error
|
||||
err = multierror.Append(err, task.ErrInvalidExpression{Expression: "foo", Operation: task.AddOperation, Err: task.ErrTagsNotAllowed})
|
||||
err = multierror.Append(err, task.ErrInvalidExpression{Expression: "bar", Operation: task.SubSelectOperation, Err: errors.New("explanation")}) // this is what makes this test different...
|
||||
|
||||
return err
|
||||
}(),
|
||||
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
|
||||
// note: the errors are removed and the help text shows the enriched error help
|
||||
expected := `2 errors occurred:
|
||||
* invalid expression: "foo": tags are not allowed with this operation (must use exact names)
|
||||
* invalid expression: "bar": explanation
|
||||
|
||||
`
|
||||
return assert.Equal(t, expected, err.Error())
|
||||
},
|
||||
wantExpErrs: []task.ErrInvalidExpression{
|
||||
{Expression: "foo", Operation: task.AddOperation, Err: task.ErrTagsNotAllowed},
|
||||
{Expression: "bar", Operation: task.SubSelectOperation, Err: errors.New("explanation")},
|
||||
},
|
||||
wantHelp: `Suggestions:
|
||||
|
||||
❖ Given expression "--select-catalogers foo"
|
||||
However, tags are not allowed with this operation (must use exact names).
|
||||
Adding groups of catalogers may result in surprising behavior (create inaccurate SBOMs).
|
||||
If you are certain this is what you want to do, use "--override-default-catalogers foo" instead.
|
||||
|
||||
`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotExpErrs := filterExpressionErrors(tt.err)
|
||||
tt.wantErr(t, tt.err) // ensure the error still remains
|
||||
assert.Equal(t, tt.wantExpErrs, gotExpErrs)
|
||||
|
||||
gotHelp := expressionErrorsHelp(gotExpErrs)
|
||||
assert.Equal(t, tt.wantHelp, gotHelp)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_expressionSuggestions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
expErr task.ErrInvalidExpression
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "no embedded error",
|
||||
expErr: task.ErrInvalidExpression{
|
||||
Expression: "example",
|
||||
},
|
||||
want: ``,
|
||||
},
|
||||
{
|
||||
name: "general error",
|
||||
expErr: task.ErrInvalidExpression{
|
||||
Err: errors.New("general error message"),
|
||||
Expression: "example",
|
||||
},
|
||||
want: ``,
|
||||
},
|
||||
{
|
||||
name: "ErrUnknownNameOrTag with add operation",
|
||||
expErr: task.ErrInvalidExpression{
|
||||
Err: task.ErrUnknownNameOrTag,
|
||||
Operation: task.AddOperation,
|
||||
Expression: "+example",
|
||||
},
|
||||
want: ``,
|
||||
},
|
||||
{
|
||||
name: "ErrUnknownNameOrTag with subselect operation",
|
||||
expErr: task.ErrInvalidExpression{
|
||||
Err: task.ErrUnknownNameOrTag,
|
||||
Operation: task.SubSelectOperation,
|
||||
Expression: "example",
|
||||
},
|
||||
want: ``,
|
||||
},
|
||||
{
|
||||
name: "ErrNamesNotAllowed with subselect operator",
|
||||
expErr: task.ErrInvalidExpression{
|
||||
Err: task.ErrNamesNotAllowed,
|
||||
Operation: task.SubSelectOperation,
|
||||
Expression: "example",
|
||||
},
|
||||
want: ` ❖ Given expression "--select-catalogers example"
|
||||
However, names are not allowed with this operation (must use tags).
|
||||
It seems like you are intending to add a cataloger in addition to the default set.
|
||||
... Did you mean "--select-catalogers +example" instead?
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "ErrTagsNotAllowed with add operation",
|
||||
expErr: task.ErrInvalidExpression{
|
||||
Err: task.ErrTagsNotAllowed,
|
||||
Operation: task.AddOperation,
|
||||
Expression: "+example",
|
||||
},
|
||||
want: ` ❖ Given expression "--select-catalogers +example"
|
||||
However, tags are not allowed with this operation (must use exact names).
|
||||
Adding groups of catalogers may result in surprising behavior (create inaccurate SBOMs).
|
||||
If you are certain this is what you want to do, use "--override-default-catalogers example" instead.
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "ErrAllNotAllowed with subselect operation",
|
||||
expErr: task.ErrInvalidExpression{
|
||||
Err: task.ErrAllNotAllowed,
|
||||
Operation: task.SubSelectOperation,
|
||||
Expression: "example",
|
||||
},
|
||||
want: ` ❖ Given expression "--select-catalogers example"
|
||||
However, you cannot use the 'all' operand in this context.
|
||||
It seems like you are intending to use all catalogers (which is not recommended).
|
||||
... Did you mean "--override-default-catalogers example" instead?
|
||||
`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.want, expressionSuggetions(tt.expErr))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
func Root(app clio.Application, packagesCmd *cobra.Command) *cobra.Command {
|
||||
id := app.ID()
|
||||
|
||||
opts := defaultPackagesOptions()
|
||||
opts := defaultScanOptions()
|
||||
|
||||
return app.SetupRootCommand(&cobra.Command{
|
||||
Use: fmt.Sprintf("%s [SOURCE]", app.ID().Name),
|
||||
@@ -25,7 +25,7 @@ func Root(app clio.Application, packagesCmd *cobra.Command) *cobra.Command {
|
||||
restoreStdout := ui.CaptureStdoutToTraceLog()
|
||||
defer restoreStdout()
|
||||
|
||||
return runPackages(id, opts, args[0])
|
||||
return runScan(cmd.Context(), id, opts, args[0])
|
||||
},
|
||||
}, opts)
|
||||
}
|
||||
@@ -0,0 +1,447 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
"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/file"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/task"
|
||||
"github.com/anchore/syft/syft"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
const (
|
||||
scanExample = ` {{.appName}} {{.command}} alpine:latest a summary of discovered packages
|
||||
{{.appName}} {{.command}} alpine:latest -o json show all possible cataloging details
|
||||
{{.appName}} {{.command}} alpine:latest -o cyclonedx show a CycloneDX formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -o cyclonedx-json show a CycloneDX JSON formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -o spdx show a SPDX 2.3 Tag-Value formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -o spdx@2.2 show a SPDX 2.2 Tag-Value formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -o spdx-json show a SPDX 2.3 JSON formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -o spdx-json@2.2 show a SPDX 2.2 JSON formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -vv show verbose debug information
|
||||
{{.appName}} {{.command}} alpine:latest -o template -t my_format.tmpl show a SBOM formatted according to given template file
|
||||
|
||||
Supports the following image sources:
|
||||
{{.appName}} {{.command}} yourrepo/yourimage:tag defaults to using images from a Docker daemon. If Docker is not present, the image is pulled directly from the registry.
|
||||
{{.appName}} {{.command}} path/to/a/file/or/dir a Docker tar, OCI tar, OCI directory, SIF container, or generic filesystem directory
|
||||
`
|
||||
|
||||
schemeHelpHeader = "You can also explicitly specify the scheme to use:"
|
||||
imageSchemeHelp = ` {{.appName}} {{.command}} docker:yourrepo/yourimage:tag explicitly use the Docker daemon
|
||||
{{.appName}} {{.command}} podman:yourrepo/yourimage:tag explicitly use the Podman daemon
|
||||
{{.appName}} {{.command}} registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required)
|
||||
{{.appName}} {{.command}} docker-archive:path/to/yourimage.tar use a tarball from disk for archives created from "docker save"
|
||||
{{.appName}} {{.command}} oci-archive:path/to/yourimage.tar use a tarball from disk for OCI archives (from Skopeo or otherwise)
|
||||
{{.appName}} {{.command}} oci-dir:path/to/yourimage read directly from a path on disk for OCI layout directories (from Skopeo or otherwise)
|
||||
{{.appName}} {{.command}} singularity:path/to/yourimage.sif read directly from a Singularity Image Format (SIF) container on disk
|
||||
`
|
||||
nonImageSchemeHelp = ` {{.appName}} {{.command}} dir:path/to/yourproject read directly from a path on disk (any directory)
|
||||
{{.appName}} {{.command}} file:path/to/yourproject/file read directly from a path on disk (any single file)
|
||||
`
|
||||
scanSchemeHelp = "\n " + schemeHelpHeader + "\n" + imageSchemeHelp + nonImageSchemeHelp
|
||||
|
||||
scanHelp = scanExample + scanSchemeHelp
|
||||
)
|
||||
|
||||
type scanOptions struct {
|
||||
options.Config `yaml:",inline" mapstructure:",squash"`
|
||||
options.Output `yaml:",inline" mapstructure:",squash"`
|
||||
options.UpdateCheck `yaml:",inline" mapstructure:",squash"`
|
||||
options.Catalog `yaml:",inline" mapstructure:",squash"`
|
||||
}
|
||||
|
||||
func defaultScanOptions() *scanOptions {
|
||||
return &scanOptions{
|
||||
Output: options.DefaultOutput(),
|
||||
UpdateCheck: options.DefaultUpdateCheck(),
|
||||
Catalog: options.DefaultCatalog(),
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:dupl
|
||||
func Scan(app clio.Application) *cobra.Command {
|
||||
id := app.ID()
|
||||
|
||||
opts := defaultScanOptions()
|
||||
|
||||
return app.SetupCommand(&cobra.Command{
|
||||
Use: "scan [SOURCE]",
|
||||
Short: "Generate an SBOM",
|
||||
Long: "Generate a packaged-based Software Bill Of Materials (SBOM) from container images and filesystems",
|
||||
Example: internal.Tprintf(scanHelp, map[string]interface{}{
|
||||
"appName": id.Name,
|
||||
"command": "scan",
|
||||
}),
|
||||
Args: validateScanArgs,
|
||||
PreRunE: applicationUpdateCheck(id, &opts.UpdateCheck),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
restoreStdout := ui.CaptureStdoutToTraceLog()
|
||||
defer restoreStdout()
|
||||
|
||||
return runScan(cmd.Context(), id, opts, args[0])
|
||||
},
|
||||
}, opts)
|
||||
}
|
||||
|
||||
func (o *scanOptions) PostLoad() error {
|
||||
return o.validateLegacyOptionsNotUsed()
|
||||
}
|
||||
|
||||
func (o *scanOptions) validateLegacyOptionsNotUsed() error {
|
||||
if o.Config.ConfigFile == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// check for legacy config file shapes that are no longer valid
|
||||
type legacyConfig struct {
|
||||
BasePath *string `yaml:"base-path" json:"base-path" mapstructure:"base-path"`
|
||||
DefaultImagePullSource *string `yaml:"default-image-pull-source" json:"default-image-pull-source" mapstructure:"default-image-pull-source"`
|
||||
ExcludeBinaryOverlapByOwnership *bool `yaml:"exclude-binary-overlap-by-ownership" json:"exclude-binary-overlap-by-ownership" mapstructure:"exclude-binary-overlap-by-ownership"`
|
||||
File any `yaml:"file" json:"file" mapstructure:"file"`
|
||||
}
|
||||
|
||||
by, err := os.ReadFile(o.Config.ConfigFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read config file during validations %q: %w", o.Config.ConfigFile, err)
|
||||
}
|
||||
|
||||
var legacy legacyConfig
|
||||
if err := yaml.Unmarshal(by, &legacy); err != nil {
|
||||
return fmt.Errorf("unable to parse config file during validations %q: %w", o.Config.ConfigFile, err)
|
||||
}
|
||||
|
||||
if legacy.DefaultImagePullSource != nil {
|
||||
return fmt.Errorf("the config file option 'default-image-pull-source' has been removed, please use 'source.image.default-pull-source' instead")
|
||||
}
|
||||
|
||||
if legacy.ExcludeBinaryOverlapByOwnership != nil {
|
||||
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 {
|
||||
return fmt.Errorf("the config file option 'base-path' has been removed, please use 'source.base-path' instead")
|
||||
}
|
||||
|
||||
if legacy.File != nil && reflect.TypeOf(legacy.File).Kind() == reflect.String {
|
||||
return fmt.Errorf("the config file option 'file' has been removed, please use 'outputs' instead")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateScanArgs(cmd *cobra.Command, args []string) error {
|
||||
return validateArgs(cmd, args, "an image/directory argument is required")
|
||||
}
|
||||
|
||||
func validateArgs(cmd *cobra.Command, args []string, error string) error {
|
||||
if len(args) == 0 {
|
||||
// in the case that no arguments are given we want to show the help text and return with a non-0 return code.
|
||||
if err := cmd.Help(); err != nil {
|
||||
return fmt.Errorf("unable to display help: %w", err)
|
||||
}
|
||||
return fmt.Errorf(error)
|
||||
}
|
||||
|
||||
return cobra.MaximumNArgs(1)(cmd, args)
|
||||
}
|
||||
|
||||
// nolint:funlen
|
||||
func runScan(ctx context.Context, id clio.Identification, opts *scanOptions, userInput string) error {
|
||||
writer, err := opts.SBOMWriter()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
src, err := getSource(&opts.Catalog, userInput)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if src != nil {
|
||||
if err := src.Close(); err != nil {
|
||||
log.Tracef("unable to close source: %+v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
s, err := generateSBOM(ctx, id, src, &opts.Catalog)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s == nil {
|
||||
return fmt.Errorf("no SBOM produced for %q", userInput)
|
||||
}
|
||||
|
||||
if err := writer.Write(*s); err != nil {
|
||||
return fmt.Errorf("failed to write SBOM: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getSource(opts *options.Catalog, userInput string, filters ...func(*source.Detection) error) (source.Source, error) {
|
||||
detection, err := source.Detect(
|
||||
userInput,
|
||||
source.DetectConfig{
|
||||
DefaultImageSource: opts.Source.Image.DefaultPullSource,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not deteremine source: %w", err)
|
||||
}
|
||||
|
||||
for _, filter := range filters {
|
||||
if err := filter(detection); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var platform *image.Platform
|
||||
|
||||
if opts.Platform != "" {
|
||||
platform, err = image.NewPlatform(opts.Platform)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid platform: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
hashers, err := file.Hashers(opts.Source.File.Digests...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid hash algorithm: %w", err)
|
||||
}
|
||||
|
||||
src, err := detection.NewSource(
|
||||
source.DetectionSourceConfig{
|
||||
Alias: source.Alias{
|
||||
Name: opts.Source.Name,
|
||||
Version: opts.Source.Version,
|
||||
},
|
||||
RegistryOptions: opts.Registry.ToOptions(),
|
||||
Platform: platform,
|
||||
Exclude: source.ExcludeConfig{
|
||||
Paths: opts.Exclusions,
|
||||
},
|
||||
DigestAlgorithms: hashers,
|
||||
BasePath: opts.Source.BasePath,
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
if userInput == "power-user" {
|
||||
bus.Notify("Note: the 'power-user' command has been removed.")
|
||||
}
|
||||
return nil, fmt.Errorf("failed to construct source from user input %q: %w", userInput, err)
|
||||
}
|
||||
|
||||
return src, nil
|
||||
}
|
||||
|
||||
func generateSBOM(ctx context.Context, id clio.Identification, src source.Source, opts *options.Catalog) (*sbom.SBOM, error) {
|
||||
s, err := syft.CreateSBOM(ctx, src, opts.ToSBOMConfig(id))
|
||||
if err != nil {
|
||||
expErrs := filterExpressionErrors(err)
|
||||
notifyExpressionErrors(expErrs)
|
||||
return nil, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func filterExpressionErrors(err error) []task.ErrInvalidExpression {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
expErrs := processErrors(err)
|
||||
|
||||
return expErrs
|
||||
}
|
||||
|
||||
// processErrors traverses error chains and multierror lists and returns all ErrInvalidExpression errors found
|
||||
func processErrors(err error) []task.ErrInvalidExpression {
|
||||
var result []task.ErrInvalidExpression
|
||||
|
||||
var processError func(...error)
|
||||
processError = func(errs ...error) {
|
||||
for _, e := range errs {
|
||||
// note: using errors.As will result in surprising behavior (since that will traverse the error chain,
|
||||
// potentially skipping over nodes in a list of errors)
|
||||
if cerr, ok := e.(task.ErrInvalidExpression); ok {
|
||||
result = append(result, cerr)
|
||||
continue
|
||||
}
|
||||
var multiErr *multierror.Error
|
||||
if errors.As(e, &multiErr) {
|
||||
processError(multiErr.Errors...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
processError(err)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func notifyExpressionErrors(expErrs []task.ErrInvalidExpression) {
|
||||
helpText := expressionErrorsHelp(expErrs)
|
||||
if helpText == "" {
|
||||
return
|
||||
}
|
||||
|
||||
bus.Notify(helpText)
|
||||
}
|
||||
|
||||
func expressionErrorsHelp(expErrs []task.ErrInvalidExpression) string {
|
||||
// enrich all errors found with CLI hints
|
||||
if len(expErrs) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
sb := strings.Builder{}
|
||||
|
||||
sb.WriteString("Suggestions:\n\n")
|
||||
|
||||
found := false
|
||||
for i, expErr := range expErrs {
|
||||
help := expressionSuggetions(expErr)
|
||||
if help == "" {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
sb.WriteString(help)
|
||||
if i != len(expErrs)-1 {
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return ""
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
const expressionHelpTemplate = " ❖ Given expression %q\n%s%s"
|
||||
|
||||
func expressionSuggetions(expErr task.ErrInvalidExpression) string {
|
||||
if expErr.Err == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
hint := getHintPhrase(expErr)
|
||||
if hint == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
return fmt.Sprintf(expressionHelpTemplate,
|
||||
getExpression(expErr),
|
||||
indentMsg(getExplanation(expErr)),
|
||||
indentMsg(hint),
|
||||
)
|
||||
}
|
||||
|
||||
func indentMsg(msg string) string {
|
||||
if msg == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
lines := strings.Split(msg, "\n")
|
||||
for i, line := range lines {
|
||||
lines[i] = " " + line
|
||||
}
|
||||
|
||||
return strings.Join(lines, "\n") + "\n"
|
||||
}
|
||||
|
||||
func getExpression(expErr task.ErrInvalidExpression) string {
|
||||
flag := "--select-catalogers"
|
||||
if expErr.Operation == task.SetOperation {
|
||||
flag = "--override-default-catalogers"
|
||||
}
|
||||
return fmt.Sprintf("%s %s", flag, expErr.Expression)
|
||||
}
|
||||
|
||||
func getExplanation(expErr task.ErrInvalidExpression) string {
|
||||
err := expErr.Err
|
||||
if errors.Is(err, task.ErrUnknownNameOrTag) {
|
||||
noun := ""
|
||||
switch expErr.Operation {
|
||||
case task.AddOperation:
|
||||
noun = "name"
|
||||
case task.SubSelectOperation:
|
||||
noun = "tag"
|
||||
default:
|
||||
noun = "name or tag"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("However, %q is not a recognized cataloger %s.", trimOperation(expErr.Expression), noun)
|
||||
}
|
||||
|
||||
if errors.Is(err, task.ErrNamesNotAllowed) {
|
||||
if expErr.Operation == task.SubSelectOperation {
|
||||
return "However, " + err.Error() + ".\nIt seems like you are intending to add a cataloger in addition to the default set." // nolint:goconst
|
||||
}
|
||||
return "However, " + err.Error() + "." // nolint:goconst
|
||||
}
|
||||
|
||||
if errors.Is(err, task.ErrTagsNotAllowed) {
|
||||
return "However, " + err.Error() + ".\nAdding groups of catalogers may result in surprising behavior (create inaccurate SBOMs)." // nolint:goconst
|
||||
}
|
||||
|
||||
if errors.Is(err, task.ErrAllNotAllowed) {
|
||||
return "However, you " + err.Error() + ".\nIt seems like you are intending to use all catalogers (which is not recommended)."
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "However, this is not valid: " + err.Error()
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func getHintPhrase(expErr task.ErrInvalidExpression) string {
|
||||
if errors.Is(expErr.Err, task.ErrUnknownNameOrTag) {
|
||||
return ""
|
||||
}
|
||||
|
||||
switch expErr.Operation {
|
||||
case task.AddOperation:
|
||||
if errors.Is(expErr.Err, task.ErrTagsNotAllowed) {
|
||||
return fmt.Sprintf("If you are certain this is what you want to do, use %q instead.", "--override-default-catalogers "+trimOperation(expErr.Expression))
|
||||
}
|
||||
|
||||
case task.SubSelectOperation:
|
||||
didYouMean := "... Did you mean %q instead?"
|
||||
if errors.Is(expErr.Err, task.ErrNamesNotAllowed) {
|
||||
return fmt.Sprintf(didYouMean, "--select-catalogers +"+expErr.Expression)
|
||||
}
|
||||
|
||||
if errors.Is(expErr.Err, task.ErrAllNotAllowed) {
|
||||
return fmt.Sprintf(didYouMean, "--override-default-catalogers "+expErr.Expression)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func trimOperation(x string) string {
|
||||
return strings.TrimLeft(x, "+-")
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/anchore/syft/cmd/syft/internal/options"
|
||||
)
|
||||
|
||||
func Test_scanOptions_validateLegacyOptionsNotUsed(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg string
|
||||
wantErr assert.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "no config file",
|
||||
},
|
||||
{
|
||||
name: "config file with no legacy options",
|
||||
cfg: "test-fixtures/scan-configs/no-legacy-options.yaml",
|
||||
},
|
||||
{
|
||||
name: "config file with default image pull source legacy option",
|
||||
cfg: "test-fixtures/scan-configs/with-default-pull-source.yaml",
|
||||
wantErr: assertErrorContains("source.image.default-pull-source"),
|
||||
},
|
||||
{
|
||||
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("package.exclude-binary-overlap-by-ownership"),
|
||||
},
|
||||
{
|
||||
name: "config file with file string legacy option",
|
||||
cfg: "test-fixtures/scan-configs/with-file-string.yaml",
|
||||
wantErr: assertErrorContains("outputs"),
|
||||
},
|
||||
{
|
||||
name: "config file with file section",
|
||||
cfg: "test-fixtures/scan-configs/with-file-section.yaml",
|
||||
},
|
||||
{
|
||||
name: "config file with base-path legacy option",
|
||||
cfg: "test-fixtures/scan-configs/with-base-path.yaml",
|
||||
wantErr: assertErrorContains("source.base-path"),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.wantErr == nil {
|
||||
tt.wantErr = assert.NoError
|
||||
}
|
||||
o := &scanOptions{
|
||||
Config: options.Config{ConfigFile: tt.cfg},
|
||||
}
|
||||
tt.wantErr(t, o.validateLegacyOptionsNotUsed())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func assertErrorContains(contains string) assert.ErrorAssertionFunc {
|
||||
return func(t assert.TestingT, err error, i ...interface{}) bool {
|
||||
return assert.ErrorContains(t, err, contains, i...)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
# simple, valid syft config with no legacy fields used
|
||||
parallelism: 5
|
||||
@@ -0,0 +1 @@
|
||||
base-path: "something"
|
||||
@@ -0,0 +1 @@
|
||||
default-image-pull-source: "something"
|
||||
+1
@@ -0,0 +1 @@
|
||||
exclude-binary-overlap-by-ownership: true
|
||||
@@ -0,0 +1,2 @@
|
||||
file:
|
||||
selection: all
|
||||
@@ -0,0 +1 @@
|
||||
file: path
|
||||
@@ -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"
|
||||
@@ -0,0 +1,224 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/iancoleman/strcase"
|
||||
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/fangs"
|
||||
intFile "github.com/anchore/syft/internal/file"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft"
|
||||
"github.com/anchore/syft/syft/cataloging"
|
||||
"github.com/anchore/syft/syft/cataloging/filecataloging"
|
||||
"github.com/anchore/syft/syft/cataloging/pkgcataloging"
|
||||
"github.com/anchore/syft/syft/file/cataloger/filecontent"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/binary"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/golang"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/java"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/javascript"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/kernel"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/python"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
type Catalog struct {
|
||||
// high-level cataloger configuration
|
||||
Catalogers []string `yaml:"-" json:"catalogers" mapstructure:"catalogers"` // deprecated and not shown in yaml output
|
||||
DefaultCatalogers []string `yaml:"default-catalogers" json:"default-catalogers" mapstructure:"default-catalogers"`
|
||||
SelectCatalogers []string `yaml:"select-catalogers" json:"select-catalogers" mapstructure:"select-catalogers"`
|
||||
Package packageConfig `yaml:"package" json:"package" mapstructure:"package"`
|
||||
File fileConfig `yaml:"file" json:"file" mapstructure:"file"`
|
||||
Scope string `yaml:"scope" json:"scope" mapstructure:"scope"`
|
||||
Parallelism int `yaml:"parallelism" json:"parallelism" mapstructure:"parallelism"` // the number of catalog workers to run in parallel
|
||||
Relationships relationshipsConfig `yaml:"relationships" json:"relationships" mapstructure:"relationships"`
|
||||
|
||||
// ecosystem-specific cataloger configuration
|
||||
Golang golangConfig `yaml:"golang" json:"golang" mapstructure:"golang"`
|
||||
Java javaConfig `yaml:"java" json:"java" mapstructure:"java"`
|
||||
JavaScript javaScriptConfig `yaml:"javascript" json:"javascript" mapstructure:"javascript"`
|
||||
LinuxKernel linuxKernelConfig `yaml:"linux-kernel" json:"linux-kernel" mapstructure:"linux-kernel"`
|
||||
Python pythonConfig `yaml:"python" json:"python" mapstructure:"python"`
|
||||
|
||||
// configuration for the source (the subject being analyzed)
|
||||
Registry registryConfig `yaml:"registry" json:"registry" mapstructure:"registry"`
|
||||
Platform string `yaml:"platform" json:"platform" mapstructure:"platform"`
|
||||
Source sourceConfig `yaml:"source" json:"source" mapstructure:"source"`
|
||||
Exclusions []string `yaml:"exclude" json:"exclude" mapstructure:"exclude"`
|
||||
}
|
||||
|
||||
var _ interface {
|
||||
clio.FlagAdder
|
||||
clio.PostLoader
|
||||
fangs.FieldDescriber
|
||||
} = (*Catalog)(nil)
|
||||
|
||||
func DefaultCatalog() Catalog {
|
||||
return Catalog{
|
||||
Scope: source.SquashedScope.String(),
|
||||
Package: defaultPackageConfig(),
|
||||
LinuxKernel: defaultLinuxKernelConfig(),
|
||||
File: defaultFileConfig(),
|
||||
Relationships: defaultRelationshipsConfig(),
|
||||
Source: defaultSourceConfig(),
|
||||
Parallelism: 1,
|
||||
}
|
||||
}
|
||||
|
||||
func (cfg Catalog) ToSBOMConfig(id clio.Identification) *syft.CreateSBOMConfig {
|
||||
return syft.DefaultCreateSBOMConfig().
|
||||
WithTool(id.Name, id.Version).
|
||||
WithParallelism(cfg.Parallelism).
|
||||
WithRelationshipsConfig(cfg.ToRelationshipsConfig()).
|
||||
WithSearchConfig(cfg.ToSearchConfig()).
|
||||
WithPackagesConfig(cfg.ToPackagesConfig()).
|
||||
WithFilesConfig(cfg.ToFilesConfig()).
|
||||
WithCatalogerSelection(
|
||||
pkgcataloging.NewSelectionRequest().
|
||||
WithDefaults(cfg.DefaultCatalogers...).
|
||||
WithExpression(cfg.SelectCatalogers...),
|
||||
)
|
||||
}
|
||||
|
||||
func (cfg Catalog) ToSearchConfig() cataloging.SearchConfig {
|
||||
return cataloging.SearchConfig{
|
||||
Scope: source.ParseScope(cfg.Scope),
|
||||
}
|
||||
}
|
||||
|
||||
func (cfg Catalog) ToRelationshipsConfig() cataloging.RelationshipsConfig {
|
||||
return cataloging.RelationshipsConfig{
|
||||
PackageFileOwnership: cfg.Relationships.PackageFileOwnership,
|
||||
PackageFileOwnershipOverlap: cfg.Relationships.PackageFileOwnershipOverlap,
|
||||
// note: this option was surfaced in the syft application configuration before this relationships section was added
|
||||
ExcludeBinaryPackagesWithFileOwnershipOverlap: cfg.Package.ExcludeBinaryOverlapByOwnership,
|
||||
}
|
||||
}
|
||||
|
||||
func (cfg Catalog) ToFilesConfig() filecataloging.Config {
|
||||
hashers, err := intFile.Hashers(cfg.File.Metadata.Digests...)
|
||||
if err != nil {
|
||||
log.WithFields("error", err).Warn("unable to configure file hashers")
|
||||
}
|
||||
|
||||
return filecataloging.Config{
|
||||
Selection: cfg.File.Metadata.Selection,
|
||||
Hashers: hashers,
|
||||
Content: filecontent.Config{
|
||||
Globs: cfg.File.Content.Globs,
|
||||
SkipFilesAboveSize: cfg.File.Content.SkipFilesAboveSize,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (cfg Catalog) ToPackagesConfig() pkgcataloging.Config {
|
||||
archiveSearch := cataloging.ArchiveSearchConfig{
|
||||
IncludeIndexedArchives: cfg.Package.SearchIndexedArchives,
|
||||
IncludeUnindexedArchives: cfg.Package.SearchUnindexedArchives,
|
||||
}
|
||||
return pkgcataloging.Config{
|
||||
Binary: binary.DefaultCatalogerConfig(),
|
||||
Golang: golang.DefaultCatalogerConfig().
|
||||
WithSearchLocalModCacheLicenses(cfg.Golang.SearchLocalModCacheLicenses).
|
||||
WithLocalModCacheDir(cfg.Golang.LocalModCacheDir).
|
||||
WithSearchRemoteLicenses(cfg.Golang.SearchRemoteLicenses).
|
||||
WithProxy(cfg.Golang.Proxy).
|
||||
WithNoProxy(cfg.Golang.NoProxy),
|
||||
JavaScript: javascript.DefaultCatalogerConfig().
|
||||
WithSearchRemoteLicenses(cfg.JavaScript.SearchRemoteLicenses).
|
||||
WithNpmBaseURL(cfg.JavaScript.NpmBaseURL),
|
||||
LinuxKernel: kernel.LinuxKernelCatalogerConfig{
|
||||
CatalogModules: cfg.LinuxKernel.CatalogModules,
|
||||
},
|
||||
Python: python.CatalogerConfig{
|
||||
GuessUnpinnedRequirements: cfg.Python.GuessUnpinnedRequirements,
|
||||
},
|
||||
JavaArchive: java.DefaultArchiveCatalogerConfig().
|
||||
WithUseNetwork(cfg.Java.UseNetwork).
|
||||
WithMavenBaseURL(cfg.Java.MavenURL).
|
||||
WithArchiveTraversal(archiveSearch, cfg.Java.MaxParentRecursiveDepth),
|
||||
}
|
||||
}
|
||||
|
||||
func (cfg *Catalog) AddFlags(flags clio.FlagSet) {
|
||||
var validScopeValues []string
|
||||
for _, scope := range source.AllScopes {
|
||||
validScopeValues = append(validScopeValues, strcase.ToDelimited(string(scope), '-'))
|
||||
}
|
||||
flags.StringVarP(&cfg.Scope, "scope", "s",
|
||||
fmt.Sprintf("selection of layers to catalog, options=%v", validScopeValues))
|
||||
|
||||
flags.StringVarP(&cfg.Platform, "platform", "",
|
||||
"an optional platform specifier for container image sources (e.g. 'linux/arm64', 'linux/arm64/v8', 'arm64', 'linux')")
|
||||
|
||||
flags.StringArrayVarP(&cfg.Exclusions, "exclude", "",
|
||||
"exclude paths from being scanned using a glob expression")
|
||||
|
||||
flags.StringArrayVarP(&cfg.Catalogers, "catalogers", "",
|
||||
"enable one or more package catalogers")
|
||||
|
||||
if pfp, ok := flags.(fangs.PFlagSetProvider); ok {
|
||||
if err := pfp.PFlagSet().MarkDeprecated("catalogers", "use: override-default-catalogers and select-catalogers"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
} else {
|
||||
panic("unable to mark flags as deprecated")
|
||||
}
|
||||
|
||||
flags.StringArrayVarP(&cfg.DefaultCatalogers, "override-default-catalogers", "",
|
||||
"set the base set of catalogers to use (defaults to 'image' or 'directory' depending on the scan source)")
|
||||
|
||||
flags.StringArrayVarP(&cfg.SelectCatalogers, "select-catalogers", "",
|
||||
"add, remove, and filter the catalogers to be used")
|
||||
|
||||
flags.StringVarP(&cfg.Source.Name, "source-name", "",
|
||||
"set the name of the target being analyzed")
|
||||
|
||||
flags.StringVarP(&cfg.Source.Version, "source-version", "",
|
||||
"set the version of the target being analyzed")
|
||||
|
||||
flags.StringVarP(&cfg.Source.BasePath, "base-path", "",
|
||||
"base directory for scanning, no links will be followed above this directory, and all paths will be reported relative to this directory")
|
||||
}
|
||||
|
||||
func (cfg *Catalog) DescribeFields(descriptions fangs.FieldDescriptionSet) {
|
||||
descriptions.Add(&cfg.Parallelism, "number of cataloger workers to run in parallel")
|
||||
}
|
||||
|
||||
func (cfg *Catalog) PostLoad() error {
|
||||
usingLegacyCatalogers := len(cfg.Catalogers) > 0
|
||||
usingNewCatalogers := len(cfg.DefaultCatalogers) > 0 || len(cfg.SelectCatalogers) > 0
|
||||
|
||||
if usingLegacyCatalogers && usingNewCatalogers {
|
||||
return fmt.Errorf("cannot use both 'catalogers' and 'select-catalogers'/'default-catalogers' flags")
|
||||
}
|
||||
|
||||
flatten := func(l []string) []string {
|
||||
var out []string
|
||||
for _, v := range l {
|
||||
for _, s := range strings.Split(v, ",") {
|
||||
out = append(out, strings.TrimSpace(s))
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
cfg.Catalogers = flatten(cfg.Catalogers)
|
||||
cfg.DefaultCatalogers = flatten(cfg.DefaultCatalogers)
|
||||
cfg.SelectCatalogers = flatten(cfg.SelectCatalogers)
|
||||
|
||||
// for backwards compatibility
|
||||
cfg.DefaultCatalogers = append(cfg.DefaultCatalogers, cfg.Catalogers...)
|
||||
|
||||
s := source.ParseScope(cfg.Scope)
|
||||
if s == source.UnknownScope {
|
||||
return fmt.Errorf("bad scope value %q", cfg.Scope)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCatalog_PostLoad(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
options Catalog
|
||||
assert func(t *testing.T, options Catalog)
|
||||
wantErr assert.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "mutually exclusive cataloger flags (cat / def-cat)",
|
||||
options: Catalog{
|
||||
Catalogers: []string{"foo,bar", "42"},
|
||||
DefaultCatalogers: []string{"some,thing"},
|
||||
Scope: "squashed",
|
||||
},
|
||||
wantErr: assert.Error,
|
||||
},
|
||||
{
|
||||
name: "mutually exclusive cataloger flags (cat / sel-cat)",
|
||||
options: Catalog{
|
||||
Catalogers: []string{"foo,bar", "42"},
|
||||
SelectCatalogers: []string{"some,thing"},
|
||||
Scope: "squashed",
|
||||
},
|
||||
wantErr: assert.Error,
|
||||
},
|
||||
{
|
||||
name: "allow old cataloger flags",
|
||||
options: Catalog{
|
||||
Catalogers: []string{"foo,bar"},
|
||||
Scope: "squashed",
|
||||
},
|
||||
assert: func(t *testing.T, options Catalog) {
|
||||
assert.Equal(t, []string{"bar", "foo"}, options.DefaultCatalogers) // note: sorted order
|
||||
assert.Equal(t, []string{"bar", "foo"}, options.Catalogers) // note: sorted order
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "allow new cataloger flags",
|
||||
options: Catalog{
|
||||
SelectCatalogers: []string{"foo,bar", "42"},
|
||||
DefaultCatalogers: []string{"some,thing"},
|
||||
Scope: "squashed",
|
||||
},
|
||||
assert: func(t *testing.T, options Catalog) {
|
||||
assert.Equal(t, []string{"42", "bar", "foo"}, options.SelectCatalogers) // note: sorted order
|
||||
assert.Equal(t, []string{"some", "thing"}, options.DefaultCatalogers) // note: sorted order
|
||||
assert.Empty(t, options.Catalogers)
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.wantErr == nil {
|
||||
tt.wantErr = assert.NoError
|
||||
}
|
||||
tt.wantErr(t, tt.options.PostLoad(), fmt.Sprintf("PostLoad()"))
|
||||
if tt.assert != nil {
|
||||
tt.assert(t, tt.options)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package options
|
||||
|
||||
import "github.com/anchore/fangs"
|
||||
|
||||
// Config holds a reference to the specific config file that was used to load application configuration
|
||||
type Config struct {
|
||||
ConfigFile string `yaml:"config" json:"config" mapstructure:"config"`
|
||||
}
|
||||
|
||||
func (cfg *Config) DescribeFields(descriptions fangs.FieldDescriptionSet) {
|
||||
descriptions.Add(&cfg.ConfigFile, "the configuration file that was used to load application configuration")
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/scylladb/go-set/strset"
|
||||
|
||||
intFile "github.com/anchore/syft/internal/file"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
)
|
||||
|
||||
type fileConfig struct {
|
||||
Metadata fileMetadata `yaml:"metadata" json:"metadata" mapstructure:"metadata"`
|
||||
Content fileContent `yaml:"content" json:"content" mapstructure:"content"`
|
||||
}
|
||||
|
||||
type fileMetadata struct {
|
||||
Selection file.Selection `yaml:"selection" json:"selection" mapstructure:"selection"`
|
||||
Digests []string `yaml:"digests" json:"digests" mapstructure:"digests"`
|
||||
}
|
||||
|
||||
type fileContent struct {
|
||||
SkipFilesAboveSize int64 `yaml:"skip-files-above-size" json:"skip-files-above-size" mapstructure:"skip-files-above-size"`
|
||||
Globs []string `yaml:"globs" json:"globs" mapstructure:"globs"`
|
||||
}
|
||||
|
||||
func defaultFileConfig() fileConfig {
|
||||
return fileConfig{
|
||||
Metadata: fileMetadata{
|
||||
Selection: file.FilesOwnedByPackageSelection,
|
||||
Digests: []string{"sha1", "sha256"},
|
||||
},
|
||||
Content: fileContent{
|
||||
SkipFilesAboveSize: 250 * intFile.KB,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
return fmt.Errorf("invalid file metadata selection: %q", c.Metadata.Selection)
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package options
|
||||
|
||||
type golang struct {
|
||||
type golangConfig struct {
|
||||
SearchLocalModCacheLicenses bool `json:"search-local-mod-cache-licenses" yaml:"search-local-mod-cache-licenses" mapstructure:"search-local-mod-cache-licenses"`
|
||||
LocalModCacheDir string `json:"local-mod-cache-dir" yaml:"local-mod-cache-dir" mapstructure:"local-mod-cache-dir"`
|
||||
SearchRemoteLicenses bool `json:"search-remote-licenses" yaml:"search-remote-licenses" mapstructure:"search-remote-licenses"`
|
||||
@@ -1,6 +1,6 @@
|
||||
package options
|
||||
|
||||
type java struct {
|
||||
type javaConfig struct {
|
||||
UseNetwork bool `yaml:"use-network" json:"use-network" mapstructure:"use-network"`
|
||||
MavenURL string `yaml:"maven-url" json:"maven-url" mapstructure:"maven-url"`
|
||||
MaxParentRecursiveDepth int `yaml:"max-parent-recursive-depth" json:"max-parent-recursive-depth" mapstructure:"max-parent-recursive-depth"`
|
||||
@@ -1,6 +1,6 @@
|
||||
package options
|
||||
|
||||
type javascript struct {
|
||||
type javaScriptConfig struct {
|
||||
SearchRemoteLicenses bool `json:"search-remote-licenses" yaml:"search-remote-licenses" mapstructure:"search-remote-licenses"`
|
||||
NpmBaseURL string `json:"npm-base-url" yaml:"npm-base-url" mapstructure:"npm-base-url"`
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
package options
|
||||
|
||||
type linuxKernel struct {
|
||||
type linuxKernelConfig struct {
|
||||
CatalogModules bool `json:"catalog-modules" yaml:"catalog-modules" mapstructure:"catalog-modules"`
|
||||
}
|
||||
|
||||
func defaultLinuxKernel() linuxKernel {
|
||||
return linuxKernel{
|
||||
func defaultLinuxKernelConfig() linuxKernelConfig {
|
||||
return linuxKernelConfig{
|
||||
CatalogModules: true,
|
||||
}
|
||||
}
|
||||
@@ -96,7 +96,7 @@ func (o Output) SBOMWriter() (sbom.Writer, error) {
|
||||
}
|
||||
}
|
||||
|
||||
return makeSBOMWriter(o.Outputs, o.File, encoders)
|
||||
return makeSBOMWriter(o.Outputs, o.LegacyFile, encoders)
|
||||
}
|
||||
|
||||
func (o Output) OutputNameSet() *strset.Set {
|
||||
@@ -1,6 +1,10 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/fangs"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
@@ -13,13 +17,13 @@ var _ interface {
|
||||
|
||||
// Deprecated: OutputFile supports the --file to write the SBOM output to
|
||||
type OutputFile struct {
|
||||
Enabled bool `yaml:"-" json:"-" mapstructure:"-"`
|
||||
File string `yaml:"file" json:"file" mapstructure:"file"`
|
||||
Enabled bool `yaml:"-" json:"-" mapstructure:"-"`
|
||||
LegacyFile string `yaml:"-" json:"-" mapstructure:"legacyFile"`
|
||||
}
|
||||
|
||||
func (o *OutputFile) AddFlags(flags clio.FlagSet) {
|
||||
if o.Enabled {
|
||||
flags.StringVarP(&o.File, "file", "",
|
||||
flags.StringVarP(&o.LegacyFile, "file", "",
|
||||
"file to write the default report output to (default is STDOUT)")
|
||||
|
||||
if pfp, ok := flags.(fangs.PFlagSetProvider); ok {
|
||||
@@ -33,12 +37,12 @@ func (o *OutputFile) PostLoad() error {
|
||||
if !o.Enabled {
|
||||
return nil
|
||||
}
|
||||
if o.File != "" {
|
||||
file, err := expandFilePath(o.File)
|
||||
if o.LegacyFile != "" {
|
||||
file, err := expandFilePath(o.LegacyFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.File = file
|
||||
o.LegacyFile = file
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -47,10 +51,21 @@ func (o *OutputFile) SBOMWriter(f sbom.FormatEncoder) (sbom.Writer, error) {
|
||||
if !o.Enabled {
|
||||
return nil, nil
|
||||
}
|
||||
writer, err := newSBOMMultiWriter(newSBOMWriterDescription(f, o.File))
|
||||
writer, err := newSBOMMultiWriter(newSBOMWriterDescription(f, o.LegacyFile))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return writer, nil
|
||||
}
|
||||
|
||||
func expandFilePath(file string) (string, error) {
|
||||
if file != "" {
|
||||
expandedPath, err := homedir.Expand(file)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to expand file path=%q: %w", file, err)
|
||||
}
|
||||
file = expandedPath
|
||||
}
|
||||
return file, nil
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package options
|
||||
|
||||
import "github.com/anchore/syft/syft/cataloging"
|
||||
|
||||
type packageConfig struct {
|
||||
SearchUnindexedArchives bool `yaml:"search-unindexed-archives" json:"search-unindexed-archives" mapstructure:"search-unindexed-archives"`
|
||||
SearchIndexedArchives bool `yaml:"search-indexed-archives" json:"search-indexed-archives" mapstructure:"search-indexed-archives"`
|
||||
ExcludeBinaryOverlapByOwnership bool `yaml:"exclude-binary-overlap-by-ownership" json:"exclude-binary-overlap-by-ownership" mapstructure:"exclude-binary-overlap-by-ownership"` // exclude synthetic binary packages owned by os package files
|
||||
}
|
||||
|
||||
func defaultPackageConfig() packageConfig {
|
||||
c := cataloging.DefaultArchiveSearchConfig()
|
||||
return packageConfig{
|
||||
SearchIndexedArchives: c.IncludeIndexedArchives,
|
||||
SearchUnindexedArchives: c.IncludeUnindexedArchives,
|
||||
ExcludeBinaryOverlapByOwnership: true,
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
package options
|
||||
|
||||
type python struct {
|
||||
type pythonConfig struct {
|
||||
GuessUnpinnedRequirements bool `json:"guess-unpinned-requirements" yaml:"guess-unpinned-requirements" mapstructure:"guess-unpinned-requirements"`
|
||||
}
|
||||
@@ -18,16 +18,16 @@ type RegistryCredentials struct {
|
||||
TLSKey string `yaml:"tls-key,omitempty" json:"tls-key,omitempty" mapstructure:"tls-key"`
|
||||
}
|
||||
|
||||
type registry struct {
|
||||
type registryConfig struct {
|
||||
InsecureSkipTLSVerify bool `yaml:"insecure-skip-tls-verify" json:"insecure-skip-tls-verify" mapstructure:"insecure-skip-tls-verify"`
|
||||
InsecureUseHTTP bool `yaml:"insecure-use-http" json:"insecure-use-http" mapstructure:"insecure-use-http"`
|
||||
Auth []RegistryCredentials `yaml:"auth" json:"auth" mapstructure:"auth"`
|
||||
CACert string `yaml:"ca-cert" json:"ca-cert" mapstructure:"ca-cert"`
|
||||
}
|
||||
|
||||
var _ clio.PostLoader = (*registry)(nil)
|
||||
var _ clio.PostLoader = (*registryConfig)(nil)
|
||||
|
||||
func (cfg *registry) PostLoad() error {
|
||||
func (cfg *registryConfig) PostLoad() error {
|
||||
// there may be additional credentials provided by env var that should be appended to the set of credentials
|
||||
authority, username, password, token, tlsCert, tlsKey :=
|
||||
os.Getenv("SYFT_REGISTRY_AUTH_AUTHORITY"),
|
||||
@@ -62,7 +62,7 @@ func hasNonEmptyCredentials(username, password, token, tlsCert, tlsKey string) b
|
||||
return hasUserPass || hasToken || hasTLSMaterial
|
||||
}
|
||||
|
||||
func (cfg *registry) ToOptions() *image.RegistryOptions {
|
||||
func (cfg *registryConfig) ToOptions() *image.RegistryOptions {
|
||||
var auth = make([]image.RegistryCredentials, len(cfg.Auth))
|
||||
for i, a := range cfg.Auth {
|
||||
auth[i] = image.RegistryCredentials{
|
||||
@@ -72,19 +72,19 @@ func TestHasNonEmptyCredentials(t *testing.T) {
|
||||
func Test_registry_ToOptions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input registry
|
||||
input registryConfig
|
||||
expected image.RegistryOptions
|
||||
}{
|
||||
{
|
||||
name: "no registry options",
|
||||
input: registry{},
|
||||
input: registryConfig{},
|
||||
expected: image.RegistryOptions{
|
||||
Credentials: []image.RegistryCredentials{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set InsecureSkipTLSVerify",
|
||||
input: registry{
|
||||
input: registryConfig{
|
||||
InsecureSkipTLSVerify: true,
|
||||
},
|
||||
expected: image.RegistryOptions{
|
||||
@@ -94,7 +94,7 @@ func Test_registry_ToOptions(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "set InsecureUseHTTP",
|
||||
input: registry{
|
||||
input: registryConfig{
|
||||
InsecureUseHTTP: true,
|
||||
},
|
||||
expected: image.RegistryOptions{
|
||||
@@ -104,7 +104,7 @@ func Test_registry_ToOptions(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "set all bool options",
|
||||
input: registry{
|
||||
input: registryConfig{
|
||||
InsecureSkipTLSVerify: true,
|
||||
InsecureUseHTTP: true,
|
||||
},
|
||||
@@ -116,7 +116,7 @@ func Test_registry_ToOptions(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "provide all tls configuration",
|
||||
input: registry{
|
||||
input: registryConfig{
|
||||
CACert: "ca.crt",
|
||||
InsecureSkipTLSVerify: true,
|
||||
Auth: []RegistryCredentials{
|
||||
@@ -0,0 +1,22 @@
|
||||
package options
|
||||
|
||||
import "github.com/anchore/fangs"
|
||||
|
||||
var _ fangs.FieldDescriber = (*relationshipsConfig)(nil)
|
||||
|
||||
type relationshipsConfig struct {
|
||||
PackageFileOwnership bool `mapstructure:"package-file-ownership" json:"package-file-ownership" yaml:"package-file-ownership"`
|
||||
PackageFileOwnershipOverlap bool `mapstructure:"package-file-ownership-overlap" json:"package-file-ownership-overlap" yaml:"package-file-ownership-overlap"`
|
||||
}
|
||||
|
||||
func defaultRelationshipsConfig() relationshipsConfig {
|
||||
return relationshipsConfig{
|
||||
PackageFileOwnership: true,
|
||||
PackageFileOwnershipOverlap: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *relationshipsConfig) DescribeFields(descriptions fangs.FieldDescriptionSet) {
|
||||
descriptions.Add(&r.PackageFileOwnership, "include package-to-file relationships that indicate which files are owned by which packages.")
|
||||
descriptions.Add(&r.PackageFileOwnershipOverlap, "include package-to-package relationships that indicate one package is owned by another due to files claimed to be owned by one package are also evidence of another package's existence.")
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/scylladb/go-set/strset"
|
||||
)
|
||||
|
||||
type sourceConfig struct {
|
||||
Name string `json:"name" yaml:"name" mapstructure:"name"`
|
||||
Version string `json:"version" yaml:"version" mapstructure:"version"`
|
||||
BasePath string `yaml:"base-path" json:"base-path" mapstructure:"base-path"` // specify base path for all file paths
|
||||
File fileSource `json:"file" yaml:"file" mapstructure:"file"`
|
||||
Image imageSource `json:"image" yaml:"image" mapstructure:"image"`
|
||||
}
|
||||
|
||||
type fileSource struct {
|
||||
Digests []string `json:"digests" yaml:"digests" mapstructure:"digests"`
|
||||
}
|
||||
|
||||
type imageSource struct {
|
||||
DefaultPullSource string `json:"default-pull-source" yaml:"default-pull-source" mapstructure:"default-pull-source"`
|
||||
}
|
||||
|
||||
func defaultSourceConfig() sourceConfig {
|
||||
return sourceConfig{
|
||||
File: fileSource{
|
||||
Digests: []string{"sha256"},
|
||||
},
|
||||
Image: imageSource{
|
||||
DefaultPullSource: "",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
var validDefaultSourceValues = []string{"registry", "docker", "podman", ""}
|
||||
|
||||
func checkDefaultSourceValues(source string) error {
|
||||
validValues := strset.New(validDefaultSourceValues...)
|
||||
if !validValues.Has(source) {
|
||||
validValuesString := strings.Join(validDefaultSourceValues, ", ")
|
||||
return fmt.Errorf("%s is not a valid default source; please use one of the following: %s''", source, validValuesString)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -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,5 +1,7 @@
|
||||
package options
|
||||
|
||||
import "github.com/anchore/fangs"
|
||||
|
||||
type UpdateCheck struct {
|
||||
CheckForAppUpdate bool `yaml:"check-for-app-update" json:"check-for-app-update" mapstructure:"check-for-app-update"` // whether to check for an application update on start up or not
|
||||
}
|
||||
@@ -9,3 +11,7 @@ func DefaultUpdateCheck() UpdateCheck {
|
||||
CheckForAppUpdate: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (cfg *UpdateCheck) DescribeFields(descriptions fangs.FieldDescriptionSet) {
|
||||
descriptions.Add(&cfg.CheckForAppUpdate, "whether to check for an application update on start up or not")
|
||||
}
|
||||
+2
-2
@@ -8,8 +8,8 @@ import (
|
||||
|
||||
func Test_AllLayersIncludesSquashed(t *testing.T) {
|
||||
// This is a verification test for issue #894 (https://github.com/anchore/syft/issues/894)
|
||||
allLayers, _ := catalogFixtureImage(t, "image-suse-all-layers", source.AllLayersScope, nil)
|
||||
squashed, _ := catalogFixtureImage(t, "image-suse-all-layers", source.SquashedScope, nil)
|
||||
allLayers, _ := catalogFixtureImage(t, "image-suse-all-layers", source.AllLayersScope)
|
||||
squashed, _ := catalogFixtureImage(t, "image-suse-all-layers", source.SquashedScope)
|
||||
|
||||
lenAllLayers := len(allLayers.Artifacts.Packages.Sorted())
|
||||
lenSquashed := len(squashed.Artifacts.Packages.Sorted())
|
||||
+19
-53
@@ -1,6 +1,7 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -9,53 +10,39 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
"github.com/anchore/syft/syft/linux"
|
||||
"github.com/anchore/syft/syft"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
func BenchmarkImagePackageCatalogers(b *testing.B) {
|
||||
// get the fixture image tar file
|
||||
fixtureImageName := "image-pkg-coverage"
|
||||
imagetest.GetFixtureImage(b, "docker-archive", fixtureImageName)
|
||||
tarPath := imagetest.GetFixtureImageTarPath(b, fixtureImageName)
|
||||
|
||||
var pc *pkg.Collection
|
||||
for _, c := range cataloger.ImageCatalogers(cataloger.DefaultConfig()) {
|
||||
// in case of future alteration where state is persisted, assume no dependency is safe to reuse
|
||||
userInput := "docker-archive:" + tarPath
|
||||
detection, err := source.Detect(userInput, source.DefaultDetectConfig())
|
||||
require.NoError(b, err)
|
||||
theSource, err := detection.NewSource(source.DefaultDetectionSourceConfig())
|
||||
if err != nil {
|
||||
b.Fatalf("unable to get source: %+v", err)
|
||||
}
|
||||
b.Cleanup(func() {
|
||||
theSource.Close()
|
||||
})
|
||||
// get the source object for the image
|
||||
userInput := "docker-archive:" + tarPath
|
||||
detection, err := source.Detect(userInput, source.DefaultDetectConfig())
|
||||
require.NoError(b, err)
|
||||
|
||||
resolver, err := theSource.FileResolver(source.SquashedScope)
|
||||
if err != nil {
|
||||
b.Fatalf("unable to get resolver: %+v", err)
|
||||
}
|
||||
theSource, err := detection.NewSource(source.DefaultDetectionSourceConfig())
|
||||
require.NoError(b, err)
|
||||
|
||||
theDistro := linux.IdentifyRelease(resolver)
|
||||
b.Cleanup(func() {
|
||||
theSource.Close()
|
||||
})
|
||||
|
||||
b.Run(c.Name(), func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
pc, _, err = cataloger.Catalog(resolver, theDistro, 1, c)
|
||||
if err != nil {
|
||||
b.Fatalf("failure during benchmark: %+v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
// build the SBOM
|
||||
s, err := syft.CreateSBOM(context.Background(), theSource, syft.DefaultCreateSBOMConfig())
|
||||
|
||||
b.Logf("catalog for %q number of packages: %d", c.Name(), pc.PackageCount())
|
||||
}
|
||||
// did it work?
|
||||
require.NoError(b, err)
|
||||
require.NotNil(b, s)
|
||||
}
|
||||
|
||||
func TestPkgCoverageImage(t *testing.T) {
|
||||
sbom, _ := catalogFixtureImage(t, "image-pkg-coverage", source.SquashedScope, nil)
|
||||
sbom, _ := catalogFixtureImage(t, "image-pkg-coverage", source.SquashedScope)
|
||||
|
||||
observedLanguages := strset.New()
|
||||
definedLanguages := strset.New()
|
||||
@@ -250,29 +237,8 @@ func TestPkgCoverageDirectory(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPkgCoverageCatalogerConfiguration(t *testing.T) {
|
||||
// Check that cataloger configuration can be used to run a cataloger on a source
|
||||
// for which that cataloger isn't enabled by defauly
|
||||
sbom, _ := catalogFixtureImage(t, "image-pkg-coverage", source.SquashedScope, []string{"rust"})
|
||||
|
||||
observedLanguages := strset.New()
|
||||
definedLanguages := strset.New()
|
||||
definedLanguages.Add("rust")
|
||||
|
||||
for actualPkg := range sbom.Artifacts.Packages.Enumerate() {
|
||||
observedLanguages.Add(actualPkg.Language.String())
|
||||
}
|
||||
|
||||
assert.Equal(t, definedLanguages, observedLanguages)
|
||||
|
||||
// Verify that rust isn't actually an image cataloger
|
||||
c := cataloger.DefaultConfig()
|
||||
c.Catalogers = []string{"rust"}
|
||||
assert.Len(t, cataloger.ImageCatalogers(c), 0)
|
||||
}
|
||||
|
||||
func TestPkgCoverageImage_HasEvidence(t *testing.T) {
|
||||
sbom, _ := catalogFixtureImage(t, "image-pkg-coverage", source.SquashedScope, nil)
|
||||
sbom, _ := catalogFixtureImage(t, "image-pkg-coverage", source.SquashedScope)
|
||||
|
||||
var cases []testCase
|
||||
cases = append(cases, commonTestCases...)
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
func TestDistroImage(t *testing.T) {
|
||||
sbom, _ := catalogFixtureImage(t, "image-distro-id", source.SquashedScope, nil)
|
||||
sbom, _ := catalogFixtureImage(t, "image-distro-id", source.SquashedScope)
|
||||
|
||||
expected := &linux.Release{
|
||||
PrettyName: "BusyBox v1.31.1",
|
||||
+10
-11
@@ -10,12 +10,11 @@ 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"
|
||||
"github.com/anchore/syft/syft/format/syftjson"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
@@ -32,12 +31,12 @@ func TestEncodeDecodeEncodeCycleComparison(t *testing.T) {
|
||||
"image-owning-package",
|
||||
}
|
||||
tests := []struct {
|
||||
formatOption sbom.FormatID
|
||||
redactor func(in []byte) []byte
|
||||
json bool
|
||||
name string
|
||||
redactor func(in []byte) []byte
|
||||
json bool
|
||||
}{
|
||||
{
|
||||
formatOption: syftjson.ID,
|
||||
name: syftjson.ID.String(),
|
||||
redactor: func(in []byte) []byte {
|
||||
// no redactions necessary
|
||||
return in
|
||||
@@ -45,7 +44,7 @@ func TestEncodeDecodeEncodeCycleComparison(t *testing.T) {
|
||||
json: true,
|
||||
},
|
||||
{
|
||||
formatOption: cyclonedxjson.ID,
|
||||
name: cyclonedxjson.ID.String(),
|
||||
redactor: func(in []byte) []byte {
|
||||
// unstable values
|
||||
in = regexp.MustCompile(`"(timestamp|serialNumber|bom-ref|ref)":\s*"(\n|[^"])+"`).ReplaceAll(in, []byte(`"$1": "redacted"`))
|
||||
@@ -55,7 +54,7 @@ func TestEncodeDecodeEncodeCycleComparison(t *testing.T) {
|
||||
json: true,
|
||||
},
|
||||
{
|
||||
formatOption: cyclonedxxml.ID,
|
||||
name: cyclonedxxml.ID.String(),
|
||||
redactor: func(in []byte) []byte {
|
||||
// unstable values
|
||||
in = regexp.MustCompile(`(serialNumber|bom-ref|ref)="[^"]+"`).ReplaceAll(in, []byte{})
|
||||
@@ -75,11 +74,11 @@ func TestEncodeDecodeEncodeCycleComparison(t *testing.T) {
|
||||
decoders := format.NewDecoderCollection(format.Decoders()...)
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(string(test.formatOption), func(t *testing.T) {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
for _, image := range images {
|
||||
originalSBOM, _ := catalogFixtureImage(t, image, source.SquashedScope, nil)
|
||||
originalSBOM, _ := catalogFixtureImage(t, image, source.SquashedScope)
|
||||
|
||||
f := encoders.GetByString(string(test.formatOption))
|
||||
f := encoders.GetByString(test.name)
|
||||
require.NotNil(t, f)
|
||||
|
||||
var buff1 bytes.Buffer
|
||||
@@ -0,0 +1,154 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/clio"
|
||||
stereoscopeFile "github.com/anchore/stereoscope/pkg/file"
|
||||
"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"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
func TestFileCataloging_Default(t *testing.T) {
|
||||
cfg := options.DefaultCatalog().ToSBOMConfig(clio.Identification{})
|
||||
cfg = cfg.WithFilesConfig(filecataloging.DefaultConfig())
|
||||
sbom, _ := catalogDirectoryWithConfig(t, "test-fixtures/files", cfg)
|
||||
|
||||
var metadata map[file.Coordinates]file.Metadata
|
||||
|
||||
var digests map[file.Coordinates][]file.Digest
|
||||
|
||||
var contents map[file.Coordinates]string
|
||||
|
||||
assertFileData(t, metadata, digests, contents, sbom)
|
||||
}
|
||||
|
||||
func TestFileCataloging_AllFiles(t *testing.T) {
|
||||
cfg := options.DefaultCatalog().ToSBOMConfig(clio.Identification{})
|
||||
cfg = cfg.WithFilesConfig(filecataloging.Config{
|
||||
Selection: file.AllFilesSelection,
|
||||
Hashers: []crypto.Hash{
|
||||
crypto.SHA256,
|
||||
},
|
||||
Content: filecontent.Config{
|
||||
// this is enough to potentially capture a/file, a-small-file, a-symlink-to-a-small-file, and a-symlink-to-file
|
||||
// but the size of a/file will cause it to be filtered, and the symlinks will not be included since
|
||||
// they are not regular files
|
||||
Globs: []string{"**/*file"},
|
||||
SkipFilesAboveSize: 30,
|
||||
},
|
||||
})
|
||||
sbom, _ := catalogDirectoryWithConfig(t, "test-fixtures/files", cfg)
|
||||
|
||||
pwd, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
|
||||
testPath := func(path string) string {
|
||||
return filepath.Join(pwd, "test-fixtures/files", path)
|
||||
}
|
||||
|
||||
metadata := map[file.Coordinates]file.Metadata{
|
||||
{RealPath: ""}: {
|
||||
Path: testPath(""),
|
||||
Type: stereoscopeFile.TypeDirectory,
|
||||
},
|
||||
{RealPath: "/somewhere"}: {
|
||||
Path: testPath("/somewhere"),
|
||||
Type: stereoscopeFile.TypeDirectory,
|
||||
},
|
||||
{RealPath: "/somewhere/there"}: {
|
||||
Path: testPath("/somewhere/there"),
|
||||
Type: stereoscopeFile.TypeDirectory,
|
||||
},
|
||||
{RealPath: "/somewhere/there/is"}: {
|
||||
Path: testPath("/somewhere/there/is"),
|
||||
Type: stereoscopeFile.TypeDirectory,
|
||||
},
|
||||
{RealPath: "/somewhere/there/is/a"}: {
|
||||
Path: testPath("/somewhere/there/is/a"),
|
||||
Type: stereoscopeFile.TypeDirectory,
|
||||
},
|
||||
{RealPath: "/somewhere/there/is/a-small-file"}: {
|
||||
Path: testPath("/somewhere/there/is/a-small-file"),
|
||||
Type: stereoscopeFile.TypeRegular,
|
||||
MIMEType: "text/plain",
|
||||
},
|
||||
{RealPath: "/somewhere/there/is/a-symlink-to-a-small-file"}: {
|
||||
Path: testPath("/somewhere/there/is/a-symlink-to-a-small-file"),
|
||||
LinkDestination: testPath("/somewhere/there/is/a-small-file"),
|
||||
Type: stereoscopeFile.TypeSymLink,
|
||||
},
|
||||
{RealPath: "/somewhere/there/is/a-symlink-to-file"}: {
|
||||
Path: testPath("/somewhere/there/is/a-symlink-to-file"),
|
||||
LinkDestination: testPath("/somewhere/there/is/a/file"),
|
||||
Type: stereoscopeFile.TypeSymLink,
|
||||
},
|
||||
{RealPath: "/somewhere/there/is/a/file"}: {
|
||||
Path: testPath("/somewhere/there/is/a/file"),
|
||||
Type: stereoscopeFile.TypeRegular,
|
||||
MIMEType: "text/plain",
|
||||
},
|
||||
}
|
||||
|
||||
digests := map[file.Coordinates][]file.Digest{
|
||||
{RealPath: "/somewhere/there/is/a-small-file"}: {
|
||||
file.Digest{Algorithm: "sha256", Value: "672c23470e4ce99cf270bb63ae66ad2b8a80aa19090c40e59fbb1229a4ab661a"},
|
||||
},
|
||||
{RealPath: "/somewhere/there/is/a/file"}: {
|
||||
file.Digest{Algorithm: "sha256", Value: "00dac26d6d94353ac0d92bb9640cba76f82f5ca8707bb845ecdc574bd002348e"},
|
||||
},
|
||||
}
|
||||
|
||||
contents := map[file.Coordinates]string{
|
||||
{RealPath: "/somewhere/there/is/a-small-file"}: "c29tZSBjb250ZW50cyE=",
|
||||
}
|
||||
|
||||
assertFileData(t, metadata, digests, contents, sbom)
|
||||
|
||||
}
|
||||
|
||||
func assertFileData(t testing.TB, metadata map[file.Coordinates]file.Metadata, digests map[file.Coordinates][]file.Digest, contents map[file.Coordinates]string, sbom sbom.SBOM) {
|
||||
metadataCompareOpts := cmp.Options{
|
||||
cmp.Comparer(func(x, y file.Metadata) bool {
|
||||
if x.Path != y.Path {
|
||||
t.Logf("path mismatch: %s != %s", x.Path, y.Path)
|
||||
return false
|
||||
}
|
||||
|
||||
if x.Type != y.Type {
|
||||
t.Logf("type mismatch: %s != %s", x.Type, y.Type)
|
||||
return false
|
||||
}
|
||||
|
||||
if x.LinkDestination != y.LinkDestination {
|
||||
t.Logf("link destination mismatch: %s != %s", x.LinkDestination, y.LinkDestination)
|
||||
return false
|
||||
}
|
||||
|
||||
if x.MIMEType != y.MIMEType {
|
||||
t.Logf("mime type mismatch: %s != %s", x.MIMEType, y.MIMEType)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}),
|
||||
}
|
||||
|
||||
if d := cmp.Diff(metadata, sbom.Artifacts.FileMetadata, metadataCompareOpts...); d != "" {
|
||||
t.Errorf("unexpected metadata (-want +got):\n%s", d)
|
||||
}
|
||||
|
||||
assert.Equal(t, digests, sbom.Artifacts.FileDigests, "different digests detected")
|
||||
assert.Equal(t, contents, sbom.Artifacts.FileContents, "different contents detected")
|
||||
|
||||
}
|
||||
+1
-1
@@ -25,7 +25,7 @@ func TestGolangCompilerDetection(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
sbom, _ := catalogFixtureImage(t, tt.image, source.SquashedScope, nil)
|
||||
sbom, _ := catalogFixtureImage(t, tt.image, source.SquashedScope)
|
||||
packages := sbom.Artifacts.Packages.PackagesByName("stdlib")
|
||||
|
||||
foundCompilerVersions := make(map[string]struct{})
|
||||
+1
-1
@@ -11,7 +11,7 @@ import (
|
||||
)
|
||||
|
||||
func TestJavaPURLs(t *testing.T) {
|
||||
sbom, _ := catalogFixtureImage(t, "image-test-java-purls", source.SquashedScope, nil)
|
||||
sbom, _ := catalogFixtureImage(t, "image-test-java-purls", source.SquashedScope)
|
||||
found := make(map[string]string)
|
||||
for _, p := range sbom.Artifacts.Packages.Sorted() {
|
||||
if p.Type != pkg.JavaPkg && p.Type != pkg.JenkinsPluginPkg {
|
||||
+1
-1
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
func TestMarinerDistroless(t *testing.T) {
|
||||
sbom, _ := catalogFixtureImage(t, "image-mariner-distroless", source.SquashedScope, nil)
|
||||
sbom, _ := catalogFixtureImage(t, "image-mariner-distroless", source.SquashedScope)
|
||||
|
||||
expectedPkgs := 12
|
||||
actualPkgs := 0
|
||||
@@ -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
|
||||
}
|
||||
+1
-1
@@ -63,7 +63,7 @@ func TestPackageDeduplication(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(string(tt.scope), func(t *testing.T) {
|
||||
sbom, _ := catalogFixtureImage(t, "image-vertical-package-dups", tt.scope, nil)
|
||||
sbom, _ := catalogFixtureImage(t, "image-vertical-package-dups", tt.scope)
|
||||
for _, p := range sbom.Artifacts.Packages.Sorted() {
|
||||
if p.Type == pkg.BinaryPkg {
|
||||
assert.NotEmpty(t, p.Name)
|
||||
+2
-2
@@ -25,7 +25,7 @@ func TestPackageOwnershipRelationships(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.fixture, func(t *testing.T) {
|
||||
sbom, _ := catalogFixtureImage(t, test.fixture, source.SquashedScope, nil)
|
||||
sbom, _ := catalogFixtureImage(t, test.fixture, source.SquashedScope)
|
||||
|
||||
output := bytes.NewBufferString("")
|
||||
err := syftjson.NewFormatEncoder().Encode(output, sbom)
|
||||
@@ -60,7 +60,7 @@ func TestPackageOwnershipExclusions(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.fixture, func(t *testing.T) {
|
||||
sbom, _ := catalogFixtureImage(t, test.fixture, source.SquashedScope, nil)
|
||||
sbom, _ := catalogFixtureImage(t, test.fixture, source.SquashedScope)
|
||||
binaryPackages := make([]pkg.Package, 0)
|
||||
apkPackages := make([]pkg.Package, 0)
|
||||
for p := range sbom.Artifacts.Packages.Enumerate() {
|
||||
+1
-1
@@ -10,7 +10,7 @@ import (
|
||||
func TestRegression212ApkBufferSize(t *testing.T) {
|
||||
// This is a regression test for issue #212 (https://github.com/anchore/syft/issues/212) in which the apk db could
|
||||
// not be processed due to a scanner buffer that was too small
|
||||
sbom, _ := catalogFixtureImage(t, "image-large-apk-data", source.SquashedScope, nil)
|
||||
sbom, _ := catalogFixtureImage(t, "image-large-apk-data", source.SquashedScope)
|
||||
|
||||
expectedPkgs := 58
|
||||
actualPkgs := 0
|
||||
+1
-1
@@ -16,7 +16,7 @@ func TestRegressionGoArchDiscovery(t *testing.T) {
|
||||
)
|
||||
// This is a regression test to make sure the way we detect go binary packages
|
||||
// stays consistent and reproducible as the tool chain evolves
|
||||
sbom, _ := catalogFixtureImage(t, "image-go-bin-arch-coverage", source.SquashedScope, nil)
|
||||
sbom, _ := catalogFixtureImage(t, "image-go-bin-arch-coverage", source.SquashedScope)
|
||||
|
||||
var actualELF, actualWIN, actualMACOS int
|
||||
|
||||
+1
-1
@@ -7,5 +7,5 @@ import (
|
||||
)
|
||||
|
||||
func TestRegressionJavaNoMainPackage(t *testing.T) { // Regression: https://github.com/anchore/syft/issues/252
|
||||
catalogFixtureImage(t, "image-java-no-main-package", source.SquashedScope, nil)
|
||||
catalogFixtureImage(t, "image-java-no-main-package", source.SquashedScope)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user