mirror of
https://github.com/anchore/syft.git
synced 2026-10-12 06:07:20 +02:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e75fcf49d |
+5
-5
@@ -1,7 +1,7 @@
|
||||
tools:
|
||||
- name: binny
|
||||
version:
|
||||
want: v0.6.3
|
||||
want: v0.6.2
|
||||
method: github-release
|
||||
with:
|
||||
repo: anchore/binny
|
||||
@@ -29,7 +29,7 @@ tools:
|
||||
|
||||
- name: cosign
|
||||
version:
|
||||
want: v2.2.2
|
||||
want: v2.2.1
|
||||
method: github-release
|
||||
with:
|
||||
repo: sigstore/cosign
|
||||
@@ -43,7 +43,7 @@ tools:
|
||||
|
||||
- name: goreleaser
|
||||
version:
|
||||
want: v1.23.0
|
||||
want: v1.22.1
|
||||
method: github-release
|
||||
with:
|
||||
repo: goreleaser/goreleaser
|
||||
@@ -83,14 +83,14 @@ tools:
|
||||
|
||||
- name: task
|
||||
version:
|
||||
want: v3.33.1
|
||||
want: v3.31.0
|
||||
method: github-release
|
||||
with:
|
||||
repo: go-task/task
|
||||
|
||||
- name: gh
|
||||
version:
|
||||
want: v2.42.0
|
||||
want: v2.39.2
|
||||
method: github-release
|
||||
with:
|
||||
repo: cli/cli
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
#!/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@e12d46a63a90f2fae62d114769bbf2a179198b5c #v3.3.3
|
||||
uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2
|
||||
with:
|
||||
path: test/results/benchmark-main.txt
|
||||
# use base sha for PR or new commit hash for main push in benchmark result key
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
OUTPUT="${OUTPUT//$'\r'/'%0D'}" # URL encode all '\r' characters
|
||||
echo "result=$OUTPUT" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/upload-artifact@1eb3cb2b3e0f29609092a73eb033bb759a334595 # v4.1.0
|
||||
- uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||
with:
|
||||
name: benchmark-test-results
|
||||
path: test/results/**/*
|
||||
|
||||
@@ -39,13 +39,13 @@ jobs:
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 #v5.0.0
|
||||
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe #v4.1.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@e5f05b81d5b6ff8cfa111c80c22c5fd02a384118 #v3.23.0
|
||||
uses: github/codeql-action/init@407ffafae6a767df3e0230c3df91b6443ae8df75 #v2.22.8
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@e5f05b81d5b6ff8cfa111c80c22c5fd02a384118 #v3.23.0
|
||||
uses: github/codeql-action/autobuild@407ffafae6a767df3e0230c3df91b6443ae8df75 #v2.22.8
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -70,4 +70,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@e5f05b81d5b6ff8cfa111c80c22c5fd02a384118 #v3.23.0
|
||||
uses: github/codeql-action/analyze@407ffafae6a767df3e0230c3df91b6443ae8df75 #v2.22.8
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
echo -n "commit=$(git rev-parse HEAD)" | tee -a $GITHUB_OUTPUT
|
||||
|
||||
- name: Update Homebrew formula
|
||||
uses: dawidd6/action-homebrew-bump-formula@75ed025ff3ad1d617862838b342b06d613a0ddf3 # v3.10.1
|
||||
uses: dawidd6/action-homebrew-bump-formula@d3667e5ae14df19579e4414897498e3e88f2f458 # v3.10.0
|
||||
with:
|
||||
token: ${{ secrets.HOMEBREW_TOKEN }}
|
||||
org: anchore
|
||||
|
||||
@@ -141,7 +141,7 @@ jobs:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.TOOLBOX_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.TOOLBOX_AWS_SECRET_ACCESS_KEY }}
|
||||
|
||||
- uses: anchore/sbom-action@c7f031d9249a826a082ea14c79d3b686a51d485a #v0.15.3
|
||||
- uses: anchore/sbom-action@fd74a6fb98a204a1ad35bbfae0122c1a302ff88b #v0.15.0
|
||||
continue-on-error: true
|
||||
with:
|
||||
artifact-name: sbom.spdx.json
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1
|
||||
|
||||
- uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 #v5.0.0
|
||||
- uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe #v4.1.0
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
stable: ${{ env.GO_STABLE_VERSION }}
|
||||
|
||||
@@ -37,31 +37,31 @@ jobs:
|
||||
uses: ./.github/actions/bootstrap
|
||||
|
||||
- name: Restore Java test-fixture cache
|
||||
uses: actions/cache@e12d46a63a90f2fae62d114769bbf2a179198b5c #v3.3.3
|
||||
uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2
|
||||
with:
|
||||
path: syft/pkg/cataloger/java/test-fixtures/java-builds/packages
|
||||
key: ${{ runner.os }}-unit-java-cache-${{ hashFiles( 'syft/pkg/cataloger/java/test-fixtures/java-builds/cache.fingerprint' ) }}
|
||||
|
||||
- name: Restore RPM test-fixture cache
|
||||
uses: actions/cache@e12d46a63a90f2fae62d114769bbf2a179198b5c #v3.3.3
|
||||
uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2
|
||||
with:
|
||||
path: syft/pkg/cataloger/redhat/test-fixtures/rpms
|
||||
key: ${{ runner.os }}-unit-rpm-cache-${{ hashFiles( 'syft/pkg/cataloger/redhat/test-fixtures/rpms.fingerprint' ) }}
|
||||
|
||||
- name: Restore go binary test-fixture cache
|
||||
uses: actions/cache@e12d46a63a90f2fae62d114769bbf2a179198b5c #v3.3.3
|
||||
uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2
|
||||
with:
|
||||
path: syft/pkg/cataloger/golang/test-fixtures/archs/binaries
|
||||
key: ${{ runner.os }}-unit-go-binaries-cache-${{ hashFiles( 'syft/pkg/cataloger/golang/test-fixtures/archs/binaries.fingerprint' ) }}
|
||||
|
||||
- name: Restore binary cataloger test-fixture cache
|
||||
uses: actions/cache@e12d46a63a90f2fae62d114769bbf2a179198b5c #v3.3.3
|
||||
uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2
|
||||
with:
|
||||
path: syft/pkg/cataloger/binary/test-fixtures/classifiers/bin
|
||||
path: syft/pkg/cataloger/binary/test-fixtures/classifiers/dynamic
|
||||
key: ${{ runner.os }}-unit-binary-cataloger-cache-${{ hashFiles( 'syft/pkg/cataloger/binary/test-fixtures/cache.fingerprint' ) }}
|
||||
|
||||
- name: Restore Kernel test-fixture cache
|
||||
uses: actions/cache@e12d46a63a90f2fae62d114769bbf2a179198b5c #v3.3.3
|
||||
uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2
|
||||
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,7 +84,7 @@ jobs:
|
||||
run: make validate-cyclonedx-schema
|
||||
|
||||
- name: Restore integration test cache
|
||||
uses: actions/cache@e12d46a63a90f2fae62d114769bbf2a179198b5c #v3.3.3
|
||||
uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2
|
||||
with:
|
||||
path: ${{ github.workspace }}/test/integration/test-fixtures/cache
|
||||
key: ${{ runner.os }}-integration-test-cache-${{ hashFiles('test/integration/test-fixtures/cache.fingerprint') }}
|
||||
@@ -110,7 +110,7 @@ jobs:
|
||||
# why not use actions/upload-artifact? It is very slow (3 minutes to upload ~600MB of data, vs 10 seconds with this approach).
|
||||
# see https://github.com/actions/upload-artifact/issues/199 for more info
|
||||
- name: Upload snapshot artifacts
|
||||
uses: actions/cache/save@e12d46a63a90f2fae62d114769bbf2a179198b5c #v3.3.3
|
||||
uses: actions/cache/save@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2
|
||||
with:
|
||||
# we need to preserve the snapshot data itself as well as the task data that confirms if the
|
||||
# snapshot build is stale or not. Otherwise the downstream jobs will attempt to rebuild the snapshot
|
||||
@@ -134,7 +134,7 @@ jobs:
|
||||
|
||||
- name: Download snapshot build
|
||||
id: snapshot-cache
|
||||
uses: actions/cache/restore@e12d46a63a90f2fae62d114769bbf2a179198b5c #v3.3.3
|
||||
uses: actions/cache/restore@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2
|
||||
with:
|
||||
path: |
|
||||
snapshot
|
||||
@@ -152,7 +152,7 @@ jobs:
|
||||
|
||||
- name: Restore install.sh test image cache
|
||||
id: install-test-image-cache
|
||||
uses: actions/cache@e12d46a63a90f2fae62d114769bbf2a179198b5c #v3.3.3
|
||||
uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2
|
||||
with:
|
||||
path: ${{ github.workspace }}/test/install/cache
|
||||
key: ${{ runner.os }}-install-test-image-cache-${{ hashFiles('test/install/cache.fingerprint') }}
|
||||
@@ -187,7 +187,7 @@ jobs:
|
||||
|
||||
- name: Download snapshot build
|
||||
id: snapshot-cache
|
||||
uses: actions/cache/restore@e12d46a63a90f2fae62d114769bbf2a179198b5c #v3.3.3
|
||||
uses: actions/cache/restore@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2
|
||||
with:
|
||||
path: |
|
||||
snapshot
|
||||
@@ -202,7 +202,7 @@ jobs:
|
||||
|
||||
- name: Restore docker image cache for compare testing
|
||||
id: mac-compare-testing-cache
|
||||
uses: actions/cache@e12d46a63a90f2fae62d114769bbf2a179198b5c #v3.3.3
|
||||
uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2
|
||||
with:
|
||||
path: image.tar
|
||||
key: ${{ runner.os }}-${{ hashFiles('test/compare/mac.sh') }}
|
||||
@@ -226,14 +226,14 @@ jobs:
|
||||
uses: ./.github/actions/bootstrap
|
||||
|
||||
- name: Restore CLI test-fixture cache
|
||||
uses: actions/cache@e12d46a63a90f2fae62d114769bbf2a179198b5c #v3.3.3
|
||||
uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2
|
||||
with:
|
||||
path: ${{ github.workspace }}/test/cli/test-fixtures/cache
|
||||
key: ${{ runner.os }}-cli-test-cache-${{ hashFiles('test/cli/test-fixtures/cache.fingerprint') }}
|
||||
|
||||
- name: Download snapshot build
|
||||
id: snapshot-cache
|
||||
uses: actions/cache/restore@e12d46a63a90f2fae62d114769bbf2a179198b5c #v3.3.3
|
||||
uses: actions/cache/restore@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2
|
||||
with:
|
||||
path: |
|
||||
snapshot
|
||||
|
||||
@@ -109,21 +109,21 @@ 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
|
||||
```
|
||||
|
||||
### Supported sources
|
||||
|
||||
Syft can generate an SBOM from a variety of sources:
|
||||
Syft can generate a 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,114 +155,69 @@ 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
|
||||
|
||||
### File selection
|
||||
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.
|
||||
|
||||
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:
|
||||
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.
|
||||
|
||||
- `all`: capture all files from the search space
|
||||
- `owned-by-package`: capture only files owned by packages (default)
|
||||
- `none`: disable capturing any file information
|
||||
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).
|
||||
|
||||
##### 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
|
||||
|
||||
### 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"
|
||||
```
|
||||
##### 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
|
||||
|
||||
##### Non Default:
|
||||
- cargo-auditable-binary
|
||||
|
||||
### Excluding file paths
|
||||
|
||||
@@ -513,18 +468,21 @@ output: "syft-table"
|
||||
# same as -q ; SYFT_QUIET env var
|
||||
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
|
||||
check-for-app-update: true
|
||||
|
||||
# maximum number of workers used to process the list of package catalogers in parallel
|
||||
parallelism: 1
|
||||
# allows users to specify which image source should be used to generate the sbom
|
||||
# valid values are: registry, docker, podman
|
||||
default-image-pull-source: ""
|
||||
|
||||
# a list of globs to exclude from scanning, for example:
|
||||
# a list of globs to exclude from scanning. same as --exclude ; for example:
|
||||
# exclude:
|
||||
# - "/etc/**"
|
||||
# - "./out/**/*.json"
|
||||
# same as --exclude ; SYFT_EXCLUDE env var
|
||||
exclude: []
|
||||
|
||||
# allows users to exclude synthetic binary packages from the sbom
|
||||
@@ -535,10 +493,6 @@ exclude-binary-overlap-by-ownership: true
|
||||
# same as --platform; SYFT_PLATFORM env var
|
||||
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:
|
||||
@@ -579,6 +533,7 @@ scope: "squashed"
|
||||
# - spm-cataloger
|
||||
catalogers:
|
||||
|
||||
|
||||
# all format configuration
|
||||
format:
|
||||
|
||||
@@ -631,36 +586,9 @@ format:
|
||||
pretty: false
|
||||
|
||||
|
||||
file:
|
||||
|
||||
metadata:
|
||||
# select which files should be captured by the file-metadata cataloger and included in the SBOM.
|
||||
# Options include:
|
||||
# - "all-files": capture all files from the search space
|
||||
# - "owned-files": capture only files owned by packages
|
||||
# - "unowned-files": capture only files not owned by packages
|
||||
# - "no-files", "": do not capture any files
|
||||
# SYFT_FILE_METADATA_SELECTION env var
|
||||
selection: "owned-files"
|
||||
|
||||
# the file digest algorithms to use when cataloging files (options: "md5", "sha1", "sha224", "sha256", "sha384", "sha512")
|
||||
# SYFT_FILE_METADATA_DIGESTS env var
|
||||
digests: ["sha256"]
|
||||
|
||||
# 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
|
||||
@@ -672,10 +600,14 @@ package:
|
||||
# SYFT_PACKAGE_SEARCH_UNINDEXED_ARCHIVES env var
|
||||
search-unindexed-archives: false
|
||||
|
||||
# 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
|
||||
cataloger:
|
||||
# enable/disable cataloging of packages
|
||||
# SYFT_PACKAGE_CATALOGER_ENABLED env var
|
||||
enabled: 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
|
||||
@@ -724,12 +656,42 @@ python:
|
||||
# when given an arbitrary constraint will be used (even if that version may not be available/published).
|
||||
guess-unpinned-requirements: false
|
||||
|
||||
javascript:
|
||||
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"
|
||||
|
||||
# configuration for the source that the SBOM is generated from (e.g. a file, directory, or container image)
|
||||
# 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
|
||||
source:
|
||||
# alias name for the source
|
||||
# SYFT_SOURCE_NAME env var; --source-name flag
|
||||
@@ -744,14 +706,6 @@ source:
|
||||
# the file digest algorithms to use on the scanned file (options: "md5", "sha1", "sha224", "sha256", "sha384", "sha512")
|
||||
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:
|
||||
# skip TLS verification when communicating with the registry
|
||||
|
||||
@@ -54,7 +54,6 @@ tasks:
|
||||
- task: check-licenses
|
||||
- task: lint
|
||||
- task: check-json-schema-drift
|
||||
- task: check-binary-fixture-size
|
||||
|
||||
test:
|
||||
desc: Run all levels of test
|
||||
@@ -170,11 +169,6 @@ 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 #################################
|
||||
|
||||
|
||||
+6
-9
@@ -77,25 +77,22 @@ 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
|
||||
scanCmd := commands.Scan(app)
|
||||
packagesCmd := commands.Packages(app)
|
||||
|
||||
// root is currently an alias for the scan command
|
||||
rootCmd := commands.Root(app, scanCmd)
|
||||
// rootCmd is currently an alias for the packages command
|
||||
rootCmd := commands.Root(app, packagesCmd)
|
||||
|
||||
// add sub-commands
|
||||
rootCmd.AddCommand(
|
||||
scanCmd,
|
||||
commands.Packages(app, scanCmd), // this is currently an alias for the scan command
|
||||
packagesCmd,
|
||||
commands.Attest(app),
|
||||
commands.Convert(app),
|
||||
clio.VersionCommand(id),
|
||||
cranecmd.NewCmdAuthLogin(id.Name), // syft login uses the same command as crane
|
||||
)
|
||||
|
||||
// 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.
|
||||
// explicitly set Cobra output to the real stdout to write things like errors and help
|
||||
rootCmd.SetOut(out)
|
||||
|
||||
return app, rootCmd
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -61,13 +60,13 @@ func Attest(app clio.Application) *cobra.Command {
|
||||
"appName": id.Name,
|
||||
"command": "attest",
|
||||
}),
|
||||
Args: validateScanArgs,
|
||||
Args: validatePackagesArgs,
|
||||
PreRunE: applicationUpdateCheck(id, &opts.UpdateCheck),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
restoreStdout := ui.CaptureStdoutToTraceLog()
|
||||
defer restoreStdout()
|
||||
|
||||
return runAttest(cmd.Context(), id, &opts, args[0])
|
||||
return runAttest(id, &opts, args[0])
|
||||
},
|
||||
}, &opts)
|
||||
}
|
||||
@@ -99,7 +98,7 @@ func defaultAttestOutputOptions() options.Output {
|
||||
}
|
||||
|
||||
//nolint:funlen
|
||||
func runAttest(ctx context.Context, id clio.Identification, opts *attestOptions, userInput string) error {
|
||||
func runAttest(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")
|
||||
@@ -112,7 +111,7 @@ func runAttest(ctx context.Context, id clio.Identification, opts *attestOptions,
|
||||
}
|
||||
defer os.Remove(f.Name())
|
||||
|
||||
s, err := generateSBOMForAttestation(ctx, id, &opts.Catalog, userInput)
|
||||
s, err := generateSBOMForAttestation(id, &opts.Catalog, userInput)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to build SBOM: %w", err)
|
||||
}
|
||||
@@ -246,7 +245,7 @@ func predicateType(outputName string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func generateSBOMForAttestation(ctx context.Context, id clio.Identification, opts *options.Catalog, userInput string) (*sbom.SBOM, error) {
|
||||
func generateSBOMForAttestation(id clio.Identification, opts *options.Catalog, userInput string) (*sbom.SBOM, error) {
|
||||
src, err := getSource(opts, userInput, onlyContainerImages)
|
||||
|
||||
if err != nil {
|
||||
@@ -261,7 +260,7 @@ func generateSBOMForAttestation(ctx context.Context, id clio.Identification, opt
|
||||
}
|
||||
}()
|
||||
|
||||
s, err := generateSBOM(ctx, id, src, opts)
|
||||
s, err := generateSBOM(id, src, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
@@ -259,7 +258,7 @@ func Test_buildSBOMForAttestation(t *testing.T) {
|
||||
if tt.wantErr == nil {
|
||||
tt.wantErr = require.NoError
|
||||
}
|
||||
_, err := generateSBOMForAttestation(context.Background(), tt.args.id, tt.args.opts, tt.args.userInput)
|
||||
_, err := generateSBOMForAttestation(tt.args.id, tt.args.opts, tt.args.userInput)
|
||||
tt.wantErr(t, err)
|
||||
if err != nil {
|
||||
return
|
||||
|
||||
@@ -1,33 +1,255 @@
|
||||
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"
|
||||
)
|
||||
|
||||
func Packages(app clio.Application, scanCmd *cobra.Command) *cobra.Command {
|
||||
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 := defaultScanOptions()
|
||||
opts := defaultPackagesOptions()
|
||||
|
||||
cmd := app.SetupCommand(&cobra.Command{
|
||||
Use: "packages [SOURCE]",
|
||||
Short: scanCmd.Short,
|
||||
Long: scanCmd.Long,
|
||||
Args: scanCmd.Args,
|
||||
Example: scanCmd.Example,
|
||||
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 runScan(cmd.Context(), id, opts, args[0])
|
||||
return runPackages(id, opts, args[0])
|
||||
},
|
||||
}, opts)
|
||||
}
|
||||
|
||||
cmd.Deprecated = "use `syft scan` instead"
|
||||
func validatePackagesArgs(cmd *cobra.Command, args []string) error {
|
||||
return validateArgs(cmd, args, "an image/directory argument is required")
|
||||
}
|
||||
|
||||
return cmd
|
||||
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,300 +0,0 @@
|
||||
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 := defaultScanOptions()
|
||||
opts := defaultPackagesOptions()
|
||||
|
||||
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 runScan(cmd.Context(), id, opts, args[0])
|
||||
return runPackages(id, opts, args[0])
|
||||
},
|
||||
}, opts)
|
||||
}
|
||||
|
||||
@@ -1,447 +0,0 @@
|
||||
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/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/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 'relationships.exclude-binary-packages-with-file-ownership-overlap' 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, "+-")
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/anchore/syft/cmd/syft/cli/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("relationships.exclude-binary-packages-with-file-ownership-overlap"),
|
||||
},
|
||||
{
|
||||
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...)
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
# simple, valid syft config with no legacy fields used
|
||||
parallelism: 5
|
||||
@@ -1 +0,0 @@
|
||||
base-path: "something"
|
||||
@@ -1 +0,0 @@
|
||||
default-image-pull-source: "something"
|
||||
-1
@@ -1 +0,0 @@
|
||||
exclude-binary-overlap-by-ownership: true
|
||||
@@ -1,2 +0,0 @@
|
||||
file:
|
||||
selection: all
|
||||
@@ -1 +0,0 @@
|
||||
file: path
|
||||
@@ -0,0 +1,151 @@
|
||||
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
|
||||
}
|
||||
+97
-161
@@ -6,141 +6,55 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/iancoleman/strcase"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/scylladb/go-set/strset"
|
||||
|
||||
"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"
|
||||
golangCataloger "github.com/anchore/syft/syft/pkg/cataloger/golang"
|
||||
javaCataloger "github.com/anchore/syft/syft/pkg/cataloger/java"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/kernel"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/python"
|
||||
pythonCataloger "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"`
|
||||
Name string `yaml:"name" json:"name" mapstructure:"name"` // deprecated
|
||||
Source sourceConfig `yaml:"source" json:"source" mapstructure:"source"`
|
||||
Exclusions []string `yaml:"exclude" json:"exclude" mapstructure:"exclude"`
|
||||
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"`
|
||||
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
|
||||
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),
|
||||
Package: defaultPkg(),
|
||||
LinuxKernel: defaultLinuxKernel(),
|
||||
FileMetadata: defaultFileMetadata(),
|
||||
FileContents: defaultFileContents(),
|
||||
Source: defaultSourceCfg(),
|
||||
Parallelism: 1,
|
||||
ExcludeBinaryOverlapByOwnership: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,7 +63,7 @@ func (cfg *Catalog) AddFlags(flags clio.FlagSet) {
|
||||
for _, scope := range source.AllScopes {
|
||||
validScopeValues = append(validScopeValues, strcase.ToDelimited(string(scope), '-'))
|
||||
}
|
||||
flags.StringVarP(&cfg.Scope, "scope", "s",
|
||||
flags.StringVarP(&cfg.Package.Cataloger.Scope, "scope", "s",
|
||||
fmt.Sprintf("selection of layers to catalog, options=%v", validScopeValues))
|
||||
|
||||
flags.StringVarP(&cfg.Platform, "platform", "",
|
||||
@@ -161,20 +75,6 @@ func (cfg *Catalog) AddFlags(flags clio.FlagSet) {
|
||||
flags.StringArrayVarP(&cfg.Catalogers, "catalogers", "",
|
||||
"enable one or more package catalogers")
|
||||
|
||||
if pfp, ok := flags.(fangs.PFlagSetProvider); ok {
|
||||
if err := pfp.PFlagSet().MarkDeprecated("catalogers", "use: override-default-catalogers and select-catalogers"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
} else {
|
||||
panic("unable to mark flags as deprecated")
|
||||
}
|
||||
|
||||
flags.StringArrayVarP(&cfg.DefaultCatalogers, "override-default-catalogers", "",
|
||||
"set the base set of catalogers to use (defaults to 'image' or 'directory' depending on the scan source)")
|
||||
|
||||
flags.StringArrayVarP(&cfg.SelectCatalogers, "select-catalogers", "",
|
||||
"add, remove, and filter the catalogers to be used")
|
||||
|
||||
flags.StringVarP(&cfg.Source.Name, "name", "",
|
||||
"set the name of the target being analyzed")
|
||||
|
||||
@@ -189,15 +89,25 @@ func (cfg *Catalog) AddFlags(flags clio.FlagSet) {
|
||||
flags.StringVarP(&cfg.Source.Version, "source-version", "",
|
||||
"set the version of the target being analyzed")
|
||||
|
||||
flags.StringVarP(&cfg.Source.BasePath, "base-path", "",
|
||||
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) DescribeFields(descriptions fangs.FieldDescriptionSet) {
|
||||
descriptions.Add(&cfg.Parallelism, "number of cataloger workers to run in parallel")
|
||||
}
|
||||
|
||||
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 == "" {
|
||||
@@ -205,36 +115,62 @@ func (cfg *Catalog) PostLoad() error {
|
||||
}
|
||||
}
|
||||
|
||||
usingLegacyCatalogers := len(cfg.Catalogers) > 0
|
||||
usingNewCatalogers := len(cfg.DefaultCatalogers) > 0 || len(cfg.SelectCatalogers) > 0
|
||||
return nil
|
||||
}
|
||||
|
||||
if usingLegacyCatalogers && usingNewCatalogers {
|
||||
return fmt.Errorf("cannot use both 'catalogers' and 'select-catalogers'/'default-catalogers' flags")
|
||||
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),
|
||||
Python: pythonCataloger.CatalogerConfig{
|
||||
GuessUnpinnedRequirements: cfg.Python.GuessUnpinnedRequirements,
|
||||
},
|
||||
ExcludeBinaryOverlapByOwnership: cfg.ExcludeBinaryOverlapByOwnership,
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
var validDefaultSourceValues = []string{"registry", "docker", "podman", ""}
|
||||
|
||||
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)
|
||||
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,72 +0,0 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,6 @@
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
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 {
|
||||
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,21 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
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,6 +1,6 @@
|
||||
package options
|
||||
|
||||
type golangConfig struct {
|
||||
type golang 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 javaConfig struct {
|
||||
type java 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 +0,0 @@
|
||||
package options
|
||||
|
||||
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 linuxKernelConfig struct {
|
||||
type linuxKernel struct {
|
||||
CatalogModules bool `json:"catalog-modules" yaml:"catalog-modules" mapstructure:"catalog-modules"`
|
||||
}
|
||||
|
||||
func defaultLinuxKernelConfig() linuxKernelConfig {
|
||||
return linuxKernelConfig{
|
||||
func defaultLinuxKernel() linuxKernel {
|
||||
return linuxKernel{
|
||||
CatalogModules: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ func (o Output) SBOMWriter() (sbom.Writer, error) {
|
||||
}
|
||||
}
|
||||
|
||||
return makeSBOMWriter(o.Outputs, o.LegacyFile, encoders)
|
||||
return makeSBOMWriter(o.Outputs, o.File, encoders)
|
||||
}
|
||||
|
||||
func (o Output) OutputNameSet() *strset.Set {
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/fangs"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
@@ -17,13 +13,13 @@ var _ interface {
|
||||
|
||||
// Deprecated: OutputFile supports the --file to write the SBOM output to
|
||||
type OutputFile struct {
|
||||
Enabled bool `yaml:"-" json:"-" mapstructure:"-"`
|
||||
LegacyFile string `yaml:"-" json:"-" mapstructure:"legacyFile"`
|
||||
Enabled bool `yaml:"-" json:"-" mapstructure:"-"`
|
||||
File string `yaml:"file" json:"file" mapstructure:"file"`
|
||||
}
|
||||
|
||||
func (o *OutputFile) AddFlags(flags clio.FlagSet) {
|
||||
if o.Enabled {
|
||||
flags.StringVarP(&o.LegacyFile, "file", "",
|
||||
flags.StringVarP(&o.File, "file", "",
|
||||
"file to write the default report output to (default is STDOUT)")
|
||||
|
||||
if pfp, ok := flags.(fangs.PFlagSetProvider); ok {
|
||||
@@ -37,12 +33,12 @@ func (o *OutputFile) PostLoad() error {
|
||||
if !o.Enabled {
|
||||
return nil
|
||||
}
|
||||
if o.LegacyFile != "" {
|
||||
file, err := expandFilePath(o.LegacyFile)
|
||||
if o.File != "" {
|
||||
file, err := expandFilePath(o.File)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.LegacyFile = file
|
||||
o.File = file
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -51,21 +47,10 @@ func (o *OutputFile) SBOMWriter(f sbom.FormatEncoder) (sbom.Writer, error) {
|
||||
if !o.Enabled {
|
||||
return nil, nil
|
||||
}
|
||||
writer, err := newSBOMMultiWriter(newSBOMWriterDescription(f, o.LegacyFile))
|
||||
writer, err := newSBOMMultiWriter(newSBOMWriterDescription(f, o.File))
|
||||
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
|
||||
}
|
||||
|
||||
+16
-11
@@ -1,18 +1,23 @@
|
||||
package options
|
||||
|
||||
import "github.com/anchore/syft/syft/cataloging"
|
||||
import (
|
||||
"github.com/anchore/syft/syft/pkg/cataloger"
|
||||
)
|
||||
|
||||
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
|
||||
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 defaultPackageConfig() packageConfig {
|
||||
c := cataloging.DefaultArchiveSearchConfig()
|
||||
return packageConfig{
|
||||
SearchIndexedArchives: c.IncludeIndexedArchives,
|
||||
SearchUnindexedArchives: c.IncludeUnindexedArchives,
|
||||
ExcludeBinaryOverlapByOwnership: true,
|
||||
func defaultPkg() pkg {
|
||||
c := cataloger.DefaultSearchConfig()
|
||||
return pkg{
|
||||
SearchIndexedArchives: c.IncludeIndexedArchives,
|
||||
SearchUnindexedArchives: c.IncludeUnindexedArchives,
|
||||
Cataloger: scope{
|
||||
Enabled: true,
|
||||
Scope: c.Scope.String(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
package options
|
||||
|
||||
type pythonConfig struct {
|
||||
type python 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 registryConfig struct {
|
||||
type registry 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 = (*registryConfig)(nil)
|
||||
var _ clio.PostLoader = (*registry)(nil)
|
||||
|
||||
func (cfg *registryConfig) PostLoad() error {
|
||||
func (cfg *registry) 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 *registryConfig) ToOptions() *image.RegistryOptions {
|
||||
func (cfg *registry) 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 registryConfig
|
||||
input registry
|
||||
expected image.RegistryOptions
|
||||
}{
|
||||
{
|
||||
name: "no registry options",
|
||||
input: registryConfig{},
|
||||
input: registry{},
|
||||
expected: image.RegistryOptions{
|
||||
Credentials: []image.RegistryCredentials{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set InsecureSkipTLSVerify",
|
||||
input: registryConfig{
|
||||
input: registry{
|
||||
InsecureSkipTLSVerify: true,
|
||||
},
|
||||
expected: image.RegistryOptions{
|
||||
@@ -94,7 +94,7 @@ func Test_registry_ToOptions(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "set InsecureUseHTTP",
|
||||
input: registryConfig{
|
||||
input: registry{
|
||||
InsecureUseHTTP: true,
|
||||
},
|
||||
expected: image.RegistryOptions{
|
||||
@@ -104,7 +104,7 @@ func Test_registry_ToOptions(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "set all bool options",
|
||||
input: registryConfig{
|
||||
input: registry{
|
||||
InsecureSkipTLSVerify: true,
|
||||
InsecureUseHTTP: true,
|
||||
},
|
||||
@@ -116,7 +116,7 @@ func Test_registry_ToOptions(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "provide all tls configuration",
|
||||
input: registryConfig{
|
||||
input: registry{
|
||||
CACert: "ca.crt",
|
||||
InsecureSkipTLSVerify: true,
|
||||
Auth: []RegistryCredentials{
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
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,27 @@
|
||||
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,51 +1,19 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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 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"`
|
||||
}
|
||||
|
||||
type imageSource struct {
|
||||
DefaultPullSource string `json:"default-pull-source" yaml:"default-pull-source" mapstructure:"default-pull-source"`
|
||||
}
|
||||
|
||||
func defaultSourceConfig() sourceConfig {
|
||||
return sourceConfig{
|
||||
func defaultSourceCfg() sourceCfg {
|
||||
return sourceCfg{
|
||||
File: fileSource{
|
||||
Digests: []string{"sha256"},
|
||||
},
|
||||
Image: imageSource{
|
||||
DefaultPullSource: "",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
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
|
||||
}
|
||||
@@ -11,7 +9,3 @@ 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")
|
||||
}
|
||||
|
||||
@@ -1,24 +1,16 @@
|
||||
|
||||
[TestHandler_handleCatalogerTaskStarted/cataloging_task_in_progress - 1]
|
||||
⠙ Cataloging contents
|
||||
⠙ some task title ━━━━━━━━━━━━━━━━━━━━ [some stage]
|
||||
some task title [some value]
|
||||
---
|
||||
|
||||
[TestHandler_handleCatalogerTaskStarted/cataloging_sub_task_in_progress - 1]
|
||||
⠙ Cataloging contents
|
||||
└── ⠙ some task title ━━━━━━━━━━━━━━━━━━━━ [some stage]
|
||||
└── some task title [some value]
|
||||
---
|
||||
|
||||
[TestHandler_handleCatalogerTaskStarted/cataloging_sub_task_complete - 1]
|
||||
⠙ Cataloging contents
|
||||
└── ✔ some task done [some stage]
|
||||
---
|
||||
|
||||
[TestHandler_handleCatalogerTaskStarted/cataloging_sub_task_complete_--_hide_stage - 1]
|
||||
⠙ Cataloging contents
|
||||
└── ✔ some task done
|
||||
✔ └── some task done [some value]
|
||||
---
|
||||
|
||||
[TestHandler_handleCatalogerTaskStarted/cataloging_sub_task_complete_with_removal - 1]
|
||||
⠙ Cataloging contents
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
[TestHandler_handleFileDigestsCatalogerStarted/cataloging_in_progress - 1]
|
||||
⠋ Cataloging file digests ━━━━━━━━━━━━━━━━━━━━ [current]
|
||||
---
|
||||
|
||||
[TestHandler_handleFileDigestsCatalogerStarted/cataloging_complete - 1]
|
||||
✔ Cataloged file digests
|
||||
---
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
[TestHandler_handleFileMetadataCatalogerStarted/cataloging_in_progress - 1]
|
||||
⠋ Cataloging file metadata ━━━━━━━━━━━━━━━━━━━━ [current]
|
||||
---
|
||||
|
||||
[TestHandler_handleFileMetadataCatalogerStarted/cataloging_complete - 1]
|
||||
✔ Cataloged file metadata
|
||||
---
|
||||
@@ -0,0 +1,16 @@
|
||||
|
||||
[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]
|
||||
---
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
[TestHandler_handleSecretsCatalogerStarted/cataloging_in_progress - 1]
|
||||
⠋ Cataloging secrets ━━━━━━━━━━━━━━━━━━━━ [64 secrets]
|
||||
---
|
||||
|
||||
[TestHandler_handleSecretsCatalogerStarted/cataloging_complete - 1]
|
||||
✔ Cataloged secrets [64 secrets]
|
||||
---
|
||||
@@ -100,21 +100,18 @@ func TestHandler_handleAttestationStarted(t *testing.T) {
|
||||
Height: 80,
|
||||
}
|
||||
|
||||
models, _ := handler.Handle(event)
|
||||
models := handler.Handle(event)
|
||||
require.Len(t, models, 2)
|
||||
|
||||
t.Run("task line", func(t *testing.T) {
|
||||
tsk, ok := models[0].(taskprogress.Model)
|
||||
require.True(t, ok)
|
||||
|
||||
gotModel := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
|
||||
got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
|
||||
Time: time.Now(),
|
||||
Sequence: tsk.Sequence(),
|
||||
ID: tsk.ID(),
|
||||
})
|
||||
|
||||
got := gotModel.View()
|
||||
|
||||
t.Log(got)
|
||||
snaps.MatchSnapshot(t, got)
|
||||
})
|
||||
@@ -122,15 +119,11 @@ func TestHandler_handleAttestationStarted(t *testing.T) {
|
||||
t.Run("log", func(t *testing.T) {
|
||||
log, ok := models[1].(attestLogFrame)
|
||||
require.True(t, ok)
|
||||
|
||||
gotModel := runModel(t, log, tt.iterations, attestLogFrameTickMsg{
|
||||
got := runModel(t, log, tt.iterations, attestLogFrameTickMsg{
|
||||
Time: time.Now(),
|
||||
Sequence: log.sequence,
|
||||
ID: log.id,
|
||||
}, log.reader.running)
|
||||
|
||||
got := gotModel.View()
|
||||
|
||||
t.Log(got)
|
||||
snaps.MatchSnapshot(t, got)
|
||||
})
|
||||
|
||||
@@ -3,121 +3,70 @@ package ui
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/google/uuid"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||
"github.com/anchore/bubbly/bubbles/tree"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/event/monitor"
|
||||
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
|
||||
)
|
||||
|
||||
// we standardize how rows are instantiated to ensure consistency in the appearance across the UI
|
||||
type taskModelFactory func(title taskprogress.Title, opts ...taskprogress.Option) taskprogress.Model
|
||||
var _ progress.Stager = (*catalogerTaskStageAdapter)(nil)
|
||||
|
||||
var _ tea.Model = (*catalogerTaskModel)(nil)
|
||||
|
||||
type catalogerTaskModel struct {
|
||||
model tree.Model
|
||||
modelFactory taskModelFactory
|
||||
type catalogerTaskStageAdapter struct {
|
||||
mon *monitor.CatalogerTask
|
||||
}
|
||||
|
||||
func newCatalogerTaskTreeModel(f taskModelFactory) *catalogerTaskModel {
|
||||
t := tree.NewModel()
|
||||
t.Padding = " "
|
||||
t.RootsWithoutPrefix = true
|
||||
return &catalogerTaskModel{
|
||||
modelFactory: f,
|
||||
model: t,
|
||||
func newCatalogerTaskStageAdapter(mon *monitor.CatalogerTask) *catalogerTaskStageAdapter {
|
||||
return &catalogerTaskStageAdapter{
|
||||
mon: mon,
|
||||
}
|
||||
}
|
||||
|
||||
type newCatalogerTaskRowEvent struct {
|
||||
info monitor.GenericTask
|
||||
prog progress.StagedProgressable
|
||||
func (c catalogerTaskStageAdapter) Stage() string {
|
||||
return c.mon.GetValue()
|
||||
}
|
||||
|
||||
func (cts catalogerTaskModel) Init() tea.Cmd {
|
||||
return cts.model.Init()
|
||||
}
|
||||
|
||||
func (cts catalogerTaskModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
event, ok := msg.(newCatalogerTaskRowEvent)
|
||||
if !ok {
|
||||
model, cmd := cts.model.Update(msg)
|
||||
cts.model = model.(tree.Model)
|
||||
|
||||
return cts, cmd
|
||||
}
|
||||
|
||||
info, prog := event.info, event.prog
|
||||
|
||||
tsk := cts.modelFactory(
|
||||
taskprogress.Title{
|
||||
Default: info.Title.Default,
|
||||
Running: info.Title.WhileRunning,
|
||||
Success: info.Title.OnSuccess,
|
||||
},
|
||||
taskprogress.WithStagedProgressable(prog),
|
||||
)
|
||||
|
||||
if info.Context != "" {
|
||||
tsk.Context = []string{info.Context}
|
||||
}
|
||||
|
||||
tsk.HideOnSuccess = info.HideOnSuccess
|
||||
tsk.HideStageOnSuccess = info.HideStageOnSuccess
|
||||
tsk.HideProgressOnSuccess = true
|
||||
|
||||
if info.ParentID != "" {
|
||||
tsk.TitleStyle = lipgloss.NewStyle()
|
||||
}
|
||||
|
||||
if err := cts.model.Add(info.ParentID, info.ID, tsk); err != nil {
|
||||
log.WithFields("error", err).Error("unable to add cataloger task to tree model")
|
||||
}
|
||||
|
||||
return cts, tsk.Init()
|
||||
}
|
||||
|
||||
func (cts catalogerTaskModel) View() string {
|
||||
return cts.model.View()
|
||||
}
|
||||
|
||||
func (m *Handler) handleCatalogerTaskStarted(e partybus.Event) ([]tea.Model, tea.Cmd) {
|
||||
mon, info, err := syftEventParsers.ParseCatalogerTaskStarted(e)
|
||||
func (m *Handler) handleCatalogerTaskStarted(e partybus.Event) []tea.Model {
|
||||
mon, err := syftEventParsers.ParseCatalogerTaskStarted(e)
|
||||
if err != nil {
|
||||
log.WithFields("error", err).Warn("unable to parse event")
|
||||
return nil, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
var models []tea.Model
|
||||
|
||||
// only create the new cataloger task tree once to manage all cataloger task events
|
||||
m.onNewCatalogerTask.Do(func() {
|
||||
models = append(models, newCatalogerTaskTreeModel(m.newTaskProgress))
|
||||
})
|
||||
|
||||
// we need to update the cataloger task model with a new row. We should never update the model outside of the
|
||||
// bubbletea update-render event loop. Instead, we return a command that will be executed by the bubbletea runtime,
|
||||
// producing a message that is passed to the cataloger task model. This is the prescribed way to update models
|
||||
// in bubbletea.
|
||||
|
||||
if info.ID == "" {
|
||||
// ID is optional from the consumer perspective, but required internally
|
||||
info.ID = uuid.Must(uuid.NewRandom()).String()
|
||||
var prefix string
|
||||
if mon.SubStatus {
|
||||
// TODO: support list of sub-statuses, not just a single leaf
|
||||
prefix = "└── "
|
||||
}
|
||||
|
||||
cmd := func() tea.Msg {
|
||||
// this message will cause the cataloger task model to add a new row to the output based on the given task
|
||||
// information and progress data.
|
||||
return newCatalogerTaskRowEvent{
|
||||
info: *info,
|
||||
prog: mon,
|
||||
}
|
||||
}
|
||||
tsk := m.newTaskProgress(
|
||||
taskprogress.Title{
|
||||
// TODO: prefix should not be part of the title, but instead a separate field that is aware of the tree structure
|
||||
Default: prefix + mon.Title,
|
||||
Running: prefix + mon.Title,
|
||||
Success: prefix + mon.TitleOnCompletion,
|
||||
},
|
||||
taskprogress.WithStagedProgressable(
|
||||
struct {
|
||||
progress.Stager
|
||||
progress.Progressable
|
||||
}{
|
||||
Progressable: mon.GetMonitor(),
|
||||
Stager: newCatalogerTaskStageAdapter(mon),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
return models, cmd
|
||||
// TODO: this isn't ideal since the model stays around after it is no longer needed, but it works for now
|
||||
tsk.HideOnSuccess = mon.RemoveOnCompletion
|
||||
tsk.HideStageOnSuccess = false
|
||||
tsk.HideProgressOnSuccess = false
|
||||
|
||||
tsk.TitleStyle = lipgloss.NewStyle()
|
||||
// TODO: this is a hack to get the spinner to not show up, but ideally the component would support making the spinner optional
|
||||
tsk.Spinner.Spinner.Frames = []string{" "}
|
||||
|
||||
return []tea.Model{tsk}
|
||||
}
|
||||
|
||||
@@ -2,22 +2,19 @@ package ui
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/gkampitakis/go-snaps/snaps"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||
syftEvent "github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/syft/event/monitor"
|
||||
)
|
||||
|
||||
func TestHandler_handleCatalogerTaskStarted(t *testing.T) {
|
||||
title := monitor.Title{
|
||||
Default: "some task title",
|
||||
OnSuccess: "some task done",
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
eventFn func(*testing.T) partybus.Event
|
||||
@@ -26,171 +23,99 @@ func TestHandler_handleCatalogerTaskStarted(t *testing.T) {
|
||||
{
|
||||
name: "cataloging task in progress",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
value := &monitor.CatalogerTaskProgress{
|
||||
AtomicStage: progress.NewAtomicStage("some stage"),
|
||||
Manual: progress.NewManual(100),
|
||||
src := &monitor.CatalogerTask{
|
||||
SubStatus: false,
|
||||
RemoveOnCompletion: false,
|
||||
Title: "some task title",
|
||||
TitleOnCompletion: "some task done",
|
||||
}
|
||||
|
||||
value.Manual.Add(50)
|
||||
src.SetValue("some value")
|
||||
|
||||
return partybus.Event{
|
||||
Type: syftEvent.CatalogerTaskStarted,
|
||||
Source: monitor.GenericTask{
|
||||
Title: title,
|
||||
HideOnSuccess: false,
|
||||
HideStageOnSuccess: false,
|
||||
ID: "my-id",
|
||||
},
|
||||
Value: value,
|
||||
Type: syftEvent.CatalogerTaskStarted,
|
||||
Source: src,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cataloging sub task in progress",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
value := &monitor.CatalogerTaskProgress{
|
||||
AtomicStage: progress.NewAtomicStage("some stage"),
|
||||
Manual: progress.NewManual(100),
|
||||
src := &monitor.CatalogerTask{
|
||||
SubStatus: true,
|
||||
RemoveOnCompletion: false,
|
||||
Title: "some task title",
|
||||
TitleOnCompletion: "some task done",
|
||||
}
|
||||
|
||||
value.Manual.Add(50)
|
||||
src.SetValue("some value")
|
||||
|
||||
return partybus.Event{
|
||||
Type: syftEvent.CatalogerTaskStarted,
|
||||
Source: monitor.GenericTask{
|
||||
Title: title,
|
||||
HideOnSuccess: false,
|
||||
HideStageOnSuccess: false,
|
||||
ID: "my-id",
|
||||
ParentID: "top-level-task",
|
||||
},
|
||||
Value: value,
|
||||
Type: syftEvent.CatalogerTaskStarted,
|
||||
Source: src,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cataloging sub task complete",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
value := &monitor.CatalogerTaskProgress{
|
||||
AtomicStage: progress.NewAtomicStage("some stage"),
|
||||
Manual: progress.NewManual(100),
|
||||
src := &monitor.CatalogerTask{
|
||||
SubStatus: true,
|
||||
RemoveOnCompletion: false,
|
||||
Title: "some task title",
|
||||
TitleOnCompletion: "some task done",
|
||||
}
|
||||
|
||||
value.SetCompleted()
|
||||
src.SetValue("some value")
|
||||
src.SetCompleted()
|
||||
|
||||
return partybus.Event{
|
||||
Type: syftEvent.CatalogerTaskStarted,
|
||||
Source: monitor.GenericTask{
|
||||
Title: title,
|
||||
HideOnSuccess: false,
|
||||
HideStageOnSuccess: false,
|
||||
ID: "my-id",
|
||||
ParentID: "top-level-task",
|
||||
},
|
||||
Value: value,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cataloging sub task complete -- hide stage",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
value := &monitor.CatalogerTaskProgress{
|
||||
AtomicStage: progress.NewAtomicStage("some stage"),
|
||||
Manual: progress.NewManual(100),
|
||||
}
|
||||
|
||||
value.SetCompleted()
|
||||
|
||||
return partybus.Event{
|
||||
Type: syftEvent.CatalogerTaskStarted,
|
||||
Source: monitor.GenericTask{
|
||||
Title: title,
|
||||
HideOnSuccess: false,
|
||||
HideStageOnSuccess: true,
|
||||
ID: "my-id",
|
||||
ParentID: "top-level-task",
|
||||
},
|
||||
Value: value,
|
||||
Type: syftEvent.CatalogerTaskStarted,
|
||||
Source: src,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cataloging sub task complete with removal",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
value := &monitor.CatalogerTaskProgress{
|
||||
AtomicStage: progress.NewAtomicStage("some stage"),
|
||||
Manual: progress.NewManual(100),
|
||||
src := &monitor.CatalogerTask{
|
||||
SubStatus: true,
|
||||
RemoveOnCompletion: true,
|
||||
Title: "some task title",
|
||||
TitleOnCompletion: "some task done",
|
||||
}
|
||||
|
||||
value.SetCompleted()
|
||||
src.SetValue("some value")
|
||||
src.SetCompleted()
|
||||
|
||||
return partybus.Event{
|
||||
Type: syftEvent.CatalogerTaskStarted,
|
||||
Source: monitor.GenericTask{
|
||||
Title: title,
|
||||
HideOnSuccess: true,
|
||||
HideStageOnSuccess: false,
|
||||
ID: "my-id",
|
||||
ParentID: "top-level-task",
|
||||
},
|
||||
Value: value,
|
||||
Type: syftEvent.CatalogerTaskStarted,
|
||||
Source: src,
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// need to be able to get the initial newCatalogerTaskRowEvent + initialize the nested taskprogress model
|
||||
if tt.iterations == 0 {
|
||||
tt.iterations = 2
|
||||
}
|
||||
|
||||
e := tt.eventFn(t)
|
||||
event := tt.eventFn(t)
|
||||
handler := New(DefaultHandlerConfig())
|
||||
handler.WindowSize = tea.WindowSizeMsg{
|
||||
Width: 100,
|
||||
Height: 80,
|
||||
}
|
||||
|
||||
info := monitor.GenericTask{
|
||||
Title: monitor.Title{
|
||||
Default: "Catalog contents",
|
||||
WhileRunning: "Cataloging contents",
|
||||
OnSuccess: "Cataloged contents",
|
||||
},
|
||||
ID: "top-level-task",
|
||||
}
|
||||
|
||||
// note: this line / event is not under test, only needed to show a sub status
|
||||
kickoffEvent := &monitor.CatalogerTaskProgress{
|
||||
AtomicStage: progress.NewAtomicStage(""),
|
||||
Manual: progress.NewManual(-1),
|
||||
}
|
||||
|
||||
models, cmd := handler.Handle(
|
||||
partybus.Event{
|
||||
Type: syftEvent.CatalogerTaskStarted,
|
||||
Source: info,
|
||||
Value: progress.StagedProgressable(kickoffEvent),
|
||||
},
|
||||
)
|
||||
models := handler.Handle(event)
|
||||
require.Len(t, models, 1)
|
||||
require.NotNil(t, cmd)
|
||||
model := models[0]
|
||||
|
||||
tr, ok := model.(*catalogerTaskModel)
|
||||
tsk, ok := model.(taskprogress.Model)
|
||||
require.True(t, ok)
|
||||
|
||||
gotModel := runModel(t, tr, tt.iterations, cmd())
|
||||
|
||||
models, cmd = handler.Handle(e)
|
||||
require.Len(t, models, 0)
|
||||
require.NotNil(t, cmd)
|
||||
|
||||
gotModel = runModel(t, gotModel, tt.iterations, cmd())
|
||||
|
||||
got := gotModel.View()
|
||||
|
||||
got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
|
||||
Time: time.Now(),
|
||||
Sequence: tsk.Sequence(),
|
||||
ID: tsk.ID(),
|
||||
})
|
||||
t.Log(got)
|
||||
snaps.MatchSnapshot(t, got)
|
||||
})
|
||||
|
||||
@@ -80,21 +80,18 @@ func TestHandler_handleFetchImage(t *testing.T) {
|
||||
Height: 80,
|
||||
}
|
||||
|
||||
models, _ := handler.Handle(event)
|
||||
models := handler.Handle(event)
|
||||
require.Len(t, models, 1)
|
||||
model := models[0]
|
||||
|
||||
tsk, ok := model.(taskprogress.Model)
|
||||
require.True(t, ok)
|
||||
|
||||
gotModel := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
|
||||
got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
|
||||
Time: time.Now(),
|
||||
Sequence: tsk.Sequence(),
|
||||
ID: tsk.ID(),
|
||||
})
|
||||
|
||||
got := gotModel.View()
|
||||
|
||||
t.Log(got)
|
||||
snaps.MatchSnapshot(t, got)
|
||||
})
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
|
||||
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
|
||||
)
|
||||
|
||||
func (m *Handler) handleFileDigestsCatalogerStarted(e partybus.Event) []tea.Model {
|
||||
prog, err := syftEventParsers.ParseFileDigestsCatalogingStarted(e)
|
||||
if err != nil {
|
||||
log.WithFields("error", err).Warn("unable to parse event")
|
||||
return nil
|
||||
}
|
||||
|
||||
tsk := m.newTaskProgress(
|
||||
taskprogress.Title{
|
||||
Default: "Catalog file digests",
|
||||
Running: "Cataloging file digests",
|
||||
Success: "Cataloged file digests",
|
||||
}, taskprogress.WithStagedProgressable(prog),
|
||||
)
|
||||
|
||||
return []tea.Model{tsk}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/gkampitakis/go-snaps/snaps"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||
syftEvent "github.com/anchore/syft/syft/event"
|
||||
)
|
||||
|
||||
func TestHandler_handleFileDigestsCatalogerStarted(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
eventFn func(*testing.T) partybus.Event
|
||||
iterations int
|
||||
}{
|
||||
{
|
||||
name: "cataloging in progress",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
prog := &progress.Manual{}
|
||||
prog.SetTotal(100)
|
||||
prog.Set(50)
|
||||
|
||||
mon := struct {
|
||||
progress.Progressable
|
||||
progress.Stager
|
||||
}{
|
||||
Progressable: prog,
|
||||
Stager: &progress.Stage{
|
||||
Current: "current",
|
||||
},
|
||||
}
|
||||
|
||||
return partybus.Event{
|
||||
Type: syftEvent.FileDigestsCatalogerStarted,
|
||||
Value: mon,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cataloging complete",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
prog := &progress.Manual{}
|
||||
prog.SetTotal(100)
|
||||
prog.Set(100)
|
||||
prog.SetCompleted()
|
||||
|
||||
mon := struct {
|
||||
progress.Progressable
|
||||
progress.Stager
|
||||
}{
|
||||
Progressable: prog,
|
||||
Stager: &progress.Stage{
|
||||
Current: "current",
|
||||
},
|
||||
}
|
||||
|
||||
return partybus.Event{
|
||||
Type: syftEvent.FileDigestsCatalogerStarted,
|
||||
Value: mon,
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
event := tt.eventFn(t)
|
||||
handler := New(DefaultHandlerConfig())
|
||||
handler.WindowSize = tea.WindowSizeMsg{
|
||||
Width: 100,
|
||||
Height: 80,
|
||||
}
|
||||
|
||||
models := handler.Handle(event)
|
||||
require.Len(t, models, 1)
|
||||
model := models[0]
|
||||
|
||||
tsk, ok := model.(taskprogress.Model)
|
||||
require.True(t, ok)
|
||||
|
||||
got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
|
||||
Time: time.Now(),
|
||||
Sequence: tsk.Sequence(),
|
||||
ID: tsk.ID(),
|
||||
})
|
||||
t.Log(got)
|
||||
snaps.MatchSnapshot(t, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -80,21 +80,18 @@ func TestHandler_handleFileIndexingStarted(t *testing.T) {
|
||||
Height: 80,
|
||||
}
|
||||
|
||||
models, _ := handler.Handle(event)
|
||||
models := handler.Handle(event)
|
||||
require.Len(t, models, 1)
|
||||
model := models[0]
|
||||
|
||||
tsk, ok := model.(taskprogress.Model)
|
||||
require.True(t, ok)
|
||||
|
||||
gotModel := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
|
||||
got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
|
||||
Time: time.Now(),
|
||||
Sequence: tsk.Sequence(),
|
||||
ID: tsk.ID(),
|
||||
})
|
||||
|
||||
got := gotModel.View()
|
||||
|
||||
t.Log(got)
|
||||
snaps.MatchSnapshot(t, got)
|
||||
})
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
|
||||
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
|
||||
)
|
||||
|
||||
func (m *Handler) handleFileMetadataCatalogerStarted(e partybus.Event) []tea.Model {
|
||||
prog, err := syftEventParsers.ParseFileMetadataCatalogingStarted(e)
|
||||
if err != nil {
|
||||
log.WithFields("error", err).Warn("unable to parse event")
|
||||
return nil
|
||||
}
|
||||
|
||||
tsk := m.newTaskProgress(
|
||||
taskprogress.Title{
|
||||
Default: "Catalog file metadata",
|
||||
Running: "Cataloging file metadata",
|
||||
Success: "Cataloged file metadata",
|
||||
},
|
||||
taskprogress.WithStagedProgressable(prog),
|
||||
)
|
||||
|
||||
return []tea.Model{tsk}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/gkampitakis/go-snaps/snaps"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||
syftEvent "github.com/anchore/syft/syft/event"
|
||||
)
|
||||
|
||||
func TestHandler_handleFileMetadataCatalogerStarted(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
eventFn func(*testing.T) partybus.Event
|
||||
iterations int
|
||||
}{
|
||||
{
|
||||
name: "cataloging in progress",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
prog := &progress.Manual{}
|
||||
prog.SetTotal(100)
|
||||
prog.Set(50)
|
||||
|
||||
mon := struct {
|
||||
progress.Progressable
|
||||
progress.Stager
|
||||
}{
|
||||
Progressable: prog,
|
||||
Stager: &progress.Stage{
|
||||
Current: "current",
|
||||
},
|
||||
}
|
||||
|
||||
return partybus.Event{
|
||||
Type: syftEvent.FileMetadataCatalogerStarted,
|
||||
Value: mon,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cataloging complete",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
prog := &progress.Manual{}
|
||||
prog.SetTotal(100)
|
||||
prog.Set(100)
|
||||
prog.SetCompleted()
|
||||
|
||||
mon := struct {
|
||||
progress.Progressable
|
||||
progress.Stager
|
||||
}{
|
||||
Progressable: prog,
|
||||
Stager: &progress.Stage{
|
||||
Current: "current",
|
||||
},
|
||||
}
|
||||
|
||||
return partybus.Event{
|
||||
Type: syftEvent.FileMetadataCatalogerStarted,
|
||||
Value: mon,
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
event := tt.eventFn(t)
|
||||
handler := New(DefaultHandlerConfig())
|
||||
handler.WindowSize = tea.WindowSizeMsg{
|
||||
Width: 100,
|
||||
Height: 80,
|
||||
}
|
||||
|
||||
models := handler.Handle(event)
|
||||
require.Len(t, models, 1)
|
||||
model := models[0]
|
||||
|
||||
tsk, ok := model.(taskprogress.Model)
|
||||
require.True(t, ok)
|
||||
|
||||
got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
|
||||
Time: time.Now(),
|
||||
Sequence: tsk.Sequence(),
|
||||
ID: tsk.ID(),
|
||||
})
|
||||
t.Log(got)
|
||||
snaps.MatchSnapshot(t, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger"
|
||||
)
|
||||
|
||||
var _ progress.StagedProgressable = (*packageCatalogerProgressAdapter)(nil)
|
||||
|
||||
type packageCatalogerProgressAdapter struct {
|
||||
monitor *cataloger.Monitor
|
||||
monitors []progress.Monitorable
|
||||
}
|
||||
|
||||
func newPackageCatalogerProgressAdapter(monitor *cataloger.Monitor) packageCatalogerProgressAdapter {
|
||||
return packageCatalogerProgressAdapter{
|
||||
monitor: monitor,
|
||||
monitors: []progress.Monitorable{
|
||||
monitor.FilesProcessed,
|
||||
monitor.PackagesDiscovered,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p packageCatalogerProgressAdapter) Stage() string {
|
||||
return fmt.Sprintf("%d packages", p.monitor.PackagesDiscovered.Current())
|
||||
}
|
||||
|
||||
func (p packageCatalogerProgressAdapter) Current() int64 {
|
||||
return p.monitor.PackagesDiscovered.Current()
|
||||
}
|
||||
|
||||
func (p packageCatalogerProgressAdapter) Error() error {
|
||||
completedMonitors := 0
|
||||
for _, monitor := range p.monitors {
|
||||
err := monitor.Error()
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
if progress.IsErrCompleted(err) {
|
||||
completedMonitors++
|
||||
continue
|
||||
}
|
||||
// something went wrong
|
||||
return err
|
||||
}
|
||||
if completedMonitors == len(p.monitors) && len(p.monitors) > 0 {
|
||||
return p.monitors[0].Error()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p packageCatalogerProgressAdapter) Size() int64 {
|
||||
// this is an inherently unknown value (indeterminate total number of packages to discover)
|
||||
return -1
|
||||
}
|
||||
|
||||
func (m *Handler) handlePackageCatalogerStarted(e partybus.Event) []tea.Model {
|
||||
monitor, err := syftEventParsers.ParsePackageCatalogerStarted(e)
|
||||
if err != nil {
|
||||
log.WithFields("error", err).Warn("unable to parse event")
|
||||
return nil
|
||||
}
|
||||
|
||||
tsk := m.newTaskProgress(
|
||||
taskprogress.Title{
|
||||
Default: "Catalog packages",
|
||||
Running: "Cataloging packages",
|
||||
Success: "Cataloged packages",
|
||||
},
|
||||
taskprogress.WithStagedProgressable(
|
||||
newPackageCatalogerProgressAdapter(monitor),
|
||||
),
|
||||
)
|
||||
|
||||
tsk.HideStageOnSuccess = false
|
||||
|
||||
return []tea.Model{tsk}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/gkampitakis/go-snaps/snaps"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||
syftEvent "github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger"
|
||||
)
|
||||
|
||||
func TestHandler_handlePackageCatalogerStarted(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
eventFn func(*testing.T) partybus.Event
|
||||
iterations int
|
||||
}{
|
||||
{
|
||||
name: "cataloging in progress",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
prog := &progress.Manual{}
|
||||
prog.SetTotal(100)
|
||||
prog.Set(50)
|
||||
|
||||
mon := cataloger.Monitor{
|
||||
FilesProcessed: progress.NewManual(-1),
|
||||
PackagesDiscovered: prog,
|
||||
}
|
||||
|
||||
return partybus.Event{
|
||||
Type: syftEvent.PackageCatalogerStarted,
|
||||
Value: mon,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cataloging only files complete",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
prog := &progress.Manual{}
|
||||
prog.SetTotal(100)
|
||||
prog.Set(50)
|
||||
|
||||
files := progress.NewManual(-1)
|
||||
files.SetCompleted()
|
||||
|
||||
mon := cataloger.Monitor{
|
||||
FilesProcessed: files,
|
||||
PackagesDiscovered: prog,
|
||||
}
|
||||
|
||||
return partybus.Event{
|
||||
Type: syftEvent.PackageCatalogerStarted,
|
||||
Value: mon,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cataloging only packages complete",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
prog := &progress.Manual{}
|
||||
prog.SetTotal(100)
|
||||
prog.Set(100)
|
||||
prog.SetCompleted()
|
||||
|
||||
files := progress.NewManual(-1)
|
||||
|
||||
mon := cataloger.Monitor{
|
||||
FilesProcessed: files,
|
||||
PackagesDiscovered: prog,
|
||||
}
|
||||
|
||||
return partybus.Event{
|
||||
Type: syftEvent.PackageCatalogerStarted,
|
||||
Value: mon,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cataloging complete",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
prog := &progress.Manual{}
|
||||
prog.SetTotal(100)
|
||||
prog.Set(100)
|
||||
prog.SetCompleted()
|
||||
|
||||
files := progress.NewManual(-1)
|
||||
files.SetCompleted()
|
||||
|
||||
mon := cataloger.Monitor{
|
||||
FilesProcessed: files,
|
||||
PackagesDiscovered: prog,
|
||||
}
|
||||
|
||||
return partybus.Event{
|
||||
Type: syftEvent.PackageCatalogerStarted,
|
||||
Value: mon,
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
event := tt.eventFn(t)
|
||||
handler := New(DefaultHandlerConfig())
|
||||
handler.WindowSize = tea.WindowSizeMsg{
|
||||
Width: 100,
|
||||
Height: 80,
|
||||
}
|
||||
|
||||
models := handler.Handle(event)
|
||||
require.Len(t, models, 1)
|
||||
model := models[0]
|
||||
|
||||
tsk, ok := model.(taskprogress.Model)
|
||||
require.True(t, ok)
|
||||
|
||||
got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
|
||||
Time: time.Now(),
|
||||
Sequence: tsk.Sequence(),
|
||||
ID: tsk.ID(),
|
||||
})
|
||||
t.Log(got)
|
||||
snaps.MatchSnapshot(t, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -98,21 +98,18 @@ func TestHandler_handleReadImage(t *testing.T) {
|
||||
Height: 80,
|
||||
}
|
||||
|
||||
models, _ := handler.Handle(event)
|
||||
models := handler.Handle(event)
|
||||
require.Len(t, models, 1)
|
||||
model := models[0]
|
||||
|
||||
tsk, ok := model.(taskprogress.Model)
|
||||
require.True(t, ok)
|
||||
|
||||
gotModel := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
|
||||
got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
|
||||
Time: time.Now(),
|
||||
Sequence: tsk.Sequence(),
|
||||
ID: tsk.ID(),
|
||||
})
|
||||
|
||||
got := gotModel.View()
|
||||
|
||||
t.Log(got)
|
||||
snaps.MatchSnapshot(t, got)
|
||||
})
|
||||
|
||||
+13
-19
@@ -29,8 +29,6 @@ type Handler struct {
|
||||
Config HandlerConfig
|
||||
|
||||
bubbly.EventHandler
|
||||
|
||||
onNewCatalogerTask *sync.Once
|
||||
}
|
||||
|
||||
func DefaultHandlerConfig() HandlerConfig {
|
||||
@@ -43,32 +41,28 @@ func New(cfg HandlerConfig) *Handler {
|
||||
d := bubbly.NewEventDispatcher()
|
||||
|
||||
h := &Handler{
|
||||
EventHandler: d,
|
||||
Running: &sync.WaitGroup{},
|
||||
Config: cfg,
|
||||
onNewCatalogerTask: &sync.Once{},
|
||||
EventHandler: d,
|
||||
Running: &sync.WaitGroup{},
|
||||
Config: cfg,
|
||||
}
|
||||
|
||||
// register all supported event types with the respective handler functions
|
||||
d.AddHandlers(map[partybus.EventType]bubbly.EventHandlerFn{
|
||||
stereoscopeEvent.PullDockerImage: simpleHandler(h.handlePullDockerImage),
|
||||
stereoscopeEvent.PullContainerdImage: simpleHandler(h.handlePullContainerdImage),
|
||||
stereoscopeEvent.ReadImage: simpleHandler(h.handleReadImage),
|
||||
stereoscopeEvent.FetchImage: simpleHandler(h.handleFetchImage),
|
||||
syftEvent.FileIndexingStarted: simpleHandler(h.handleFileIndexingStarted),
|
||||
syftEvent.AttestationStarted: simpleHandler(h.handleAttestationStarted),
|
||||
syftEvent.CatalogerTaskStarted: h.handleCatalogerTaskStarted,
|
||||
stereoscopeEvent.PullDockerImage: h.handlePullDockerImage,
|
||||
stereoscopeEvent.PullContainerdImage: h.handlePullContainerdImage,
|
||||
stereoscopeEvent.ReadImage: h.handleReadImage,
|
||||
stereoscopeEvent.FetchImage: h.handleFetchImage,
|
||||
syftEvent.PackageCatalogerStarted: h.handlePackageCatalogerStarted,
|
||||
syftEvent.FileDigestsCatalogerStarted: h.handleFileDigestsCatalogerStarted,
|
||||
syftEvent.FileMetadataCatalogerStarted: h.handleFileMetadataCatalogerStarted,
|
||||
syftEvent.FileIndexingStarted: h.handleFileIndexingStarted,
|
||||
syftEvent.AttestationStarted: h.handleAttestationStarted,
|
||||
syftEvent.CatalogerTaskStarted: h.handleCatalogerTaskStarted,
|
||||
})
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func simpleHandler(fn func(partybus.Event) []tea.Model) bubbly.EventHandlerFn {
|
||||
return func(e partybus.Event) ([]tea.Model, tea.Cmd) {
|
||||
return fn(e), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Handler) OnMessage(msg tea.Msg) {
|
||||
if msg, ok := msg.(tea.WindowSizeMsg); ok {
|
||||
m.WindowSize = msg
|
||||
|
||||
@@ -4,11 +4,12 @@ import (
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
func runModel(t testing.TB, m tea.Model, iterations int, message tea.Msg, h ...*sync.WaitGroup) tea.Model {
|
||||
func runModel(t testing.TB, m tea.Model, iterations int, message tea.Msg, h ...*sync.WaitGroup) string {
|
||||
t.Helper()
|
||||
if iterations == 0 {
|
||||
iterations = 1
|
||||
@@ -36,21 +37,34 @@ func runModel(t testing.TB, m tea.Model, iterations int, message tea.Msg, h ...*
|
||||
cmd = tea.Batch(nextCmds...)
|
||||
}
|
||||
|
||||
return m
|
||||
return m.View()
|
||||
}
|
||||
|
||||
func flatten(ps ...tea.Msg) (msgs []tea.Msg) {
|
||||
for _, p := range ps {
|
||||
if bm, ok := p.(tea.BatchMsg); ok {
|
||||
for _, m := range bm {
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
msgs = append(msgs, flatten(m())...)
|
||||
}
|
||||
} else {
|
||||
msgs = []tea.Msg{p}
|
||||
func flatten(p tea.Msg) (msgs []tea.Msg) {
|
||||
if reflect.TypeOf(p).Name() == "batchMsg" {
|
||||
partials := extractBatchMessages(p)
|
||||
for _, m := range partials {
|
||||
msgs = append(msgs, flatten(m)...)
|
||||
}
|
||||
} else {
|
||||
msgs = []tea.Msg{p}
|
||||
}
|
||||
return msgs
|
||||
}
|
||||
|
||||
func extractBatchMessages(m tea.Msg) (ret []tea.Msg) {
|
||||
sliceMsgType := reflect.SliceOf(reflect.TypeOf(tea.Cmd(nil)))
|
||||
value := reflect.ValueOf(m) // note: this is technically unaddressable
|
||||
|
||||
// make our own instance that is addressable
|
||||
valueCopy := reflect.New(value.Type()).Elem()
|
||||
valueCopy.Set(value)
|
||||
|
||||
cmds := reflect.NewAt(sliceMsgType, unsafe.Pointer(valueCopy.UnsafeAddr())).Elem()
|
||||
for i := 0; i < cmds.Len(); i++ {
|
||||
item := cmds.Index(i)
|
||||
r := item.Call(nil)
|
||||
ret = append(ret, r[0].Interface().(tea.Msg))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
@@ -155,11 +155,7 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
models, cmd := m.handler.Handle(msg)
|
||||
if cmd != nil {
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
for _, newModel := range models {
|
||||
for _, newModel := range m.handler.Handle(msg) {
|
||||
if newModel == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -3,25 +3,25 @@ module github.com/anchore/syft
|
||||
go 1.21.0
|
||||
|
||||
require (
|
||||
github.com/CycloneDX/cyclonedx-go v0.8.0
|
||||
github.com/CycloneDX/cyclonedx-go v0.7.2
|
||||
github.com/Masterminds/semver v1.5.0
|
||||
github.com/Masterminds/sprig/v3 v3.2.3
|
||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
|
||||
github.com/acobaugh/osrelease v0.1.0
|
||||
github.com/anchore/bubbly v0.0.0-20231115134915-def0aba654a9
|
||||
github.com/anchore/bubbly v0.0.0-20230801194016-acdb4981b461
|
||||
github.com/anchore/clio v0.0.0-20231016125544-c98a83e1c7fc
|
||||
github.com/anchore/fangs v0.0.0-20231201140849-5075d28d6d8b
|
||||
github.com/anchore/fangs v0.0.0-20231103141714-84c94dc43a2e
|
||||
github.com/anchore/go-logger v0.0.0-20230725134548-c21dafa1ec5a
|
||||
github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb
|
||||
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
|
||||
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
|
||||
github.com/anchore/packageurl-go v0.1.1-0.20230104203445-02e0a6721501
|
||||
github.com/anchore/stereoscope v0.0.0-20231220161148-590920dabc54
|
||||
github.com/anchore/stereoscope v0.0.0-20231117203853-3610f4ef3e83
|
||||
// we are hinting brotli to latest due to warning when installing archiver v3:
|
||||
// go: warning: github.com/andybalholm/brotli@v1.0.1: retracted by module author: occasional panics and data corruption
|
||||
github.com/aquasecurity/go-pep440-version v0.0.0-20210121094942-22b2f8951d46
|
||||
github.com/bmatcuk/doublestar/v4 v4.6.1
|
||||
github.com/charmbracelet/bubbletea v0.25.0
|
||||
github.com/charmbracelet/bubbletea v0.24.2
|
||||
github.com/charmbracelet/lipgloss v0.9.1
|
||||
github.com/dave/jennifer v1.7.0
|
||||
github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da
|
||||
@@ -32,12 +32,12 @@ require (
|
||||
github.com/github/go-spdx/v2 v2.2.0
|
||||
github.com/gkampitakis/go-snaps v0.4.12
|
||||
github.com/go-git/go-billy/v5 v5.5.0
|
||||
github.com/go-git/go-git/v5 v5.11.0
|
||||
github.com/go-git/go-git/v5 v5.10.1
|
||||
github.com/go-test/deep v1.1.0
|
||||
github.com/google/go-cmp v0.6.0
|
||||
github.com/google/go-containerregistry v0.17.0
|
||||
github.com/google/go-containerregistry v0.16.1
|
||||
github.com/google/licensecheck v0.3.1
|
||||
github.com/google/uuid v1.5.0
|
||||
github.com/google/uuid v1.4.0
|
||||
github.com/gookit/color v1.5.4
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/iancoleman/strcase v0.3.0
|
||||
@@ -53,7 +53,7 @@ require (
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
github.com/opencontainers/go-digest v1.0.0
|
||||
github.com/pelletier/go-toml v1.9.5
|
||||
github.com/saferwall/pe v1.4.8
|
||||
github.com/saferwall/pe v1.4.7
|
||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d
|
||||
github.com/sanity-io/litter v1.5.5
|
||||
github.com/sassoftware/go-rpmutils v0.2.0
|
||||
@@ -71,15 +71,9 @@ require (
|
||||
github.com/xeipuuv/gojsonschema v1.2.0
|
||||
github.com/zyedidia/generic v1.2.2-0.20230320175451-4410d2372cb1
|
||||
golang.org/x/mod v0.14.0
|
||||
golang.org/x/net v0.20.0
|
||||
golang.org/x/net v0.19.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
modernc.org/sqlite v1.28.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
|
||||
github.com/charmbracelet/bubbles v0.17.1
|
||||
github.com/jedib0t/go-pretty/v6 v6.5.3
|
||||
modernc.org/sqlite v1.27.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -91,20 +85,20 @@ require (
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.2.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||
github.com/Microsoft/hcsshim v0.11.4 // indirect
|
||||
github.com/Microsoft/hcsshim v0.11.1 // indirect
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect
|
||||
github.com/adrg/xdg v0.4.0 // indirect
|
||||
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 // indirect
|
||||
github.com/andybalholm/brotli v1.0.4 // indirect
|
||||
github.com/aquasecurity/go-version v0.0.0-20210121072130-637058cfe492 // indirect
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/becheran/wildmatch-go v1.0.0 // indirect
|
||||
github.com/charmbracelet/bubbles v0.16.1 // indirect
|
||||
github.com/charmbracelet/harmonica v0.2.0 // indirect
|
||||
github.com/cloudflare/circl v1.3.3 // indirect
|
||||
github.com/containerd/cgroups v1.1.0 // indirect
|
||||
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
|
||||
github.com/containerd/containerd v1.7.11 // indirect
|
||||
github.com/containerd/containerd v1.7.8 // indirect
|
||||
github.com/containerd/continuity v0.4.2 // indirect
|
||||
github.com/containerd/fifo v1.1.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
@@ -123,13 +117,12 @@ require (
|
||||
github.com/edsrzf/mmap-go v1.1.0 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/felixge/fgprof v0.9.3 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.3 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.0 // indirect
|
||||
github.com/gkampitakis/ciinfo v0.3.0 // indirect
|
||||
github.com/gkampitakis/go-diff v1.3.2 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-logr/logr v1.2.4 // indirect
|
||||
github.com/go-logr/logr v1.2.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-restruct/restruct v1.2.0-alpha // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
@@ -186,7 +179,6 @@ require (
|
||||
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.3.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect
|
||||
github.com/shopspring/decimal v1.2.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/skeema/knownhosts v1.2.1 // indirect
|
||||
@@ -211,17 +203,15 @@ require (
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
|
||||
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 // indirect
|
||||
go.opentelemetry.io/otel v1.19.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.19.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.19.0 // indirect
|
||||
go.opentelemetry.io/otel v1.14.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.14.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
golang.org/x/crypto v0.18.0 // indirect
|
||||
golang.org/x/crypto v0.16.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||
golang.org/x/sync v0.5.0 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
golang.org/x/term v0.16.0 // indirect
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
golang.org/x/term v0.15.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/tools v0.13.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
|
||||
@@ -58,8 +58,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
|
||||
github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/CycloneDX/cyclonedx-go v0.8.0 h1:FyWVj6x6hoJrui5uRQdYZcSievw3Z32Z88uYzG/0D6M=
|
||||
github.com/CycloneDX/cyclonedx-go v0.8.0/go.mod h1:K2bA+324+Og0X84fA8HhN2X066K7Bxz4rpMQ4ZhjtSk=
|
||||
github.com/CycloneDX/cyclonedx-go v0.7.2 h1:kKQ0t1dPOlugSIYVOMiMtFqeXI2wp/f5DBIdfux8gnQ=
|
||||
github.com/CycloneDX/cyclonedx-go v0.7.2/go.mod h1:K2bA+324+Og0X84fA8HhN2X066K7Bxz4rpMQ4ZhjtSk=
|
||||
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
|
||||
github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
|
||||
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
|
||||
@@ -74,8 +74,8 @@ github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBa
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
||||
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
||||
github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8=
|
||||
github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w=
|
||||
github.com/Microsoft/hcsshim v0.11.1 h1:hJ3s7GbWlGK4YVV92sO88BQSyF4ZLVy7/awqOlPxFbA=
|
||||
github.com/Microsoft/hcsshim v0.11.1/go.mod h1:nFJmaO4Zr5Y7eADdFOpYswDDlNVbvcIJJNJLECr5JQg=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
||||
@@ -89,12 +89,12 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/anchore/bubbly v0.0.0-20231115134915-def0aba654a9 h1:p0ZIe0htYOX284Y4axJaGBvXHU0VCCzLN5Wf5XbKStU=
|
||||
github.com/anchore/bubbly v0.0.0-20231115134915-def0aba654a9/go.mod h1:3ZsFB9tzW3vl4gEiUeuSOMDnwroWxIxJelOOHUp8dSw=
|
||||
github.com/anchore/bubbly v0.0.0-20230801194016-acdb4981b461 h1:xGu4/uMWucwWV0YV3fpFIQZ6KVfS/Wfhmma8t0s0vRo=
|
||||
github.com/anchore/bubbly v0.0.0-20230801194016-acdb4981b461/go.mod h1:Ger02eh5NpPm2IqkPAy396HU1KlK3BhOeCljDYXySSk=
|
||||
github.com/anchore/clio v0.0.0-20231016125544-c98a83e1c7fc h1:A1KFO+zZZmbNlz1+WKsCF0RKVx6XRoxsAG3lrqH9hUQ=
|
||||
github.com/anchore/clio v0.0.0-20231016125544-c98a83e1c7fc/go.mod h1:QeWvNzxsrUNxcs6haQo3OtISfXUXW0qAuiG4EQiz0GU=
|
||||
github.com/anchore/fangs v0.0.0-20231201140849-5075d28d6d8b h1:L/djgY7ZbZ/38+wUtdkk398W3PIBJLkt1N8nU/7e47A=
|
||||
github.com/anchore/fangs v0.0.0-20231201140849-5075d28d6d8b/go.mod h1:TLcE0RE5+8oIx2/NPWem/dq1DeaMoC+fPEH7hoSzPLo=
|
||||
github.com/anchore/fangs v0.0.0-20231103141714-84c94dc43a2e h1:O8ZubApaSl7dRzKNvyfGq9cLIPLQ5v3Iz0Y3huHKCgg=
|
||||
github.com/anchore/fangs v0.0.0-20231103141714-84c94dc43a2e/go.mod h1:yPsN3NUGhU5dcBtYBa1dMNzGu1yT5ZAfSjKq9DY4aV8=
|
||||
github.com/anchore/go-logger v0.0.0-20230725134548-c21dafa1ec5a h1:nJ2G8zWKASyVClGVgG7sfM5mwoZlZ2zYpIzN2OhjWkw=
|
||||
github.com/anchore/go-logger v0.0.0-20230725134548-c21dafa1ec5a/go.mod h1:ubLFmlsv8/DFUQrZwY5syT5/8Er3ugSr4rDFwHsE3hg=
|
||||
github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb h1:iDMnx6LIjtjZ46C0akqveX83WFzhpTD3eqOthawb5vU=
|
||||
@@ -107,8 +107,8 @@ github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZV
|
||||
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E=
|
||||
github.com/anchore/packageurl-go v0.1.1-0.20230104203445-02e0a6721501 h1:AV7qjwMcM4r8wFhJq3jLRztew3ywIyPTRapl2T1s9o8=
|
||||
github.com/anchore/packageurl-go v0.1.1-0.20230104203445-02e0a6721501/go.mod h1:Blo6OgJNiYF41ufcgHKkbCKF2MDOMlrqhXv/ij6ocR4=
|
||||
github.com/anchore/stereoscope v0.0.0-20231220161148-590920dabc54 h1:i2YK5QEs9H2YB3B2zv+AGR44ves0nmAGOD07lMphH14=
|
||||
github.com/anchore/stereoscope v0.0.0-20231220161148-590920dabc54/go.mod h1:IylG7ofLoUKHwS1XDF6rPhOmaE3GgpAgsMdvvYfooTU=
|
||||
github.com/anchore/stereoscope v0.0.0-20231117203853-3610f4ef3e83 h1:mxGIOmj+asEm8LUkPTG3/v0hi27WIlDVjiEVsUB9eqY=
|
||||
github.com/anchore/stereoscope v0.0.0-20231117203853-3610f4ef3e83/go.mod h1:GKAnytSVV1hoqB5r5Gd9M5Ph3Rzqq0zPdEJesewjC2w=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
||||
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
||||
@@ -127,8 +127,6 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj
|
||||
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/becheran/wildmatch-go v1.0.0 h1:mE3dGGkTmpKtT4Z+88t8RStG40yN9T+kFEGj2PZFSzA=
|
||||
@@ -147,10 +145,10 @@ github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/charmbracelet/bubbles v0.17.1 h1:0SIyjOnkrsfDo88YvPgAWvZMwXe26TP6drRvmkjyUu4=
|
||||
github.com/charmbracelet/bubbles v0.17.1/go.mod h1:9HxZWlkCqz2PRwsCbYl7a3KXvGzFaDHpYbSYMJ+nE3o=
|
||||
github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM=
|
||||
github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg=
|
||||
github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY=
|
||||
github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc=
|
||||
github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06RaW2cx/SY=
|
||||
github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg=
|
||||
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
|
||||
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
|
||||
github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg=
|
||||
@@ -180,8 +178,8 @@ github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHq
|
||||
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
|
||||
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
|
||||
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
|
||||
github.com/containerd/containerd v1.7.11 h1:lfGKw3eU35sjV0aG2eYZTiwFEY1pCzxdzicHP3SZILw=
|
||||
github.com/containerd/containerd v1.7.11/go.mod h1:5UluHxHTX2rdvYuZ5OJTC5m/KJNs0Zs9wVoJm9zf5ZE=
|
||||
github.com/containerd/containerd v1.7.8 h1:RkwgOW3AVUT3H/dyT0W03Dc8AzlpMG65lX48KftOFSM=
|
||||
github.com/containerd/containerd v1.7.8/go.mod h1:L/Hn9qylJtUFT7cPeM0Sr3fATj+WjHwRQ0lyrYk3OPY=
|
||||
github.com/containerd/continuity v0.4.2 h1:v3y/4Yz5jwnvqPKJJ+7Wf93fyWoCB3F5EclWG023MDM=
|
||||
github.com/containerd/continuity v0.4.2/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ=
|
||||
github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY=
|
||||
@@ -263,8 +261,6 @@ github.com/fatih/set v0.2.1 h1:nn2CaJyknWE/6txyUDGwysr3G5QC6xWB/PtVjPBbeaA=
|
||||
github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI=
|
||||
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
|
||||
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
|
||||
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
|
||||
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
|
||||
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
|
||||
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
@@ -292,8 +288,8 @@ github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+
|
||||
github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||
github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4=
|
||||
github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY=
|
||||
github.com/go-git/go-git/v5 v5.10.1 h1:tu8/D8i+TWxgKpzQ3Vc43e+kkhXqtsZCKI/egajKnxk=
|
||||
github.com/go-git/go-git/v5 v5.10.1/go.mod h1:uEuHjxkHap8kAl//V5F/nNWwqIYtP/402ddd05mp0wg=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
@@ -302,8 +298,8 @@ github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
|
||||
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-restruct/restruct v1.2.0-alpha h1:2Lp474S/9660+SJjpVxoKuWX09JsXHSrdV7Nv3/gkvc=
|
||||
@@ -372,8 +368,8 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-containerregistry v0.17.0 h1:5p+zYs/R4VGHkhyvgWurWrpJ2hW4Vv9fQI+GzdcwXLk=
|
||||
github.com/google/go-containerregistry v0.17.0/go.mod h1:u0qB2l7mvtWVR5kNcbFIhFY1hLbf8eeGapA+vbFDCtQ=
|
||||
github.com/google/go-containerregistry v0.16.1 h1:rUEt426sR6nyrL3gt+18ibRcvYpKYdpsa5ZW7MA08dQ=
|
||||
github.com/google/go-containerregistry v0.16.1/go.mod h1:u0qB2l7mvtWVR5kNcbFIhFY1hLbf8eeGapA+vbFDCtQ=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/licensecheck v0.3.1 h1:QoxgoDkaeC4nFrtGN1jV7IPmDCHFNIVh54e5hSt6sPs=
|
||||
github.com/google/licensecheck v0.3.1/go.mod h1:ORkR35t/JjW+emNKtfJDII0zlciG9JgbT7SmsohlHmY=
|
||||
@@ -401,8 +397,8 @@ github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S3
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
|
||||
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
|
||||
@@ -466,8 +462,6 @@ github.com/invopop/jsonschema v0.7.0 h1:2vgQcBz1n256N+FpX3Jq7Y17AjYt46Ig3zIWyy77
|
||||
github.com/invopop/jsonschema v0.7.0/go.mod h1:O9uiLokuu0+MGFlyiaqtWxwqJm41/+8Nj0lD7A36YH0=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jedib0t/go-pretty/v6 v6.5.3 h1:GIXn6Er/anHTkVUoufs7ptEvxdD6KIhR7Axa2wYCPF0=
|
||||
github.com/jedib0t/go-pretty/v6 v6.5.3/go.mod h1:5LQIxa52oJ/DlDSLv0HEkWOFMDGoWkJb9ss5KqPpJBg=
|
||||
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
|
||||
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
@@ -507,8 +501,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 h1:bqDmpDG49ZRnB5PcgP0RXtQvnMSgIF14M7CBd2shtXs=
|
||||
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
@@ -672,15 +664,13 @@ github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUz
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/saferwall/pe v1.4.8 h1:ey/L8FGBMrJ1Xh+Rltj1MAFPZ4LOQYGJqNa5B1Na6B0=
|
||||
github.com/saferwall/pe v1.4.8/go.mod h1:SNzv3cdgk8SBI0UwHfyTcdjawfdnN+nbydnEL7GZ25s=
|
||||
github.com/saferwall/pe v1.4.7 h1:A+G3DxX49paJ5OsxBfHKskhyDtmTjShlDmBd81IsHlQ=
|
||||
github.com/saferwall/pe v1.4.7/go.mod h1:SNzv3cdgk8SBI0UwHfyTcdjawfdnN+nbydnEL7GZ25s=
|
||||
github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig=
|
||||
github.com/sagikazarmark/locafero v0.3.0 h1:zT7VEGWC2DTflmccN/5T1etyKvxSxpHsjb9cJvm4SvQ=
|
||||
github.com/sagikazarmark/locafero v0.3.0/go.mod h1:w+v7UsPNFwzF1cHuOajOOzoq4U7v/ig1mpRjqV+Bu1U=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f h1:MvTmaQdww/z0Q4wrYjDSCcZ78NoftLQyHBSLW/Cx79Y=
|
||||
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA=
|
||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
|
||||
github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo=
|
||||
@@ -827,14 +817,10 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 h1:x8Z78aZx8cOF0+Kkazoc7lwUNMGy0LrzEMxTm4BbTxg=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0/go.mod h1:62CPTSry9QZtOaSsE3tOzhx6LzDhHnXJ6xHeMNNiM6Q=
|
||||
go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs=
|
||||
go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY=
|
||||
go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE=
|
||||
go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8=
|
||||
go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg=
|
||||
go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo=
|
||||
go.opentelemetry.io/otel v1.14.0 h1:/79Huy8wbf5DnIPhemGB+zEPVwnN6fuQybr/SRXa6hM=
|
||||
go.opentelemetry.io/otel v1.14.0/go.mod h1:o4buv+dJzx8rohcUeRmWUZhqupFvzWis188WlggnNeU=
|
||||
go.opentelemetry.io/otel/trace v1.14.0 h1:wp2Mmvj41tDsyAJXiWDWpfNsOiIyd38fy85pyKcFq/M=
|
||||
go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8=
|
||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
@@ -862,8 +848,8 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0
|
||||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
|
||||
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -954,8 +940,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -1071,16 +1057,16 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
|
||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
|
||||
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -1367,8 +1353,8 @@ modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
|
||||
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
|
||||
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ=
|
||||
modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
|
||||
modernc.org/sqlite v1.27.0 h1:MpKAHoyYB7xqcwnUwkuD+npwEa0fojF0B5QRbN+auJ8=
|
||||
modernc.org/sqlite v1.27.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
|
||||
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
|
||||
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
|
||||
modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY=
|
||||
|
||||
@@ -2,12 +2,10 @@ package bus
|
||||
|
||||
import (
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/syft/internal/redact"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/syft/event/monitor"
|
||||
)
|
||||
|
||||
func Exit() {
|
||||
@@ -35,18 +33,3 @@ func Notify(message string) {
|
||||
Value: message,
|
||||
})
|
||||
}
|
||||
|
||||
func StartCatalogerTask(info monitor.GenericTask, size int64, initialStage string) *monitor.CatalogerTaskProgress {
|
||||
t := &monitor.CatalogerTaskProgress{
|
||||
AtomicStage: progress.NewAtomicStage(initialStage),
|
||||
Manual: progress.NewManual(size),
|
||||
}
|
||||
|
||||
Publish(partybus.Event{
|
||||
Type: event.CatalogerTaskStarted,
|
||||
Source: info,
|
||||
Value: progress.StagedProgressable(t),
|
||||
})
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
package relationship
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/internal/sbomsync"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/cataloging"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
func Finalize(builder sbomsync.Builder, cfg cataloging.RelationshipsConfig, src artifact.Identifiable) {
|
||||
accessor := builder.(sbomsync.Accessor)
|
||||
|
||||
// add relationships showing packages that are evident by a file which is owned by another package (package-to-package)
|
||||
if cfg.PackageFileOwnershipOverlap {
|
||||
byFileOwnershipOverlapWorker(accessor)
|
||||
}
|
||||
|
||||
// conditionally remove binary packages based on file ownership overlap relationships found
|
||||
// https://github.com/anchore/syft/issues/931
|
||||
if cfg.ExcludeBinaryPackagesWithFileOwnershipOverlap {
|
||||
excludeBinariesByFileOwnershipOverlap(accessor)
|
||||
}
|
||||
|
||||
// add source "contains package" relationship (source-to-package)
|
||||
var sourceRelationships []artifact.Relationship
|
||||
accessor.ReadFromSBOM(func(s *sbom.SBOM) {
|
||||
sourceRelationships = toSource(src, s.Artifacts.Packages)
|
||||
})
|
||||
builder.AddRelationships(sourceRelationships...)
|
||||
|
||||
// add evident-by relationships (package-to-file)
|
||||
var evidentByRelationships []artifact.Relationship
|
||||
accessor.ReadFromSBOM(func(s *sbom.SBOM) {
|
||||
evidentByRelationships = evidentBy(s.Artifacts.Packages)
|
||||
})
|
||||
builder.AddRelationships(evidentByRelationships...)
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package relationship
|
||||
|
||||
import "github.com/anchore/syft/syft/artifact"
|
||||
|
||||
// TODO: put under test...
|
||||
func RemoveRelationshipsByID(relationships []artifact.Relationship, id artifact.ID) []artifact.Relationship {
|
||||
var filtered []artifact.Relationship
|
||||
for _, r := range relationships {
|
||||
if r.To.ID() != id && r.From.ID() != id {
|
||||
filtered = append(filtered, r)
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package relationship
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func toSource(src artifact.Identifiable, c *pkg.Collection) []artifact.Relationship {
|
||||
relationships := make([]artifact.Relationship, 0) // Should we pre-allocate this by giving catalog a Len() method?
|
||||
for p := range c.Enumerate() {
|
||||
relationships = append(relationships, artifact.Relationship{
|
||||
From: src,
|
||||
To: p,
|
||||
Type: artifact.ContainsRelationship,
|
||||
})
|
||||
}
|
||||
|
||||
return relationships
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
package sbomsync
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/linux"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
var _ interface {
|
||||
Accessor
|
||||
Builder
|
||||
} = (*sbomBuilder)(nil) // integrity check
|
||||
|
||||
// Builder provides a simple facade for simple additions to the SBOM
|
||||
type Builder interface {
|
||||
// nodes
|
||||
|
||||
AddPackages(...pkg.Package)
|
||||
|
||||
// edges
|
||||
|
||||
AddRelationships(...artifact.Relationship)
|
||||
|
||||
// other
|
||||
|
||||
SetLinuxDistribution(linux.Release)
|
||||
}
|
||||
|
||||
// Accessor allows for low-level access to the SBOM
|
||||
type Accessor interface {
|
||||
WriteToSBOM(func(*sbom.SBOM))
|
||||
ReadFromSBOM(func(*sbom.SBOM))
|
||||
}
|
||||
|
||||
type sbomBuilder struct {
|
||||
sbom *sbom.SBOM
|
||||
lock *sync.RWMutex
|
||||
}
|
||||
|
||||
func NewBuilder(s *sbom.SBOM) Builder {
|
||||
return &sbomBuilder{
|
||||
sbom: s,
|
||||
lock: &sync.RWMutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func (b sbomBuilder) WriteToSBOM(fn func(*sbom.SBOM)) {
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
|
||||
fn(b.sbom)
|
||||
}
|
||||
|
||||
func (b sbomBuilder) ReadFromSBOM(fn func(*sbom.SBOM)) {
|
||||
b.lock.RLock()
|
||||
defer b.lock.RUnlock()
|
||||
|
||||
fn(b.sbom)
|
||||
}
|
||||
|
||||
func (b sbomBuilder) AddPackages(p ...pkg.Package) {
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
|
||||
b.sbom.Artifacts.Packages.Add(p...)
|
||||
}
|
||||
|
||||
func (b sbomBuilder) AddRelationships(relationship ...artifact.Relationship) {
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
|
||||
b.sbom.Relationships = append(b.sbom.Relationships, relationship...)
|
||||
}
|
||||
|
||||
func (b sbomBuilder) SetLinuxDistribution(release linux.Release) {
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
|
||||
b.sbom.Artifacts.LinuxDistribution = &release
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/anchore/syft/internal/sbomsync"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/linux"
|
||||
)
|
||||
|
||||
// TODO: add tui element here?
|
||||
|
||||
func NewEnvironmentTask() Task {
|
||||
fn := func(ctx context.Context, resolver file.Resolver, builder sbomsync.Builder) error {
|
||||
release := linux.IdentifyRelease(resolver)
|
||||
if release != nil {
|
||||
builder.SetLinuxDistribution(*release)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return NewTask("environment-cataloger", fn)
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
|
||||
"github.com/anchore/syft/internal/sbomsync"
|
||||
"github.com/anchore/syft/syft/event/monitor"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
)
|
||||
|
||||
type Executor struct {
|
||||
numWorkers int
|
||||
tasks chan Task
|
||||
}
|
||||
|
||||
func NewTaskExecutor(tasks []Task, numWorkers int) *Executor {
|
||||
p := &Executor{
|
||||
numWorkers: numWorkers,
|
||||
tasks: make(chan Task, len(tasks)),
|
||||
}
|
||||
|
||||
for i := range tasks {
|
||||
p.tasks <- tasks[i]
|
||||
}
|
||||
close(p.tasks)
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *Executor) Execute(ctx context.Context, resolver file.Resolver, s sbomsync.Builder, prog *monitor.CatalogerTaskProgress) error {
|
||||
var errs error
|
||||
wg := &sync.WaitGroup{}
|
||||
for i := 0; i < p.numWorkers; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
for {
|
||||
tsk, ok := <-p.tasks
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if err := runTaskSafely(ctx, tsk, resolver, s); err != nil {
|
||||
errs = multierror.Append(errs, fmt.Errorf("failed to run task: %w", err))
|
||||
prog.SetError(err)
|
||||
}
|
||||
prog.Increment()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func runTaskSafely(ctx context.Context, t Task, resolver file.Resolver, s sbomsync.Builder) (err error) {
|
||||
// handle individual cataloger panics
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
err = fmt.Errorf("%v at:\n%s", e, string(debug.Stack()))
|
||||
}
|
||||
}()
|
||||
|
||||
return t.Execute(ctx, resolver, s)
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/syft/internal/sbomsync"
|
||||
"github.com/anchore/syft/syft/event/monitor"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
)
|
||||
|
||||
func Test_TaskExecutor_PanicHandling(t *testing.T) {
|
||||
tsk := NewTask("panicking-cataloger", func(_ context.Context, _ file.Resolver, _ sbomsync.Builder) error {
|
||||
panic("something bad happened")
|
||||
})
|
||||
ex := NewTaskExecutor([]Task{tsk}, 1)
|
||||
|
||||
err := ex.Execute(context.Background(), nil, nil, &monitor.CatalogerTaskProgress{
|
||||
Manual: progress.NewManual(-1),
|
||||
})
|
||||
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "executor_test.go")
|
||||
}
|
||||
@@ -1,238 +0,0 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/scylladb/go-set/strset"
|
||||
|
||||
"github.com/anchore/syft/syft/cataloging/pkgcataloging"
|
||||
)
|
||||
|
||||
var expressionNodePattern = regexp.MustCompile(`^([a-zA-Z0-9][a-zA-Z0-9-+]*)+$`)
|
||||
|
||||
const (
|
||||
SetOperation Operation = "set"
|
||||
AddOperation Operation = "add"
|
||||
SubSelectOperation Operation = "sub-select"
|
||||
RemoveOperation Operation = "remove"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrEmptyToken = fmt.Errorf("no value given")
|
||||
ErrInvalidToken = fmt.Errorf("invalid token given: only alphanumeric characters and hyphens are allowed")
|
||||
ErrInvalidOperator = fmt.Errorf("invalid operator given")
|
||||
ErrUnknownNameOrTag = fmt.Errorf("unknown name or tag given")
|
||||
ErrTagsNotAllowed = fmt.Errorf("tags are not allowed with this operation (must use exact names)")
|
||||
ErrNamesNotAllowed = fmt.Errorf("names are not allowed with this operation (must use tags)")
|
||||
ErrAllNotAllowed = fmt.Errorf("cannot use the 'all' operand in this context")
|
||||
)
|
||||
|
||||
// ErrInvalidExpression represents an expression that cannot be parsed or can be parsed but is logically invalid.
|
||||
type ErrInvalidExpression struct {
|
||||
Expression string
|
||||
Operation Operation
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e ErrInvalidExpression) Error() string {
|
||||
return fmt.Sprintf("invalid expression: %q: %s", e.Expression, e.Err.Error())
|
||||
}
|
||||
|
||||
func newErrInvalidExpression(exp string, op Operation, err error) ErrInvalidExpression {
|
||||
return ErrInvalidExpression{
|
||||
Expression: exp,
|
||||
Operation: op,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// Expression represents a single operation-operand pair with (all validation errors).
|
||||
// E.g. "+foo", "-bar", or "something" are all expressions. Some validations are relevant to not only the
|
||||
// syntax (operation and operator) but other are sensitive to the context of the operand (e.g. if a given operand
|
||||
// is a tag or a name, validated against the operation).
|
||||
type Expression struct {
|
||||
Operation Operation
|
||||
Operand string
|
||||
Errors []error
|
||||
}
|
||||
|
||||
// Operation represents the type of operation to perform on the operand (set, add, remove, sub-select).
|
||||
type Operation string
|
||||
|
||||
// Expressions represents a list of expressions.
|
||||
type Expressions []Expression
|
||||
|
||||
// expressionContext represents all information needed to validate an expression (e.g. the set of all tasks and their tags).
|
||||
type expressionContext struct {
|
||||
Names *strset.Set
|
||||
Tags *strset.Set
|
||||
}
|
||||
|
||||
func newExpressionContext(ts []Task) *expressionContext {
|
||||
ec := &expressionContext{
|
||||
Names: strset.New(tasks(ts).Names()...),
|
||||
Tags: strset.New(tasks(ts).Tags()...),
|
||||
}
|
||||
|
||||
ec.Tags.Add("all")
|
||||
|
||||
return ec
|
||||
}
|
||||
|
||||
// newExpression creates a new validated Expression object relative to the task names and tags.
|
||||
func (ec expressionContext) newExpression(exp string, operation Operation, token string) Expression {
|
||||
if token == "" {
|
||||
return Expression{
|
||||
Operation: operation,
|
||||
Operand: token,
|
||||
Errors: []error{newErrInvalidExpression(exp, operation, ErrEmptyToken)},
|
||||
}
|
||||
}
|
||||
|
||||
if !isValidNode(token) {
|
||||
return Expression{
|
||||
Operation: operation,
|
||||
Operand: token,
|
||||
Errors: []error{newErrInvalidExpression(exp, operation, ErrInvalidToken)},
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
switch operation {
|
||||
case SetOperation, RemoveOperation:
|
||||
// names and tags allowed
|
||||
if !ec.Tags.Has(token) && !ec.Names.Has(token) {
|
||||
err = newErrInvalidExpression(exp, operation, ErrUnknownNameOrTag)
|
||||
}
|
||||
case AddOperation:
|
||||
// only names are allowed
|
||||
if !ec.Names.Has(token) {
|
||||
if ec.Tags.Has(token) {
|
||||
err = newErrInvalidExpression(exp, operation, ErrTagsNotAllowed)
|
||||
} else {
|
||||
err = newErrInvalidExpression(exp, operation, ErrUnknownNameOrTag)
|
||||
}
|
||||
}
|
||||
case SubSelectOperation:
|
||||
if token == "all" {
|
||||
// special case: we cannot sub-select all (this is most likely a misconfiguration and the user intended to use the set operation)
|
||||
err = newErrInvalidExpression(exp, operation, ErrAllNotAllowed)
|
||||
} else if !ec.Tags.Has(token) {
|
||||
// only tags are allowed...
|
||||
if ec.Names.Has(token) {
|
||||
err = newErrInvalidExpression(exp, operation, ErrNamesNotAllowed)
|
||||
} else {
|
||||
err = newErrInvalidExpression(exp, operation, ErrUnknownNameOrTag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var errs []error
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
return Expression{
|
||||
Operation: operation,
|
||||
Operand: token,
|
||||
Errors: errs,
|
||||
}
|
||||
}
|
||||
|
||||
func newExpressionsFromSelectionRequest(nc *expressionContext, selectionRequest pkgcataloging.SelectionRequest) Expressions {
|
||||
var all Expressions
|
||||
|
||||
for _, exp := range selectionRequest.DefaultNamesOrTags {
|
||||
all = append(all, nc.newExpression(exp, SetOperation, exp))
|
||||
}
|
||||
|
||||
for _, exp := range selectionRequest.SubSelectTags {
|
||||
all = append(all, nc.newExpression(exp, SubSelectOperation, exp))
|
||||
}
|
||||
|
||||
for _, exp := range selectionRequest.AddNames {
|
||||
all = append(all, nc.newExpression(exp, AddOperation, exp))
|
||||
}
|
||||
|
||||
for _, exp := range selectionRequest.RemoveNamesOrTags {
|
||||
all = append(all, nc.newExpression(exp, RemoveOperation, exp))
|
||||
}
|
||||
|
||||
sort.Sort(all)
|
||||
return all
|
||||
}
|
||||
|
||||
func isValidNode(s string) bool {
|
||||
return expressionNodePattern.Match([]byte(s))
|
||||
}
|
||||
|
||||
func (e Expressions) Clone() Expressions {
|
||||
clone := make(Expressions, len(e))
|
||||
copy(clone, e)
|
||||
return clone
|
||||
}
|
||||
|
||||
func (e Expression) String() string {
|
||||
var op string
|
||||
switch e.Operation {
|
||||
case AddOperation:
|
||||
op = "+"
|
||||
case RemoveOperation:
|
||||
op = "-"
|
||||
case SubSelectOperation:
|
||||
op = ""
|
||||
case SetOperation:
|
||||
op = ""
|
||||
default:
|
||||
op = "?"
|
||||
}
|
||||
return op + e.Operand
|
||||
}
|
||||
|
||||
func (e Expressions) Len() int {
|
||||
return len(e)
|
||||
}
|
||||
|
||||
func (e Expressions) Swap(i, j int) {
|
||||
e[i], e[j] = e[j], e[i]
|
||||
}
|
||||
|
||||
// order of operations
|
||||
var orderOfOps = map[Operation]int{
|
||||
SetOperation: 1,
|
||||
SubSelectOperation: 2,
|
||||
RemoveOperation: 3,
|
||||
AddOperation: 4,
|
||||
}
|
||||
|
||||
func (e Expressions) Less(i, j int) bool {
|
||||
ooi := orderOfOps[e[i].Operation]
|
||||
ooj := orderOfOps[e[j].Operation]
|
||||
|
||||
if ooi != ooj {
|
||||
return ooi < ooj
|
||||
}
|
||||
|
||||
return i < j
|
||||
}
|
||||
|
||||
func (e Expressions) Errors() (errs []error) {
|
||||
for _, n := range e {
|
||||
if len(n.Errors) > 0 {
|
||||
errs = append(errs, n.Errors...)
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func (e Expressions) Validate() error {
|
||||
errs := e.Errors()
|
||||
if len(errs) == 0 {
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
return multierror.Append(err, e.Errors()...)
|
||||
}
|
||||
@@ -1,242 +0,0 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/syft/cataloging/pkgcataloging"
|
||||
)
|
||||
|
||||
func Test_newExpressionsFromSelectionRequest(t *testing.T) {
|
||||
ts := []Task{
|
||||
dummyTask("1", "t1"),
|
||||
dummyTask("2", "t2"),
|
||||
dummyTask("3", "t3"),
|
||||
dummyTask("4", "t4"),
|
||||
dummyTask("5"),
|
||||
dummyTask("6"),
|
||||
}
|
||||
|
||||
nc := newExpressionContext(ts)
|
||||
|
||||
var tests = []struct {
|
||||
name string
|
||||
basis []string
|
||||
expressions []string
|
||||
expected Expressions
|
||||
expectedErrors []error
|
||||
}{
|
||||
{
|
||||
name: "empty input",
|
||||
basis: []string{},
|
||||
expressions: []string{},
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "valid single set operation",
|
||||
basis: []string{"1"},
|
||||
expressions: []string{},
|
||||
expected: []Expression{
|
||||
{Operation: SetOperation, Operand: "1"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "add operation",
|
||||
basis: []string{},
|
||||
expressions: []string{"+4"},
|
||||
expected: []Expression{
|
||||
{Operation: AddOperation, Operand: "4"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remove operation",
|
||||
basis: []string{},
|
||||
expressions: []string{"-3"},
|
||||
expected: []Expression{
|
||||
{Operation: RemoveOperation, Operand: "3"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "select operation",
|
||||
basis: []string{},
|
||||
expressions: []string{"t2"},
|
||||
expected: []Expression{
|
||||
{Operation: SubSelectOperation, Operand: "t2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mixed operations order",
|
||||
basis: []string{"1"},
|
||||
expressions: []string{"+4", "-3", "t2"},
|
||||
expected: []Expression{
|
||||
// note they are sorted by operation
|
||||
{Operation: SetOperation, Operand: "1"},
|
||||
{Operation: SubSelectOperation, Operand: "t2"},
|
||||
{Operation: RemoveOperation, Operand: "3"},
|
||||
{Operation: AddOperation, Operand: "4"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid token",
|
||||
basis: []string{"!1"},
|
||||
expressions: []string{},
|
||||
expected: nil,
|
||||
expectedErrors: []error{ErrInvalidToken},
|
||||
},
|
||||
{
|
||||
name: "use + operator in basis",
|
||||
basis: []string{"+1"},
|
||||
expressions: []string{},
|
||||
expected: nil,
|
||||
expectedErrors: []error{ErrInvalidToken},
|
||||
},
|
||||
{
|
||||
name: "use - operator in basis",
|
||||
basis: []string{"-1"},
|
||||
expressions: []string{},
|
||||
expected: nil,
|
||||
expectedErrors: []error{ErrInvalidToken},
|
||||
},
|
||||
{
|
||||
name: "invalid name",
|
||||
basis: []string{},
|
||||
expressions: []string{"+t1"},
|
||||
expected: nil,
|
||||
expectedErrors: []error{ErrTagsNotAllowed},
|
||||
},
|
||||
{
|
||||
name: "invalid tag",
|
||||
basis: []string{},
|
||||
expressions: []string{"1"},
|
||||
expected: nil,
|
||||
expectedErrors: []error{ErrNamesNotAllowed},
|
||||
},
|
||||
{
|
||||
name: "invalid use of all",
|
||||
basis: []string{},
|
||||
expressions: []string{"all"},
|
||||
expected: nil,
|
||||
expectedErrors: []error{ErrAllNotAllowed},
|
||||
},
|
||||
{
|
||||
name: "allow all operand",
|
||||
basis: []string{"all"},
|
||||
expressions: []string{},
|
||||
expected: []Expression{
|
||||
// note they are sorted by operation
|
||||
{Operation: SetOperation, Operand: "all"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
req := pkgcataloging.NewSelectionRequest().WithDefaults(tt.basis...).WithExpression(tt.expressions...)
|
||||
|
||||
result := newExpressionsFromSelectionRequest(nc, req)
|
||||
if tt.expectedErrors != nil {
|
||||
errs := result.Errors()
|
||||
require.Len(t, errs, len(tt.expectedErrors))
|
||||
for i, err := range tt.expectedErrors {
|
||||
var target ErrInvalidExpression
|
||||
require.ErrorAs(t, errs[i], &target)
|
||||
assert.Equal(t, err, target.Err)
|
||||
}
|
||||
} else {
|
||||
assert.Empty(t, result.Errors())
|
||||
assert.Equal(t, tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_expressionNodes_sort(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
subject Expressions
|
||||
want Expressions
|
||||
}{
|
||||
{
|
||||
name: "sort operations but keep token order",
|
||||
subject: []Expression{
|
||||
{
|
||||
Operation: AddOperation,
|
||||
Operand: "8",
|
||||
},
|
||||
{
|
||||
Operation: AddOperation,
|
||||
Operand: "7",
|
||||
},
|
||||
{
|
||||
Operation: RemoveOperation,
|
||||
Operand: "6",
|
||||
},
|
||||
{
|
||||
Operation: RemoveOperation,
|
||||
Operand: "5",
|
||||
},
|
||||
{
|
||||
Operation: SetOperation,
|
||||
Operand: "2",
|
||||
},
|
||||
{
|
||||
Operation: SetOperation,
|
||||
Operand: "1",
|
||||
},
|
||||
{
|
||||
Operation: SubSelectOperation,
|
||||
Operand: "4",
|
||||
},
|
||||
{
|
||||
Operation: SubSelectOperation,
|
||||
Operand: "3",
|
||||
},
|
||||
},
|
||||
want: []Expression{
|
||||
{
|
||||
Operation: SetOperation,
|
||||
Operand: "2",
|
||||
},
|
||||
{
|
||||
Operation: SetOperation,
|
||||
Operand: "1",
|
||||
},
|
||||
{
|
||||
Operation: SubSelectOperation,
|
||||
Operand: "4",
|
||||
},
|
||||
{
|
||||
Operation: SubSelectOperation,
|
||||
Operand: "3",
|
||||
},
|
||||
{
|
||||
Operation: RemoveOperation,
|
||||
Operand: "6",
|
||||
},
|
||||
{
|
||||
Operation: RemoveOperation,
|
||||
Operand: "5",
|
||||
},
|
||||
{
|
||||
Operation: AddOperation,
|
||||
Operand: "8",
|
||||
},
|
||||
{
|
||||
Operation: AddOperation,
|
||||
Operand: "7",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := tt.subject.Clone()
|
||||
sort.Sort(s)
|
||||
assert.Equal(t, tt.want, s)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"fmt"
|
||||
|
||||
"github.com/anchore/syft/internal/sbomsync"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"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/pkg"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
func NewFileDigestCatalogerTask(selection file.Selection, hashers ...crypto.Hash) Task {
|
||||
if selection == file.NoFilesSelection || len(hashers) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
digestsCataloger := filedigest.NewCataloger(hashers)
|
||||
|
||||
fn := func(ctx context.Context, resolver file.Resolver, builder sbomsync.Builder) error {
|
||||
accessor := builder.(sbomsync.Accessor)
|
||||
|
||||
coordinates, ok := coordinatesForSelection(selection, builder.(sbomsync.Accessor))
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
result, err := digestsCataloger.Catalog(resolver, coordinates...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to catalog file digests: %w", err)
|
||||
}
|
||||
|
||||
accessor.WriteToSBOM(func(sbom *sbom.SBOM) {
|
||||
sbom.Artifacts.FileDigests = result
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return NewTask("file-digest-cataloger", fn)
|
||||
}
|
||||
|
||||
func NewFileMetadataCatalogerTask(selection file.Selection) Task {
|
||||
if selection == file.NoFilesSelection {
|
||||
return nil
|
||||
}
|
||||
|
||||
metadataCataloger := filemetadata.NewCataloger()
|
||||
|
||||
fn := func(ctx context.Context, resolver file.Resolver, builder sbomsync.Builder) error {
|
||||
accessor := builder.(sbomsync.Accessor)
|
||||
|
||||
coordinates, ok := coordinatesForSelection(selection, builder.(sbomsync.Accessor))
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
result, err := metadataCataloger.Catalog(resolver, coordinates...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
accessor.WriteToSBOM(func(sbom *sbom.SBOM) {
|
||||
sbom.Artifacts.FileMetadata = result
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return NewTask("file-metadata-cataloger", fn)
|
||||
}
|
||||
|
||||
func NewFileContentCatalogerTask(cfg filecontent.Config) Task {
|
||||
if len(cfg.Globs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
cat := filecontent.NewCataloger(cfg)
|
||||
|
||||
fn := func(ctx context.Context, resolver file.Resolver, builder sbomsync.Builder) error {
|
||||
accessor := builder.(sbomsync.Accessor)
|
||||
|
||||
result, err := cat.Catalog(resolver)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
accessor.WriteToSBOM(func(sbom *sbom.SBOM) {
|
||||
sbom.Artifacts.FileContents = result
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return NewTask("file-content-cataloger", fn)
|
||||
}
|
||||
|
||||
// TODO: this should be replaced with a fix that allows passing a coordinate or location iterator to the cataloger
|
||||
// Today internal to both cataloger this functions differently: a slice of coordinates vs a channel of locations
|
||||
func coordinatesForSelection(selection file.Selection, accessor sbomsync.Accessor) ([]file.Coordinates, bool) {
|
||||
if selection == file.AllFilesSelection {
|
||||
return nil, true
|
||||
}
|
||||
|
||||
if selection == file.FilesOwnedByPackageSelection {
|
||||
var coordinates []file.Coordinates
|
||||
|
||||
accessor.ReadFromSBOM(func(sbom *sbom.SBOM) {
|
||||
for _, r := range sbom.Relationships {
|
||||
if r.Type != artifact.ContainsRelationship {
|
||||
continue
|
||||
}
|
||||
if _, ok := r.From.(pkg.Package); !ok {
|
||||
continue
|
||||
}
|
||||
if c, ok := r.To.(file.Coordinates); ok {
|
||||
coordinates = append(coordinates, c)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if len(coordinates) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return coordinates, true
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/anchore/syft/internal/sbomsync"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
func Test_coordinatesForSelection(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
selection file.Selection
|
||||
sbom *sbom.SBOM
|
||||
files []file.Coordinates
|
||||
ok bool
|
||||
}{
|
||||
{
|
||||
name: "all files",
|
||||
selection: file.AllFilesSelection,
|
||||
files: nil,
|
||||
ok: true,
|
||||
},
|
||||
{
|
||||
name: "no files",
|
||||
selection: file.NoFilesSelection,
|
||||
files: nil,
|
||||
ok: false,
|
||||
},
|
||||
{
|
||||
name: "specific files with hits",
|
||||
selection: file.FilesOwnedByPackageSelection,
|
||||
sbom: &sbom.SBOM{
|
||||
Relationships: []artifact.Relationship{
|
||||
{
|
||||
From: pkg.Package{},
|
||||
To: file.Coordinates{
|
||||
RealPath: "path",
|
||||
FileSystemID: "fs",
|
||||
},
|
||||
Type: artifact.ContainsRelationship,
|
||||
},
|
||||
},
|
||||
},
|
||||
files: []file.Coordinates{
|
||||
{
|
||||
RealPath: "path",
|
||||
FileSystemID: "fs",
|
||||
},
|
||||
},
|
||||
ok: true,
|
||||
},
|
||||
{
|
||||
name: "specific files no hits (by wrong type)",
|
||||
selection: file.FilesOwnedByPackageSelection,
|
||||
sbom: &sbom.SBOM{
|
||||
Relationships: []artifact.Relationship{
|
||||
{
|
||||
From: pkg.Package{},
|
||||
To: file.Coordinates{
|
||||
RealPath: "path",
|
||||
FileSystemID: "fs",
|
||||
},
|
||||
// wrong type
|
||||
Type: artifact.DependencyOfRelationship,
|
||||
},
|
||||
},
|
||||
},
|
||||
files: nil,
|
||||
ok: false,
|
||||
},
|
||||
{
|
||||
name: "specific files no hits (by wrong node types)",
|
||||
selection: file.FilesOwnedByPackageSelection,
|
||||
sbom: &sbom.SBOM{
|
||||
Relationships: []artifact.Relationship{
|
||||
{
|
||||
From: file.Coordinates{}, // wrong type
|
||||
To: file.Coordinates{
|
||||
RealPath: "path",
|
||||
FileSystemID: "fs",
|
||||
},
|
||||
Type: artifact.ContainsRelationship,
|
||||
},
|
||||
},
|
||||
},
|
||||
files: nil,
|
||||
ok: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
files, ok := coordinatesForSelection(tt.selection, sbomsync.NewBuilder(tt.sbom).(sbomsync.Accessor))
|
||||
assert.Equal(t, tt.files, files)
|
||||
assert.Equal(t, tt.ok, ok)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,207 +0,0 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/scylladb/go-set/strset"
|
||||
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/sbomsync"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/cataloging"
|
||||
"github.com/anchore/syft/syft/cataloging/pkgcataloging"
|
||||
"github.com/anchore/syft/syft/event/monitor"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/common/cpe"
|
||||
)
|
||||
|
||||
type packageTaskFactory func(cfg CatalogingFactoryConfig) Task
|
||||
|
||||
type PackageTaskFactories []packageTaskFactory
|
||||
|
||||
type CatalogingFactoryConfig struct {
|
||||
SearchConfig cataloging.SearchConfig
|
||||
RelationshipsConfig cataloging.RelationshipsConfig
|
||||
DataGenerationConfig cataloging.DataGenerationConfig
|
||||
PackagesConfig pkgcataloging.Config
|
||||
}
|
||||
|
||||
func DefaultCatalogingFactoryConfig() CatalogingFactoryConfig {
|
||||
return CatalogingFactoryConfig{
|
||||
SearchConfig: cataloging.DefaultSearchConfig(),
|
||||
RelationshipsConfig: cataloging.DefaultRelationshipsConfig(),
|
||||
DataGenerationConfig: cataloging.DefaultDataGenerationConfig(),
|
||||
PackagesConfig: pkgcataloging.DefaultConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
func newPackageTaskFactory(catalogerFactory func(CatalogingFactoryConfig) pkg.Cataloger, tags ...string) packageTaskFactory {
|
||||
return func(cfg CatalogingFactoryConfig) Task {
|
||||
return NewPackageTask(cfg, catalogerFactory(cfg), tags...)
|
||||
}
|
||||
}
|
||||
|
||||
func newSimplePackageTaskFactory(catalogerFactory func() pkg.Cataloger, tags ...string) packageTaskFactory {
|
||||
return func(cfg CatalogingFactoryConfig) Task {
|
||||
return NewPackageTask(cfg, catalogerFactory(), tags...)
|
||||
}
|
||||
}
|
||||
|
||||
func (f PackageTaskFactories) Tasks(cfg CatalogingFactoryConfig) ([]Task, error) {
|
||||
var allTasks []Task
|
||||
taskNames := strset.New()
|
||||
duplicateTaskNames := strset.New()
|
||||
var err error
|
||||
for _, factory := range f {
|
||||
tsk := factory(cfg)
|
||||
if tsk == nil {
|
||||
continue
|
||||
}
|
||||
tskName := tsk.Name()
|
||||
if taskNames.Has(tskName) {
|
||||
duplicateTaskNames.Add(tskName)
|
||||
}
|
||||
|
||||
allTasks = append(allTasks, tsk)
|
||||
taskNames.Add(tskName)
|
||||
}
|
||||
if duplicateTaskNames.Size() > 0 {
|
||||
names := duplicateTaskNames.List()
|
||||
sort.Strings(names)
|
||||
err = fmt.Errorf("duplicate cataloger task names: %v", strings.Join(names, ", "))
|
||||
}
|
||||
|
||||
return allTasks, err
|
||||
}
|
||||
|
||||
// NewPackageTask creates a Task function for a generic pkg.Cataloger, honoring the common configuration options.
|
||||
//
|
||||
//nolint:funlen
|
||||
func NewPackageTask(cfg CatalogingFactoryConfig, c pkg.Cataloger, tags ...string) Task {
|
||||
fn := func(ctx context.Context, resolver file.Resolver, sbom sbomsync.Builder) error {
|
||||
catalogerName := c.Name()
|
||||
log.WithFields("name", catalogerName).Trace("starting package cataloger")
|
||||
|
||||
info := monitor.GenericTask{
|
||||
Title: monitor.Title{
|
||||
Default: prettyName(catalogerName),
|
||||
},
|
||||
ID: catalogerName,
|
||||
ParentID: monitor.PackageCatalogingTaskID,
|
||||
Context: "",
|
||||
HideOnSuccess: true,
|
||||
}
|
||||
|
||||
t := bus.StartCatalogerTask(info, -1, "")
|
||||
|
||||
pkgs, relationships, err := c.Catalog(resolver)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to catalog packages with %q: %w", c.Name(), err)
|
||||
}
|
||||
|
||||
log.WithFields("cataloger", c.Name()).Debugf("discovered %d packages", len(pkgs))
|
||||
|
||||
for i, p := range pkgs {
|
||||
if cfg.DataGenerationConfig.GenerateCPEs {
|
||||
// generate CPEs (note: this is excluded from package ID, so is safe to mutate)
|
||||
// we might have binary classified CPE already with the package so we want to append here
|
||||
dictionaryCPE, ok := cpe.DictionaryFind(p)
|
||||
if ok {
|
||||
log.Tracef("used CPE dictionary to find CPE for %s package %q: %s", p.Type, p.Name, dictionaryCPE.BindToFmtString())
|
||||
p.CPEs = append(p.CPEs, dictionaryCPE)
|
||||
} else {
|
||||
p.CPEs = append(p.CPEs, cpe.Generate(p)...)
|
||||
}
|
||||
}
|
||||
|
||||
// if we were not able to identify the language we have an opportunity
|
||||
// to try and get this value from the PURL. Worst case we assert that
|
||||
// we could not identify the language at either stage and set UnknownLanguage
|
||||
if p.Language == "" {
|
||||
p.Language = pkg.LanguageFromPURL(p.PURL)
|
||||
}
|
||||
|
||||
if cfg.RelationshipsConfig.PackageFileOwnership {
|
||||
// create file-to-package relationships for files owned by the package
|
||||
owningRelationships, err := packageFileOwnershipRelationships(p, resolver)
|
||||
if err != nil {
|
||||
log.Warnf("unable to create any package-file relationships for package name=%q type=%q: %w", p.Name, p.Type, err)
|
||||
} else {
|
||||
relationships = append(relationships, owningRelationships...)
|
||||
}
|
||||
}
|
||||
|
||||
pkgs[i] = p
|
||||
}
|
||||
|
||||
sbom.AddPackages(pkgs...)
|
||||
sbom.AddRelationships(relationships...)
|
||||
t.Add(int64(len(pkgs)))
|
||||
|
||||
t.SetCompleted()
|
||||
log.WithFields("name", c.Name()).Trace("package cataloger completed")
|
||||
|
||||
return nil
|
||||
}
|
||||
tags = append(tags, pkgcataloging.PackageTag)
|
||||
|
||||
return NewTask(c.Name(), fn, tags...)
|
||||
}
|
||||
|
||||
func prettyName(s string) string {
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Convert first character to uppercase
|
||||
r := []rune(s)
|
||||
r[0] = unicode.ToUpper(r[0])
|
||||
|
||||
return strings.ReplaceAll(string(r), "-", " ")
|
||||
}
|
||||
|
||||
func packageFileOwnershipRelationships(p pkg.Package, resolver file.PathResolver) ([]artifact.Relationship, error) {
|
||||
fileOwner, ok := p.Metadata.(pkg.FileOwner)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
locations := map[artifact.ID]file.Location{}
|
||||
|
||||
for _, path := range fileOwner.OwnedFiles() {
|
||||
pathRefs, err := resolver.FilesByPath(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to find path for path=%q: %w", path, err)
|
||||
}
|
||||
|
||||
if len(pathRefs) == 0 {
|
||||
// ideally we want to warn users about missing files from a package, however, it is very common for
|
||||
// container image authors to delete files that are not needed in order to keep image sizes small. Adding
|
||||
// a warning here would be needlessly noisy (even for popular base images).
|
||||
continue
|
||||
}
|
||||
|
||||
for _, ref := range pathRefs {
|
||||
if oldRef, ok := locations[ref.Coordinates.ID()]; ok {
|
||||
log.Debugf("found path duplicate of %s", oldRef.RealPath)
|
||||
}
|
||||
locations[ref.Coordinates.ID()] = ref
|
||||
}
|
||||
}
|
||||
|
||||
var relationships []artifact.Relationship
|
||||
for _, location := range locations {
|
||||
relationships = append(relationships, artifact.Relationship{
|
||||
From: p,
|
||||
To: location.Coordinates,
|
||||
Type: artifact.ContainsRelationship,
|
||||
})
|
||||
}
|
||||
return relationships, nil
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/syft/cataloging/pkgcataloging"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/alpine"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/arch"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/binary"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/cpp"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/dart"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/debian"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/dotnet"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/elixir"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/erlang"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/gentoo"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/githubactions"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/golang"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/haskell"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/java"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/javascript"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/nix"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/php"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/python"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/r"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/redhat"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/ruby"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/rust"
|
||||
sbomCataloger "github.com/anchore/syft/syft/pkg/cataloger/sbom"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/swift"
|
||||
)
|
||||
|
||||
//nolint:funlen
|
||||
func DefaultPackageTaskFactories() PackageTaskFactories {
|
||||
return []packageTaskFactory{
|
||||
// OS package installed catalogers ///////////////////////////////////////////////////////////////////////////
|
||||
newSimplePackageTaskFactory(arch.NewDBCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, pkgcataloging.OSTag, "linux", "alpm", "archlinux"),
|
||||
newSimplePackageTaskFactory(alpine.NewDBCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, pkgcataloging.OSTag, "linux", "apk", "alpine"),
|
||||
newSimplePackageTaskFactory(debian.NewDBCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, pkgcataloging.OSTag, "linux", "dpkg", "debian"),
|
||||
newSimplePackageTaskFactory(gentoo.NewPortageCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, pkgcataloging.OSTag, "linux", "portage", "gentoo"),
|
||||
newSimplePackageTaskFactory(redhat.NewDBCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, pkgcataloging.OSTag, "linux", "rpm", "redhat"),
|
||||
|
||||
// OS package declared catalogers ///////////////////////////////////////////////////////////////////////////
|
||||
newSimplePackageTaskFactory(redhat.NewArchiveCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.OSTag, "linux", "rpm", "redhat"),
|
||||
|
||||
// language-specific package installed catalogers ///////////////////////////////////////////////////////////////////////////
|
||||
newSimplePackageTaskFactory(cpp.NewConanInfoCataloger, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, pkgcataloging.LanguageTag, "cpp", "conan"),
|
||||
newSimplePackageTaskFactory(javascript.NewPackageCataloger, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, pkgcataloging.LanguageTag, "javascript", "node"),
|
||||
newSimplePackageTaskFactory(php.NewComposerInstalledCataloger, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, pkgcataloging.LanguageTag, "php", "composer"),
|
||||
newSimplePackageTaskFactory(r.NewPackageCataloger, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, pkgcataloging.LanguageTag, "r"),
|
||||
newSimplePackageTaskFactory(ruby.NewInstalledGemSpecCataloger, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, pkgcataloging.LanguageTag, "ruby", "gem", "gemspec"),
|
||||
newSimplePackageTaskFactory(rust.NewAuditBinaryCataloger, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, pkgcataloging.LanguageTag, "rust", "binary"),
|
||||
|
||||
// language-specific package declared catalogers ///////////////////////////////////////////////////////////////////////////
|
||||
newSimplePackageTaskFactory(cpp.NewConanCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "cpp", "conan"),
|
||||
newSimplePackageTaskFactory(dart.NewPubspecLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "dart"),
|
||||
newSimplePackageTaskFactory(dotnet.NewDotnetDepsCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "dotnet", "c#"),
|
||||
newSimplePackageTaskFactory(elixir.NewMixLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "elixir"),
|
||||
newSimplePackageTaskFactory(erlang.NewRebarLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "erlang"),
|
||||
newSimplePackageTaskFactory(haskell.NewHackageCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "haskell", "hackage", "cabal"),
|
||||
newPackageTaskFactory(
|
||||
func(cfg CatalogingFactoryConfig) pkg.Cataloger {
|
||||
return golang.NewGoModuleFileCataloger(cfg.PackagesConfig.Golang)
|
||||
},
|
||||
pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "go", "golang", "gomod",
|
||||
),
|
||||
newSimplePackageTaskFactory(java.NewGradleLockfileCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "java", "gradle"),
|
||||
newPackageTaskFactory(
|
||||
func(cfg CatalogingFactoryConfig) pkg.Cataloger {
|
||||
return java.NewPomCataloger(cfg.PackagesConfig.JavaArchive)
|
||||
},
|
||||
pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "java", "maven",
|
||||
),
|
||||
newPackageTaskFactory(
|
||||
func(cfg CatalogingFactoryConfig) pkg.Cataloger {
|
||||
return javascript.NewLockCataloger(cfg.PackagesConfig.JavaScript)
|
||||
},
|
||||
pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "javascript", "node", "npm",
|
||||
),
|
||||
newSimplePackageTaskFactory(php.NewComposerLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "php", "composer"),
|
||||
newPackageTaskFactory(
|
||||
func(cfg CatalogingFactoryConfig) pkg.Cataloger {
|
||||
return python.NewPackageCataloger(cfg.PackagesConfig.Python)
|
||||
},
|
||||
pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "python",
|
||||
),
|
||||
newSimplePackageTaskFactory(ruby.NewGemFileLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "ruby", "gem"),
|
||||
newSimplePackageTaskFactory(ruby.NewGemSpecCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "ruby", "gem", "gemspec"),
|
||||
newSimplePackageTaskFactory(rust.NewCargoLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "rust", "cargo"),
|
||||
newSimplePackageTaskFactory(swift.NewCocoapodsCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "swift", "cocoapods"),
|
||||
newSimplePackageTaskFactory(swift.NewSwiftPackageManagerCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "swift", "spm"),
|
||||
|
||||
// language-specific package for both image and directory scans (but not necessarily declared) ////////////////////////////////////////
|
||||
newSimplePackageTaskFactory(dotnet.NewDotnetPortableExecutableCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, pkgcataloging.LanguageTag, "dotnet", "c#", "binary"),
|
||||
newSimplePackageTaskFactory(python.NewInstalledPackageCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, pkgcataloging.LanguageTag, "python"),
|
||||
newPackageTaskFactory(
|
||||
func(cfg CatalogingFactoryConfig) pkg.Cataloger {
|
||||
return golang.NewGoModuleBinaryCataloger(cfg.PackagesConfig.Golang)
|
||||
},
|
||||
pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, pkgcataloging.LanguageTag, "go", "golang", "gomod", "binary",
|
||||
),
|
||||
newPackageTaskFactory(
|
||||
func(cfg CatalogingFactoryConfig) pkg.Cataloger {
|
||||
return java.NewArchiveCataloger(cfg.PackagesConfig.JavaArchive)
|
||||
},
|
||||
pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, pkgcataloging.LanguageTag, "java", "maven",
|
||||
),
|
||||
newSimplePackageTaskFactory(java.NewNativeImageCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, pkgcataloging.LanguageTag, "java"),
|
||||
newSimplePackageTaskFactory(nix.NewStoreCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, pkgcataloging.LanguageTag, "nix"),
|
||||
|
||||
// other package catalogers ///////////////////////////////////////////////////////////////////////////
|
||||
newPackageTaskFactory(
|
||||
func(cfg CatalogingFactoryConfig) pkg.Cataloger {
|
||||
return binary.NewCataloger(cfg.PackagesConfig.Binary)
|
||||
},
|
||||
pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "binary"),
|
||||
newSimplePackageTaskFactory(githubactions.NewActionUsageCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, "github", "github-actions"),
|
||||
newSimplePackageTaskFactory(githubactions.NewWorkflowUsageCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, "github", "github-actions"),
|
||||
newSimplePackageTaskFactory(sbomCataloger.NewCataloger, pkgcataloging.ImageTag, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.ImageTag, "sbom"), // note: not evidence of installed packages
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/anchore/syft/internal/relationship"
|
||||
"github.com/anchore/syft/internal/sbomsync"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/cataloging"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
var _ artifact.Identifiable = (*sourceIdentifierAdapter)(nil)
|
||||
|
||||
type sourceIdentifierAdapter struct {
|
||||
desc source.Description
|
||||
}
|
||||
|
||||
func (s sourceIdentifierAdapter) ID() artifact.ID {
|
||||
return artifact.ID(s.desc.ID)
|
||||
}
|
||||
|
||||
func NewRelationshipsTask(cfg cataloging.RelationshipsConfig, src source.Description) Task {
|
||||
fn := func(ctx context.Context, resolver file.Resolver, builder sbomsync.Builder) error {
|
||||
relationship.Finalize(
|
||||
builder,
|
||||
cfg,
|
||||
&sourceIdentifierAdapter{desc: src})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return NewTask("relationships-cataloger", fn)
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/scylladb/go-set/strset"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/cataloging/pkgcataloging"
|
||||
)
|
||||
|
||||
// Selection represents the users request for a subset of tasks to run and the resulting set of task names that were
|
||||
// selected. Additionally, all tokens that were matched on to reach the returned conclusion are also provided.
|
||||
type Selection struct {
|
||||
Request pkgcataloging.SelectionRequest
|
||||
Result *strset.Set
|
||||
TokensByTask map[string]TokenSelection
|
||||
}
|
||||
|
||||
// TokenSelection represents the tokens that were matched on to either include or exclude a given task (based on expression evaluation).
|
||||
type TokenSelection struct {
|
||||
SelectedOn *strset.Set
|
||||
DeselectedOn *strset.Set
|
||||
}
|
||||
|
||||
func newTokenSelection(selected, deselected []string) TokenSelection {
|
||||
return TokenSelection{
|
||||
SelectedOn: strset.New(selected...),
|
||||
DeselectedOn: strset.New(deselected...),
|
||||
}
|
||||
}
|
||||
|
||||
func (ts *TokenSelection) merge(other ...TokenSelection) {
|
||||
for _, o := range other {
|
||||
if ts.SelectedOn != nil {
|
||||
ts.SelectedOn.Add(o.SelectedOn.List()...)
|
||||
}
|
||||
if ts.DeselectedOn != nil {
|
||||
ts.DeselectedOn.Add(o.DeselectedOn.List()...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newSelection() Selection {
|
||||
return Selection{
|
||||
Result: strset.New(),
|
||||
TokensByTask: make(map[string]TokenSelection),
|
||||
}
|
||||
}
|
||||
|
||||
// Select parses the given expressions as two sets: expressions that represent a "set" operation, and expressions that
|
||||
// represent all other operations. The parsed expressions are then evaluated against the given tasks to return
|
||||
// a subset (or the same) set of tasks.
|
||||
func Select(allTasks []Task, selectionRequest pkgcataloging.SelectionRequest) ([]Task, Selection, error) {
|
||||
nodes := newExpressionsFromSelectionRequest(newExpressionContext(allTasks), selectionRequest)
|
||||
|
||||
finalTasks, selection := selectByExpressions(allTasks, nodes)
|
||||
|
||||
selection.Request = selectionRequest
|
||||
|
||||
return finalTasks, selection, nodes.Validate()
|
||||
}
|
||||
|
||||
// selectByExpressions the set of tasks to run based on the given expression(s).
|
||||
func selectByExpressions(ts tasks, nodes Expressions) (tasks, Selection) {
|
||||
if len(nodes) == 0 {
|
||||
return ts, newSelection()
|
||||
}
|
||||
|
||||
finalSet := newSet()
|
||||
selectionSet := newSet()
|
||||
addSet := newSet()
|
||||
removeSet := newSet()
|
||||
|
||||
allSelections := make(map[string]TokenSelection)
|
||||
|
||||
nodes = nodes.Clone()
|
||||
sort.Sort(nodes)
|
||||
|
||||
for i, node := range nodes {
|
||||
if len(node.Errors) > 0 {
|
||||
continue
|
||||
}
|
||||
selectedTasks, selections := evaluateExpression(ts, node)
|
||||
|
||||
for name, ss := range selections {
|
||||
if selection, exists := allSelections[name]; exists {
|
||||
ss.merge(selection)
|
||||
}
|
||||
allSelections[name] = ss
|
||||
}
|
||||
|
||||
if len(selectedTasks) == 0 {
|
||||
log.WithFields("selection", fmt.Sprintf("%q", node.String())).Warn("no cataloger tasks selected found for given selection (this might be a misconfiguration)")
|
||||
}
|
||||
|
||||
switch node.Operation {
|
||||
case SetOperation:
|
||||
finalSet.Add(selectedTasks...)
|
||||
case AddOperation, "":
|
||||
addSet.Add(selectedTasks...)
|
||||
case RemoveOperation:
|
||||
removeSet.Add(selectedTasks...)
|
||||
case SubSelectOperation:
|
||||
selectionSet.Add(selectedTasks...)
|
||||
default:
|
||||
nodes[i].Errors = append(nodes[i].Errors, ErrInvalidOperator)
|
||||
}
|
||||
}
|
||||
|
||||
if len(selectionSet.tasks) > 0 {
|
||||
finalSet.Intersect(selectionSet.Tasks()...)
|
||||
}
|
||||
finalSet.Remove(removeSet.Tasks()...)
|
||||
finalSet.Add(addSet.Tasks()...)
|
||||
|
||||
finalTasks := finalSet.Tasks()
|
||||
|
||||
return finalTasks, Selection{
|
||||
Result: strset.New(finalTasks.Names()...),
|
||||
TokensByTask: allSelections,
|
||||
}
|
||||
}
|
||||
|
||||
// evaluateExpression returns the set of tasks that match the given expression (as well as all tokens that were matched
|
||||
// on to reach the returned conclusion).
|
||||
func evaluateExpression(ts tasks, node Expression) ([]Task, map[string]TokenSelection) {
|
||||
selection := make(map[string]TokenSelection)
|
||||
var finalTasks []Task
|
||||
|
||||
for _, t := range ts {
|
||||
if !isSelected(t, node.Operand) {
|
||||
continue
|
||||
}
|
||||
|
||||
s := newTokenSelection(nil, nil)
|
||||
|
||||
switch node.Operation {
|
||||
case SetOperation, SubSelectOperation, AddOperation:
|
||||
s.SelectedOn.Add(node.Operand)
|
||||
case RemoveOperation:
|
||||
s.DeselectedOn.Add(node.Operand)
|
||||
}
|
||||
|
||||
finalTasks = append(finalTasks, t)
|
||||
|
||||
if og, exists := selection[t.Name()]; exists {
|
||||
s.merge(og)
|
||||
}
|
||||
|
||||
selection[t.Name()] = s
|
||||
}
|
||||
return finalTasks, selection
|
||||
}
|
||||
|
||||
// isSelected returns true if the given task matches the given token. If the token is "all" then the task is always selected.
|
||||
func isSelected(td Task, token string) bool {
|
||||
if token == "all" {
|
||||
return true
|
||||
}
|
||||
|
||||
if ts, ok := td.(Selector); ok {
|
||||
// use the selector to verify all tags
|
||||
if ts.HasAllSelectors(token) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// only do exact name matching
|
||||
if td.Name() == token {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -1,369 +0,0 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"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/syft/cataloging/pkgcataloging"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
)
|
||||
|
||||
func dummyTask(name string, tags ...string) Task {
|
||||
return NewTask(name, func(ctx context.Context, resolver file.Resolver, sbom sbomsync.Builder) error {
|
||||
panic("not implemented")
|
||||
}, tags...)
|
||||
}
|
||||
|
||||
// note: this test fixture does not need to be kept up to date here, but makes a great test subject
|
||||
func createDummyTasks() tasks {
|
||||
return []Task{
|
||||
// OS package installed catalogers
|
||||
dummyTask("alpm-db-cataloger", "directory", "installed", "image", "os", "alpm", "archlinux"),
|
||||
dummyTask("apk-db-cataloger", "directory", "installed", "image", "os", "apk", "alpine"),
|
||||
dummyTask("dpkg-db-cataloger", "directory", "installed", "image", "os", "dpkg", "debian"),
|
||||
dummyTask("portage-cataloger", "directory", "installed", "image", "os", "portage", "gentoo"),
|
||||
dummyTask("rpm-db-cataloger", "directory", "installed", "image", "os", "rpm", "redhat"),
|
||||
|
||||
// OS package declared catalogers
|
||||
dummyTask("rpm-archive-cataloger", "declared", "directory", "os", "rpm", "redhat"),
|
||||
|
||||
// language-specific package installed catalogers
|
||||
dummyTask("conan-info-cataloger", "installed", "image", "language", "cpp", "conan"),
|
||||
dummyTask("javascript-package-cataloger", "installed", "image", "language", "javascript", "node"),
|
||||
dummyTask("php-composer-installed-cataloger", "installed", "image", "language", "php", "composer"),
|
||||
dummyTask("ruby-installed-gemspec-cataloger", "installed", "image", "language", "ruby", "gem", "gemspec"),
|
||||
dummyTask("rust-cargo-lock-cataloger", "installed", "image", "language", "rust", "binary"),
|
||||
|
||||
// language-specific package declared catalogers
|
||||
dummyTask("conan-cataloger", "declared", "directory", "language", "cpp", "conan"),
|
||||
dummyTask("dart-pubspec-lock-cataloger", "declared", "directory", "language", "dart"),
|
||||
dummyTask("dotnet-deps-cataloger", "declared", "directory", "language", "dotnet", "c#"),
|
||||
dummyTask("elixir-mix-lock-cataloger", "declared", "directory", "language", "elixir"),
|
||||
dummyTask("erlang-rebar-lock-cataloger", "declared", "directory", "language", "erlang"),
|
||||
dummyTask("javascript-lock-cataloger", "declared", "directory", "language", "javascript", "node", "npm"),
|
||||
|
||||
// language-specific package for both image and directory scans (but not necessarily declared)
|
||||
dummyTask("dotnet-portable-executable-cataloger", "directory", "installed", "image", "language", "dotnet", "c#"),
|
||||
dummyTask("python-installed-package-cataloger", "directory", "installed", "image", "language", "python"),
|
||||
dummyTask("go-module-binary-cataloger", "directory", "installed", "image", "language", "go", "golang", "gomod", "binary"),
|
||||
dummyTask("java-archive-cataloger", "directory", "installed", "image", "language", "java", "maven"),
|
||||
dummyTask("graalvm-native-image-cataloger", "directory", "installed", "image", "language", "java"),
|
||||
|
||||
// other package catalogers
|
||||
dummyTask("binary-cataloger", "declared", "directory", "image", "binary"),
|
||||
dummyTask("github-actions-usage-cataloger", "declared", "directory", "github", "github-actions"),
|
||||
dummyTask("github-action-workflow-usage-cataloger", "declared", "directory", "github", "github-actions"),
|
||||
dummyTask("sbom-cataloger", "declared", "directory", "image", "sbom"),
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelect(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
allTasks []Task
|
||||
basis []string
|
||||
expressions []string
|
||||
wantNames []string
|
||||
wantTokens map[string]TokenSelection
|
||||
wantRequest pkgcataloging.SelectionRequest
|
||||
wantErr assert.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "empty input",
|
||||
allTasks: []Task{},
|
||||
basis: []string{},
|
||||
expressions: []string{},
|
||||
wantNames: []string{},
|
||||
wantTokens: map[string]TokenSelection{},
|
||||
wantRequest: pkgcataloging.SelectionRequest{},
|
||||
},
|
||||
{
|
||||
name: "use default tasks",
|
||||
allTasks: createDummyTasks(),
|
||||
basis: []string{
|
||||
"image",
|
||||
},
|
||||
expressions: []string{},
|
||||
wantNames: []string{
|
||||
"alpm-db-cataloger",
|
||||
"apk-db-cataloger",
|
||||
"dpkg-db-cataloger",
|
||||
"portage-cataloger",
|
||||
"rpm-db-cataloger",
|
||||
"conan-info-cataloger",
|
||||
"javascript-package-cataloger",
|
||||
"php-composer-installed-cataloger",
|
||||
"ruby-installed-gemspec-cataloger",
|
||||
"rust-cargo-lock-cataloger",
|
||||
"dotnet-portable-executable-cataloger",
|
||||
"python-installed-package-cataloger",
|
||||
"go-module-binary-cataloger",
|
||||
"java-archive-cataloger",
|
||||
"graalvm-native-image-cataloger",
|
||||
"binary-cataloger",
|
||||
"sbom-cataloger",
|
||||
},
|
||||
wantTokens: map[string]TokenSelection{
|
||||
"alpm-db-cataloger": newTokenSelection([]string{"image"}, nil),
|
||||
"apk-db-cataloger": newTokenSelection([]string{"image"}, nil),
|
||||
"dpkg-db-cataloger": newTokenSelection([]string{"image"}, nil),
|
||||
"portage-cataloger": newTokenSelection([]string{"image"}, nil),
|
||||
"rpm-db-cataloger": newTokenSelection([]string{"image"}, nil),
|
||||
"conan-info-cataloger": newTokenSelection([]string{"image"}, nil),
|
||||
"javascript-package-cataloger": newTokenSelection([]string{"image"}, nil),
|
||||
"php-composer-installed-cataloger": newTokenSelection([]string{"image"}, nil),
|
||||
"ruby-installed-gemspec-cataloger": newTokenSelection([]string{"image"}, nil),
|
||||
"rust-cargo-lock-cataloger": newTokenSelection([]string{"image"}, nil),
|
||||
"dotnet-portable-executable-cataloger": newTokenSelection([]string{"image"}, nil),
|
||||
"python-installed-package-cataloger": newTokenSelection([]string{"image"}, nil),
|
||||
"go-module-binary-cataloger": newTokenSelection([]string{"image"}, nil),
|
||||
"java-archive-cataloger": newTokenSelection([]string{"image"}, nil),
|
||||
"graalvm-native-image-cataloger": newTokenSelection([]string{"image"}, nil),
|
||||
"binary-cataloger": newTokenSelection([]string{"image"}, nil),
|
||||
"sbom-cataloger": newTokenSelection([]string{"image"}, nil),
|
||||
},
|
||||
wantRequest: pkgcataloging.SelectionRequest{
|
||||
DefaultNamesOrTags: []string{"image"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "select, add, and remove tasks",
|
||||
allTasks: createDummyTasks(),
|
||||
basis: []string{
|
||||
"image",
|
||||
},
|
||||
expressions: []string{
|
||||
"+github-actions-usage-cataloger",
|
||||
"-dpkg",
|
||||
"os",
|
||||
},
|
||||
wantNames: []string{
|
||||
"alpm-db-cataloger",
|
||||
"apk-db-cataloger",
|
||||
"portage-cataloger",
|
||||
"rpm-db-cataloger",
|
||||
"github-actions-usage-cataloger",
|
||||
},
|
||||
wantTokens: map[string]TokenSelection{
|
||||
// selected
|
||||
"alpm-db-cataloger": newTokenSelection([]string{"image", "os"}, nil),
|
||||
"apk-db-cataloger": newTokenSelection([]string{"image", "os"}, nil),
|
||||
"dpkg-db-cataloger": newTokenSelection([]string{"image", "os"}, []string{"dpkg"}),
|
||||
"portage-cataloger": newTokenSelection([]string{"image", "os"}, nil),
|
||||
"rpm-db-cataloger": newTokenSelection([]string{"image", "os"}, nil),
|
||||
"github-actions-usage-cataloger": newTokenSelection([]string{"github-actions-usage-cataloger"}, nil),
|
||||
|
||||
// ultimately not selected
|
||||
"rpm-archive-cataloger": newTokenSelection([]string{"os"}, nil),
|
||||
"conan-info-cataloger": newTokenSelection([]string{"image"}, nil),
|
||||
"javascript-package-cataloger": newTokenSelection([]string{"image"}, nil),
|
||||
"php-composer-installed-cataloger": newTokenSelection([]string{"image"}, nil),
|
||||
"ruby-installed-gemspec-cataloger": newTokenSelection([]string{"image"}, nil),
|
||||
"rust-cargo-lock-cataloger": newTokenSelection([]string{"image"}, nil),
|
||||
"dotnet-portable-executable-cataloger": newTokenSelection([]string{"image"}, nil),
|
||||
"python-installed-package-cataloger": newTokenSelection([]string{"image"}, nil),
|
||||
"go-module-binary-cataloger": newTokenSelection([]string{"image"}, nil),
|
||||
"java-archive-cataloger": newTokenSelection([]string{"image"}, nil),
|
||||
"graalvm-native-image-cataloger": newTokenSelection([]string{"image"}, nil),
|
||||
"binary-cataloger": newTokenSelection([]string{"image"}, nil),
|
||||
"sbom-cataloger": newTokenSelection([]string{"image"}, nil),
|
||||
},
|
||||
wantRequest: pkgcataloging.SelectionRequest{
|
||||
DefaultNamesOrTags: []string{"image"},
|
||||
SubSelectTags: []string{"os"},
|
||||
RemoveNamesOrTags: []string{"dpkg"},
|
||||
AddNames: []string{"github-actions-usage-cataloger"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "allow for partial selections",
|
||||
allTasks: createDummyTasks(),
|
||||
basis: []string{
|
||||
"image",
|
||||
},
|
||||
expressions: []string{
|
||||
// valid...
|
||||
"+github-actions-usage-cataloger",
|
||||
"-dpkg",
|
||||
"os",
|
||||
// invalid...
|
||||
"+python",
|
||||
"rust-cargo-lock-cataloger",
|
||||
},
|
||||
wantNames: []string{
|
||||
"alpm-db-cataloger",
|
||||
"apk-db-cataloger",
|
||||
"portage-cataloger",
|
||||
"rpm-db-cataloger",
|
||||
"github-actions-usage-cataloger",
|
||||
},
|
||||
wantTokens: map[string]TokenSelection{
|
||||
// selected
|
||||
"alpm-db-cataloger": newTokenSelection([]string{"image", "os"}, nil),
|
||||
"apk-db-cataloger": newTokenSelection([]string{"image", "os"}, nil),
|
||||
"dpkg-db-cataloger": newTokenSelection([]string{"image", "os"}, []string{"dpkg"}),
|
||||
"portage-cataloger": newTokenSelection([]string{"image", "os"}, nil),
|
||||
"rpm-db-cataloger": newTokenSelection([]string{"image", "os"}, nil),
|
||||
"github-actions-usage-cataloger": newTokenSelection([]string{"github-actions-usage-cataloger"}, nil),
|
||||
|
||||
// ultimately not selected
|
||||
"rpm-archive-cataloger": newTokenSelection([]string{"os"}, nil),
|
||||
"conan-info-cataloger": newTokenSelection([]string{"image"}, nil),
|
||||
"javascript-package-cataloger": newTokenSelection([]string{"image"}, nil),
|
||||
"php-composer-installed-cataloger": newTokenSelection([]string{"image"}, nil),
|
||||
"ruby-installed-gemspec-cataloger": newTokenSelection([]string{"image"}, nil),
|
||||
"rust-cargo-lock-cataloger": newTokenSelection([]string{"image"}, nil),
|
||||
"dotnet-portable-executable-cataloger": newTokenSelection([]string{"image"}, nil),
|
||||
"python-installed-package-cataloger": newTokenSelection([]string{"image"}, nil), // note: there is no python token used for selection
|
||||
"go-module-binary-cataloger": newTokenSelection([]string{"image"}, nil),
|
||||
"java-archive-cataloger": newTokenSelection([]string{"image"}, nil),
|
||||
"graalvm-native-image-cataloger": newTokenSelection([]string{"image"}, nil),
|
||||
"binary-cataloger": newTokenSelection([]string{"image"}, nil),
|
||||
"sbom-cataloger": newTokenSelection([]string{"image"}, nil),
|
||||
},
|
||||
wantRequest: pkgcataloging.SelectionRequest{
|
||||
DefaultNamesOrTags: []string{"image"},
|
||||
SubSelectTags: []string{"os", "rust-cargo-lock-cataloger"},
|
||||
RemoveNamesOrTags: []string{"dpkg"},
|
||||
AddNames: []string{"github-actions-usage-cataloger", "python"},
|
||||
},
|
||||
wantErr: assert.Error, // !important!
|
||||
},
|
||||
{
|
||||
name: "select all tasks",
|
||||
allTasks: createDummyTasks(),
|
||||
basis: []string{
|
||||
"all",
|
||||
},
|
||||
expressions: []string{},
|
||||
wantNames: []string{
|
||||
"alpm-db-cataloger",
|
||||
"apk-db-cataloger",
|
||||
"dpkg-db-cataloger",
|
||||
"portage-cataloger",
|
||||
"rpm-db-cataloger",
|
||||
"rpm-archive-cataloger",
|
||||
"conan-info-cataloger",
|
||||
"javascript-package-cataloger",
|
||||
"php-composer-installed-cataloger",
|
||||
"ruby-installed-gemspec-cataloger",
|
||||
"rust-cargo-lock-cataloger",
|
||||
"conan-cataloger",
|
||||
"dart-pubspec-lock-cataloger",
|
||||
"dotnet-deps-cataloger",
|
||||
"elixir-mix-lock-cataloger",
|
||||
"erlang-rebar-lock-cataloger",
|
||||
"javascript-lock-cataloger",
|
||||
"dotnet-portable-executable-cataloger",
|
||||
"python-installed-package-cataloger",
|
||||
"go-module-binary-cataloger",
|
||||
"java-archive-cataloger",
|
||||
"graalvm-native-image-cataloger",
|
||||
"binary-cataloger",
|
||||
"github-actions-usage-cataloger",
|
||||
"github-action-workflow-usage-cataloger",
|
||||
"sbom-cataloger",
|
||||
},
|
||||
wantTokens: map[string]TokenSelection{
|
||||
"alpm-db-cataloger": newTokenSelection([]string{"all"}, nil),
|
||||
"apk-db-cataloger": newTokenSelection([]string{"all"}, nil),
|
||||
"dpkg-db-cataloger": newTokenSelection([]string{"all"}, nil),
|
||||
"portage-cataloger": newTokenSelection([]string{"all"}, nil),
|
||||
"rpm-db-cataloger": newTokenSelection([]string{"all"}, nil),
|
||||
"rpm-archive-cataloger": newTokenSelection([]string{"all"}, nil),
|
||||
"conan-info-cataloger": newTokenSelection([]string{"all"}, nil),
|
||||
"javascript-package-cataloger": newTokenSelection([]string{"all"}, nil),
|
||||
"php-composer-installed-cataloger": newTokenSelection([]string{"all"}, nil),
|
||||
"ruby-installed-gemspec-cataloger": newTokenSelection([]string{"all"}, nil),
|
||||
"rust-cargo-lock-cataloger": newTokenSelection([]string{"all"}, nil),
|
||||
"conan-cataloger": newTokenSelection([]string{"all"}, nil),
|
||||
"dart-pubspec-lock-cataloger": newTokenSelection([]string{"all"}, nil),
|
||||
"dotnet-deps-cataloger": newTokenSelection([]string{"all"}, nil),
|
||||
"elixir-mix-lock-cataloger": newTokenSelection([]string{"all"}, nil),
|
||||
"erlang-rebar-lock-cataloger": newTokenSelection([]string{"all"}, nil),
|
||||
"javascript-lock-cataloger": newTokenSelection([]string{"all"}, nil),
|
||||
"dotnet-portable-executable-cataloger": newTokenSelection([]string{"all"}, nil),
|
||||
"python-installed-package-cataloger": newTokenSelection([]string{"all"}, nil),
|
||||
"go-module-binary-cataloger": newTokenSelection([]string{"all"}, nil),
|
||||
"java-archive-cataloger": newTokenSelection([]string{"all"}, nil),
|
||||
"graalvm-native-image-cataloger": newTokenSelection([]string{"all"}, nil),
|
||||
"binary-cataloger": newTokenSelection([]string{"all"}, nil),
|
||||
"github-actions-usage-cataloger": newTokenSelection([]string{"all"}, nil),
|
||||
"github-action-workflow-usage-cataloger": newTokenSelection([]string{"all"}, nil),
|
||||
"sbom-cataloger": newTokenSelection([]string{"all"}, nil),
|
||||
},
|
||||
wantRequest: pkgcataloging.SelectionRequest{
|
||||
DefaultNamesOrTags: []string{"all"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set default with multiple tags",
|
||||
allTasks: createDummyTasks(),
|
||||
basis: []string{
|
||||
"gemspec",
|
||||
"python",
|
||||
},
|
||||
expressions: []string{},
|
||||
wantNames: []string{
|
||||
"ruby-installed-gemspec-cataloger",
|
||||
"python-installed-package-cataloger",
|
||||
},
|
||||
wantTokens: map[string]TokenSelection{
|
||||
"ruby-installed-gemspec-cataloger": newTokenSelection([]string{"gemspec"}, nil),
|
||||
"python-installed-package-cataloger": newTokenSelection([]string{"python"}, nil),
|
||||
},
|
||||
wantRequest: pkgcataloging.SelectionRequest{
|
||||
DefaultNamesOrTags: []string{"gemspec", "python"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.wantErr == nil {
|
||||
tt.wantErr = assert.NoError
|
||||
}
|
||||
|
||||
req := pkgcataloging.NewSelectionRequest().WithDefaults(tt.basis...).WithExpression(tt.expressions...)
|
||||
|
||||
got, gotEvidence, err := Select(tt.allTasks, req)
|
||||
tt.wantErr(t, err)
|
||||
if err != nil {
|
||||
// dev note: this is useful for debugging when needed...
|
||||
//for _, e := range gotEvidence.Request.Expressions {
|
||||
// t.Logf("expression (errors %q): %#v", e.Errors, e)
|
||||
//}
|
||||
|
||||
// note: we DON'T bail early in validations... this is because we should always return the full set of
|
||||
// of selected tasks and surrounding evidence.
|
||||
}
|
||||
|
||||
gotNames := make([]string, 0)
|
||||
for _, g := range got {
|
||||
gotNames = append(gotNames, g.Name())
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.wantNames, gotNames)
|
||||
|
||||
// names in selection should match all tasks returned
|
||||
require.Len(t, tt.wantNames, gotEvidence.Result.Size(), "selected tasks should match all tasks returned (but does not)")
|
||||
assert.ElementsMatch(t, tt.wantNames, gotEvidence.Result.List(), "selected tasks should match all tasks returned (but does not)")
|
||||
|
||||
setCompare := cmp.Comparer(func(x, y *strset.Set) bool {
|
||||
return x.IsEqual(y)
|
||||
})
|
||||
|
||||
if d := cmp.Diff(tt.wantTokens, gotEvidence.TokensByTask, setCompare); d != "" {
|
||||
t.Errorf("unexpected tokens by task (-want +got):\n%s", d)
|
||||
}
|
||||
assert.Equal(t, tt.wantRequest, gotEvidence.Request)
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package task
|
||||
|
||||
type set struct {
|
||||
order []string
|
||||
tasks map[string]Task
|
||||
}
|
||||
|
||||
func newSet(tasks ...Task) *set {
|
||||
s := &set{
|
||||
order: []string{},
|
||||
tasks: make(map[string]Task),
|
||||
}
|
||||
|
||||
s.Add(tasks...)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (ts *set) Len() int {
|
||||
return len(ts.tasks)
|
||||
}
|
||||
|
||||
func (ts *set) Add(tasks ...Task) {
|
||||
for _, t := range tasks {
|
||||
taskName := t.Name()
|
||||
if _, exists := ts.tasks[taskName]; exists {
|
||||
continue
|
||||
}
|
||||
ts.tasks[taskName] = t
|
||||
ts.order = append(ts.order, taskName)
|
||||
}
|
||||
}
|
||||
|
||||
func (ts *set) Remove(tasks ...Task) {
|
||||
for _, t := range tasks {
|
||||
taskName := t.Name()
|
||||
if _, exists := ts.tasks[taskName]; !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
delete(ts.tasks, taskName)
|
||||
for i, t := range ts.order {
|
||||
if t == taskName {
|
||||
ts.order = append(ts.order[:i], ts.order[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ts *set) Intersect(tasks ...Task) {
|
||||
other := newSet(tasks...)
|
||||
result := newSet()
|
||||
for _, taskName := range ts.order {
|
||||
// we make a new set to prevent the original set from being modified while we are iterating over "order"
|
||||
if _, exists := other.tasks[taskName]; exists {
|
||||
// note: keep the original task and ordering
|
||||
result.Add(ts.tasks[taskName])
|
||||
}
|
||||
}
|
||||
*ts = *result
|
||||
}
|
||||
|
||||
func (ts set) Tasks() tasks {
|
||||
var result []Task
|
||||
for _, name := range ts.order {
|
||||
result = append(result, ts.tasks[name])
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/anchore/syft/internal/sbomsync"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
)
|
||||
|
||||
var _ Task = (*mockTask)(nil)
|
||||
|
||||
type mockTask struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func (m mockTask) Execute(_ context.Context, _ file.Resolver, _ sbomsync.Builder) error {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (m mockTask) Name() string {
|
||||
return m.name
|
||||
}
|
||||
|
||||
func Test_set_Add(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
initialTasks []Task
|
||||
newTasks []Task
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "add unique tasks",
|
||||
initialTasks: []Task{mockTask{"task2"}, mockTask{"task1"}},
|
||||
newTasks: []Task{mockTask{"task3"}},
|
||||
expected: []string{
|
||||
"task2", // note order is honored
|
||||
"task1",
|
||||
"task3",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "add duplicate tasks",
|
||||
initialTasks: []Task{mockTask{"task1"}, mockTask{"task2"}},
|
||||
newTasks: []Task{mockTask{"task1"}},
|
||||
expected: []string{
|
||||
"task1",
|
||||
"task2",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := newSet(tt.initialTasks...)
|
||||
s.Add(tt.newTasks...)
|
||||
got := s.Tasks()
|
||||
var gotNames []string
|
||||
for _, tsk := range got {
|
||||
gotNames = append(gotNames, tsk.Name())
|
||||
}
|
||||
assert.Equal(t, tt.expected, gotNames)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_set_Remove(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
initialTasks []Task
|
||||
tasksToRemove []Task
|
||||
expectedOrder []string
|
||||
}{
|
||||
{
|
||||
name: "remove existing tasks",
|
||||
initialTasks: []Task{mockTask{"task1"}, mockTask{"task2"}, mockTask{"task3"}},
|
||||
tasksToRemove: []Task{mockTask{"task2"}},
|
||||
expectedOrder: []string{"task1", "task3"},
|
||||
},
|
||||
{
|
||||
name: "remove non-existing tasks",
|
||||
initialTasks: []Task{mockTask{"task1"}, mockTask{"task2"}},
|
||||
tasksToRemove: []Task{mockTask{"task3"}},
|
||||
expectedOrder: []string{"task1", "task2"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := newSet(tt.initialTasks...)
|
||||
s.Remove(tt.tasksToRemove...)
|
||||
assert.Equal(t, tt.expectedOrder, s.order)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_set_Intersect(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
initialTasks []Task
|
||||
intersectTasks []Task
|
||||
expectedOrder []string
|
||||
}{
|
||||
{
|
||||
name: "intersect with overlapping tasks",
|
||||
initialTasks: []Task{mockTask{"task1"}, mockTask{"task2"}},
|
||||
intersectTasks: []Task{mockTask{"task2"}, mockTask{"task3"}},
|
||||
expectedOrder: []string{"task2"},
|
||||
},
|
||||
{
|
||||
name: "intersect with non-overlapping tasks",
|
||||
initialTasks: []Task{mockTask{"task1"}, mockTask{"task4"}},
|
||||
intersectTasks: []Task{mockTask{"task2"}, mockTask{"task3"}},
|
||||
expectedOrder: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := newSet(tt.initialTasks...)
|
||||
s.Intersect(tt.intersectTasks...)
|
||||
assert.Equal(t, tt.expectedOrder, s.order)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_set_Tasks(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
initialTasks []Task
|
||||
expectedTasks tasks
|
||||
}{
|
||||
{
|
||||
name: "empty set",
|
||||
initialTasks: []Task{},
|
||||
expectedTasks: nil,
|
||||
},
|
||||
{
|
||||
name: "get tasks from set",
|
||||
initialTasks: []Task{mockTask{"task1"}, mockTask{"task2"}},
|
||||
expectedTasks: []Task{mockTask{"task1"}, mockTask{"task2"}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := newSet(tt.initialTasks...)
|
||||
resultTasks := s.Tasks()
|
||||
assert.Equal(t, tt.expectedTasks, resultTasks)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/scylladb/go-set/strset"
|
||||
|
||||
"github.com/anchore/syft/internal/sbomsync"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
)
|
||||
|
||||
var _ interface {
|
||||
Task
|
||||
Selector
|
||||
} = (*task)(nil)
|
||||
|
||||
// Task is a function that can wrap a cataloger to populate the SBOM with data (coordinated through the mutex).
|
||||
type Task interface {
|
||||
Name() string
|
||||
Execute(context.Context, file.Resolver, sbomsync.Builder) error
|
||||
}
|
||||
|
||||
type Selector interface {
|
||||
HasAllSelectors(...string) bool
|
||||
Selectors() []string
|
||||
}
|
||||
|
||||
type tasks []Task
|
||||
|
||||
type task struct {
|
||||
name string
|
||||
selectors *strset.Set
|
||||
task func(context.Context, file.Resolver, sbomsync.Builder) error
|
||||
}
|
||||
|
||||
func NewTask(name string, tsk func(context.Context, file.Resolver, sbomsync.Builder) error, tags ...string) Task {
|
||||
if tsk == nil {
|
||||
panic(fmt.Errorf("task cannot be nil"))
|
||||
}
|
||||
tags = append(tags, name)
|
||||
return &task{
|
||||
name: name,
|
||||
selectors: strset.New(tags...),
|
||||
task: tsk,
|
||||
}
|
||||
}
|
||||
|
||||
func (t task) HasAllSelectors(ids ...string) bool {
|
||||
// tags or name
|
||||
return t.selectors.Has(ids...)
|
||||
}
|
||||
|
||||
func (t task) Selectors() []string {
|
||||
return t.selectors.List()
|
||||
}
|
||||
|
||||
func (t task) Name() string {
|
||||
return t.name
|
||||
}
|
||||
|
||||
func (t task) Execute(ctx context.Context, resolver file.Resolver, sbom sbomsync.Builder) error {
|
||||
return t.task(ctx, resolver, sbom)
|
||||
}
|
||||
|
||||
func (ts tasks) Names() []string {
|
||||
var names []string
|
||||
for _, td := range ts {
|
||||
names = append(names, td.Name())
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func (ts tasks) Tags() []string {
|
||||
tags := strset.New()
|
||||
for _, td := range ts {
|
||||
if s, ok := td.(Selector); ok {
|
||||
tags.Add(s.Selectors()...)
|
||||
}
|
||||
|
||||
tags.Remove(td.Name())
|
||||
}
|
||||
|
||||
tagsList := tags.List()
|
||||
sort.Strings(tagsList)
|
||||
|
||||
return tagsList
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package cataloging
|
||||
|
||||
type ArchiveSearchConfig struct {
|
||||
IncludeIndexedArchives bool `yaml:"include-indexed-archives" json:"include-indexed-archives" mapstructure:"include-indexed-archives"`
|
||||
IncludeUnindexedArchives bool `yaml:"include-unindexed-archives" json:"include-unindexed-archives" mapstructure:"include-unindexed-archives"`
|
||||
}
|
||||
|
||||
func DefaultArchiveSearchConfig() ArchiveSearchConfig {
|
||||
return ArchiveSearchConfig{
|
||||
IncludeIndexedArchives: true,
|
||||
IncludeUnindexedArchives: false,
|
||||
}
|
||||
}
|
||||
|
||||
func (c ArchiveSearchConfig) WithIncludeIndexedArchives(include bool) ArchiveSearchConfig {
|
||||
c.IncludeIndexedArchives = include
|
||||
return c
|
||||
}
|
||||
|
||||
func (c ArchiveSearchConfig) WithIncludeUnindexedArchives(include bool) ArchiveSearchConfig {
|
||||
c.IncludeUnindexedArchives = include
|
||||
return c
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package cataloging
|
||||
|
||||
type ArchiveSearchConfig struct {
|
||||
IncludeIndexedArchives bool `yaml:"include-indexed-archives" json:"include-indexed-archives" mapstructure:"include-indexed-archives"`
|
||||
IncludeUnindexedArchives bool `yaml:"include-unindexed-archives" json:"include-unindexed-archives" mapstructure:"include-unindexed-archives"`
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package cataloging
|
||||
|
||||
type DataGenerationConfig struct {
|
||||
GenerateCPEs bool `yaml:"generate-cpes" json:"generate-cpes" mapstructure:"generate-cpes"`
|
||||
}
|
||||
|
||||
func DefaultDataGenerationConfig() DataGenerationConfig {
|
||||
return DataGenerationConfig{
|
||||
GenerateCPEs: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (c DataGenerationConfig) WithGenerateCPEs(generate bool) DataGenerationConfig {
|
||||
c.GenerateCPEs = generate
|
||||
return c
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
package filecataloging
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
intFile "github.com/anchore/syft/internal/file"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/file/cataloger/filecontent"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Selection file.Selection `yaml:"selection" json:"selection" mapstructure:"selection"`
|
||||
Hashers []crypto.Hash `yaml:"hashers" json:"hashers" mapstructure:"hashers"`
|
||||
Content filecontent.Config `yaml:"content" json:"content" mapstructure:"content"`
|
||||
}
|
||||
|
||||
type configMarshaledForm struct {
|
||||
Selection file.Selection `yaml:"selection" json:"selection" mapstructure:"selection"`
|
||||
Hashers []string `yaml:"hashers" json:"hashers" mapstructure:"hashers"`
|
||||
Content filecontent.Config `yaml:"content" json:"content" mapstructure:"content"`
|
||||
}
|
||||
|
||||
func DefaultConfig() Config {
|
||||
hashers, err := intFile.Hashers("sha256")
|
||||
if err != nil {
|
||||
log.WithFields("error", err).Warn("unable to create file hashers")
|
||||
}
|
||||
return Config{
|
||||
Selection: file.FilesOwnedByPackageSelection,
|
||||
Hashers: hashers,
|
||||
Content: filecontent.DefaultConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
func (cfg Config) MarshalJSON() ([]byte, error) {
|
||||
marshaled := configMarshaledForm{
|
||||
Selection: cfg.Selection,
|
||||
Hashers: hashersToString(cfg.Hashers),
|
||||
}
|
||||
return json.Marshal(marshaled)
|
||||
}
|
||||
|
||||
func hashersToString(hashers []crypto.Hash) []string {
|
||||
var result []string
|
||||
for _, h := range hashers {
|
||||
result = append(result, strings.ToLower(h.String()))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (cfg *Config) UnmarshalJSON(data []byte) error {
|
||||
var marshaled configMarshaledForm
|
||||
if err := json.Unmarshal(data, &marshaled); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hashers, err := intFile.Hashers(marshaled.Hashers...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse configured hashers: %w", err)
|
||||
}
|
||||
cfg.Selection = marshaled.Selection
|
||||
cfg.Hashers = hashers
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg Config) WithSelection(selection file.Selection) Config {
|
||||
cfg.Selection = selection
|
||||
return cfg
|
||||
}
|
||||
|
||||
func (cfg Config) WithHashers(hashers []crypto.Hash) Config {
|
||||
cfg.Hashers = hashers
|
||||
return cfg
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
package filecataloging
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/syft/file"
|
||||
)
|
||||
|
||||
func TestConfig_MarshalJSON(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg Config
|
||||
want []byte
|
||||
wantErr require.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "converts hashers to strings",
|
||||
cfg: Config{
|
||||
Selection: file.FilesOwnedByPackageSelection,
|
||||
Hashers: []crypto.Hash{crypto.SHA256},
|
||||
},
|
||||
want: []byte(`{"selection":"owned-by-package","hashers":["sha-256"],"content":{"globs":null,"skip-files-above-size":0}}`),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.wantErr == nil {
|
||||
tt.wantErr = require.NoError
|
||||
}
|
||||
got, err := tt.cfg.MarshalJSON()
|
||||
tt.wantErr(t, err)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if d := cmp.Diff(got, tt.want); d != "" {
|
||||
t.Errorf("MarshalJSON() mismatch (-want +got):\n%s", d)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_UnmarshalJSON(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
data []byte
|
||||
want Config
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "converts strings to hashers",
|
||||
data: []byte(`{"selection":"owned-by-package","hashers":["sha-256"]}`),
|
||||
want: Config{
|
||||
Selection: file.FilesOwnedByPackageSelection,
|
||||
Hashers: []crypto.Hash{crypto.SHA256},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := Config{}
|
||||
if err := cfg.UnmarshalJSON(tt.data); (err != nil) != tt.wantErr {
|
||||
t.Errorf("UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.want, cfg)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package pkgcataloging
|
||||
|
||||
import "github.com/anchore/syft/syft/pkg"
|
||||
|
||||
type CatalogerReference struct {
|
||||
Cataloger pkg.Cataloger
|
||||
AlwaysEnabled bool
|
||||
Tags []string
|
||||
}
|
||||
|
||||
func NewCatalogerReference(cataloger pkg.Cataloger, tags []string) CatalogerReference {
|
||||
return CatalogerReference{
|
||||
Cataloger: cataloger,
|
||||
Tags: tags,
|
||||
}
|
||||
}
|
||||
|
||||
func NewAlwaysEnabledCatalogerReference(cataloger pkg.Cataloger) CatalogerReference {
|
||||
return CatalogerReference{
|
||||
Cataloger: cataloger,
|
||||
AlwaysEnabled: true,
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package pkgcataloging
|
||||
|
||||
import (
|
||||
"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"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Binary binary.CatalogerConfig `yaml:"binary" json:"binary" mapstructure:"binary"`
|
||||
Golang golang.CatalogerConfig `yaml:"golang" json:"golang" mapstructure:"golang"`
|
||||
JavaArchive java.ArchiveCatalogerConfig `yaml:"java-archive" json:"java-archive" mapstructure:"java-archive"`
|
||||
JavaScript javascript.CatalogerConfig `yaml:"javascript" json:"javascript" mapstructure:"javascript"`
|
||||
LinuxKernel kernel.LinuxKernelCatalogerConfig `yaml:"linux-kernel" json:"linux-kernel" mapstructure:"linux-kernel"`
|
||||
Python python.CatalogerConfig `yaml:"python" json:"python" mapstructure:"python"`
|
||||
}
|
||||
|
||||
func DefaultConfig() Config {
|
||||
return Config{
|
||||
Golang: golang.DefaultCatalogerConfig(),
|
||||
LinuxKernel: kernel.DefaultLinuxCatalogerConfig(),
|
||||
Python: python.DefaultCatalogerConfig(),
|
||||
JavaArchive: java.DefaultArchiveCatalogerConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
func (c Config) WithBinaryConfig(cfg binary.CatalogerConfig) Config {
|
||||
c.Binary = cfg
|
||||
return c
|
||||
}
|
||||
|
||||
func (c Config) WithGolangConfig(cfg golang.CatalogerConfig) Config {
|
||||
c.Golang = cfg
|
||||
return c
|
||||
}
|
||||
|
||||
func (c Config) WithJavascriptConfig(cfg javascript.CatalogerConfig) Config {
|
||||
c.JavaScript = cfg
|
||||
return c
|
||||
}
|
||||
|
||||
func (c Config) WithLinuxKernelConfig(cfg kernel.LinuxKernelCatalogerConfig) Config {
|
||||
c.LinuxKernel = cfg
|
||||
return c
|
||||
}
|
||||
|
||||
func (c Config) WithPythonConfig(cfg python.CatalogerConfig) Config {
|
||||
c.Python = cfg
|
||||
return c
|
||||
}
|
||||
|
||||
func (c Config) WithJavaArchiveConfig(cfg java.ArchiveCatalogerConfig) Config {
|
||||
c.JavaArchive = cfg
|
||||
return c
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package pkgcataloging
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type SelectionRequest struct {
|
||||
DefaultNamesOrTags []string `json:"default,omitempty"`
|
||||
SubSelectTags []string `json:"selection,omitempty"`
|
||||
AddNames []string `json:"addition,omitempty"`
|
||||
RemoveNamesOrTags []string `json:"removal,omitempty"`
|
||||
}
|
||||
|
||||
func NewSelectionRequest() SelectionRequest {
|
||||
return SelectionRequest{}
|
||||
}
|
||||
|
||||
func (s SelectionRequest) WithExpression(expressions ...string) SelectionRequest {
|
||||
expressions = cleanSelection(expressions)
|
||||
for _, expr := range expressions {
|
||||
switch {
|
||||
case strings.HasPrefix(expr, "+"):
|
||||
s = s.WithAdditions(strings.TrimPrefix(expr, "+"))
|
||||
case strings.HasPrefix(expr, "-"):
|
||||
s = s.WithRemovals(strings.TrimPrefix(expr, "-"))
|
||||
default:
|
||||
s = s.WithSubSelections(expr)
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s SelectionRequest) WithDefaults(nameOrTags ...string) SelectionRequest {
|
||||
s.DefaultNamesOrTags = append(s.DefaultNamesOrTags, nameOrTags...)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s SelectionRequest) WithSubSelections(tags ...string) SelectionRequest {
|
||||
s.SubSelectTags = append(s.SubSelectTags, tags...)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s SelectionRequest) WithAdditions(names ...string) SelectionRequest {
|
||||
s.AddNames = append(s.AddNames, names...)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s SelectionRequest) WithRemovals(nameOrTags ...string) SelectionRequest {
|
||||
s.RemoveNamesOrTags = append(s.RemoveNamesOrTags, nameOrTags...)
|
||||
return s
|
||||
}
|
||||
|
||||
func cleanSelection(tags []string) []string {
|
||||
var cleaned []string
|
||||
for _, tag := range tags {
|
||||
for _, t := range strings.Split(tag, ",") {
|
||||
t = strings.TrimSpace(t)
|
||||
if t == "" {
|
||||
continue
|
||||
}
|
||||
cleaned = append(cleaned, t)
|
||||
}
|
||||
}
|
||||
return cleaned
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package pkgcataloging
|
||||
|
||||
const (
|
||||
// InstalledTag is to identify packages found to be positively installed.
|
||||
InstalledTag = "installed"
|
||||
|
||||
// DeclaredTag is to identify packages described but not necessarily installed.
|
||||
DeclaredTag = "declared"
|
||||
|
||||
// ImageTag indicates the cataloger should be used when cataloging images.
|
||||
ImageTag = "image"
|
||||
|
||||
// DirectoryTag indicates the cataloger should be used when cataloging directories.
|
||||
DirectoryTag = "directory"
|
||||
|
||||
// PackageTag should be used to identify catalogers that are package-based.
|
||||
PackageTag = "package"
|
||||
|
||||
// OSTag should be used to identify catalogers that cataloging OS packages.
|
||||
OSTag = "os"
|
||||
|
||||
// LanguageTag should be used to identify catalogers that cataloging language-specific packages.
|
||||
LanguageTag = "language"
|
||||
)
|
||||
@@ -1,39 +0,0 @@
|
||||
package cataloging
|
||||
|
||||
type RelationshipsConfig struct {
|
||||
// PackageFileOwnership will include package-to-file relationships that indicate which files are owned by which packages.
|
||||
PackageFileOwnership bool `yaml:"package-file-ownership" json:"package-file-ownership" mapstructure:"package-file-ownership"`
|
||||
|
||||
// PackageFileOwnershipOverlap will 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.
|
||||
// For example, if an RPM package is installed and claims to own /etc/app/package.lock and a separate NPM package was discovered by cataloging /etc/app/package.lock, then the two packages will
|
||||
// have ownership overlap relationship.
|
||||
PackageFileOwnershipOverlap bool `yaml:"package-file-ownership-overlap" json:"package-file-ownership-overlap" mapstructure:"package-file-ownership-overlap"`
|
||||
|
||||
// ExcludeBinaryPackagesWithFileOwnershipOverlap will exclude binary packages from the package catalog that are evident by files also owned by another package.
|
||||
// For example, if a binary package representing the /bin/python binary is discovered and there is a python RPM package installed which claims to
|
||||
// orn /bin/python, then the binary package will be excluded from the catalog altogether if this configuration is set to true.
|
||||
ExcludeBinaryPackagesWithFileOwnershipOverlap bool `yaml:"exclude-binary-packages-with-file-ownership-overlap" json:"exclude-binary-packages-with-file-ownership-overlap" mapstructure:"exclude-binary-packages-with-file-ownership-overlap"`
|
||||
}
|
||||
|
||||
func DefaultRelationshipsConfig() RelationshipsConfig {
|
||||
return RelationshipsConfig{
|
||||
PackageFileOwnership: true,
|
||||
PackageFileOwnershipOverlap: true,
|
||||
ExcludeBinaryPackagesWithFileOwnershipOverlap: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (c RelationshipsConfig) WithPackageFileOwnership(ownership bool) RelationshipsConfig {
|
||||
c.PackageFileOwnership = ownership
|
||||
return c
|
||||
}
|
||||
|
||||
func (c RelationshipsConfig) WithPackageFileOwnershipOverlap(overlap bool) RelationshipsConfig {
|
||||
c.PackageFileOwnershipOverlap = overlap
|
||||
return c
|
||||
}
|
||||
|
||||
func (c RelationshipsConfig) WithExcludeBinaryPackagesWithFileOwnershipOverlap(exclude bool) RelationshipsConfig {
|
||||
c.ExcludeBinaryPackagesWithFileOwnershipOverlap = exclude
|
||||
return c
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package cataloging
|
||||
|
||||
import "github.com/anchore/syft/syft/source"
|
||||
|
||||
type SearchConfig struct {
|
||||
Scope source.Scope `yaml:"scope" json:"scope" mapstructure:"scope"`
|
||||
}
|
||||
|
||||
func DefaultSearchConfig() SearchConfig {
|
||||
return SearchConfig{
|
||||
Scope: source.SquashedScope,
|
||||
}
|
||||
}
|
||||
|
||||
func (c SearchConfig) WithScope(scope source.Scope) SearchConfig {
|
||||
c.Scope = scope
|
||||
return c
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
package syft
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
|
||||
"github.com/anchore/syft/syft/cataloging"
|
||||
"github.com/anchore/syft/syft/cataloging/filecataloging"
|
||||
"github.com/anchore/syft/syft/cataloging/pkgcataloging"
|
||||
)
|
||||
|
||||
// configurationAuditTrail is all input configuration was used to generate the SBOM
|
||||
type configurationAuditTrail struct {
|
||||
Search cataloging.SearchConfig `json:"search" yaml:"search" mapstructure:"search"`
|
||||
Relationships cataloging.RelationshipsConfig `json:"relationships" yaml:"relationships" mapstructure:"relationships"`
|
||||
DataGeneration cataloging.DataGenerationConfig `json:"data-generation" yaml:"data-generation" mapstructure:"data-generation"`
|
||||
Packages pkgcataloging.Config `json:"packages" yaml:"packages" mapstructure:"packages"`
|
||||
Files filecataloging.Config `json:"files" yaml:"files" mapstructure:"files"`
|
||||
Catalogers catalogerManifest `json:"catalogers" yaml:"catalogers" mapstructure:"catalogers"`
|
||||
ExtraConfigs any `json:"extra,omitempty" yaml:"extra" mapstructure:"extra"`
|
||||
}
|
||||
|
||||
type catalogerManifest struct {
|
||||
Requested pkgcataloging.SelectionRequest `json:"requested" yaml:"requested" mapstructure:"requested"`
|
||||
Used []string `json:"used" yaml:"used" mapstructure:"used"`
|
||||
}
|
||||
|
||||
type marshalAPIConfiguration configurationAuditTrail
|
||||
|
||||
func (cfg configurationAuditTrail) MarshalJSON() ([]byte, error) {
|
||||
// since the api configuration is placed into the SBOM in an empty interface, and we want a stable ordering of
|
||||
// keys (not guided by the struct ordering) we need to convert the struct to a map. This is best done with
|
||||
// simply marshalling and unmarshalling. Mapstructure is used to ensure we are honoring all json struct
|
||||
// tags. Once we have a map, we can lean on the stable ordering of json map keys in the stdlib. This is an
|
||||
// implementation detail that can be at least relied on until Go 2 (at which point it can change).
|
||||
// This dance allows us to guarantee ordering of keys in the configuration section of the SBOM.
|
||||
|
||||
initialJSON, err := json.Marshal(marshalAPIConfiguration(cfg))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var dataMap map[string]interface{}
|
||||
if err := json.Unmarshal(initialJSON, &dataMap); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if v, exists := dataMap["extra"]; exists && v == nil {
|
||||
// remove the extra key if it renders as nil
|
||||
delete(dataMap, "extra")
|
||||
}
|
||||
|
||||
return marshalSorted(dataMap)
|
||||
}
|
||||
|
||||
// marshalSorted recursively marshals a map with sorted keys
|
||||
func marshalSorted(m interface{}) ([]byte, error) {
|
||||
if reflect.TypeOf(m).Kind() != reflect.Map {
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
||||
val := reflect.ValueOf(m)
|
||||
sortedMap := make(map[string]interface{})
|
||||
|
||||
for _, key := range val.MapKeys() {
|
||||
value := val.MapIndex(key).Interface()
|
||||
|
||||
if value != nil && reflect.TypeOf(value).Kind() == reflect.Map {
|
||||
sortedValue, err := marshalSorted(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sortedMap[key.String()] = json.RawMessage(sortedValue)
|
||||
} else {
|
||||
sortedMap[key.String()] = value
|
||||
}
|
||||
}
|
||||
|
||||
return json.Marshal(sortedMap)
|
||||
}
|
||||
@@ -1,346 +0,0 @@
|
||||
package syft
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/iancoleman/strcase"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/syft/cataloging/filecataloging"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
)
|
||||
|
||||
func Test_configurationAuditTrail_StructTags(t *testing.T) {
|
||||
// we need to ensure that the output for any configuration is well-formed and follows conventions.
|
||||
// We ensure that:
|
||||
// 1. all fields have a JSON tag
|
||||
// 2. the tag value follows lowercase kebab-case style
|
||||
|
||||
jsonTags := getJSONTags(t, configurationAuditTrail{})
|
||||
|
||||
for _, tag := range jsonTags {
|
||||
assertLowercaseKebab(t, tag)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func getJSONTags(t *testing.T, v interface{}) []string {
|
||||
var tags []string
|
||||
err := collectJSONTags(t, reflect.ValueOf(v), &tags, "", "")
|
||||
require.NoError(t, err)
|
||||
return tags
|
||||
}
|
||||
|
||||
func collectJSONTags(t *testing.T, v reflect.Value, tags *[]string, parentTag string, path string) error {
|
||||
var errs error
|
||||
|
||||
if v.Kind() == reflect.Ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
if v.Kind() != reflect.Struct {
|
||||
return errs
|
||||
}
|
||||
|
||||
tType := v.Type()
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
field := v.Field(i)
|
||||
fieldType := tType.Field(i)
|
||||
|
||||
curPath := path + "." + fieldType.Name
|
||||
|
||||
// account for embeddings
|
||||
if fieldType.Anonymous {
|
||||
embeddedField := field
|
||||
|
||||
if embeddedField.Kind() == reflect.Ptr {
|
||||
// this can be enhanced in the future if the need arises...
|
||||
errs = multierror.Append(errs, fmt.Errorf("field '%s' is a pointer to an embedded struct, this is not supported in the test helper", curPath))
|
||||
}
|
||||
|
||||
if embeddedField.Kind() == reflect.Struct {
|
||||
err := collectJSONTags(t, field, tags, parentTag, curPath)
|
||||
if err != nil {
|
||||
errs = multierror.Append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
var tag string
|
||||
var ok bool
|
||||
if fieldType.PkgPath == "" {
|
||||
tag, ok = fieldType.Tag.Lookup("json")
|
||||
if !ok || (tag == "" && parentTag == "") {
|
||||
errs = multierror.Append(errs, fmt.Errorf("field '%s' does not have a json tag", curPath))
|
||||
return errs
|
||||
}
|
||||
if tag != "" && tag != "-" {
|
||||
*tags = append(*tags, tag)
|
||||
}
|
||||
}
|
||||
|
||||
if field.Kind() == reflect.Struct || (field.Kind() == reflect.Ptr && field.Elem().Kind() == reflect.Struct) {
|
||||
err := collectJSONTags(t, field, tags, tag, curPath)
|
||||
if err != nil {
|
||||
errs = multierror.Append(errs, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func assertLowercaseKebab(t *testing.T, tag string) {
|
||||
t.Helper()
|
||||
require.NotEmpty(t, tag)
|
||||
assert.Equal(t, strcase.ToKebab(tag), tag)
|
||||
}
|
||||
|
||||
func Test_collectJSONTags(t *testing.T) {
|
||||
// though this is not used in production, this is a sensitive and complex enough of a check to warrant testing the test helper.
|
||||
type good struct {
|
||||
A string `json:"a"`
|
||||
}
|
||||
|
||||
type missing struct {
|
||||
A string `json:"a"`
|
||||
B string
|
||||
}
|
||||
|
||||
type exclude struct {
|
||||
A string `json:"a"`
|
||||
B string `json:"-"`
|
||||
}
|
||||
|
||||
type goodEmbedded struct {
|
||||
good `json:""`
|
||||
}
|
||||
|
||||
type badEmbedded struct {
|
||||
missing `json:""`
|
||||
}
|
||||
|
||||
// simply not covered and require further development to support
|
||||
type goodPtrEmbedded struct {
|
||||
*good `json:""`
|
||||
}
|
||||
|
||||
// simply not covered and require further development to support
|
||||
type badPtrEmbedded struct {
|
||||
*missing `json:""`
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
v interface{}
|
||||
want []string
|
||||
wantErr require.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "good",
|
||||
v: good{},
|
||||
want: []string{
|
||||
"a",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing",
|
||||
v: missing{},
|
||||
wantErr: require.Error,
|
||||
},
|
||||
{
|
||||
name: "exclude",
|
||||
v: exclude{},
|
||||
want: []string{
|
||||
"a",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bad embedded",
|
||||
v: badEmbedded{},
|
||||
wantErr: require.Error,
|
||||
},
|
||||
{
|
||||
name: "good embedded",
|
||||
v: goodEmbedded{},
|
||||
want: []string{
|
||||
"a",
|
||||
},
|
||||
},
|
||||
// these cases are simply not covered and require further development to support
|
||||
{
|
||||
name: "bad ptr embedded",
|
||||
v: badPtrEmbedded{},
|
||||
wantErr: require.Error,
|
||||
},
|
||||
{
|
||||
name: "good ptr embedded",
|
||||
v: goodPtrEmbedded{},
|
||||
want: []string{
|
||||
"a",
|
||||
},
|
||||
wantErr: require.Error,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.wantErr == nil {
|
||||
tt.wantErr = require.NoError
|
||||
}
|
||||
|
||||
var tags []string
|
||||
|
||||
err := collectJSONTags(t, reflect.ValueOf(tt.v), &tags, "", "")
|
||||
|
||||
tt.wantErr(t, err)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.want, tags)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_configurationAuditTrail_MarshalJSON(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg configurationAuditTrail
|
||||
assert func(t *testing.T, got []byte)
|
||||
}{
|
||||
{
|
||||
name: "ensure other marshallers are called",
|
||||
cfg: configurationAuditTrail{
|
||||
|
||||
Files: filecataloging.Config{
|
||||
Selection: file.FilesOwnedByPackageSelection,
|
||||
Hashers: []crypto.Hash{
|
||||
crypto.SHA256,
|
||||
},
|
||||
},
|
||||
},
|
||||
// the custom file marshaller swaps ints for strings for hashers
|
||||
assert: func(t *testing.T, got []byte) {
|
||||
assert.Contains(t, string(got), `"hashers":["sha-256"]`)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ensure maps are sorted",
|
||||
cfg: configurationAuditTrail{},
|
||||
assert: func(t *testing.T, got []byte) {
|
||||
assert.NoError(t, assertJSONKeysSorted(got))
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
got, err := tt.cfg.MarshalJSON()
|
||||
require.NoError(t, err)
|
||||
if tt.assert == nil {
|
||||
t.Fatal("assert function must be provided")
|
||||
}
|
||||
tt.assert(t, got)
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// assertJSONKeysSorted asserts that all keys in JSON maps are sorted.
|
||||
func assertJSONKeysSorted(jsonBytes []byte) error {
|
||||
var errs error
|
||||
decoder := json.NewDecoder(bytes.NewReader(jsonBytes))
|
||||
var keys []string
|
||||
var inObject bool
|
||||
|
||||
for {
|
||||
token, err := decoder.Token()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
errs = multierror.Append(errs, fmt.Errorf("error decoding JSON: %w", err))
|
||||
}
|
||||
|
||||
switch v := token.(type) {
|
||||
case json.Delim:
|
||||
switch v {
|
||||
case '{':
|
||||
inObject = true
|
||||
keys = nil // Reset keys for a new object
|
||||
case '}':
|
||||
inObject = false
|
||||
if !sort.StringsAreSorted(keys) {
|
||||
errs = multierror.Append(errs, fmt.Errorf("Keys are not sorted: %v", keys))
|
||||
}
|
||||
}
|
||||
case string:
|
||||
if inObject && v != "" {
|
||||
keys = append(keys, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func Test_assertJSONKeysSorted(t *testing.T) {
|
||||
// this test function is sufficiently complicated enough to warrant its own test...
|
||||
|
||||
sorted := []byte(`{"a":1,"b":2}`)
|
||||
unsorted := []byte(`{"b":2,"a":1}`)
|
||||
|
||||
nestedSorted := []byte(`{"a":1,"b":{"a":1,"b":2}}`)
|
||||
nestedUnsorted := []byte(`{"a":1,"b":{"b":2,"a":1}}`)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
json []byte
|
||||
wantErr require.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "sorted",
|
||||
json: sorted,
|
||||
wantErr: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "unsorted",
|
||||
json: unsorted,
|
||||
wantErr: require.Error,
|
||||
},
|
||||
{
|
||||
name: "nested sorted",
|
||||
json: nestedSorted,
|
||||
wantErr: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "nested unsorted",
|
||||
json: nestedUnsorted,
|
||||
wantErr: require.Error,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.wantErr == nil {
|
||||
tt.wantErr = require.NoError
|
||||
}
|
||||
|
||||
err := assertJSONKeysSorted(tt.json)
|
||||
tt.wantErr(t, err)
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user