mirror of
https://github.com/anchore/syft.git
synced 2026-10-13 22:57:19 +02:00
Compare commits
72
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
821210006d | ||
|
|
053768c6c6 | ||
|
|
9189ed68df | ||
|
|
ba1cd8d753 | ||
|
|
dd2c178085 | ||
|
|
1b23a94015 | ||
|
|
f47a6a88b1 | ||
|
|
4b7217f052 | ||
|
|
5e4b668e5a | ||
|
|
6480f06173 | ||
|
|
75aed5f3ec | ||
|
|
270fbb7665 | ||
|
|
0395c47445 | ||
|
|
e5caba043d | ||
|
|
d1ac476986 | ||
|
|
dd23d49986 | ||
|
|
6d4d083acc | ||
|
|
316d4341c8 | ||
|
|
3e8afc5274 | ||
|
|
93d00dc340 | ||
|
|
9fe1da8ee6 | ||
|
|
95df7dd035 | ||
|
|
ddfc8e20c0 | ||
|
|
3f305aa6cd | ||
|
|
2f99a35f51 | ||
|
|
06dcd3261d | ||
|
|
abbba3fc19 | ||
|
|
0799fd9d46 | ||
|
|
5de1a0a236 | ||
|
|
035ef03b0c | ||
|
|
9acabb8307 | ||
|
|
0f0fe59bf2 | ||
|
|
ff828fbac2 | ||
|
|
cba5b5723b | ||
|
|
b0e9b92a25 | ||
|
|
d25d3b1fb3 | ||
|
|
93b4bf9379 | ||
|
|
58f2be95fd | ||
|
|
6f038e9208 | ||
|
|
32a54b94be | ||
|
|
98d4749f86 | ||
|
|
3a5168917e | ||
|
|
58b21c03c3 | ||
|
|
2093d2a1dc | ||
|
|
dd56450f92 | ||
|
|
6f2b52971d | ||
|
|
fee74d57b8 | ||
|
|
fee35dd175 | ||
|
|
ecf4e5546a | ||
|
|
2de56c0749 | ||
|
|
fb0857ff93 | ||
|
|
962e82297c | ||
|
|
c833472b6e | ||
|
|
c825ae2404 | ||
|
|
706322f826 | ||
|
|
66e71c39e2 | ||
|
|
6dd698d89e | ||
|
|
50928ebd05 | ||
|
|
6e70d8af98 | ||
|
|
67b7d63875 | ||
|
|
5a2e2eb679 | ||
|
|
801e662633 | ||
|
|
6a398f9771 | ||
|
|
bf1bf057ab | ||
|
|
36b44b1d8e | ||
|
|
8f85c8affc | ||
|
|
776164a078 | ||
|
|
5ea1d78464 | ||
|
|
afbd8f8ea0 | ||
|
|
2f81a2548c | ||
|
|
2754c889eb | ||
|
|
8be0d988e7 |
+9
-1
@@ -6,4 +6,12 @@ permit:
|
||||
- ISC
|
||||
ignore-packages:
|
||||
# packageurl-go is released under the MIT license located in the root of the repo at /mit.LICENSE
|
||||
- github.com/package-url/packageurl-go
|
||||
- github.com/anchore/packageurl-go
|
||||
|
||||
# from: https://github.com/spdx/tools-golang/blob/main/LICENSE.code
|
||||
# The tools-golang source code is provided and may be used, at your option,
|
||||
# under either:
|
||||
# * Apache License, version 2.0 (Apache-2.0), OR
|
||||
# * GNU General Public License, version 2.0 or later (GPL-2.0-or-later).
|
||||
# (we choose Apache-2.0)
|
||||
- github.com/spdx/tools-golang
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
contact_links:
|
||||
|
||||
- name: Help & Support Slack Channel ⛑️
|
||||
# link to toolbox-help channel
|
||||
url: https://anchorecommunity.slack.com/archives/C019BUXV7R6
|
||||
about: Ask a questions and get answers
|
||||
|
||||
- name: Developer Slack Channel 💬
|
||||
# link to toolbox-dev channel
|
||||
url: https://anchorecommunity.slack.com/archives/C0190NF9TSM
|
||||
about: Participate in design discussions and feature development
|
||||
- name: Join the Slack community 💬
|
||||
# link to our community Slack registration page
|
||||
url: https://anchore.com/slack
|
||||
about: 'Come chat with us! Ask for help, join our software development efforts, or just give us feedback!'
|
||||
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eu
|
||||
|
||||
ORIGINAL_STATE_DIR=$(mktemp -d "TEMP-original-state-XXXXXXXXX")
|
||||
TIDY_STATE_DIR=$(mktemp -d "TEMP-tidy-state-XXXXXXXXX")
|
||||
|
||||
trap "cp -v ${ORIGINAL_STATE_DIR}/* ./ && rm -fR ${ORIGINAL_STATE_DIR} ${TIDY_STATE_DIR}" EXIT
|
||||
|
||||
echo "Capturing original state of files..."
|
||||
cp -v go.mod go.sum "${ORIGINAL_STATE_DIR}"
|
||||
|
||||
echo "Capturing state of go.mod and go.sum after running go mod tidy..."
|
||||
go mod tidy
|
||||
cp -v go.mod go.sum "${TIDY_STATE_DIR}"
|
||||
echo ""
|
||||
|
||||
set +e
|
||||
|
||||
# Detect difference between the git HEAD state and the go mod tidy state
|
||||
DIFF_MOD=$(diff -u "${ORIGINAL_STATE_DIR}/go.mod" "${TIDY_STATE_DIR}/go.mod")
|
||||
DIFF_SUM=$(diff -u "${ORIGINAL_STATE_DIR}/go.sum" "${TIDY_STATE_DIR}/go.sum")
|
||||
|
||||
if [[ -n "${DIFF_MOD}" || -n "${DIFF_SUM}" ]]; then
|
||||
echo "go.mod diff:"
|
||||
echo "${DIFF_MOD}"
|
||||
echo "go.sum diff:"
|
||||
echo "${DIFF_SUM}"
|
||||
echo ""
|
||||
printf "FAILED! go.mod and/or go.sum are NOT tidy; please run 'go mod tidy'.\n\n"
|
||||
exit 1
|
||||
fi
|
||||
@@ -158,6 +158,10 @@ jobs:
|
||||
AC_USERNAME: ${{ secrets.ENG_CI_APPLE_ID }} # Used during macOS notarization.
|
||||
AC_PASSWORD: ${{ secrets.ENG_CI_APPLE_ID_PASS }} # Used during macOS notarization.
|
||||
|
||||
- uses: anchore/sbom-action@v0
|
||||
with:
|
||||
artifact-name: sbom.spdx.json
|
||||
|
||||
- uses: 8398a7/action-slack@v3
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
|
||||
@@ -13,9 +13,6 @@ jobs:
|
||||
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
|
||||
name: "Static analysis"
|
||||
runs-on: ubuntu-20.04
|
||||
# run only on push event (internal PRs) or on a pull_request event that is from a fork (external PR)
|
||||
# skip if this is a pull_request event on an internal PR (which is already covered by push events)
|
||||
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
|
||||
steps:
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
@@ -53,9 +50,6 @@ jobs:
|
||||
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
|
||||
name: "Unit tests"
|
||||
runs-on: ubuntu-20.04
|
||||
# run only on push event (internal PRs) or on a pull_request event that is from a fork (external PR)
|
||||
# skip if this is a pull_request event on an internal PR (which is already covered by push events)
|
||||
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
|
||||
steps:
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
@@ -108,9 +102,6 @@ jobs:
|
||||
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
|
||||
name: "Integration tests"
|
||||
runs-on: ubuntu-20.04
|
||||
# run only on push event (internal PRs) or on a pull_request event that is from a fork (external PR)
|
||||
# skip if this is a pull_request event on an internal PR (which is already covered by push events)
|
||||
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
|
||||
steps:
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
@@ -234,9 +225,6 @@ jobs:
|
||||
Build-Snapshot-Artifacts:
|
||||
name: "Build snapshot artifacts"
|
||||
runs-on: ubuntu-20.04
|
||||
# run only on push event (internal PRs) or on a pull_request event that is from a fork (external PR)
|
||||
# skip if this is a pull_request event on an internal PR (which is already covered by push events)
|
||||
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
|
||||
steps:
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
@@ -277,9 +265,6 @@ jobs:
|
||||
name: "Acceptance tests (Linux)"
|
||||
needs: [Build-Snapshot-Artifacts]
|
||||
runs-on: ubuntu-20.04
|
||||
# run only on push event (internal PRs) or on a pull_request event that is from a fork (external PR)
|
||||
# skip if this is a pull_request event on an internal PR (which is already covered by push events)
|
||||
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
@@ -296,9 +281,6 @@ jobs:
|
||||
name: "Acceptance tests (Mac)"
|
||||
needs: [Build-Snapshot-Artifacts]
|
||||
runs-on: macos-latest
|
||||
# run only on push event (internal PRs) or on a pull_request event that is from a fork (external PR)
|
||||
# skip if this is a pull_request event on an internal PR (which is already covered by push events)
|
||||
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
@@ -322,9 +304,6 @@ jobs:
|
||||
name: "CLI tests (Linux)"
|
||||
needs: [Build-Snapshot-Artifacts]
|
||||
runs-on: ubuntu-20.04
|
||||
# run only on push event (internal PRs) or on a pull_request event that is from a fork (external PR)
|
||||
# skip if this is a pull_request event on an internal PR (which is already covered by push events)
|
||||
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
|
||||
+39
-5
@@ -4,35 +4,59 @@
|
||||
# include:
|
||||
# - EXC0002 # disable excluding of issues about comments from golint
|
||||
|
||||
linters-settings:
|
||||
|
||||
cyclop:
|
||||
# the maximal code complexity to report
|
||||
max-complexity: 15
|
||||
|
||||
linters:
|
||||
# inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint
|
||||
disable-all: true
|
||||
enable:
|
||||
- asciicheck
|
||||
- bodyclose
|
||||
- cyclop
|
||||
- deadcode
|
||||
- depguard
|
||||
- dogsled
|
||||
- dupl
|
||||
- durationcheck
|
||||
- errcheck
|
||||
- errname
|
||||
- errorlint
|
||||
- exportloopref
|
||||
- forbidigo
|
||||
- forcetypeassert
|
||||
- funlen
|
||||
- gci
|
||||
- gocognit
|
||||
- goconst
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- gofmt
|
||||
- gofumpt
|
||||
- goimports
|
||||
- golint
|
||||
- gomoddirectives
|
||||
- gomodguard
|
||||
- goprintffuncname
|
||||
- gosec
|
||||
- gosimple
|
||||
- govet
|
||||
- ifshort
|
||||
- importas
|
||||
- ineffassign
|
||||
# - ireturn # will be added in 1.43.0 (accept interfaces, return structs)
|
||||
- makezero
|
||||
- misspell
|
||||
- nakedret
|
||||
- nilerr
|
||||
# - nilnil # will be added in 1.43.0
|
||||
- nolintlint
|
||||
- paralleltest
|
||||
- predeclared
|
||||
- revive
|
||||
- rowserrcheck
|
||||
- scopelint
|
||||
- staticcheck
|
||||
- structcheck
|
||||
- stylecheck
|
||||
@@ -41,19 +65,29 @@ linters:
|
||||
- unparam
|
||||
- unused
|
||||
- varcheck
|
||||
- wastedassign
|
||||
- whitespace
|
||||
# - wrapcheck
|
||||
|
||||
|
||||
# do not enable...
|
||||
# - exhaustive # makes to many assumptions about enums and if a switch case should contain all enum instances
|
||||
# - exhaustivestruct # we like to depend on zero values
|
||||
# - gochecknoglobals
|
||||
# - gochecknoinits # this is too aggressive
|
||||
# - godot
|
||||
# - godox
|
||||
# - goerr113
|
||||
# - goerr113 # does not allow use of dynamic errors
|
||||
# - golint # replaced by revive
|
||||
# - gomnd # this is too aggressive
|
||||
# - interfacer # this is a good idea, but is no longer supported and is prone to false positives
|
||||
# - lll # without a way to specify per-line exception cases, this is not usable
|
||||
# - maligned # this is an excellent linter, but tricky to optimize and we are not sensitive to memory layout optimizations
|
||||
# - nestif
|
||||
# - nestif # other complexity linters already cover this well
|
||||
# - nlreturn # no robust auto-fixers supported
|
||||
# - noctx # we don't use context a lot
|
||||
# - prealloc # following this rule isn't consistently a good idea, as it sometimes forces unnecessary allocations that result in less idiomatic code
|
||||
# - scopelint # replaced by exportloopref
|
||||
# - tagliatelle # too presumptive at which struct tag conventions should be used
|
||||
# - testpackage
|
||||
# - wsl
|
||||
# - wsl # very verbose and there is no auto-fixer for this project yet https://github.com/bombsimon/wsl/issues/90
|
||||
|
||||
+11
-6
@@ -1,12 +1,12 @@
|
||||
# Contributing to `syft`
|
||||
# Contributing to Syft
|
||||
|
||||
If you are looking to contribute to this project and want to open a Github pull request ("PR"), there are a few guidelines of what we are looking for in patches. Make sure you go through this document and ensure that your code proposal is aligned.
|
||||
If you are looking to contribute to this project and want to open a GitHub pull request ("PR"), there are a few guidelines of what we are looking for in patches. Make sure you go through this document and ensure that your code proposal is aligned.
|
||||
|
||||
## Sign your work
|
||||
## Sign off your work
|
||||
|
||||
The `sign-off` is an added line at the end of the explanation for the commit, certifying that you wrote it or otherwise have the right to submit it as an open-source patch. By submitting a contribution, you agree to be bound by the terms of the DCO Version 1.1 and Apache License Version 2.0.
|
||||
|
||||
Signing a commit certifies the below Developer's Certificate of Origin (DCO):
|
||||
Signing off a commit certifies the below Developer's Certificate of Origin (DCO):
|
||||
|
||||
```text
|
||||
Developer's Certificate of Origin 1.1
|
||||
@@ -51,13 +51,13 @@ $ git config --global user.name "John Doe"
|
||||
$ git config --global user.email "john.doe@example.com"
|
||||
```
|
||||
|
||||
Creating a signed commit is then possible with `-s`:
|
||||
Creating a signed-off commit is then possible with `-s` or `--signoff`:
|
||||
|
||||
```text
|
||||
$ git commit -s -m "this is a commit message"
|
||||
```
|
||||
|
||||
To double check if the commit was signed, look at the log output:
|
||||
To double-check that the commit was signed-off, look at the log output:
|
||||
|
||||
```text
|
||||
$ git log -1
|
||||
@@ -82,6 +82,11 @@ Date: Mon Aug 1 11:27:13 2020 -0400
|
||||
|
||||
[//]: # (TODO: PR title and description)
|
||||
|
||||
## Sign your commits
|
||||
|
||||
To ensure the authenticity and integrity of code contributions, **we require that all commits are signed**. Signing commits proves that your commits were truly created by you, as the holder of a private key.
|
||||
|
||||
Configuring git to sign your commits is a straightforward process. To get set up, see [GitHub's documentation on signing commits](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits).
|
||||
|
||||
## Test your changes
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ RESET := $(shell tput -T linux sgr0)
|
||||
TITLE := $(BOLD)$(PURPLE)
|
||||
SUCCESS := $(BOLD)$(GREEN)
|
||||
# the quality gate lower threshold for unit test total % coverage (by function statements)
|
||||
COVERAGE_THRESHOLD := 70
|
||||
COVERAGE_THRESHOLD := 65
|
||||
# CI cache busting values; change these if you want CI to not use previous stored cache
|
||||
INTEGRATION_CACHE_BUSTER="88738d2f"
|
||||
CLI_CACHE_BUSTER="789bacdf"
|
||||
@@ -101,21 +101,20 @@ $(TEMPDIR):
|
||||
.PHONY: bootstrap-tools
|
||||
bootstrap-tools: $(TEMPDIR)
|
||||
GO111MODULE=off GOBIN=$(shell realpath $(TEMPDIR)) go get -u golang.org/x/perf/cmd/benchstat
|
||||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(TEMPDIR)/ v1.26.0
|
||||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(TEMPDIR)/ v1.42.1
|
||||
curl -sSfL https://raw.githubusercontent.com/wagoodman/go-bouncer/master/bouncer.sh | sh -s -- -b $(TEMPDIR)/ v0.2.0
|
||||
curl -sfL https://install.goreleaser.com/github.com/goreleaser/goreleaser.sh | sh -s -- -b $(TEMPDIR)/ v0.160.0
|
||||
curl -sfL https://install.goreleaser.com/github.com/goreleaser/goreleaser.sh | sh -s -- -b $(TEMPDIR)/ v0.177.0
|
||||
|
||||
.PHONY: bootstrap-go
|
||||
bootstrap-go:
|
||||
go mod download
|
||||
go mod tidy # note: it is important that the go.sum is kept in a consistent state at all times (especially during release)
|
||||
|
||||
.PHONY: bootstrap
|
||||
bootstrap: $(RESULTSDIR) bootstrap-go bootstrap-tools ## Download and install all go dependencies (+ prep tooling in the ./tmp dir)
|
||||
$(call title,Bootstrapping dependencies)
|
||||
|
||||
.PHONY: static-analysis
|
||||
static-analysis: lint check-licenses
|
||||
static-analysis: lint check-go-mod-tidy check-licenses
|
||||
|
||||
.PHONY: lint
|
||||
lint: ## Run gofmt + golangci lint checks
|
||||
@@ -136,11 +135,15 @@ lint-fix: ## Auto-format all source code + run golangci lint fixers
|
||||
$(call title,Running lint fixers)
|
||||
gofmt -w -s .
|
||||
$(LINTCMD) --fix
|
||||
go mod tidy
|
||||
|
||||
.PHONY: check-licenses
|
||||
check-licenses:
|
||||
$(TEMPDIR)/bouncer check
|
||||
|
||||
check-go-mod-tidy:
|
||||
@ .github/scripts/go-mod-tidy-check.sh && echo "go.mod and go.sum are tidy!"
|
||||
|
||||
.PHONY: validate-cyclonedx-schema
|
||||
validate-cyclonedx-schema:
|
||||
cd schema/cyclonedx && make
|
||||
@@ -346,4 +349,4 @@ show-test-image-cache: ## Show all docker and image tar cache
|
||||
.PHONY: show-test-snapshots
|
||||
show-test-snapshots: ## Show all test snapshots
|
||||
$(call title,Test snapshots)
|
||||
@find . -type f -wholename "**/test-fixtures/snapshot/*" | sort
|
||||
@find . -type f -wholename "**/test-fixtures/snapshot/*" | sort
|
||||
|
||||
@@ -3,11 +3,20 @@
|
||||
[](https://github.com/anchore/syft/actions/workflows/validations.yaml)
|
||||
[](https://goreportcard.com/report/github.com/anchore/syft)
|
||||
[](https://github.com/anchore/syft/releases/latest)
|
||||
[](https://github.com/anchore/syft/blob/main/LICENSE)
|
||||
[](https://github.com/anchore/syft)
|
||||
[](https://github.com/anchore/syft/blob/main/LICENSE)
|
||||
[](https://anchore.com/slack)
|
||||
|
||||
A CLI tool and go library for generating a Software Bill of Materials (SBOM) from container images and filesystems. Exceptional for vulnerability detection when used with a scanner tool like [Grype](https://github.com/anchore/grype).
|
||||
|
||||
## We'll be at KubeCon 2021!
|
||||
|
||||
Attending KubeCon 2021 in person? Join us for a meetup on **Tuesday, October 12th**.
|
||||
|
||||
We’ll have free swag, giveaways, snacks, and sips. Space will be limited, so make sure to [save your seat](https://get.anchore.com/2021-kubecon-na-opensource-happy-hour/)!
|
||||
|
||||
---
|
||||
|
||||

|
||||
|
||||
**Features**
|
||||
@@ -20,6 +29,27 @@ A CLI tool and go library for generating a Software Bill of Materials (SBOM) fro
|
||||
|
||||
If you encounter an issue, please [let us know using the issue tracker](https://github.com/anchore/syft/issues).
|
||||
|
||||
## Installation
|
||||
|
||||
### Recommended
|
||||
```bash
|
||||
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
|
||||
```
|
||||
|
||||
...or, you can specify a release version and destination directory for the installation:
|
||||
|
||||
```
|
||||
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b <DESTINATION_DIR> <RELEASE_VERSION>
|
||||
```
|
||||
|
||||
### Homebrew
|
||||
```bash
|
||||
brew tap anchore/syft
|
||||
brew install syft
|
||||
```
|
||||
|
||||
**Note**: Currently, Syft is built only for macOS and Linux.
|
||||
|
||||
## Getting started
|
||||
|
||||
To generate an SBOM for a Docker or OCI image:
|
||||
@@ -39,7 +69,10 @@ To include software from all image layers in the SBOM, regardless of its presenc
|
||||
syft packages <image> --scope all-layers
|
||||
```
|
||||
|
||||
### Supported sources
|
||||
|
||||
Syft can generate a SBOM from a variety of sources:
|
||||
|
||||
```
|
||||
# catalog a container image archive (from the result of `docker image save ...`, `podman save ...`, or `skopeo copy` commands)
|
||||
syft packages path/to/image.tar
|
||||
@@ -49,6 +82,7 @@ syft packages path/to/dir
|
||||
```
|
||||
|
||||
Sources can be explicitly provided with a scheme:
|
||||
|
||||
```
|
||||
docker:yourrepo/yourimage:tag use images from the Docker daemon
|
||||
docker-archive:path/to/yourimage.tar use a tarball from disk for archives created from "docker save"
|
||||
@@ -58,7 +92,10 @@ dir:path/to/yourproject read directly from a path on disk (any di
|
||||
registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required)
|
||||
```
|
||||
|
||||
### Output formats
|
||||
|
||||
The output format for Syft is configurable as well:
|
||||
|
||||
```
|
||||
syft packages <image> -o <format>
|
||||
```
|
||||
@@ -66,26 +103,11 @@ syft packages <image> -o <format>
|
||||
Where the `format`s available are:
|
||||
- `json`: Use this to get as much information out of Syft as possible!
|
||||
- `text`: A row-oriented, human-and-machine-friendly output.
|
||||
- `cyclonedx`: A XML report conforming to the [CycloneDX 1.2](https://cyclonedx.org/) specification.
|
||||
- `cyclonedx`: A XML report conforming to the [CycloneDX 1.2 specification](https://cyclonedx.org/specification/overview/).
|
||||
- `spdx`: A tag-value formatted report conforming to the [SPDX 2.2 specification](https://spdx.github.io/spdx-spec/).
|
||||
- `spdx-json`: A JSON report conforming to the [SPDX 2.2 JSON Schema](https://github.com/spdx/spdx-spec/blob/v2.2/schemas/spdx-schema.json).
|
||||
- `table`: A columnar summary (default).
|
||||
|
||||
## Installation
|
||||
|
||||
**Recommended (macOS and Linux)**
|
||||
```bash
|
||||
# install the latest version to /usr/local/bin
|
||||
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
|
||||
|
||||
# install a specific version into a specific dir
|
||||
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b <SOME_BIN_PATH> <RELEASE_VERSION>
|
||||
```
|
||||
|
||||
**Homebrew (macOS)**
|
||||
```bash
|
||||
brew tap anchore/syft
|
||||
brew install syft
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration search paths:
|
||||
@@ -106,6 +128,9 @@ output: "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
|
||||
@@ -202,6 +227,9 @@ registry:
|
||||
# skip TLS verification when communicating with the registry
|
||||
# SYFT_REGISTRY_INSECURE_SKIP_TLS_VERIFY env var
|
||||
insecure-skip-tls-verify: false
|
||||
# use http instead of https when connecting to the registry
|
||||
# SYFT_REGISTRY_INSECURE_USE_HTTP env var
|
||||
insecure-use-http: false
|
||||
|
||||
# credentials for specific registries
|
||||
auth:
|
||||
|
||||
+5
-2
@@ -87,9 +87,12 @@ This release process itself should be as automated as possible, and have only a
|
||||
|
||||
1. Tag the main branch with a full semantic-version, prefixed with a `v`. If there is a
|
||||
milestone with a partial version, the full version should be used for the git tag (e.g.
|
||||
with a Milestone of `v0.1` the tag should be `v0.1.0`).
|
||||
with a Milestone of `v0.1` the tag should be `v0.1.0`). You can determine the changes going
|
||||
into a release by running `make changelog-unreleased`. Use this change list to determine the
|
||||
release increment. After determining the release increment (major, minor, patch), create the tag.
|
||||
Given the above example the command to create the tag would be `git tag v0.1.0`.
|
||||
|
||||
1. Push the tag.
|
||||
1. Push the tag. Given the above example the command to push the tag would be `git push origin v0.1.0`.
|
||||
|
||||
1. A release admin must approve the release on the GitHub Actions release pipeline run page.
|
||||
Once approved, the release pipeline will generate all assets and draft a GitHub Release.
|
||||
|
||||
+2
-2
@@ -4,14 +4,13 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/anchore/stereoscope"
|
||||
"github.com/anchore/syft/internal/config"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/logger"
|
||||
"github.com/anchore/syft/syft"
|
||||
"github.com/gookit/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
)
|
||||
@@ -67,6 +66,7 @@ func initCmdAliasBindings() {
|
||||
}
|
||||
}
|
||||
|
||||
// nolint:forbidigo
|
||||
func initAppConfig() {
|
||||
cfg, err := config.LoadApplicationConfig(viper.GetViper(), persistentOpts)
|
||||
if err != nil {
|
||||
|
||||
+1
-2
@@ -8,7 +8,6 @@ import (
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/client"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -87,7 +86,7 @@ func dockerImageValidArgsFunction(cmd *cobra.Command, args []string, toComplete
|
||||
}
|
||||
|
||||
func listLocalDockerImages(prefix string) ([]string, error) {
|
||||
var repoTags = make([]string, 0)
|
||||
repoTags := make([]string, 0)
|
||||
ctx := context.Background()
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/ui"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
)
|
||||
|
||||
// eventLoop listens to worker errors (from execution path), worker events (from a partybus subscription), and
|
||||
// signal interrupts. Is responsible for handling each event relative to a given UI an to coordinate eventing until
|
||||
// an eventual graceful exit.
|
||||
// nolint:gocognit,funlen
|
||||
func eventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription *partybus.Subscription, cleanupFn func(), uxs ...ui.UI) error {
|
||||
defer cleanupFn()
|
||||
events := subscription.Events()
|
||||
var err error
|
||||
var ux ui.UI
|
||||
|
||||
if ux, err = setupUI(subscription.Unsubscribe, uxs...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var retErr error
|
||||
var forceTeardown bool
|
||||
|
||||
for {
|
||||
if workerErrs == nil && events == nil {
|
||||
break
|
||||
}
|
||||
select {
|
||||
case err, isOpen := <-workerErrs:
|
||||
if !isOpen {
|
||||
workerErrs = nil
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
// capture the error from the worker and unsubscribe to complete a graceful shutdown
|
||||
retErr = multierror.Append(retErr, err)
|
||||
if err := subscription.Unsubscribe(); err != nil {
|
||||
retErr = multierror.Append(retErr, err)
|
||||
}
|
||||
}
|
||||
case e, isOpen := <-events:
|
||||
if !isOpen {
|
||||
events = nil
|
||||
continue
|
||||
}
|
||||
|
||||
if err := ux.Handle(e); err != nil {
|
||||
if errors.Is(err, partybus.ErrUnsubscribe) {
|
||||
log.Warnf("unable to unsubscribe from the event bus")
|
||||
events = nil
|
||||
} else {
|
||||
retErr = multierror.Append(retErr, err)
|
||||
// TODO: should we unsubscribe? should we try to halt execution? or continue?
|
||||
}
|
||||
}
|
||||
case <-signals:
|
||||
// ignore further results from any event source and exit ASAP, but ensure that all cache is cleaned up.
|
||||
// we ignore further errors since cleaning up the tmp directories will affect running catalogers that are
|
||||
// reading/writing from/to their nested temp dirs. This is acceptable since we are bailing without result.
|
||||
|
||||
// TODO: potential future improvement would be to pass context into workers with a cancel function that is
|
||||
// to the event loop. In this way we can have a more controlled shutdown even at the most nested levels
|
||||
// of processing.
|
||||
events = nil
|
||||
workerErrs = nil
|
||||
forceTeardown = true
|
||||
}
|
||||
}
|
||||
|
||||
if err := ux.Teardown(forceTeardown); err != nil {
|
||||
retErr = multierror.Append(retErr, err)
|
||||
}
|
||||
|
||||
return retErr
|
||||
}
|
||||
|
||||
// setupUI takes one or more UIs that responds to events and takes a event bus unsubscribe function for use
|
||||
// during teardown. With the given UIs, the first UI which the ui.Setup() function does not return an error
|
||||
// will be utilized in execution. Providing a set of UIs allows for the caller to provide graceful fallbacks
|
||||
// when there are environmental problem (e.g. unable to setup a TUI with the current TTY).
|
||||
func setupUI(unsubscribe func() error, uis ...ui.UI) (ui.UI, error) {
|
||||
for _, ux := range uis {
|
||||
if err := ux.Setup(unsubscribe); err != nil {
|
||||
log.Warnf("unable to setup given UI, falling back to alternative UI: %+v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
return ux, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unable to setup any UI")
|
||||
}
|
||||
@@ -0,0 +1,455 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/anchore/syft/internal/ui"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
)
|
||||
|
||||
var _ ui.UI = (*uiMock)(nil)
|
||||
|
||||
type uiMock struct {
|
||||
t *testing.T
|
||||
finalEvent partybus.Event
|
||||
unsubscribe func() error
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (u *uiMock) Setup(unsubscribe func() error) error {
|
||||
u.t.Logf("UI Setup called")
|
||||
u.unsubscribe = unsubscribe
|
||||
return u.Called(unsubscribe).Error(0)
|
||||
}
|
||||
|
||||
func (u *uiMock) Handle(event partybus.Event) error {
|
||||
u.t.Logf("UI Handle called: %+v", event.Type)
|
||||
if event == u.finalEvent {
|
||||
assert.NoError(u.t, u.unsubscribe())
|
||||
}
|
||||
return u.Called(event).Error(0)
|
||||
}
|
||||
|
||||
func (u *uiMock) Teardown(_ bool) error {
|
||||
u.t.Logf("UI Teardown called")
|
||||
return u.Called().Error(0)
|
||||
}
|
||||
|
||||
func Test_eventLoop_gracefulExit(t *testing.T) {
|
||||
test := func(t *testing.T) {
|
||||
|
||||
testBus := partybus.NewBus()
|
||||
subscription := testBus.Subscribe()
|
||||
t.Cleanup(testBus.Close)
|
||||
|
||||
finalEvent := partybus.Event{
|
||||
Type: event.PresenterReady,
|
||||
}
|
||||
|
||||
worker := func() <-chan error {
|
||||
ret := make(chan error)
|
||||
go func() {
|
||||
t.Log("worker running")
|
||||
// send an empty item (which is ignored) ensuring we've entered the select statement,
|
||||
// then close (a partial shutdown).
|
||||
ret <- nil
|
||||
t.Log("worker sent nothing")
|
||||
close(ret)
|
||||
t.Log("worker closed")
|
||||
// do the other half of the shutdown
|
||||
testBus.Publish(finalEvent)
|
||||
t.Log("worker published final event")
|
||||
}()
|
||||
return ret
|
||||
}
|
||||
|
||||
signaler := func() <-chan os.Signal {
|
||||
return nil
|
||||
}
|
||||
|
||||
ux := &uiMock{
|
||||
t: t,
|
||||
finalEvent: finalEvent,
|
||||
}
|
||||
|
||||
// ensure the mock sees at least the final event
|
||||
ux.On("Handle", finalEvent).Return(nil)
|
||||
// ensure the mock sees basic setup/teardown events
|
||||
ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
|
||||
ux.On("Teardown").Return(nil)
|
||||
|
||||
var cleanupCalled bool
|
||||
cleanupFn := func() {
|
||||
t.Log("cleanup called")
|
||||
cleanupCalled = true
|
||||
}
|
||||
|
||||
assert.NoError(t,
|
||||
eventLoop(
|
||||
worker(),
|
||||
signaler(),
|
||||
subscription,
|
||||
cleanupFn,
|
||||
ux,
|
||||
),
|
||||
)
|
||||
|
||||
assert.True(t, cleanupCalled, "cleanup function not called")
|
||||
ux.AssertExpectations(t)
|
||||
}
|
||||
|
||||
// if there is a bug, then there is a risk of the event loop never returning
|
||||
testWithTimeout(t, 5*time.Second, test)
|
||||
}
|
||||
|
||||
func Test_eventLoop_workerError(t *testing.T) {
|
||||
test := func(t *testing.T) {
|
||||
|
||||
testBus := partybus.NewBus()
|
||||
subscription := testBus.Subscribe()
|
||||
t.Cleanup(testBus.Close)
|
||||
|
||||
workerErr := fmt.Errorf("worker error")
|
||||
|
||||
worker := func() <-chan error {
|
||||
ret := make(chan error)
|
||||
go func() {
|
||||
t.Log("worker running")
|
||||
// send an empty item (which is ignored) ensuring we've entered the select statement,
|
||||
// then close (a partial shutdown).
|
||||
ret <- nil
|
||||
t.Log("worker sent nothing")
|
||||
ret <- workerErr
|
||||
t.Log("worker sent error")
|
||||
close(ret)
|
||||
t.Log("worker closed")
|
||||
// note: NO final event is fired
|
||||
}()
|
||||
return ret
|
||||
}
|
||||
|
||||
signaler := func() <-chan os.Signal {
|
||||
return nil
|
||||
}
|
||||
|
||||
ux := &uiMock{
|
||||
t: t,
|
||||
}
|
||||
|
||||
// ensure the mock sees basic setup/teardown events
|
||||
ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
|
||||
ux.On("Teardown").Return(nil)
|
||||
|
||||
var cleanupCalled bool
|
||||
cleanupFn := func() {
|
||||
t.Log("cleanup called")
|
||||
cleanupCalled = true
|
||||
}
|
||||
|
||||
// ensure we see an error returned
|
||||
assert.ErrorIs(t,
|
||||
eventLoop(
|
||||
worker(),
|
||||
signaler(),
|
||||
subscription,
|
||||
cleanupFn,
|
||||
ux,
|
||||
),
|
||||
workerErr,
|
||||
"should have seen a worker error, but did not",
|
||||
)
|
||||
|
||||
assert.True(t, cleanupCalled, "cleanup function not called")
|
||||
ux.AssertExpectations(t)
|
||||
}
|
||||
|
||||
// if there is a bug, then there is a risk of the event loop never returning
|
||||
testWithTimeout(t, 5*time.Second, test)
|
||||
}
|
||||
|
||||
func Test_eventLoop_unsubscribeError(t *testing.T) {
|
||||
test := func(t *testing.T) {
|
||||
|
||||
testBus := partybus.NewBus()
|
||||
subscription := testBus.Subscribe()
|
||||
t.Cleanup(testBus.Close)
|
||||
|
||||
finalEvent := partybus.Event{
|
||||
Type: event.PresenterReady,
|
||||
}
|
||||
|
||||
worker := func() <-chan error {
|
||||
ret := make(chan error)
|
||||
go func() {
|
||||
t.Log("worker running")
|
||||
// send an empty item (which is ignored) ensuring we've entered the select statement,
|
||||
// then close (a partial shutdown).
|
||||
ret <- nil
|
||||
t.Log("worker sent nothing")
|
||||
close(ret)
|
||||
t.Log("worker closed")
|
||||
// do the other half of the shutdown
|
||||
testBus.Publish(finalEvent)
|
||||
t.Log("worker published final event")
|
||||
}()
|
||||
return ret
|
||||
}
|
||||
|
||||
signaler := func() <-chan os.Signal {
|
||||
return nil
|
||||
}
|
||||
|
||||
ux := &uiMock{
|
||||
t: t,
|
||||
finalEvent: finalEvent,
|
||||
}
|
||||
|
||||
// ensure the mock sees at least the final event... note the unsubscribe error here
|
||||
ux.On("Handle", finalEvent).Return(partybus.ErrUnsubscribe)
|
||||
// ensure the mock sees basic setup/teardown events
|
||||
ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
|
||||
ux.On("Teardown").Return(nil)
|
||||
|
||||
var cleanupCalled bool
|
||||
cleanupFn := func() {
|
||||
t.Log("cleanup called")
|
||||
cleanupCalled = true
|
||||
}
|
||||
|
||||
// unsubscribe errors should be handled and ignored, not propagated. We are additionally asserting that
|
||||
// this case is handled as a controlled shutdown (this test should not timeout)
|
||||
assert.NoError(t,
|
||||
eventLoop(
|
||||
worker(),
|
||||
signaler(),
|
||||
subscription,
|
||||
cleanupFn,
|
||||
ux,
|
||||
),
|
||||
)
|
||||
|
||||
assert.True(t, cleanupCalled, "cleanup function not called")
|
||||
ux.AssertExpectations(t)
|
||||
}
|
||||
|
||||
// if there is a bug, then there is a risk of the event loop never returning
|
||||
testWithTimeout(t, 5*time.Second, test)
|
||||
}
|
||||
|
||||
func Test_eventLoop_handlerError(t *testing.T) {
|
||||
test := func(t *testing.T) {
|
||||
|
||||
testBus := partybus.NewBus()
|
||||
subscription := testBus.Subscribe()
|
||||
t.Cleanup(testBus.Close)
|
||||
|
||||
finalEvent := partybus.Event{
|
||||
Type: event.PresenterReady,
|
||||
Error: fmt.Errorf("unable to create presenter"),
|
||||
}
|
||||
|
||||
worker := func() <-chan error {
|
||||
ret := make(chan error)
|
||||
go func() {
|
||||
t.Log("worker running")
|
||||
// send an empty item (which is ignored) ensuring we've entered the select statement,
|
||||
// then close (a partial shutdown).
|
||||
ret <- nil
|
||||
t.Log("worker sent nothing")
|
||||
close(ret)
|
||||
t.Log("worker closed")
|
||||
// do the other half of the shutdown
|
||||
testBus.Publish(finalEvent)
|
||||
t.Log("worker published final event")
|
||||
}()
|
||||
return ret
|
||||
}
|
||||
|
||||
signaler := func() <-chan os.Signal {
|
||||
return nil
|
||||
}
|
||||
|
||||
ux := &uiMock{
|
||||
t: t,
|
||||
finalEvent: finalEvent,
|
||||
}
|
||||
|
||||
// ensure the mock sees at least the final event... note the event error is propagated
|
||||
ux.On("Handle", finalEvent).Return(finalEvent.Error)
|
||||
// ensure the mock sees basic setup/teardown events
|
||||
ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
|
||||
ux.On("Teardown").Return(nil)
|
||||
|
||||
var cleanupCalled bool
|
||||
cleanupFn := func() {
|
||||
t.Log("cleanup called")
|
||||
cleanupCalled = true
|
||||
}
|
||||
|
||||
// handle errors SHOULD propagate the event loop. We are additionally asserting that this case is
|
||||
// handled as a controlled shutdown (this test should not timeout)
|
||||
assert.ErrorIs(t,
|
||||
eventLoop(
|
||||
worker(),
|
||||
signaler(),
|
||||
subscription,
|
||||
cleanupFn,
|
||||
ux,
|
||||
),
|
||||
finalEvent.Error,
|
||||
"should have seen a event error, but did not",
|
||||
)
|
||||
|
||||
assert.True(t, cleanupCalled, "cleanup function not called")
|
||||
ux.AssertExpectations(t)
|
||||
}
|
||||
|
||||
// if there is a bug, then there is a risk of the event loop never returning
|
||||
testWithTimeout(t, 5*time.Second, test)
|
||||
}
|
||||
|
||||
func Test_eventLoop_signalsStopExecution(t *testing.T) {
|
||||
test := func(t *testing.T) {
|
||||
|
||||
testBus := partybus.NewBus()
|
||||
subscription := testBus.Subscribe()
|
||||
t.Cleanup(testBus.Close)
|
||||
|
||||
worker := func() <-chan error {
|
||||
// the worker will never return work and the event loop will always be waiting...
|
||||
return make(chan error)
|
||||
}
|
||||
|
||||
signaler := func() <-chan os.Signal {
|
||||
ret := make(chan os.Signal)
|
||||
go func() {
|
||||
ret <- syscall.SIGINT
|
||||
// note: we do NOT close the channel to ensure the event loop does not depend on that behavior to exit
|
||||
}()
|
||||
return ret
|
||||
}
|
||||
|
||||
ux := &uiMock{
|
||||
t: t,
|
||||
}
|
||||
|
||||
// ensure the mock sees basic setup/teardown events
|
||||
ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
|
||||
ux.On("Teardown").Return(nil)
|
||||
|
||||
var cleanupCalled bool
|
||||
cleanupFn := func() {
|
||||
t.Log("cleanup called")
|
||||
cleanupCalled = true
|
||||
}
|
||||
|
||||
assert.NoError(t,
|
||||
eventLoop(
|
||||
worker(),
|
||||
signaler(),
|
||||
subscription,
|
||||
cleanupFn,
|
||||
ux,
|
||||
),
|
||||
)
|
||||
|
||||
assert.True(t, cleanupCalled, "cleanup function not called")
|
||||
ux.AssertExpectations(t)
|
||||
}
|
||||
|
||||
// if there is a bug, then there is a risk of the event loop never returning
|
||||
testWithTimeout(t, 5*time.Second, test)
|
||||
}
|
||||
|
||||
func Test_eventLoop_uiTeardownError(t *testing.T) {
|
||||
test := func(t *testing.T) {
|
||||
|
||||
testBus := partybus.NewBus()
|
||||
subscription := testBus.Subscribe()
|
||||
t.Cleanup(testBus.Close)
|
||||
|
||||
finalEvent := partybus.Event{
|
||||
Type: event.PresenterReady,
|
||||
}
|
||||
|
||||
worker := func() <-chan error {
|
||||
ret := make(chan error)
|
||||
go func() {
|
||||
t.Log("worker running")
|
||||
// send an empty item (which is ignored) ensuring we've entered the select statement,
|
||||
// then close (a partial shutdown).
|
||||
ret <- nil
|
||||
t.Log("worker sent nothing")
|
||||
close(ret)
|
||||
t.Log("worker closed")
|
||||
// do the other half of the shutdown
|
||||
testBus.Publish(finalEvent)
|
||||
t.Log("worker published final event")
|
||||
}()
|
||||
return ret
|
||||
}
|
||||
|
||||
signaler := func() <-chan os.Signal {
|
||||
return nil
|
||||
}
|
||||
|
||||
ux := &uiMock{
|
||||
t: t,
|
||||
finalEvent: finalEvent,
|
||||
}
|
||||
|
||||
teardownError := fmt.Errorf("sorry, dave, the UI doesn't want to be torn down")
|
||||
|
||||
// ensure the mock sees at least the final event... note the event error is propagated
|
||||
ux.On("Handle", finalEvent).Return(nil)
|
||||
// ensure the mock sees basic setup/teardown events
|
||||
ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
|
||||
ux.On("Teardown").Return(teardownError)
|
||||
|
||||
var cleanupCalled bool
|
||||
cleanupFn := func() {
|
||||
t.Log("cleanup called")
|
||||
cleanupCalled = true
|
||||
}
|
||||
|
||||
// ensure we see an error returned
|
||||
assert.ErrorIs(t,
|
||||
eventLoop(
|
||||
worker(),
|
||||
signaler(),
|
||||
subscription,
|
||||
cleanupFn,
|
||||
ux,
|
||||
),
|
||||
teardownError,
|
||||
"should have seen a UI teardown error, but did not",
|
||||
)
|
||||
|
||||
assert.True(t, cleanupCalled, "cleanup function not called")
|
||||
ux.AssertExpectations(t)
|
||||
}
|
||||
|
||||
// if there is a bug, then there is a risk of the event loop never returning
|
||||
testWithTimeout(t, 5*time.Second, test)
|
||||
}
|
||||
|
||||
func testWithTimeout(t *testing.T, timeout time.Duration, test func(*testing.T)) {
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
test(t)
|
||||
done <- true
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-time.After(timeout):
|
||||
t.Fatal("test timed out")
|
||||
case <-done:
|
||||
}
|
||||
}
|
||||
+48
-20
@@ -6,34 +6,36 @@ import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/anchore/stereoscope"
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/anchore"
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/presenter/packages"
|
||||
"github.com/anchore/syft/internal/ui"
|
||||
"github.com/anchore/syft/syft"
|
||||
"github.com/anchore/syft/syft/distro"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/presenter/packages"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/pkg/profile"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
)
|
||||
|
||||
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 SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -o cyclonedx show a CycloneDX formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -o spdx show a SPDX 2.2 tag-value formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -o spdx-json show a SPDX 2.2 JSON formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -vv show verbose debug information
|
||||
|
||||
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, or generic filesystem directory
|
||||
{{.appName}} {{.command}} path/to/a/file/or/dir a Docker tar, OCI tar, OCI directory, or generic filesystem directory
|
||||
|
||||
You can also explicitly specify the scheme to use:
|
||||
{{.appName}} {{.command}} docker:yourrepo/yourimage:tag explicitly use the Docker daemon
|
||||
@@ -47,7 +49,7 @@ const (
|
||||
|
||||
var (
|
||||
packagesPresenterOpt packages.PresenterOption
|
||||
packagesArgs = cobra.MinimumNArgs(1)
|
||||
packagesArgs = cobra.MaximumNArgs(1)
|
||||
packagesCmd = &cobra.Command{
|
||||
Use: "packages [SOURCE]",
|
||||
Short: "Generate a package SBOM",
|
||||
@@ -65,8 +67,7 @@ var (
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// silently exit
|
||||
return fmt.Errorf("")
|
||||
return fmt.Errorf("an image/directory argument is required")
|
||||
}
|
||||
|
||||
// set the presenter
|
||||
@@ -101,7 +102,7 @@ func init() {
|
||||
}
|
||||
|
||||
func setPackageFlags(flags *pflag.FlagSet) {
|
||||
///////// Formatting & Input options //////////////////////////////////////////////
|
||||
// Formatting & Input options //////////////////////////////////////////////
|
||||
|
||||
flags.StringP(
|
||||
"scope", "s", source.SquashedScope.String(),
|
||||
@@ -112,7 +113,12 @@ func setPackageFlags(flags *pflag.FlagSet) {
|
||||
fmt.Sprintf("report output formatter, options=%v", packages.AllPresenters),
|
||||
)
|
||||
|
||||
///////// Upload options //////////////////////////////////////////////////////////
|
||||
flags.StringP(
|
||||
"file", "", "",
|
||||
"file to write the report output to (default is STDOUT)",
|
||||
)
|
||||
|
||||
// Upload options //////////////////////////////////////////////////////////
|
||||
flags.StringP(
|
||||
"host", "H", "",
|
||||
"the hostname or URL of the Anchore Enterprise instance to upload to",
|
||||
@@ -145,7 +151,7 @@ func setPackageFlags(flags *pflag.FlagSet) {
|
||||
}
|
||||
|
||||
func bindPackagesConfigOptions(flags *pflag.FlagSet) error {
|
||||
///////// Formatting & Input options //////////////////////////////////////////////
|
||||
// Formatting & Input options //////////////////////////////////////////////
|
||||
|
||||
if err := viper.BindPFlag("package.cataloger.scope", flags.Lookup("scope")); err != nil {
|
||||
return err
|
||||
@@ -155,7 +161,11 @@ func bindPackagesConfigOptions(flags *pflag.FlagSet) error {
|
||||
return err
|
||||
}
|
||||
|
||||
///////// Upload options //////////////////////////////////////////////////////////
|
||||
if err := viper.BindPFlag("file", flags.Lookup("file")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Upload options //////////////////////////////////////////////////////////
|
||||
|
||||
if err := viper.BindPFlag("anchore.host", flags.Lookup("host")); err != nil {
|
||||
return err
|
||||
@@ -185,9 +195,27 @@ func bindPackagesConfigOptions(flags *pflag.FlagSet) error {
|
||||
}
|
||||
|
||||
func packagesExec(_ *cobra.Command, args []string) error {
|
||||
errs := packagesExecWorker(args[0])
|
||||
ux := ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet)
|
||||
return ux(errs, eventSubscription)
|
||||
// could be an image or a directory, with or without a scheme
|
||||
userInput := args[0]
|
||||
|
||||
reporter, closer, err := reportWriter()
|
||||
defer func() {
|
||||
if err := closer(); err != nil {
|
||||
log.Warnf("unable to write to report destination: %+v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return eventLoop(
|
||||
packagesExecWorker(userInput),
|
||||
setupSignals(),
|
||||
eventSubscription,
|
||||
stereoscope.Cleanup,
|
||||
ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet, reporter)...,
|
||||
)
|
||||
}
|
||||
|
||||
func packagesExecWorker(userInput string) <-chan error {
|
||||
@@ -199,14 +227,14 @@ func packagesExecWorker(userInput string) <-chan error {
|
||||
|
||||
src, cleanup, err := source.New(userInput, appConfig.Registry.ToOptions())
|
||||
if err != nil {
|
||||
errs <- fmt.Errorf("failed to determine image source: %+v", err)
|
||||
errs <- fmt.Errorf("failed to determine image source: %w", err)
|
||||
return
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
catalog, d, err := syft.CatalogPackages(src, appConfig.Package.Cataloger.ScopeOpt)
|
||||
if err != nil {
|
||||
errs <- fmt.Errorf("failed to catalog input: %+v", err)
|
||||
errs <- fmt.Errorf("failed to catalog input: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -230,7 +258,7 @@ func packagesExecWorker(userInput string) <-chan error {
|
||||
return errs
|
||||
}
|
||||
|
||||
func runPackageSbomUpload(src source.Source, s source.Metadata, catalog *pkg.Catalog, d *distro.Distro, scope source.Scope) error {
|
||||
func runPackageSbomUpload(src *source.Source, s source.Metadata, catalog *pkg.Catalog, d *distro.Distro, scope source.Scope) error {
|
||||
log.Infof("uploading results to %s", appConfig.Anchore.Host)
|
||||
|
||||
if src.Metadata.Scheme != source.ImageScheme {
|
||||
@@ -260,7 +288,7 @@ func runPackageSbomUpload(src source.Source, s source.Metadata, catalog *pkg.Cat
|
||||
Password: appConfig.Anchore.Password,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create anchore client: %+v", err)
|
||||
return fmt.Errorf("failed to create anchore client: %w", err)
|
||||
}
|
||||
|
||||
importCfg := anchore.ImportConfig{
|
||||
@@ -275,7 +303,7 @@ func runPackageSbomUpload(src source.Source, s source.Metadata, catalog *pkg.Cat
|
||||
}
|
||||
|
||||
if err := c.Import(context.Background(), importCfg); err != nil {
|
||||
return fmt.Errorf("failed to upload results to host=%s: %+v", appConfig.Anchore.Host, err)
|
||||
return fmt.Errorf("failed to upload results to host=%s: %w", appConfig.Anchore.Host, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
+32
-10
@@ -4,9 +4,10 @@ import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/anchore/stereoscope"
|
||||
"github.com/anchore/syft/internal"
|
||||
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/presenter/poweruser"
|
||||
"github.com/anchore/syft/internal/ui"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
@@ -34,11 +35,19 @@ var powerUserCmd = &cobra.Command{
|
||||
"appName": internal.ApplicationName,
|
||||
"command": "power-user",
|
||||
}),
|
||||
Args: cobra.ExactArgs(1),
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Hidden: true,
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) == 0 {
|
||||
err := cmd.Help()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("an image/directory argument is required")
|
||||
}
|
||||
|
||||
if appConfig.Dev.ProfileCPU && appConfig.Dev.ProfileMem {
|
||||
return fmt.Errorf("cannot profile CPU and memory simultaneously")
|
||||
}
|
||||
@@ -63,9 +72,27 @@ func init() {
|
||||
}
|
||||
|
||||
func powerUserExec(_ *cobra.Command, args []string) error {
|
||||
errs := powerUserExecWorker(args[0])
|
||||
ux := ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet)
|
||||
return ux(errs, eventSubscription)
|
||||
// could be an image or a directory, with or without a scheme
|
||||
userInput := args[0]
|
||||
|
||||
reporter, closer, err := reportWriter()
|
||||
defer func() {
|
||||
if err := closer(); err != nil {
|
||||
log.Warnf("unable to write to report destination: %+v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return eventLoop(
|
||||
powerUserExecWorker(userInput),
|
||||
setupSignals(),
|
||||
eventSubscription,
|
||||
stereoscope.Cleanup,
|
||||
ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet, reporter)...,
|
||||
)
|
||||
}
|
||||
|
||||
func powerUserExecWorker(userInput string) <-chan error {
|
||||
@@ -88,11 +115,6 @@ func powerUserExecWorker(userInput string) <-chan error {
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
if src.Metadata.Scheme != source.ImageScheme {
|
||||
errs <- fmt.Errorf("the power-user subcommand only allows for 'image' schemes, given %q", src.Metadata.Scheme)
|
||||
return
|
||||
}
|
||||
|
||||
analysisResults := poweruser.JSONDocumentConfig{
|
||||
SourceMetadata: src.Metadata,
|
||||
ApplicationConfig: *appConfig,
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
type powerUserTask func(*poweruser.JSONDocumentConfig, source.Source) error
|
||||
type powerUserTask func(*poweruser.JSONDocumentConfig, *source.Source) error
|
||||
|
||||
func powerUserTasks() ([]powerUserTask, error) {
|
||||
var tasks []powerUserTask
|
||||
@@ -42,7 +42,7 @@ func catalogPackagesTask() (powerUserTask, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
task := func(results *poweruser.JSONDocumentConfig, src source.Source) error {
|
||||
task := func(results *poweruser.JSONDocumentConfig, src *source.Source) error {
|
||||
packageCatalog, theDistro, err := syft.CatalogPackages(src, appConfig.Package.Cataloger.ScopeOpt)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -64,7 +64,7 @@ func catalogFileMetadataTask() (powerUserTask, error) {
|
||||
|
||||
metadataCataloger := file.NewMetadataCataloger()
|
||||
|
||||
task := func(results *poweruser.JSONDocumentConfig, src source.Source) error {
|
||||
task := func(results *poweruser.JSONDocumentConfig, src *source.Source) error {
|
||||
resolver, err := src.FileResolver(appConfig.FileMetadata.Cataloger.ScopeOpt)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -110,7 +110,7 @@ func catalogFileDigestsTask() (powerUserTask, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
task := func(results *poweruser.JSONDocumentConfig, src source.Source) error {
|
||||
task := func(results *poweruser.JSONDocumentConfig, src *source.Source) error {
|
||||
resolver, err := src.FileResolver(appConfig.FileMetadata.Cataloger.ScopeOpt)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -142,7 +142,7 @@ func catalogSecretsTask() (powerUserTask, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
task := func(results *poweruser.JSONDocumentConfig, src source.Source) error {
|
||||
task := func(results *poweruser.JSONDocumentConfig, src *source.Source) error {
|
||||
resolver, err := src.FileResolver(appConfig.Secrets.Cataloger.ScopeOpt)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -170,7 +170,7 @@ func catalogFileClassificationsTask() (powerUserTask, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
task := func(results *poweruser.JSONDocumentConfig, src source.Source) error {
|
||||
task := func(results *poweruser.JSONDocumentConfig, src *source.Source) error {
|
||||
resolver, err := src.FileResolver(appConfig.FileClassification.Cataloger.ScopeOpt)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -197,7 +197,7 @@ func catalogContentsTask() (powerUserTask, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
task := func(results *poweruser.JSONDocumentConfig, src source.Source) error {
|
||||
task := func(results *poweruser.JSONDocumentConfig, src *source.Source) error {
|
||||
resolver, err := src.FileResolver(appConfig.FileContents.Cataloger.ScopeOpt)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
)
|
||||
|
||||
func reportWriter() (io.Writer, func() error, error) {
|
||||
nop := func() error { return nil }
|
||||
|
||||
path := strings.TrimSpace(appConfig.File)
|
||||
switch len(path) {
|
||||
case 0:
|
||||
return os.Stdout, nop, nil
|
||||
default:
|
||||
reportFile, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o644)
|
||||
if err != nil {
|
||||
return nil, nop, fmt.Errorf("unable to create report file: %w", err)
|
||||
}
|
||||
return reportFile, func() error {
|
||||
log.Infof("report written to file=%q", path)
|
||||
return reportFile.Close()
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ var rootCmd = &cobra.Command{
|
||||
ValidArgsFunction: packagesCmd.ValidArgsFunction,
|
||||
}
|
||||
|
||||
// nolint:forbidigo
|
||||
func init() {
|
||||
// set universal flags
|
||||
rootCmd.PersistentFlags().StringVarP(&persistentOpts.ConfigPath, "config", "c", "", "application config file")
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func setupSignals() <-chan os.Signal {
|
||||
c := make(chan os.Signal, 1) // Note: A buffered channel is recommended for this; see https://golang.org/pkg/os/signal/#Notify
|
||||
|
||||
interruptions := []os.Signal{
|
||||
syscall.SIGINT,
|
||||
syscall.SIGTERM,
|
||||
}
|
||||
|
||||
signal.Notify(c, interruptions...)
|
||||
|
||||
return c
|
||||
}
|
||||
+1
-1
@@ -6,7 +6,6 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
|
||||
"github.com/anchore/syft/internal/version"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -24,6 +23,7 @@ func init() {
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
}
|
||||
|
||||
// nolint:forbidigo
|
||||
func printVersion(_ *cobra.Command, _ []string) {
|
||||
versionInfo := version.FromBuild()
|
||||
|
||||
|
||||
@@ -7,10 +7,11 @@ require (
|
||||
github.com/adrg/xdg v0.2.1
|
||||
github.com/alecthomas/jsonschema v0.0.0-20210301060011-54c507b6f074
|
||||
github.com/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf
|
||||
github.com/anchore/go-rpmdb v0.0.0-20210415132930-2460011e83c6
|
||||
github.com/anchore/go-rpmdb v0.0.0-20210914181456-a9c52348da63
|
||||
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
|
||||
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
|
||||
github.com/anchore/stereoscope v0.0.0-20210524175238-3b7662f3a66f
|
||||
github.com/anchore/packageurl-go v0.0.0-20210922164639-b3fa992ebd29
|
||||
github.com/anchore/stereoscope v0.0.0-20211005213828-538011008578
|
||||
github.com/antihax/optional v1.0.0
|
||||
github.com/bmatcuk/doublestar/v2 v2.0.4
|
||||
github.com/docker/docker v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible
|
||||
@@ -24,23 +25,25 @@ require (
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/mitchellh/mapstructure v1.3.1
|
||||
github.com/olekukonko/tablewriter v0.0.4
|
||||
github.com/package-url/packageurl-go v0.1.0
|
||||
github.com/pelletier/go-toml v1.8.0
|
||||
github.com/pkg/profile v1.5.0
|
||||
github.com/scylladb/go-set v1.0.2
|
||||
github.com/sergi/go-diff v1.1.0
|
||||
github.com/sirupsen/logrus v1.6.0
|
||||
github.com/spdx/tools-golang v0.1.0
|
||||
github.com/spf13/afero v1.2.2
|
||||
github.com/spf13/cobra v1.0.1-0.20200909172742-8a63648dd905
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/spf13/viper v1.7.0
|
||||
github.com/stretchr/testify v1.6.0
|
||||
github.com/wagoodman/go-partybus v0.0.0-20200526224238-eb215533f07d
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/vifraa/gopom v0.1.0
|
||||
github.com/wagoodman/go-partybus v0.0.0-20210627031916-db1f5573bbc5
|
||||
github.com/wagoodman/go-progress v0.0.0-20200731105512-1020f39e6240
|
||||
github.com/wagoodman/jotframe v0.0.0-20200730190914-3517092dd163
|
||||
github.com/x-cray/logrus-prefixed-formatter v0.5.2
|
||||
github.com/xeipuuv/gojsonschema v1.2.0
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
|
||||
golang.org/x/mod v0.3.0
|
||||
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d
|
||||
gopkg.in/yaml.v2 v2.3.0
|
||||
)
|
||||
|
||||
@@ -109,21 +109,20 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf h1:DYssiUV1pBmKqzKsm4mqXx8artqC0Q8HgZsVI3lMsAg=
|
||||
github.com/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf/go.mod h1:FaODhIA06mxO1E6R32JE0TL1JWZZkmjRIAd4ULvHUKk=
|
||||
github.com/anchore/go-rpmdb v0.0.0-20210415132930-2460011e83c6 h1:wEN3HXc3VuC4wo7Cz27YCpeQ4gaB5ASKwMwM5GdFsew=
|
||||
github.com/anchore/go-rpmdb v0.0.0-20210415132930-2460011e83c6/go.mod h1:8jNYOxCJC5kyD/Ct4MbzsDN2hOhRoCAzQcb/7KdYYGw=
|
||||
github.com/anchore/go-rpmdb v0.0.0-20210914181456-a9c52348da63 h1:C9W/LAydEz/qdUhx1MdjO9l8NEcFKYknkxDVyo9LAoM=
|
||||
github.com/anchore/go-rpmdb v0.0.0-20210914181456-a9c52348da63/go.mod h1:6qH8c6U/3CBVvDDDBZnPSTbTINq3cIdADUYTaVf75EM=
|
||||
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 h1:VzprUTpc0vW0nnNKJfJieyH/TZ9UYAnTZs5/gHTdAe8=
|
||||
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04/go.mod h1:6dK64g27Qi1qGQZ67gFmBFvEHScy0/C8qhQhNe5B5pQ=
|
||||
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZVsCYMrIZBpFxwV26CbsuoEh5muXD5I1Ods=
|
||||
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E=
|
||||
github.com/anchore/stereoscope v0.0.0-20210413221244-d577f30b19e6 h1:g9ZS2V/T0wxseccI4t1hQTqWBek5DVOQZOzzdWBjwnU=
|
||||
github.com/anchore/stereoscope v0.0.0-20210413221244-d577f30b19e6/go.mod h1:vhh1M99rfWx5ejMvz1lkQiFZUrC5wu32V12R4JXH+ZI=
|
||||
github.com/anchore/stereoscope v0.0.0-20210524175238-3b7662f3a66f h1:bFadyOLOkzME3BrZFZ5m8cf/b2hsn3aMSS9s+SKubRk=
|
||||
github.com/anchore/stereoscope v0.0.0-20210524175238-3b7662f3a66f/go.mod h1:vhh1M99rfWx5ejMvz1lkQiFZUrC5wu32V12R4JXH+ZI=
|
||||
github.com/anchore/packageurl-go v0.0.0-20210922164639-b3fa992ebd29 h1:K9LfnxwhqvihqU0+MF325FNy7fsKV9EGaUxdfR4gnWk=
|
||||
github.com/anchore/packageurl-go v0.0.0-20210922164639-b3fa992ebd29/go.mod h1:Oc1UkGaJwY6ND6vtAqPSlYrptKRJngHwkwB6W7l1uP0=
|
||||
github.com/anchore/stereoscope v0.0.0-20211005213828-538011008578 h1:gSpftl0RWfdTwlPmsOLgEawHIw16xwwG1mFD/wrVDRE=
|
||||
github.com/anchore/stereoscope v0.0.0-20211005213828-538011008578/go.mod h1:kL7jfbXblrDcBhu5ja/s+VTYL3Mzof4eQNMJiSqcwXQ=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/apex/log v1.1.4/go.mod h1:AlpoD9aScyQfJDVHmLMEcx4oU6LqzkWp4Mg9GdAcEvQ=
|
||||
github.com/apex/log v1.3.0 h1:1fyfbPvUwD10nMoh3hY6MXzvZShJQn9/ck7ATgAt5pA=
|
||||
github.com/apex/log v1.3.0/go.mod h1:jd8Vpsr46WAe3EZSQ/IUMs2qQD/GOycT5rPWCO1yGcs=
|
||||
github.com/apex/logs v0.0.4/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo=
|
||||
github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE=
|
||||
@@ -243,6 +242,8 @@ github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHqu
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/gabriel-vasile/mimetype v1.3.0 h1:4YOHITFLyYwF+iqG0ybSLGArRItynpfwdlWRmJnd75E=
|
||||
github.com/gabriel-vasile/mimetype v1.3.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8=
|
||||
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-critic/go-critic v0.4.1/go.mod h1:7/14rZGnZbY6E38VEGk2kVhoq6itzc1E68facVDK23g=
|
||||
@@ -268,8 +269,8 @@ github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8
|
||||
github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
|
||||
github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-restruct/restruct v0.0.0-20191227155143-5734170a48a1 h1:LoN2wx/aN8JPGebG+2DaUyk4M+xRcqJXfuIbs8AWHdE=
|
||||
github.com/go-restruct/restruct v0.0.0-20191227155143-5734170a48a1/go.mod h1:KqrpKpn4M8OLznErihXTGLlsXFGeLxHUrLRRI/1YjGk=
|
||||
github.com/go-restruct/restruct v1.2.0-alpha h1:2Lp474S/9660+SJjpVxoKuWX09JsXHSrdV7Nv3/gkvc=
|
||||
github.com/go-restruct/restruct v1.2.0-alpha/go.mod h1:KqrpKpn4M8OLznErihXTGLlsXFGeLxHUrLRRI/1YjGk=
|
||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
@@ -591,8 +592,6 @@ github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5X
|
||||
github.com/opencontainers/runc v0.1.1 h1:GlxAyO6x8rfZYN9Tt0Kti5a/cP41iuiO2yYT0IJGY8Y=
|
||||
github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
|
||||
github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/package-url/packageurl-go v0.1.0 h1:efWBc98O/dBZRg1pw2xiDzovnlMjCa9NPnfaiBduh8I=
|
||||
github.com/package-url/packageurl-go v0.1.0/go.mod h1:C/ApiuWpmbpni4DIOECf6WCjFUZV7O1Fx7VAzrZHgBw=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
@@ -674,6 +673,9 @@ github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k
|
||||
github.com/sourcegraph/go-diff v0.5.1/go.mod h1:j2dHj3m8aZgQO8lMTcTnBcXkRRRqi34cd2MNlA9u1mE=
|
||||
github.com/sourcegraph/go-diff v0.5.3/go.mod h1:v9JDtjCE4HHHCZGId75rg8gkKKa98RVjBcBGsVmMmak=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spdx/gordf v0.0.0-20201111095634-7098f93598fb/go.mod h1:uKWaldnbMnjsSAXRurWqqrdyZen1R7kxl8TkmWk2OyM=
|
||||
github.com/spdx/tools-golang v0.1.0 h1:iDMNEPqQk6CdiDj6eWDIDw85j0wQ3IR3pH9p0X05TSQ=
|
||||
github.com/spdx/tools-golang v0.1.0/go.mod h1:RO4Y3IFROJnz+43JKm1YOrbtgQNljW4gAPpA/sY2eqo=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
|
||||
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||
@@ -702,6 +704,7 @@ github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM=
|
||||
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
@@ -709,8 +712,10 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
|
||||
github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.0 h1:jlIyCplCJFULU/01vCkhKuTyc3OorI3bJFuw6obfgho=
|
||||
github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/tdakkota/asciicheck v0.0.0-20200416190851-d7f85be797a2/go.mod h1:yHp0ai0Z9gUljN3o0xMhYJnH/IcvkdTBOX2fmJ93JEM=
|
||||
@@ -719,7 +724,6 @@ github.com/tetafro/godot v0.3.7/go.mod h1:/7NLHhv08H1+8DNj0MElpAACw1ajsCuf3TKNQx
|
||||
github.com/tetafro/godot v0.4.2/go.mod h1:/7NLHhv08H1+8DNj0MElpAACw1ajsCuf3TKNQxA5S+0=
|
||||
github.com/timakin/bodyclose v0.0.0-20190930140734-f7f2e9bca95e/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
|
||||
github.com/timakin/bodyclose v0.0.0-20200424151742-cb6215831a94/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
|
||||
github.com/tj/assert v0.0.0-20171129193455-018094318fb0 h1:Rw8kxzWo1mr6FSaYXjQELRe88y2KdfynXdnK72rdjtA=
|
||||
github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0=
|
||||
github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0=
|
||||
github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao=
|
||||
@@ -742,9 +746,12 @@ github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk
|
||||
github.com/valyala/quicktemplate v1.2.0/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4=
|
||||
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
|
||||
github.com/vdemeester/k8s-pkg-credentialprovider v1.17.4/go.mod h1:inCTmtUdr5KJbreVojo06krnTgaeAz/Z7lynpPk/Q2c=
|
||||
github.com/vifraa/gopom v0.1.0 h1:v897eVxf6lflkEXzPmKbo4YhX2oS/LGjz7cqjWnSmCU=
|
||||
github.com/vifraa/gopom v0.1.0/go.mod h1:oPa1dcrGrtlO37WPDBm5SqHAT+wTgF8An1Q71Z6Vv4o=
|
||||
github.com/vmware/govmomi v0.20.3/go.mod h1:URlwyTFZX72RmxtxuaFL2Uj3fD1JTvZdx59bHWk6aFU=
|
||||
github.com/wagoodman/go-partybus v0.0.0-20200526224238-eb215533f07d h1:KOxOL6qpmqwoPloNwi+CEgc1ayjHNOFNrvoOmeDOjDg=
|
||||
github.com/wagoodman/go-partybus v0.0.0-20200526224238-eb215533f07d/go.mod h1:JPirS5jde/CF5qIjcK4WX+eQmKXdPc6vcZkJ/P0hfPw=
|
||||
github.com/wagoodman/go-partybus v0.0.0-20210627031916-db1f5573bbc5 h1:phTLPgMRDYTizrBSKsNSOa2zthoC2KsJsaY/8sg3rD8=
|
||||
github.com/wagoodman/go-partybus v0.0.0-20210627031916-db1f5573bbc5/go.mod h1:JPirS5jde/CF5qIjcK4WX+eQmKXdPc6vcZkJ/P0hfPw=
|
||||
github.com/wagoodman/go-progress v0.0.0-20200621122631-1a2120f0695a/go.mod h1:jLXFoL31zFaHKAAyZUh+sxiTDFe1L1ZHrcK2T1itVKA=
|
||||
github.com/wagoodman/go-progress v0.0.0-20200731105512-1020f39e6240 h1:r6BlIP7CVZtMlxUQhT40h1IE1TzEgKVqwmsVGuscvdk=
|
||||
github.com/wagoodman/go-progress v0.0.0-20200731105512-1020f39e6240/go.mod h1:jLXFoL31zFaHKAAyZUh+sxiTDFe1L1ZHrcK2T1itVKA=
|
||||
@@ -867,8 +874,9 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM=
|
||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d h1:LO7XpTYMwTqxjLcGWPijK3vRXg1aWdlNOVOHRq45d7c=
|
||||
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -933,14 +941,17 @@ golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 h1:OjiUf46hAmXblsZdnoSXsEUSKU8r1UEzcL5RVZ4gO9Y=
|
||||
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
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=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
|
||||
@@ -6,8 +6,6 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/antihax/optional"
|
||||
|
||||
"github.com/anchore/client-go/pkg/external"
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
@@ -15,6 +13,7 @@ import (
|
||||
"github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/antihax/optional"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
)
|
||||
@@ -64,7 +63,7 @@ func (c *Client) Import(ctx context.Context, cfg ImportConfig) error {
|
||||
stage.Current = "starting session"
|
||||
startOperation, _, err := c.client.ImportsApi.CreateOperation(authedCtx)
|
||||
if err != nil {
|
||||
var detail = "no details given"
|
||||
detail := "no details given"
|
||||
var openAPIErr external.GenericOpenAPIError
|
||||
if errors.As(err, &openAPIErr) {
|
||||
detail = string(openAPIErr.Body())
|
||||
@@ -106,7 +105,7 @@ func (c *Client) Import(ctx context.Context, cfg ImportConfig) error {
|
||||
|
||||
_, _, err = c.client.ImagesApi.AddImage(authedCtx, imageModel, &opts)
|
||||
if err != nil {
|
||||
var detail = "no details given"
|
||||
detail := "no details given"
|
||||
var openAPIErr external.GenericOpenAPIError
|
||||
if errors.As(err, &openAPIErr) {
|
||||
detail = string(openAPIErr.Body())
|
||||
@@ -122,7 +121,7 @@ func (c *Client) Import(ctx context.Context, cfg ImportConfig) error {
|
||||
}
|
||||
|
||||
func addImageModel(imageMetadata image.Metadata, packageDigest, manifestDigest, dockerfileDigest, configDigest, sessionID string) external.ImageAnalysisRequest {
|
||||
var tags = make([]string, len(imageMetadata.Tags))
|
||||
tags := make([]string, len(imageMetadata.Tags))
|
||||
for i, t := range imageMetadata.Tags {
|
||||
tags[i] = t.String()
|
||||
}
|
||||
|
||||
@@ -8,10 +8,9 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/client-go/pkg/external"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/wagoodman/go-progress"
|
||||
)
|
||||
|
||||
type configImportAPI interface {
|
||||
|
||||
@@ -6,11 +6,9 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
|
||||
"github.com/anchore/client-go/pkg/external"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/wagoodman/go-progress"
|
||||
)
|
||||
|
||||
type dockerfileImportAPI interface {
|
||||
|
||||
@@ -8,10 +8,9 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/client-go/pkg/external"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/wagoodman/go-progress"
|
||||
)
|
||||
|
||||
type manifestImportAPI interface {
|
||||
|
||||
@@ -8,16 +8,13 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/anchore/syft/internal/presenter/packages"
|
||||
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/syft/syft/distro"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
|
||||
"github.com/anchore/client-go/pkg/external"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/presenter/packages"
|
||||
"github.com/anchore/syft/syft/distro"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/wagoodman/go-progress"
|
||||
)
|
||||
|
||||
type packageSBOMImportAPI interface {
|
||||
|
||||
+4
-2
@@ -15,8 +15,10 @@ package bus
|
||||
|
||||
import "github.com/wagoodman/go-partybus"
|
||||
|
||||
var publisher partybus.Publisher
|
||||
var active bool
|
||||
var (
|
||||
publisher partybus.Publisher
|
||||
active bool
|
||||
)
|
||||
|
||||
// SetPublisher sets the singleton event bus publisher. This is optional; if no bus is provided, the library will
|
||||
// behave no differently than if a bus had been provided.
|
||||
|
||||
@@ -29,6 +29,7 @@ type parser interface {
|
||||
type Application struct {
|
||||
ConfigPath string `yaml:",omitempty" json:"configPath"` // the location where the application config was read from (either from -c or discovered while loading)
|
||||
Output string `yaml:"output" json:"output" mapstructure:"output"` // -o, the Presenter hint string to use for report formatting
|
||||
File string `yaml:"file" json:"file" mapstructure:"file"` // --file, the file to write report output to
|
||||
Quiet bool `yaml:"quiet" json:"quiet" mapstructure:"quiet"` // -q, indicates to not show any status output to stderr (ETUI or logging UI)
|
||||
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
|
||||
Anchore anchore `yaml:"anchore" json:"anchore" mapstructure:"anchore"` // options for interacting with Anchore Engine/Enterprise
|
||||
@@ -143,7 +144,6 @@ func (cfg *Application) parseConfigValues() error {
|
||||
func (cfg Application) String() string {
|
||||
// yaml is pretty human friendly (at least when compared to json)
|
||||
appCfgStr, err := yaml.Marshal(&cfg)
|
||||
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
@@ -20,11 +19,13 @@ type RegistryCredentials 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"`
|
||||
}
|
||||
|
||||
func (cfg registry) loadDefaultValues(v *viper.Viper) {
|
||||
v.SetDefault("registry.insecure-skip-tls-verify", false)
|
||||
v.SetDefault("registry.insecure-use-http", false)
|
||||
v.SetDefault("registry.auth", []RegistryCredentials{})
|
||||
}
|
||||
|
||||
@@ -56,7 +57,7 @@ func hasNonEmptyCredentials(username, password, token string) bool {
|
||||
}
|
||||
|
||||
func (cfg *registry) ToOptions() *image.RegistryOptions {
|
||||
var auth = make([]image.RegistryCredentials, len(cfg.Auth))
|
||||
auth := make([]image.RegistryCredentials, len(cfg.Auth))
|
||||
for i, a := range cfg.Auth {
|
||||
auth[i] = image.RegistryCredentials{
|
||||
Authority: a.Authority,
|
||||
@@ -67,6 +68,7 @@ func (cfg *registry) ToOptions() *image.RegistryOptions {
|
||||
}
|
||||
return &image.RegistryOptions{
|
||||
InsecureSkipTLSVerify: cfg.InsecureSkipTLSVerify,
|
||||
InsecureUseHTTP: cfg.InsecureUseHTTP,
|
||||
Credentials: auth,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -54,3 +55,57 @@ func TestHasNonEmptyCredentials(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_registry_ToOptions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input registry
|
||||
expected image.RegistryOptions
|
||||
}{
|
||||
{
|
||||
name: "no registry options",
|
||||
input: registry{},
|
||||
expected: image.RegistryOptions{
|
||||
Credentials: []image.RegistryCredentials{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set InsecureSkipTLSVerify",
|
||||
input: registry{
|
||||
InsecureSkipTLSVerify: true,
|
||||
},
|
||||
expected: image.RegistryOptions{
|
||||
InsecureSkipTLSVerify: true,
|
||||
Credentials: []image.RegistryCredentials{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set InsecureUseHTTP",
|
||||
input: registry{
|
||||
InsecureUseHTTP: true,
|
||||
},
|
||||
expected: image.RegistryOptions{
|
||||
InsecureUseHTTP: true,
|
||||
Credentials: []image.RegistryCredentials{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set all bool options",
|
||||
input: registry{
|
||||
InsecureSkipTLSVerify: true,
|
||||
InsecureUseHTTP: true,
|
||||
},
|
||||
expected: image.RegistryOptions{
|
||||
InsecureSkipTLSVerify: true,
|
||||
InsecureUseHTTP: true,
|
||||
Credentials: []image.RegistryCredentials{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, &test.expected, test.input.ToOptions())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
)
|
||||
@@ -12,3 +15,24 @@ func CloseAndLogError(closer io.Closer, location string) {
|
||||
log.Warnf("unable to close file for location=%q: %+v", location, err)
|
||||
}
|
||||
}
|
||||
|
||||
type PathError struct {
|
||||
Path string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e PathError) Error() string {
|
||||
return fmt.Sprintf("unable to observe contents of %+v: %v", e.Path, e.Err)
|
||||
}
|
||||
|
||||
func IsPathError(err error) bool {
|
||||
return errors.As(err, &PathError{})
|
||||
}
|
||||
|
||||
func IsErrPathPermission(err error) bool {
|
||||
var pathErr *PathError
|
||||
if errors.As(err, pathErr) {
|
||||
return os.IsPermission(pathErr.Err)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -9,6 +8,8 @@ import (
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var expectedZipArchiveEntries = []string{
|
||||
@@ -18,35 +19,21 @@ var expectedZipArchiveEntries = []string{
|
||||
"nested.zip",
|
||||
}
|
||||
|
||||
// fatalIfError calls the supplied function. If the function returns a non-nil error, t.Fatal(err) is called.
|
||||
func fatalIfError(t *testing.T, fn func() error) {
|
||||
t.Helper()
|
||||
|
||||
if fn == nil {
|
||||
return
|
||||
}
|
||||
|
||||
err := fn()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// createZipArchive creates a new ZIP archive file at destinationArchivePath based on the directory found at
|
||||
// sourceDirPath.
|
||||
func createZipArchive(t *testing.T, sourceDirPath, destinationArchivePath string) error {
|
||||
func createZipArchive(t testing.TB, sourceDirPath, destinationArchivePath string) {
|
||||
t.Helper()
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get cwd: %+v", err)
|
||||
t.Fatalf("unable to get cwd: %+v", err)
|
||||
}
|
||||
|
||||
cmd := exec.Command("./generate-zip-fixture.sh", destinationArchivePath, path.Base(sourceDirPath))
|
||||
cmd := exec.Command("./generate-zip-fixture-from-source-dir.sh", destinationArchivePath, path.Base(sourceDirPath))
|
||||
cmd.Dir = filepath.Join(cwd, "test-fixtures")
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("unable to start generate zip fixture script: %+v", err)
|
||||
t.Fatalf("unable to start generate zip fixture script: %+v", err)
|
||||
}
|
||||
|
||||
if err := cmd.Wait(); err != nil {
|
||||
@@ -59,61 +46,62 @@ func createZipArchive(t *testing.T, sourceDirPath, destinationArchivePath string
|
||||
// an ExitStatus() method with the same signature.
|
||||
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
|
||||
if status.ExitStatus() != 0 {
|
||||
return fmt.Errorf("failed to generate fixture: rc=%d", status.ExitStatus())
|
||||
t.Fatalf("failed to generate fixture: rc=%d", status.ExitStatus())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("unable to get generate fixture script result: %+v", err)
|
||||
t.Fatalf("unable to get generate fixture script result: %+v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func assertNoError(t testing.TB, fn func() error) func() {
|
||||
return func() {
|
||||
assert.NoError(t, fn())
|
||||
}
|
||||
}
|
||||
|
||||
// setupZipFileTest encapsulates common test setup work for zip file tests. It returns a cleanup function,
|
||||
// which should be called (typically deferred) by the caller, the path of the created zip archive, and an error,
|
||||
// which should trigger a fatal test failure in the consuming test. The returned cleanup function will never be nil
|
||||
// (even if there's an error), and it should always be called.
|
||||
func setupZipFileTest(t *testing.T, sourceDirPath string) (func() error, string, error) {
|
||||
func setupZipFileTest(t testing.TB, sourceDirPath string) string {
|
||||
t.Helper()
|
||||
|
||||
// Keep track of any needed cleanup work as we go
|
||||
var cleanupFns []func() error
|
||||
cleanup := func(fns []func() error) func() error {
|
||||
return func() error {
|
||||
for _, fn := range fns {
|
||||
err := fn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
archivePrefix, err := ioutil.TempFile("", "syft-ziputil-archive-TEST-")
|
||||
if err != nil {
|
||||
return cleanup(cleanupFns), "", fmt.Errorf("unable to create tempfile: %+v", err)
|
||||
t.Fatalf("unable to create tempfile: %+v", err)
|
||||
}
|
||||
cleanupFns = append(cleanupFns, func() error { return os.Remove(archivePrefix.Name()) })
|
||||
|
||||
t.Cleanup(
|
||||
assertNoError(t,
|
||||
func() error {
|
||||
return os.Remove(archivePrefix.Name())
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
destinationArchiveFilePath := archivePrefix.Name() + ".zip"
|
||||
t.Logf("archive path: %s", destinationArchiveFilePath)
|
||||
err = createZipArchive(t, sourceDirPath, destinationArchiveFilePath)
|
||||
cleanupFns = append(cleanupFns, func() error { return os.Remove(destinationArchiveFilePath) })
|
||||
if err != nil {
|
||||
return cleanup(cleanupFns), "", err
|
||||
}
|
||||
createZipArchive(t, sourceDirPath, destinationArchiveFilePath)
|
||||
|
||||
t.Cleanup(
|
||||
assertNoError(t,
|
||||
func() error {
|
||||
return os.Remove(destinationArchiveFilePath)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return cleanup(cleanupFns), "", fmt.Errorf("unable to get cwd: %+v", err)
|
||||
t.Fatalf("unable to get cwd: %+v", err)
|
||||
}
|
||||
|
||||
t.Logf("running from: %s", cwd)
|
||||
|
||||
return cleanup(cleanupFns), destinationArchiveFilePath, nil
|
||||
return destinationArchiveFilePath
|
||||
}
|
||||
|
||||
// TODO: Consider moving any non-git asset generation to a task (e.g. make) that's run ahead of running go tests.
|
||||
@@ -121,11 +109,7 @@ func ensureNestedZipExists(t *testing.T, sourceDirPath string) error {
|
||||
t.Helper()
|
||||
|
||||
nestedArchiveFilePath := path.Join(sourceDirPath, "nested.zip")
|
||||
err := createZipArchive(t, sourceDirPath, nestedArchiveFilePath)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create nested archive for test fixture: %+v", err)
|
||||
}
|
||||
createZipArchive(t, sourceDirPath, nestedArchiveFilePath)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,23 +1,36 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
)
|
||||
|
||||
// ZipFileManifest is a collection of paths and their file metadata.
|
||||
type ZipFileManifest map[string]os.FileInfo
|
||||
|
||||
// newZipManifest creates an empty ZipFileManifest.
|
||||
func newZipManifest() ZipFileManifest {
|
||||
return make(ZipFileManifest)
|
||||
// NewZipFileManifest creates and returns a new ZipFileManifest populated with path and metadata from the given zip archive path.
|
||||
func NewZipFileManifest(archivePath string) (ZipFileManifest, error) {
|
||||
zipReader, err := OpenZip(archivePath)
|
||||
manifest := make(ZipFileManifest)
|
||||
if err != nil {
|
||||
return manifest, fmt.Errorf("unable to open zip archive (%s): %w", archivePath, err)
|
||||
}
|
||||
defer func() {
|
||||
err = zipReader.Close()
|
||||
if err != nil {
|
||||
log.Warnf("unable to close zip archive (%s): %+v", archivePath, err)
|
||||
}
|
||||
}()
|
||||
|
||||
for _, file := range zipReader.Reader.File {
|
||||
manifest.Add(file.Name, file.FileInfo())
|
||||
}
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
// Add a new path and it's file metadata to the collection.
|
||||
@@ -47,26 +60,6 @@ func (z ZipFileManifest) GlobMatch(patterns ...string) []string {
|
||||
return results
|
||||
}
|
||||
|
||||
// NewZipFileManifest creates and returns a new ZipFileManifest populated with path and metadata from the given zip archive path.
|
||||
func NewZipFileManifest(archivePath string) (ZipFileManifest, error) {
|
||||
zipReader, err := zip.OpenReader(archivePath)
|
||||
manifest := newZipManifest()
|
||||
if err != nil {
|
||||
return manifest, fmt.Errorf("unable to open zip archive (%s): %w", archivePath, err)
|
||||
}
|
||||
defer func() {
|
||||
err = zipReader.Close()
|
||||
if err != nil {
|
||||
log.Errorf("unable to close zip archive (%s): %+v", archivePath, err)
|
||||
}
|
||||
}()
|
||||
|
||||
for _, file := range zipReader.Reader.File {
|
||||
manifest.Add(file.Name, file.FileInfo())
|
||||
}
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
// normalizeZipEntryName takes the given path entry and ensures it is prefixed with "/".
|
||||
func normalizeZipEntryName(entry string) string {
|
||||
if !strings.HasPrefix(entry, "/") {
|
||||
|
||||
@@ -19,11 +19,7 @@ func TestNewZipFileManifest(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cleanup, archiveFilePath, err := setupZipFileTest(t, sourceDirPath)
|
||||
defer fatalIfError(t, cleanup)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
archiveFilePath := setupZipFileTest(t, sourceDirPath)
|
||||
|
||||
actual, err := NewZipFileManifest(archiveFilePath)
|
||||
if err != nil {
|
||||
@@ -63,12 +59,7 @@ func TestZipFileManifest_GlobMatch(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cleanup, archiveFilePath, err := setupZipFileTest(t, sourceDirPath)
|
||||
//goland:noinspection GoNilness
|
||||
defer fatalIfError(t, cleanup)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
archiveFilePath := setupZipFileTest(t, sourceDirPath)
|
||||
|
||||
z, err := NewZipFileManifest(archiveFilePath)
|
||||
if err != nil {
|
||||
|
||||
@@ -24,12 +24,12 @@ const (
|
||||
|
||||
const perFileReadLimit = 2 * GB
|
||||
|
||||
type errZipSlipDetected struct {
|
||||
type zipSlipDetectedError struct {
|
||||
Prefix string
|
||||
JoinArgs []string
|
||||
}
|
||||
|
||||
func (e *errZipSlipDetected) Error() string {
|
||||
func (e *zipSlipDetectedError) Error() string {
|
||||
return fmt.Sprintf("paths are not allowed to resolve outside of the root prefix (%q). Destination: %q", e.Prefix, e.JoinArgs)
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ func newZipTraverseRequest(paths ...string) zipTraversalRequest {
|
||||
func TraverseFilesInZip(archivePath string, visitor func(*zip.File) error, paths ...string) error {
|
||||
request := newZipTraverseRequest(paths...)
|
||||
|
||||
zipReader, err := zip.OpenReader(archivePath)
|
||||
zipReader, err := OpenZip(archivePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to open zip archive (%s): %w", archivePath, err)
|
||||
}
|
||||
@@ -197,7 +197,7 @@ func safeJoin(prefix string, dest ...string) (string, error) {
|
||||
joinResult := filepath.Join(append([]string{prefix}, dest...)...)
|
||||
cleanJoinResult := filepath.Clean(joinResult)
|
||||
if !strings.HasPrefix(cleanJoinResult, filepath.Clean(prefix)) {
|
||||
return "", &errZipSlipDetected{
|
||||
return "", &zipSlipDetectedError{
|
||||
Prefix: prefix,
|
||||
JoinArgs: dest,
|
||||
}
|
||||
|
||||
@@ -44,14 +44,12 @@ func TestUnzipToDir(t *testing.T) {
|
||||
|
||||
goldenRootDir := filepath.Join(cwd, "test-fixtures")
|
||||
sourceDirPath := path.Join(goldenRootDir, "zip-source")
|
||||
cleanup, archiveFilePath, err := setupZipFileTest(t, sourceDirPath)
|
||||
defer fatalIfError(t, cleanup)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
archiveFilePath := setupZipFileTest(t, sourceDirPath)
|
||||
|
||||
unzipDestinationDir, err := ioutil.TempDir("", "syft-ziputil-contents-TEST-")
|
||||
defer os.RemoveAll(unzipDestinationDir)
|
||||
t.Cleanup(assertNoError(t, func() error {
|
||||
return os.RemoveAll(unzipDestinationDir)
|
||||
}))
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create tempdir: %+v", err)
|
||||
}
|
||||
@@ -127,41 +125,119 @@ func TestUnzipToDir(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestContentsFromZip(t *testing.T) {
|
||||
archivePrefix, err := ioutil.TempFile("", "syft-ziputil-archive-TEST-")
|
||||
tests := []struct {
|
||||
name string
|
||||
archivePrep func(tb testing.TB) string
|
||||
}{
|
||||
{
|
||||
name: "standard, non-nested zip",
|
||||
archivePrep: prepZipSourceFixture,
|
||||
},
|
||||
{
|
||||
name: "zip with prepended bytes",
|
||||
archivePrep: prependZipSourceFixtureWithString(t, "junk at the beginning of the file..."),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
archivePath := test.archivePrep(t)
|
||||
expected := zipSourceFixtureExpectedContents()
|
||||
|
||||
var paths []string
|
||||
for p := range expected {
|
||||
paths = append(paths, p)
|
||||
}
|
||||
|
||||
actual, err := ContentsFromZip(archivePath, paths...)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to extract from unzip archive: %+v", err)
|
||||
}
|
||||
|
||||
assertZipSourceFixtureContents(t, actual, expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func prependZipSourceFixtureWithString(tb testing.TB, value string) func(tb testing.TB) string {
|
||||
if len(value) == 0 {
|
||||
tb.Fatalf("no bytes given to prefix")
|
||||
}
|
||||
return func(t testing.TB) string {
|
||||
archivePath := prepZipSourceFixture(t)
|
||||
|
||||
// create a temp file
|
||||
tmpFile, err := ioutil.TempFile("", "syft-ziputil-prependZipSourceFixtureWithString-")
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create tempfile: %+v", err)
|
||||
}
|
||||
defer tmpFile.Close()
|
||||
|
||||
// write value to the temp file
|
||||
if _, err := tmpFile.WriteString(value); err != nil {
|
||||
t.Fatalf("unable to write to tempfile: %+v", err)
|
||||
}
|
||||
|
||||
// open the original archive
|
||||
sourceFile, err := os.Open(archivePath)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to read source file: %+v", err)
|
||||
}
|
||||
|
||||
// copy all contents from the archive to the temp file
|
||||
if _, err := io.Copy(tmpFile, sourceFile); err != nil {
|
||||
t.Fatalf("unable to copy source to dest: %+v", err)
|
||||
}
|
||||
|
||||
sourceFile.Close()
|
||||
|
||||
// remove the original archive and replace it with the temp file
|
||||
if err := os.Remove(archivePath); err != nil {
|
||||
t.Fatalf("unable to remove original source archive (%q): %+v", archivePath, err)
|
||||
}
|
||||
|
||||
if err := os.Rename(tmpFile.Name(), archivePath); err != nil {
|
||||
t.Fatalf("unable to move new archive to old path (%q): %+v", tmpFile.Name(), err)
|
||||
}
|
||||
|
||||
return archivePath
|
||||
}
|
||||
}
|
||||
|
||||
func prepZipSourceFixture(t testing.TB) string {
|
||||
t.Helper()
|
||||
archivePrefix, err := ioutil.TempFile("", "syft-ziputil-prepZipSourceFixture-")
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create tempfile: %+v", err)
|
||||
}
|
||||
defer os.Remove(archivePrefix.Name())
|
||||
|
||||
t.Cleanup(func() {
|
||||
assert.NoError(t, os.Remove(archivePrefix.Name()))
|
||||
})
|
||||
|
||||
// the zip utility will add ".zip" to the end of the given name
|
||||
archivePath := archivePrefix.Name() + ".zip"
|
||||
defer os.Remove(archivePath)
|
||||
|
||||
t.Cleanup(func() {
|
||||
assert.NoError(t, os.Remove(archivePath))
|
||||
})
|
||||
|
||||
t.Logf("archive path: %s", archivePath)
|
||||
|
||||
err = createZipArchive(t, "zip-source", archivePrefix.Name())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
createZipArchive(t, "zip-source", archivePrefix.Name())
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Errorf("unable to get cwd: %+v", err)
|
||||
}
|
||||
return archivePath
|
||||
}
|
||||
|
||||
t.Logf("running from: %s", cwd)
|
||||
|
||||
aFilePath := filepath.Join("some-dir", "a-file.txt")
|
||||
bFilePath := filepath.Join("b-file.txt")
|
||||
|
||||
expected := map[string]string{
|
||||
aFilePath: "A file! nice!",
|
||||
bFilePath: "B file...",
|
||||
}
|
||||
|
||||
actual, err := ContentsFromZip(archivePath, aFilePath, bFilePath)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to extract from unzip archive: %+v", err)
|
||||
func zipSourceFixtureExpectedContents() map[string]string {
|
||||
return map[string]string{
|
||||
filepath.Join("some-dir", "a-file.txt"): "A file! nice!",
|
||||
filepath.Join("b-file.txt"): "B file...",
|
||||
}
|
||||
}
|
||||
|
||||
func assertZipSourceFixtureContents(t testing.TB, actual map[string]string, expected map[string]string) {
|
||||
t.Helper()
|
||||
diffs := deep.Equal(actual, expected)
|
||||
if len(diffs) > 0 {
|
||||
for _, d := range diffs {
|
||||
@@ -223,7 +299,7 @@ func TestSafeJoin(t *testing.T) {
|
||||
"../../../etc/passwd",
|
||||
},
|
||||
expected: "",
|
||||
errAssertion: assertErrorAs(&errZipSlipDetected{}),
|
||||
errAssertion: assertErrorAs(&zipSlipDetectedError{}),
|
||||
},
|
||||
{
|
||||
prefix: "/a/place",
|
||||
@@ -232,7 +308,7 @@ func TestSafeJoin(t *testing.T) {
|
||||
"../",
|
||||
},
|
||||
expected: "",
|
||||
errAssertion: assertErrorAs(&errZipSlipDetected{}),
|
||||
errAssertion: assertErrorAs(&zipSlipDetectedError{}),
|
||||
},
|
||||
{
|
||||
prefix: "/a/place",
|
||||
@@ -240,7 +316,7 @@ func TestSafeJoin(t *testing.T) {
|
||||
"../",
|
||||
},
|
||||
expected: "",
|
||||
errAssertion: assertErrorAs(&errZipSlipDetected{}),
|
||||
errAssertion: assertErrorAs(&zipSlipDetectedError{}),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// directoryEndLen, readByf, directoryEnd, and findSignatureInBlock were copied from the golang stdlib, specifically:
|
||||
// - https://github.com/golang/go/blob/go1.16.4/src/archive/zip/struct.go
|
||||
// - https://github.com/golang/go/blob/go1.16.4/src/archive/zip/reader.go
|
||||
// findArchiveStartOffset is derived from the same stdlib utils, specifically the readDirectoryEnd function.
|
||||
|
||||
const directoryEndLen = 22
|
||||
|
||||
// ZipReadCloser is a drop-in replacement for zip.ReadCloser (from zip.OpenReader) that additionally considers zips
|
||||
// that have bytes prefixed to the front of the archive (common with self-extracting jars).
|
||||
type ZipReadCloser struct {
|
||||
*zip.Reader
|
||||
io.Closer
|
||||
}
|
||||
|
||||
// OpenZip provides a ZipReadCloser for the given filepath.
|
||||
func OpenZip(filepath string) (*ZipReadCloser, error) {
|
||||
f, err := os.Open(filepath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// some archives may have bytes prepended to the front of the archive, such as with self executing JARs. We first
|
||||
// need to find the start of the archive and keep track of this offset.
|
||||
offset, err := findArchiveStartOffset(f, fi.Size())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot find beginning of zip archive=%q : %w", filepath, err)
|
||||
}
|
||||
|
||||
if _, err := f.Seek(0, io.SeekStart); err != nil {
|
||||
return nil, fmt.Errorf("unable to seek to beginning of archive: %w", err)
|
||||
}
|
||||
|
||||
size := fi.Size() - int64(offset)
|
||||
|
||||
r, err := zip.NewReader(io.NewSectionReader(f, int64(offset), size), size)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to open ZipReadCloser @ %q: %w", filepath, err)
|
||||
}
|
||||
|
||||
return &ZipReadCloser{
|
||||
Reader: r,
|
||||
Closer: f,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type readBuf []byte
|
||||
|
||||
func (b *readBuf) uint16() uint16 {
|
||||
v := binary.LittleEndian.Uint16(*b)
|
||||
*b = (*b)[2:]
|
||||
return v
|
||||
}
|
||||
|
||||
func (b *readBuf) uint32() uint32 {
|
||||
v := binary.LittleEndian.Uint32(*b)
|
||||
*b = (*b)[4:]
|
||||
return v
|
||||
}
|
||||
|
||||
type directoryEnd struct {
|
||||
diskNbr uint32 // unused
|
||||
dirDiskNbr uint32 // unused
|
||||
dirRecordsThisDisk uint64 // unused
|
||||
directoryRecords uint64
|
||||
directorySize uint64
|
||||
directoryOffset uint64 // relative to file
|
||||
}
|
||||
|
||||
// note: this is derived from readDirectoryEnd within the archive/zip package
|
||||
func findArchiveStartOffset(r io.ReaderAt, size int64) (startOfArchive uint64, err error) {
|
||||
// look for directoryEndSignature in the last 1k, then in the last 65k
|
||||
var buf []byte
|
||||
var directoryEndOffset int64
|
||||
for i, bLen := range []int64{1024, 65 * 1024} {
|
||||
if bLen > size {
|
||||
bLen = size
|
||||
}
|
||||
buf = make([]byte, int(bLen))
|
||||
if _, err := r.ReadAt(buf, size-bLen); err != nil && !errors.Is(err, io.EOF) {
|
||||
return 0, err
|
||||
}
|
||||
if p := findSignatureInBlock(buf); p >= 0 {
|
||||
buf = buf[p:]
|
||||
directoryEndOffset = size - bLen + int64(p)
|
||||
break
|
||||
}
|
||||
if i == 1 || bLen == size {
|
||||
return 0, zip.ErrFormat
|
||||
}
|
||||
}
|
||||
|
||||
if buf == nil {
|
||||
// we were unable to find the directoryEndSignature block
|
||||
return 0, zip.ErrFormat
|
||||
}
|
||||
|
||||
// read header into struct
|
||||
b := readBuf(buf[4:]) // skip signature
|
||||
d := &directoryEnd{
|
||||
diskNbr: uint32(b.uint16()),
|
||||
dirDiskNbr: uint32(b.uint16()),
|
||||
dirRecordsThisDisk: uint64(b.uint16()),
|
||||
directoryRecords: uint64(b.uint16()),
|
||||
directorySize: uint64(b.uint32()),
|
||||
directoryOffset: uint64(b.uint32()),
|
||||
}
|
||||
// Calculate where the zip data actually begins
|
||||
startOfArchive = uint64(directoryEndOffset) - d.directorySize - d.directoryOffset
|
||||
|
||||
// These values mean that the file can be a zip64 file
|
||||
if d.directoryRecords == 0xffff || d.directorySize == 0xffff || d.directoryOffset == 0xffffffff {
|
||||
startOfArchive = 0 // Prefixed data not supported
|
||||
}
|
||||
|
||||
// Make sure directoryOffset points to somewhere in our file.
|
||||
if o := int64(d.directoryOffset); o < 0 || o >= size {
|
||||
return 0, zip.ErrFormat
|
||||
}
|
||||
return startOfArchive, nil
|
||||
}
|
||||
|
||||
func findSignatureInBlock(b []byte) int {
|
||||
for i := len(b) - directoryEndLen; i >= 0; i-- {
|
||||
// defined from directoryEndSignature
|
||||
if b[i] == 'P' && b[i+1] == 'K' && b[i+2] == 0x05 && b[i+3] == 0x06 {
|
||||
// n is length of comment
|
||||
n := int(b[i+directoryEndLen-2]) | int(b[i+directoryEndLen-1])<<8
|
||||
if n+directoryEndLen+i <= len(b) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFindArchiveStartOffset(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
archivePrep func(tb testing.TB) string
|
||||
expected uint64
|
||||
}{
|
||||
{
|
||||
name: "standard, non-nested zip",
|
||||
archivePrep: prepZipSourceFixture,
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
name: "zip with prepended bytes",
|
||||
archivePrep: prependZipSourceFixtureWithString(t, "junk at the beginning of the file..."),
|
||||
expected: 36,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
archivePath := test.archivePrep(t)
|
||||
f, err := os.Open(archivePath)
|
||||
if err != nil {
|
||||
t.Fatalf("could not open archive %q: %+v", archivePath, err)
|
||||
}
|
||||
fi, err := os.Stat(f.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("unable to stat archive: %+v", err)
|
||||
}
|
||||
|
||||
actual, err := findArchiveStartOffset(f, fi.Size())
|
||||
if err != nil {
|
||||
t.Fatalf("unable to find offset: %+v", err)
|
||||
}
|
||||
assert.Equal(t, test.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package logger
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
@@ -10,6 +11,8 @@ import (
|
||||
prefixed "github.com/x-cray/logrus-prefixed-formatter"
|
||||
)
|
||||
|
||||
const defaultLogFilePermissions fs.FileMode = 0o644
|
||||
|
||||
// LogrusConfig contains all configurable values for the Logrus logger
|
||||
type LogrusConfig struct {
|
||||
EnableConsole bool
|
||||
@@ -38,7 +41,7 @@ func NewLogrusLogger(cfg LogrusConfig) *LogrusLogger {
|
||||
var output io.Writer
|
||||
switch {
|
||||
case cfg.EnableConsole && cfg.EnableFile:
|
||||
logFile, err := os.OpenFile(cfg.FileLocation, os.O_WRONLY|os.O_CREATE, 0755)
|
||||
logFile, err := os.OpenFile(cfg.FileLocation, os.O_WRONLY|os.O_CREATE, defaultLogFilePermissions)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("unable to setup log file: %w", err))
|
||||
}
|
||||
@@ -46,7 +49,7 @@ func NewLogrusLogger(cfg LogrusConfig) *LogrusLogger {
|
||||
case cfg.EnableConsole:
|
||||
output = os.Stderr
|
||||
case cfg.EnableFile:
|
||||
logFile, err := os.OpenFile(cfg.FileLocation, os.O_WRONLY|os.O_CREATE, 0755)
|
||||
logFile, err := os.OpenFile(cfg.FileLocation, os.O_WRONLY|os.O_CREATE, defaultLogFilePermissions)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("unable to setup log file: %w", err))
|
||||
}
|
||||
|
||||
@@ -1,169 +1,34 @@
|
||||
package packages
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/filetree"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
|
||||
"github.com/anchore/go-testutils"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/sergi/go-diff/diffmatchpatch"
|
||||
)
|
||||
|
||||
var update = flag.Bool("update", false, "update the *.golden files for json presenters")
|
||||
|
||||
func TestCycloneDxDirsPresenter(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
|
||||
catalog := pkg.NewCatalog()
|
||||
|
||||
// populate catalog with test data
|
||||
catalog.Add(pkg.Package{
|
||||
Name: "package1",
|
||||
Version: "1.0.1",
|
||||
Type: pkg.DebPkg,
|
||||
FoundBy: "the-cataloger-1",
|
||||
Locations: []source.Location{
|
||||
{RealPath: "/some/path/pkg1"},
|
||||
},
|
||||
Metadata: pkg.DpkgMetadata{
|
||||
Package: "package1",
|
||||
Version: "1.0.1",
|
||||
Architecture: "amd64",
|
||||
},
|
||||
})
|
||||
catalog.Add(pkg.Package{
|
||||
Name: "package2",
|
||||
Version: "2.0.1",
|
||||
Type: pkg.DebPkg,
|
||||
FoundBy: "the-cataloger-2",
|
||||
Locations: []source.Location{
|
||||
{RealPath: "/some/path/pkg1"},
|
||||
},
|
||||
Licenses: []string{
|
||||
"MIT",
|
||||
"Apache-v2",
|
||||
},
|
||||
Metadata: pkg.DpkgMetadata{
|
||||
Package: "package2",
|
||||
Version: "1.0.2",
|
||||
Architecture: "amd64",
|
||||
},
|
||||
})
|
||||
|
||||
s, err := source.NewFromDirectory("/some/path")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pres := NewCycloneDxPresenter(catalog, s.Metadata)
|
||||
|
||||
// run presenter
|
||||
err = pres.Present(&buffer)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
actual := buffer.Bytes()
|
||||
|
||||
if *update {
|
||||
testutils.UpdateGoldenFileContents(t, actual)
|
||||
}
|
||||
|
||||
var expected = testutils.GetGoldenFileContents(t)
|
||||
|
||||
// remove dynamic values, which are tested independently
|
||||
actual = redact(actual)
|
||||
expected = redact(expected)
|
||||
|
||||
if !bytes.Equal(expected, actual) {
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(string(actual), string(expected), true)
|
||||
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
|
||||
}
|
||||
var updateCycloneDx = flag.Bool("update-cyclonedx", false, "update the *.golden files for cyclone-dx presenters")
|
||||
|
||||
func TestCycloneDxDirectoryPresenter(t *testing.T) {
|
||||
catalog, metadata, _ := presenterDirectoryInput(t)
|
||||
assertPresenterAgainstGoldenSnapshot(t,
|
||||
NewCycloneDxPresenter(catalog, metadata),
|
||||
*updateCycloneDx,
|
||||
cycloneDxRedactor,
|
||||
)
|
||||
}
|
||||
|
||||
func TestCycloneDxImgsPresenter(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
|
||||
catalog := pkg.NewCatalog()
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", "image-simple")
|
||||
|
||||
_, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks)
|
||||
_, ref2, _ := img.SquashedTree().File("/somefile-2.txt", filetree.FollowBasenameLinks)
|
||||
|
||||
// populate catalog with test data
|
||||
catalog.Add(pkg.Package{
|
||||
Name: "package1",
|
||||
Version: "1.0.1",
|
||||
Locations: []source.Location{
|
||||
source.NewLocationFromImage(string(ref1.RealPath), *ref1, img),
|
||||
},
|
||||
Type: pkg.RpmPkg,
|
||||
FoundBy: "the-cataloger-1",
|
||||
PURL: "the-purl-1",
|
||||
})
|
||||
catalog.Add(pkg.Package{
|
||||
Name: "package2",
|
||||
Version: "2.0.1",
|
||||
Locations: []source.Location{
|
||||
source.NewLocationFromImage(string(ref2.RealPath), *ref2, img),
|
||||
},
|
||||
Type: pkg.RpmPkg,
|
||||
FoundBy: "the-cataloger-2",
|
||||
Licenses: []string{
|
||||
"MIT",
|
||||
"Apache-v2",
|
||||
},
|
||||
PURL: "the-purl-2",
|
||||
})
|
||||
|
||||
s, err := source.NewFromImage(img, "user-image-input")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// This accounts for the non-deterministic digest value that we end up with when
|
||||
// we build a container image dynamically during testing. Ultimately, we should
|
||||
// use a golden image as a test fixture in place of building this image during
|
||||
// testing. At that time, this line will no longer be necessary.
|
||||
//
|
||||
// This value is sourced from the "version" node in "./test-fixtures/snapshot/TestCycloneDxImgsPresenter.golden"
|
||||
s.Metadata.ImageMetadata.ManifestDigest = "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368"
|
||||
|
||||
pres := NewCycloneDxPresenter(catalog, s.Metadata)
|
||||
|
||||
// run presenter
|
||||
err = pres.Present(&buffer)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
actual := buffer.Bytes()
|
||||
|
||||
if *update {
|
||||
testutils.UpdateGoldenFileContents(t, actual)
|
||||
}
|
||||
|
||||
var expected = testutils.GetGoldenFileContents(t)
|
||||
|
||||
// remove dynamic values, which are tested independently
|
||||
actual = redact(actual)
|
||||
expected = redact(expected)
|
||||
|
||||
if !bytes.Equal(expected, actual) {
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(string(expected), string(actual), true)
|
||||
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
|
||||
}
|
||||
func TestCycloneDxImagePresenter(t *testing.T) {
|
||||
testImage := "image-simple"
|
||||
catalog, metadata, _ := presenterImageInput(t, testImage)
|
||||
assertPresenterAgainstGoldenImageSnapshot(t,
|
||||
NewCycloneDxPresenter(catalog, metadata),
|
||||
testImage,
|
||||
*updateCycloneDx,
|
||||
cycloneDxRedactor,
|
||||
)
|
||||
}
|
||||
|
||||
func redact(s []byte) []byte {
|
||||
func cycloneDxRedactor(s []byte) []byte {
|
||||
serialPattern := regexp.MustCompile(`serialNumber="[a-zA-Z0-9\-:]+"`)
|
||||
rfc3339Pattern := regexp.MustCompile(`([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(([Zz])|([\+|\-]([01][0-9]|2[0-3]):[0-5][0-9]))`)
|
||||
|
||||
|
||||
@@ -38,18 +38,18 @@ func NewJSONPackages(catalog *pkg.Catalog) ([]JSONPackage, error) {
|
||||
|
||||
// NewJSONPackage crates a new JSONPackage from the given pkg.Package.
|
||||
func NewJSONPackage(p *pkg.Package) (JSONPackage, error) {
|
||||
var cpes = make([]string, len(p.CPEs))
|
||||
cpes := make([]string, len(p.CPEs))
|
||||
for i, c := range p.CPEs {
|
||||
cpes[i] = c.BindToFmtString()
|
||||
}
|
||||
|
||||
// ensure collections are never nil for presentation reasons
|
||||
var locations = make([]source.Location, 0)
|
||||
locations := make([]source.Location, 0)
|
||||
if p.Locations != nil {
|
||||
locations = p.Locations
|
||||
}
|
||||
|
||||
var licenses = make([]string, 0)
|
||||
licenses := make([]string, 0)
|
||||
if p.Licenses != nil {
|
||||
licenses = p.Licenses
|
||||
}
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
package packages
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/filetree"
|
||||
|
||||
"github.com/anchore/go-testutils"
|
||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
"github.com/anchore/syft/syft/distro"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/sergi/go-diff/diffmatchpatch"
|
||||
)
|
||||
|
||||
var updateJSONGoldenFiles = flag.Bool("update-json", false, "update the *.golden files for json presenters")
|
||||
@@ -24,160 +17,21 @@ func must(c pkg.CPE, e error) pkg.CPE {
|
||||
return c
|
||||
}
|
||||
|
||||
func TestJSONDirsPresenter(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
|
||||
catalog := pkg.NewCatalog()
|
||||
|
||||
// populate catalog with test data
|
||||
catalog.Add(pkg.Package{
|
||||
ID: "package-1-id",
|
||||
Name: "package-1",
|
||||
Version: "1.0.1",
|
||||
Type: pkg.PythonPkg,
|
||||
FoundBy: "the-cataloger-1",
|
||||
Locations: []source.Location{
|
||||
{RealPath: "/some/path/pkg1"},
|
||||
},
|
||||
Language: pkg.Python,
|
||||
MetadataType: pkg.PythonPackageMetadataType,
|
||||
Licenses: []string{"MIT"},
|
||||
Metadata: pkg.PythonPackageMetadata{
|
||||
Name: "package-1",
|
||||
Version: "1.0.1",
|
||||
},
|
||||
PURL: "a-purl-2",
|
||||
CPEs: []pkg.CPE{
|
||||
must(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")),
|
||||
},
|
||||
})
|
||||
catalog.Add(pkg.Package{
|
||||
ID: "package-2-id",
|
||||
Name: "package-2",
|
||||
Version: "2.0.1",
|
||||
Type: pkg.DebPkg,
|
||||
FoundBy: "the-cataloger-2",
|
||||
Locations: []source.Location{
|
||||
{RealPath: "/some/path/pkg1"},
|
||||
},
|
||||
MetadataType: pkg.DpkgMetadataType,
|
||||
Metadata: pkg.DpkgMetadata{
|
||||
Package: "package-2",
|
||||
Version: "2.0.1",
|
||||
},
|
||||
PURL: "a-purl-2",
|
||||
CPEs: []pkg.CPE{
|
||||
must(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")),
|
||||
},
|
||||
})
|
||||
var d *distro.Distro
|
||||
s, err := source.NewFromDirectory("/some/path")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pres := NewJSONPresenter(catalog, s.Metadata, d, source.SquashedScope)
|
||||
|
||||
// run presenter
|
||||
err = pres.Present(&buffer)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
actual := buffer.Bytes()
|
||||
|
||||
if *updateJSONGoldenFiles {
|
||||
testutils.UpdateGoldenFileContents(t, actual)
|
||||
}
|
||||
|
||||
var expected = testutils.GetGoldenFileContents(t)
|
||||
|
||||
if !bytes.Equal(expected, actual) {
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(string(expected), string(actual), true)
|
||||
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
|
||||
}
|
||||
func TestJSONDirectoryPresenter(t *testing.T) {
|
||||
catalog, metadata, dist := presenterDirectoryInput(t)
|
||||
assertPresenterAgainstGoldenSnapshot(t,
|
||||
NewJSONPresenter(catalog, metadata, dist, source.SquashedScope),
|
||||
*updateJSONGoldenFiles,
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
func TestJSONImgsPresenter(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
|
||||
func TestJSONImagePresenter(t *testing.T) {
|
||||
testImage := "image-simple"
|
||||
|
||||
if *updateJSONGoldenFiles {
|
||||
imagetest.UpdateGoldenFixtureImage(t, testImage)
|
||||
}
|
||||
|
||||
catalog := pkg.NewCatalog()
|
||||
img := imagetest.GetGoldenFixtureImage(t, testImage)
|
||||
|
||||
_, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks)
|
||||
_, ref2, _ := img.SquashedTree().File("/somefile-2.txt", filetree.FollowBasenameLinks)
|
||||
|
||||
// populate catalog with test data
|
||||
catalog.Add(pkg.Package{
|
||||
ID: "package-1-id",
|
||||
Name: "package-1",
|
||||
Version: "1.0.1",
|
||||
Locations: []source.Location{
|
||||
source.NewLocationFromImage(string(ref1.RealPath), *ref1, img),
|
||||
},
|
||||
Type: pkg.PythonPkg,
|
||||
FoundBy: "the-cataloger-1",
|
||||
Language: pkg.Python,
|
||||
MetadataType: pkg.PythonPackageMetadataType,
|
||||
Licenses: []string{"MIT"},
|
||||
Metadata: pkg.PythonPackageMetadata{
|
||||
Name: "package-1",
|
||||
Version: "1.0.1",
|
||||
},
|
||||
PURL: "a-purl-1",
|
||||
CPEs: []pkg.CPE{
|
||||
must(pkg.NewCPE("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*")),
|
||||
},
|
||||
})
|
||||
catalog.Add(pkg.Package{
|
||||
ID: "package-2-id",
|
||||
Name: "package-2",
|
||||
Version: "2.0.1",
|
||||
Locations: []source.Location{
|
||||
source.NewLocationFromImage(string(ref2.RealPath), *ref2, img),
|
||||
},
|
||||
Type: pkg.DebPkg,
|
||||
FoundBy: "the-cataloger-2",
|
||||
MetadataType: pkg.DpkgMetadataType,
|
||||
Metadata: pkg.DpkgMetadata{
|
||||
Package: "package-2",
|
||||
Version: "2.0.1",
|
||||
},
|
||||
PURL: "a-purl-2",
|
||||
CPEs: []pkg.CPE{
|
||||
must(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")),
|
||||
},
|
||||
})
|
||||
|
||||
// this is a hard coded value that is not given by the fixture helper and must be provided manually
|
||||
img.Metadata.ManifestDigest = "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368"
|
||||
|
||||
s, err := source.NewFromImage(img, "user-image-input")
|
||||
var d *distro.Distro
|
||||
pres := NewJSONPresenter(catalog, s.Metadata, d, source.SquashedScope)
|
||||
|
||||
// run presenter
|
||||
err = pres.Present(&buffer)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
actual := buffer.Bytes()
|
||||
|
||||
if *updateJSONGoldenFiles {
|
||||
testutils.UpdateGoldenFileContents(t, actual)
|
||||
}
|
||||
|
||||
var expected = testutils.GetGoldenFileContents(t)
|
||||
|
||||
if !bytes.Equal(expected, actual) {
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(string(expected), string(actual), true)
|
||||
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
|
||||
}
|
||||
catalog, metadata, dist := presenterImageInput(t, testImage)
|
||||
assertPresenterAgainstGoldenImageSnapshot(t,
|
||||
NewJSONPresenter(catalog, metadata, dist, source.SquashedScope),
|
||||
testImage,
|
||||
*updateJSONGoldenFiles,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package spdx22
|
||||
|
||||
import "time"
|
||||
|
||||
type AnnotationType string
|
||||
|
||||
const (
|
||||
ReviewerAnnotationType AnnotationType = "REVIEWER"
|
||||
OtherAnnotationType AnnotationType = "OTHER"
|
||||
)
|
||||
|
||||
type Annotation struct {
|
||||
// Identify when the comment was made. This is to be specified according to the combined date and time in the
|
||||
// UTC format, as specified in the ISO 8601 standard.
|
||||
AnnotationDate time.Time `json:"annotationDate"`
|
||||
// Type of the annotation
|
||||
AnnotationType AnnotationType `json:"annotationType"`
|
||||
// This field identifies the person, organization or tool that has commented on a file, package, or the entire document.
|
||||
Annotator string `json:"annotator"`
|
||||
Comment string `json:"comment"`
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package spdx22
|
||||
|
||||
type Checksum struct {
|
||||
// Identifies the algorithm used to produce the subject Checksum. One of: "SHA256", "SHA1", "SHA384", "MD2", "MD4", "SHA512", "MD6", "MD5", "SHA224"
|
||||
Algorithm string `json:"algorithm"`
|
||||
ChecksumValue string `json:"checksumValue"`
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package spdx22
|
||||
|
||||
import "time"
|
||||
|
||||
type CreationInfo struct {
|
||||
Comment string `json:"comment,omitempty"`
|
||||
// Identify when the SPDX file was originally created. The date is to be specified according to combined date and
|
||||
// time in UTC format as specified in ISO 8601 standard. This field is distinct from the fields in section 8,
|
||||
// which involves the addition of information during a subsequent review.
|
||||
Created time.Time `json:"created"`
|
||||
// Identify who (or what, in the case of a tool) created the SPDX file. If the SPDX file was created by an
|
||||
// individual, indicate the person's name. If the SPDX file was created on behalf of a company or organization,
|
||||
// indicate the entity name. If the SPDX file was created using a software tool, indicate the name and version
|
||||
// for that tool. If multiple participants or tools were involved, use multiple instances of this field. Person
|
||||
// name or organization name may be designated as “anonymous” if appropriate.
|
||||
Creators []string `json:"creators"`
|
||||
// An optional field for creators of the SPDX file to provide the version of the SPDX License List used when the SPDX file was created.
|
||||
LicenseListVersion string `json:"licenseListVersion"`
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package spdx22
|
||||
|
||||
// derived from:
|
||||
// - https://spdx.github.io/spdx-spec/appendix-III-RDF-data-model-implementation-and-identifier-syntax/
|
||||
// - https://github.com/spdx/spdx-spec/blob/v2.2/schemas/spdx-schema.json
|
||||
// - https://github.com/spdx/spdx-spec/tree/v2.2/ontology
|
||||
|
||||
type Document struct {
|
||||
Element
|
||||
SPDXVersion string `json:"spdxVersion"`
|
||||
// One instance is required for each SPDX file produced. It provides the necessary information for forward
|
||||
// and backward compatibility for processing tools.
|
||||
CreationInfo CreationInfo `json:"creationInfo"`
|
||||
// 2.2: Data License; should be "CC0-1.0"
|
||||
// Cardinality: mandatory, one
|
||||
// License expression for dataLicense. Compliance with the SPDX specification includes populating the SPDX
|
||||
// fields therein with data related to such fields (\"SPDX-Metadata\"). The SPDX specification contains numerous
|
||||
// fields where an SPDX document creator may provide relevant explanatory text in SPDX-Metadata. Without
|
||||
// opining on the lawfulness of \"database rights\" (in jurisdictions where applicable), such explanatory text
|
||||
// is copyrightable subject matter in most Berne Convention countries. By using the SPDX specification, or any
|
||||
// portion hereof, you hereby agree that any copyright rights (as determined by your jurisdiction) in any
|
||||
// SPDX-Metadata, including without limitation explanatory text, shall be subject to the terms of the Creative
|
||||
// Commons CC0 1.0 Universal license. For SPDX-Metadata not containing any copyright rights, you hereby agree
|
||||
// and acknowledge that the SPDX-Metadata is provided to you \"as-is\" and without any representations or
|
||||
// warranties of any kind concerning the SPDX-Metadata, express, implied, statutory or otherwise, including
|
||||
// without limitation warranties of title, merchantability, fitness for a particular purpose, non-infringement,
|
||||
// or the absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not
|
||||
// discoverable, all to the greatest extent permissible under applicable law.
|
||||
DataLicense string `json:"dataLicense"`
|
||||
// Information about an external SPDX document reference including the checksum. This allows for verification of the external references.
|
||||
ExternalDocumentRefs []ExternalDocumentRef `json:"externalDocumentRefs,omitempty"`
|
||||
// Indicates that a particular ExtractedLicensingInfo was defined in the subject SpdxDocument.
|
||||
HasExtractedLicensingInfos []HasExtractedLicensingInfo `json:"hasExtractedLicensingInfos,omitempty"`
|
||||
// note: found in example documents from SPDX, but not in the JSON schema. See https://spdx.github.io/spdx-spec/2-document-creation-information/#25-spdx-document-namespace
|
||||
DocumentNamespace string `json:"documentNamespace"`
|
||||
// note: found in example documents from SPDX, but not in the JSON schema
|
||||
// DocumentDescribes []string `json:"documentDescribes"`
|
||||
Packages []Package `json:"packages"`
|
||||
// Files referenced in the SPDX document
|
||||
Files []File `json:"files,omitempty"`
|
||||
// Snippets referenced in the SPDX document
|
||||
Snippets []Snippet `json:"snippets,omitempty"`
|
||||
// Relationships referenced in the SPDX document
|
||||
Relationships []Relationship `json:"relationships,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package spdx22
|
||||
|
||||
type Element struct {
|
||||
SPDXID string `json:"SPDXID"`
|
||||
// Identify name of this SpdxElement.
|
||||
Name string `json:"name"`
|
||||
// Relationships referenced in the SPDX document
|
||||
Relationships []Relationship `json:"relationships,omitempty"`
|
||||
// Provide additional information about an SpdxElement.
|
||||
Annotations []Annotation `json:"annotations,omitempty"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package spdx22
|
||||
|
||||
// ElementID represents the identifier string portion of an SPDX element
|
||||
// identifier. DocElementID should be used for any attributes which can
|
||||
// contain identifiers defined in a different SPDX document.
|
||||
// ElementIDs should NOT contain the mandatory 'SPDXRef-' portion.
|
||||
type ElementID string
|
||||
|
||||
func (e ElementID) String() string {
|
||||
return "SPDXRef-" + string(e)
|
||||
}
|
||||
|
||||
// DocElementID represents an SPDX element identifier that could be defined
|
||||
// in a different SPDX document, and therefore could have a "DocumentRef-"
|
||||
// portion, such as Relationship and Annotations.
|
||||
// ElementID is used for attributes in which a "DocumentRef-" portion cannot
|
||||
// appear, such as a Package or File definition (since it is necessarily
|
||||
// being defined in the present document).
|
||||
// DocumentRefID will be the empty string for elements defined in the
|
||||
// present document.
|
||||
// DocElementIDs should NOT contain the mandatory 'DocumentRef-' or
|
||||
// 'SPDXRef-' portions.
|
||||
type DocElementID struct {
|
||||
DocumentRefID string
|
||||
ElementRefID ElementID
|
||||
}
|
||||
|
||||
// RenderDocElementID takes a DocElementID and returns the string equivalent,
|
||||
// with the SPDXRef- prefix (and, if applicable, the DocumentRef- prefix)
|
||||
// reinserted.
|
||||
func (d DocElementID) String() string {
|
||||
prefix := ""
|
||||
if d.DocumentRefID != "" {
|
||||
prefix = "DocumentRef-" + d.DocumentRefID + ":"
|
||||
}
|
||||
return prefix + d.ElementRefID.String()
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package spdx22
|
||||
|
||||
type ExternalDocumentRef struct {
|
||||
// externalDocumentId is a string containing letters, numbers, ., - and/or + which uniquely identifies an external document within this document.
|
||||
ExternalDocumentID string `json:"externalDocumentId"`
|
||||
Checksum Checksum `json:"checksum"`
|
||||
// SPDX ID for SpdxDocument. A propoerty containing an SPDX document.
|
||||
SpdxDocument string `json:"spdxDocument"`
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package spdx22
|
||||
|
||||
type ReferenceCategory string
|
||||
|
||||
const (
|
||||
SecurityReferenceCategory ReferenceCategory = "SECURITY"
|
||||
PackageManagerReferenceCategory ReferenceCategory = "PACKAGE_MANAGER"
|
||||
OtherReferenceCategory ReferenceCategory = "OTHER"
|
||||
)
|
||||
|
||||
// source: https://spdx.github.io/spdx-spec/appendix-VI-external-repository-identifiers/
|
||||
|
||||
type ExternalRefType string
|
||||
|
||||
const (
|
||||
// see https://nvd.nist.gov/cpe
|
||||
Cpe22ExternalRefType ExternalRefType = "cpe22Type"
|
||||
// see https://nvd.nist.gov/cpe
|
||||
Cpe23ExternalRefType ExternalRefType = "cpe23Type"
|
||||
// see http://repo1.maven.org/maven2/
|
||||
MavenCentralExternalRefType ExternalRefType = "maven-central"
|
||||
// see https://www.npmjs.com/
|
||||
NpmExternalRefType ExternalRefType = "npm"
|
||||
// see https://www.nuget.org/
|
||||
NugetExternalRefType ExternalRefType = "nuget"
|
||||
// see http://bower.io/
|
||||
BowerExternalRefType ExternalRefType = "bower"
|
||||
// see https://github.com/package-url/purl-spec
|
||||
PurlExternalRefType ExternalRefType = "purl"
|
||||
// These point to objects present in the Software Heritage archive by the means of SoftWare Heritage persistent Identifiers (SWHID)
|
||||
SwhExternalRefType ExternalRefType = "swh"
|
||||
)
|
||||
|
||||
type ExternalRef struct {
|
||||
Comment string `json:"comment,omitempty"`
|
||||
// Category for the external reference.
|
||||
ReferenceCategory ReferenceCategory `json:"referenceCategory"`
|
||||
// The unique string with no spaces necessary to access the package-specific information, metadata, or content
|
||||
// within the target location. The format of the locator is subject to constraints defined by the <type>.
|
||||
ReferenceLocator string `json:"referenceLocator"`
|
||||
// Type of the external reference. These are defined in an appendix in the SPDX specification.
|
||||
ReferenceType ExternalRefType `json:"referenceType"`
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package spdx22
|
||||
|
||||
type FileType string
|
||||
|
||||
const (
|
||||
DocumentationFileType FileType = "DOCUMENTATION"
|
||||
ImageFileType FileType = "IMAGE"
|
||||
VideoFileType FileType = "VIDEO"
|
||||
ArchiveFileType FileType = "ARCHIVE"
|
||||
SpdxFileType FileType = "SPDX"
|
||||
ApplicationFileType FileType = "APPLICATION"
|
||||
SourceFileType FileType = "SOURCE"
|
||||
BinaryFileType FileType = "BINARY"
|
||||
TextFileType FileType = "TEXT"
|
||||
AudioFileType FileType = "AUDIO"
|
||||
OtherFileType FileType = "OTHER"
|
||||
)
|
||||
|
||||
type File struct {
|
||||
Item
|
||||
// (At least one is required.) The checksum property provides a mechanism that can be used to verify that the
|
||||
// contents of a File or Package have not changed.
|
||||
Checksums []Checksum `json:"checksums,omitempty"`
|
||||
// This field provides a place for the SPDX file creator to record file contributors. Contributors could include
|
||||
// names of copyright holders and/or authors who may not be copyright holders yet contributed to the file content.
|
||||
FileContributors []string `json:"fileContributors,omitempty"`
|
||||
// Each element is a SPDX ID for a File.
|
||||
FileDependencies []string `json:"fileDependencies,omitempty"`
|
||||
// The name of the file relative to the root of the package.
|
||||
FileName string `json:"fileName"`
|
||||
// The type of the file
|
||||
FileTypes []string `json:"fileTypes,omitempty"`
|
||||
// This field provides a place for the SPDX file creator to record potential legal notices found in the file.
|
||||
// This may or may not include copyright statements.
|
||||
NoticeText string `json:"noticeText,omitempty"`
|
||||
// Indicates the project in which the SpdxElement originated. Tools must preserve doap:homepage and doap:name
|
||||
// properties and the URI (if one is known) of doap:Project resources that are values of this property. All other
|
||||
// properties of doap:Projects are not directly supported by SPDX and may be dropped when translating to or
|
||||
// from some SPDX formats(deprecated).
|
||||
ArtifactOf []string `json:"artifactOf,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package spdx22
|
||||
|
||||
type HasExtractedLicensingInfo struct {
|
||||
// Verbatim license or licensing notice text that was discovered.
|
||||
ExtractedText string `json:"extractedText"`
|
||||
// A human readable short form license identifier for a license. The license ID is iether on the standard license
|
||||
// oist or the form \"LicenseRef-\"[idString] where [idString] is a unique string containing letters,
|
||||
// numbers, \".\", \"-\" or \"+\".
|
||||
LicenseID string `json:"licenseId"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
// Identify name of this SpdxElement.
|
||||
Name string `json:"name,omitempty"`
|
||||
SeeAlsos []string `json:"seeAlsos,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package spdx22
|
||||
|
||||
type Item struct {
|
||||
Element
|
||||
// The licenseComments property allows the preparer of the SPDX document to describe why the licensing in
|
||||
// spdx:licenseConcluded was chosen.
|
||||
LicenseComments string `json:"licenseComments,omitempty"`
|
||||
LicenseConcluded string `json:"licenseConcluded"`
|
||||
// The licensing information that was discovered directly within the package. There will be an instance of this
|
||||
// property for each distinct value of alllicenseInfoInFile properties of all files contained in the package.
|
||||
LicenseInfoFromFiles []string `json:"licenseInfoFromFiles,omitempty"`
|
||||
// Licensing information that was discovered directly in the subject file. This is also considered a declared license for the file.
|
||||
LicenseInfoInFiles []string `json:"licenseInfoInFiles,omitempty"`
|
||||
// The text of copyright declarations recited in the Package or File.
|
||||
CopyrightText string `json:"copyrightText,omitempty"`
|
||||
// This field provides a place for the SPDX data creator to record acknowledgements that may be required to be
|
||||
// communicated in some contexts. This is not meant to include the actual complete license text (see
|
||||
// licenseConculded and licenseDeclared), and may or may not include copyright notices (see also copyrightText).
|
||||
// The SPDX data creator may use this field to record other acknowledgements, such as particular clauses from
|
||||
// license texts, which may be necessary or desirable to reproduce.
|
||||
AttributionTexts []string `json:"attributionTexts,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package spdx22
|
||||
|
||||
type Package struct {
|
||||
Item
|
||||
// The checksum property provides a mechanism that can be used to verify that the contents of a File or
|
||||
// Package have not changed.
|
||||
Checksums []Checksum `json:"checksums,omitempty"`
|
||||
// Provides a detailed description of the package.
|
||||
Description string `json:"description,omitempty"`
|
||||
// The URI at which this package is available for download. Private (i.e., not publicly reachable) URIs are
|
||||
// acceptable as values of this property. The values http://spdx.org/rdf/terms#none and http://spdx.org/rdf/terms#noassertion
|
||||
// may be used to specify that the package is not downloadable or that no attempt was made to determine its
|
||||
// download location, respectively.
|
||||
DownloadLocation string `json:"downloadLocation,omitempty"`
|
||||
// An External Reference allows a Package to reference an external source of additional information, metadata,
|
||||
// enumerations, asset identifiers, or downloadable content believed to be relevant to the Package.
|
||||
ExternalRefs []ExternalRef `json:"externalRefs,omitempty"`
|
||||
// Indicates whether the file content of this package has been available for or subjected to analysis when
|
||||
// creating the SPDX document. If false indicates packages that represent metadata or URI references to a
|
||||
// project, product, artifact, distribution or a component. If set to false, the package must not contain any files
|
||||
FilesAnalyzed bool `json:"filesAnalyzed"`
|
||||
// Indicates that a particular file belongs to a package (elements are SPDX ID for a File).
|
||||
HasFiles []string `json:"hasFiles,omitempty"`
|
||||
// Provide a place for the SPDX file creator to record a web site that serves as the package's home page.
|
||||
// This link can also be used to reference further information about the package referenced by the SPDX file creator.
|
||||
Homepage string `json:"homepage,omitempty"`
|
||||
// List the licenses that have been declared by the authors of the package. Any license information that does not
|
||||
// originate from the package authors, e.g. license information from a third party repository, should not be included in this field.
|
||||
LicenseDeclared string `json:"licenseDeclared"`
|
||||
// The name and, optionally, contact information of the person or organization that originally created the package.
|
||||
// Values of this property must conform to the agent and tool syntax.
|
||||
Originator string `json:"originator,omitempty"`
|
||||
// The base name of the package file name. For example, zlib-1.2.5.tar.gz.
|
||||
PackageFileName string `json:"packageFileName,omitempty"`
|
||||
// A manifest based verification code (the algorithm is defined in section 4.7 of the full specification) of the
|
||||
// SPDX Item. This allows consumers of this data and/or database to determine if an SPDX item they have in hand
|
||||
// is identical to the SPDX item from which the data was produced. This algorithm works even if the SPDX document
|
||||
// is included in the SPDX item.
|
||||
PackageVerificationCode *PackageVerificationCode `json:"packageVerificationCode,omitempty"`
|
||||
// Allows the producer(s) of the SPDX document to describe how the package was acquired and/or changed from the original source.
|
||||
SourceInfo string `json:"sourceInfo,omitempty"`
|
||||
// Provides a short description of the package.
|
||||
Summary string `json:"summary,omitempty"`
|
||||
// The name and, optionally, contact information of the person or organization who was the immediate supplier
|
||||
// of this package to the recipient. The supplier may be different than originator when the software has been
|
||||
// repackaged. Values of this property must conform to the agent and tool syntax.
|
||||
Supplier string `json:"supplier,omitempty"`
|
||||
// Provides an indication of the version of the package that is described by this SpdxDocument.
|
||||
VersionInfo string `json:"versionInfo,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package spdx22
|
||||
|
||||
// Why are there two package identifier fields Package Checksum and Package Verification?
|
||||
// Although the values of the two fields Package Checksum and Package Verification are similar, they each serve a
|
||||
// different purpose. The Package Checksum provides a unique identifier of a software package which is computed by
|
||||
// taking the SHA1 of the entire software package file. This enables one to quickly determine if two different copies
|
||||
// of a package are the same. One disadvantage of this approach is that one cannot add an SPDX data file into the
|
||||
// original package without changing the Package Checksum value. Alternatively, the Package Verification field enables
|
||||
// the inclusion of an SPDX file. It enables one to quickly verify if one or more of the original package files has
|
||||
// changed. The Package Verification field is a unique identifier that is based on SHAing only the original package
|
||||
// files (e.g., excluding the SPDX file). This allows one to add an SPDX file to the original package without changing
|
||||
// this unique identifier.
|
||||
// source: https://wiki.spdx.org/view/SPDX_FAQ
|
||||
type PackageVerificationCode struct {
|
||||
// "A file that was excluded when calculating the package verification code. This is usually a file containing
|
||||
// SPDX data regarding the package. If a package contains more than one SPDX file all SPDX files must be excluded
|
||||
// from the package verification code. If this is not done it would be impossible to correctly calculate the
|
||||
// verification codes in both files.
|
||||
PackageVerificationCodeExcludedFiles []string `json:"packageVerificationCodeExcludedFiles"`
|
||||
|
||||
// The actual package verification code as a hex encoded value.
|
||||
PackageVerificationCodeValue string `json:"packageVerificationCodeValue"`
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
package spdx22
|
||||
|
||||
type Relationship struct {
|
||||
// Id to which the SPDX element is related
|
||||
SpdxElementID string `json:"spdxElementId"`
|
||||
// Describes the type of relationship between two SPDX elements.
|
||||
RelationshipType RelationshipType `json:"relationshipType"`
|
||||
// SPDX ID for SpdxElement. A related SpdxElement.
|
||||
RelatedSpdxElement string `json:"relatedSpdxElement"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// source: https://spdx.github.io/spdx-spec/7-relationships-between-SPDX-elements/
|
||||
type RelationshipType string
|
||||
|
||||
const (
|
||||
// DescribedByRelationship is to be used when SPDXRef-A is described by SPDXREF-Document.
|
||||
// Example: The package 'WildFly' is described by SPDX document WildFly.spdx.
|
||||
DescribedByRelationship RelationshipType = "DESCRIBED_BY"
|
||||
|
||||
// ContainsRelationship is to be used when SPDXRef-A contains SPDXRef-B.
|
||||
// Example: An ARCHIVE file bar.tgz contains a SOURCE file foo.c.
|
||||
ContainsRelationship RelationshipType = "CONTAINS"
|
||||
|
||||
// ContainedByRelationship is to be used when SPDXRef-A is contained by SPDXRef-B.
|
||||
// Example: A SOURCE file foo.c is contained by ARCHIVE file bar.tgz
|
||||
ContainedByRelationship RelationshipType = "CONTAINED_BY"
|
||||
|
||||
// DependsOnRelationship is to be used when SPDXRef-A depends on SPDXRef-B.
|
||||
// Example: Package A depends on the presence of package B in order to build and run
|
||||
DependsOnRelationship RelationshipType = "DEPENDS_ON"
|
||||
|
||||
// DependencyOfRelationship is to be used when SPDXRef-A is dependency of SPDXRef-B.
|
||||
// Example: A is explicitly stated as a dependency of B in a machine-readable file. Use when a package manager does not define scopes.
|
||||
DependencyOfRelationship RelationshipType = "DEPENDENCY_OF"
|
||||
|
||||
// DependencyManifestOfRelationship is to be used when SPDXRef-A is a manifest file that lists a set of dependencies for SPDXRef-B.
|
||||
// Example: A file package.json is the dependency manifest of a package foo. Note that only one manifest should be used to define the same dependency graph.
|
||||
DependencyManifestOfRelationship RelationshipType = "DEPENDENCY_MANIFEST_OF"
|
||||
|
||||
// BuildDependencyOfRelationship is to be used when SPDXRef-A is a build dependency of SPDXRef-B.
|
||||
// Example: A is in the compile scope of B in a Maven project.
|
||||
BuildDependencyOfRelationship RelationshipType = "BUILD_DEPENDENCY_OF"
|
||||
|
||||
// DevDependencyOfRelationship is to be used when SPDXRef-A is a development dependency of SPDXRef-B.
|
||||
// Example: A is in the devDependencies scope of B in a Maven project.
|
||||
DevDependencyOfRelationship RelationshipType = "DEV_DEPENDENCY_OF"
|
||||
|
||||
// OptionalDependencyOfRelationship is to be used when SPDXRef-A is an optional dependency of SPDXRef-B.
|
||||
// Example: Use when building the code will proceed even if a dependency cannot be found, fails to install, or is only installed on a specific platform. For example, A is in the optionalDependencies scope of npm project B.
|
||||
OptionalDependencyOfRelationship RelationshipType = "OPTIONAL_DEPENDENCY_OF"
|
||||
|
||||
// ProvidedDependencyOfRelationship is to be used when SPDXRef-A is a to be provided dependency of SPDXRef-B.
|
||||
// Example: A is in the provided scope of B in a Maven project, indicating that the project expects it to be provided, for instance, by the container or JDK.
|
||||
ProvidedDependencyOfRelationship RelationshipType = "PROVIDED_DEPENDENCY_OF"
|
||||
|
||||
// TestDependencyOfRelationship is to be used when SPDXRef-A is a test dependency of SPDXRef-B.
|
||||
// Example: A is in the test scope of B in a Maven project.
|
||||
TestDependencyOfRelationship RelationshipType = "TEST_DEPENDENCY_OF"
|
||||
|
||||
// RuntimeDependencyOfRelationship is to be used when SPDXRef-A is a dependency required for the execution of SPDXRef-B.
|
||||
// Example: A is in the runtime scope of B in a Maven project.
|
||||
RuntimeDependencyOfRelationship RelationshipType = "RUNTIME_DEPENDENCY_OF"
|
||||
|
||||
// ExampleOfRelationship is to be used when SPDXRef-A is an example of SPDXRef-B.
|
||||
// Example: The file or snippet that illustrates how to use an application or library.
|
||||
ExampleOfRelationship RelationshipType = "EXAMPLE_OF"
|
||||
|
||||
// GeneratesRelationship is to be used when SPDXRef-A generates SPDXRef-B.
|
||||
// Example: A SOURCE file makefile.mk generates a BINARY file a.out
|
||||
GeneratesRelationship RelationshipType = "GENERATES"
|
||||
|
||||
// GeneratedFromRelationship is to be used when SPDXRef-A was generated from SPDXRef-B.
|
||||
// Example: A BINARY file a.out has been generated from a SOURCE file makefile.mk. A BINARY file foolib.a is generated from a SOURCE file bar.c.
|
||||
GeneratedFromRelationship RelationshipType = "GENERATED_FROM"
|
||||
|
||||
// AncestorOfRelationship is to be used when SPDXRef-A is an ancestor (same lineage but pre-dates) SPDXRef-B.
|
||||
// Example: A SOURCE file makefile.mk is a version of the original ancestor SOURCE file 'makefile2.mk'
|
||||
AncestorOfRelationship RelationshipType = "ANCESTOR_OF"
|
||||
|
||||
// DescendantOfRelationship is to be used when SPDXRef-A is a descendant of (same lineage but postdates) SPDXRef-B.
|
||||
// Example: A SOURCE file makefile2.mk is a descendant of the original SOURCE file 'makefile.mk'
|
||||
DescendantOfRelationship RelationshipType = "DESCENDANT_OF"
|
||||
|
||||
// VariantOfRelationship is to be used when SPDXRef-A is a variant of (same lineage but not clear which came first) SPDXRef-B.
|
||||
// Example: A SOURCE file makefile2.mk is a variant of SOURCE file makefile.mk if they differ by some edit, but there is no way to tell which came first (no reliable date information).
|
||||
VariantOfRelationship RelationshipType = "VARIANT_OF"
|
||||
|
||||
// DistributionArtifactRelationship is to be used when distributing SPDXRef-A requires that SPDXRef-B also be distributed.
|
||||
// Example: A BINARY file foo.o requires that the ARCHIVE file bar-sources.tgz be made available on distribution.
|
||||
DistributionArtifactRelationship RelationshipType = "DISTRIBUTION_ARTIFACT"
|
||||
|
||||
// PatchForRelationship is to be used when SPDXRef-A is a patch file for (to be applied to) SPDXRef-B.
|
||||
// Example: A SOURCE file foo.diff is a patch file for SOURCE file foo.c.
|
||||
PatchForRelationship RelationshipType = "PATCH_FOR"
|
||||
|
||||
// PatchAppliedRelationship is to be used when SPDXRef-A is a patch file that has been applied to SPDXRef-B.
|
||||
// Example: A SOURCE file foo.diff is a patch file that has been applied to SOURCE file 'foo-patched.c'.
|
||||
PatchAppliedRelationship RelationshipType = "PATCH_APPLIED"
|
||||
|
||||
// CopyOfRelationship is to be used when SPDXRef-A is an exact copy of SPDXRef-B.
|
||||
// Example: A BINARY file alib.a is an exact copy of BINARY file a2lib.a.
|
||||
CopyOfRelationship RelationshipType = "COPY_OF"
|
||||
|
||||
// FileAddedRelationship is to be used when SPDXRef-A is a file that was added to SPDXRef-B.
|
||||
// Example: A SOURCE file foo.c has been added to package ARCHIVE bar.tgz.
|
||||
FileAddedRelationship RelationshipType = "FILE_ADDED"
|
||||
|
||||
// FileDeletedRelationship is to be used when SPDXRef-A is a file that was deleted from SPDXRef-B.
|
||||
// Example: A SOURCE file foo.diff has been deleted from package ARCHIVE bar.tgz.
|
||||
FileDeletedRelationship RelationshipType = "FILE_DELETED"
|
||||
|
||||
// FileModifiedRelationship is to be used when SPDXRef-A is a file that was modified from SPDXRef-B.
|
||||
// Example: A SOURCE file foo.c has been modified from SOURCE file foo.orig.c.
|
||||
FileModifiedRelationship RelationshipType = "FILE_MODIFIED"
|
||||
|
||||
// ExpandedFromArchiveRelationship is to be used when SPDXRef-A is expanded from the archive SPDXRef-B.
|
||||
// Example: A SOURCE file foo.c, has been expanded from the archive ARCHIVE file xyz.tgz.
|
||||
ExpandedFromArchiveRelationship RelationshipType = "EXPANDED_FROM_ARCHIVE"
|
||||
|
||||
// DynamicLinkRelationship is to be used when SPDXRef-A dynamically links to SPDXRef-B.
|
||||
// Example: An APPLICATION file 'myapp' dynamically links to BINARY file zlib.so.
|
||||
DynamicLinkRelationship RelationshipType = "DYNAMIC_LINK"
|
||||
|
||||
// StaticLinkRelationship is to be used when SPDXRef-A statically links to SPDXRef-B.
|
||||
// Example: An APPLICATION file 'myapp' statically links to BINARY zlib.a.
|
||||
StaticLinkRelationship RelationshipType = "STATIC_LINK"
|
||||
|
||||
// DataFileOfRelationship is to be used when SPDXRef-A is a data file used in SPDXRef-B.
|
||||
// Example: An IMAGE file 'kitty.jpg' is a data file of an APPLICATION 'hellokitty'.
|
||||
DataFileOfRelationship RelationshipType = "DATA_FILE_OF"
|
||||
|
||||
// TestCaseOfRelationship is to be used when SPDXRef-A is a test case used in testing SPDXRef-B.
|
||||
// Example: A SOURCE file testMyCode.java is a unit test file used to test an APPLICATION MyPackage.
|
||||
TestCaseOfRelationship RelationshipType = "TEST_CASE_OF"
|
||||
|
||||
// BuildToolOfRelationship is to be used when SPDXRef-A is used to build SPDXRef-B.
|
||||
// Example: A SOURCE file makefile.mk is used to build an APPLICATION 'zlib'.
|
||||
BuildToolOfRelationship RelationshipType = "BUILD_TOOL_OF"
|
||||
|
||||
// DevToolOfRelationship is to be used when SPDXRef-A is used as a development tool for SPDXRef-B.
|
||||
// Example: Any tool used for development such as a code debugger.
|
||||
DevToolOfRelationship RelationshipType = "DEV_TOOL_OF"
|
||||
|
||||
// TestOfRelationship is to be used when SPDXRef-A is used for testing SPDXRef-B.
|
||||
// Example: Generic relationship for cases where it's clear that something is used for testing but unclear whether it's TEST_CASE_OF or TEST_TOOL_OF.
|
||||
TestOfRelationship RelationshipType = "TEST_OF"
|
||||
|
||||
// TestToolOfRelationship is to be used when SPDXRef-A is used as a test tool for SPDXRef-B.
|
||||
// Example: Any tool used to test the code such as ESlint.
|
||||
TestToolOfRelationship RelationshipType = "TEST_TOOL_OF"
|
||||
|
||||
// DocumentationOfRelationship is to be used when SPDXRef-A provides documentation of SPDXRef-B.
|
||||
// Example: A DOCUMENTATION file readme.txt documents the APPLICATION 'zlib'.
|
||||
DocumentationOfRelationship RelationshipType = "DOCUMENTATION_OF"
|
||||
|
||||
// OptionalComponentOfRelationship is to be used when SPDXRef-A is an optional component of SPDXRef-B.
|
||||
// Example: A SOURCE file fool.c (which is in the contributors directory) may or may not be included in the build of APPLICATION 'atthebar'.
|
||||
OptionalComponentOfRelationship RelationshipType = "OPTIONAL_COMPONENT_OF"
|
||||
|
||||
// MetafileOfRelationship is to be used when SPDXRef-A is a metafile of SPDXRef-B.
|
||||
// Example: A SOURCE file pom.xml is a metafile of the APPLICATION 'Apache Xerces'.
|
||||
MetafileOfRelationship RelationshipType = "METAFILE_OF"
|
||||
|
||||
// PackageOfRelationship is to be used when SPDXRef-A is used as a package as part of SPDXRef-B.
|
||||
// Example: A Linux distribution contains an APPLICATION package gawk as part of the distribution MyLinuxDistro.
|
||||
PackageOfRelationship RelationshipType = "PACKAGE_OF"
|
||||
|
||||
// AmendsRelationship is to be used when (current) SPDXRef-DOCUMENT amends the SPDX information in SPDXRef-B.
|
||||
// Example: (Current) SPDX document A version 2 contains a correction to a previous version of the SPDX document A version 1. Note the reserved identifier SPDXRef-DOCUMENT for the current document is required.
|
||||
AmendsRelationship RelationshipType = "AMENDS"
|
||||
|
||||
// PrerequisiteForRelationship is to be used when SPDXRef-A is a prerequisite for SPDXRef-B.
|
||||
// Example: A library bar.dll is a prerequisite or dependency for APPLICATION foo.exe
|
||||
PrerequisiteForRelationship RelationshipType = "PREREQUISITE_FOR"
|
||||
|
||||
// HasPrerequisiteRelationship is to be used when SPDXRef-A has as a prerequisite SPDXRef-B.
|
||||
// Example: An APPLICATION foo.exe has prerequisite or dependency on bar.dll
|
||||
HasPrerequisiteRelationship RelationshipType = "HAS_PREREQUISITE"
|
||||
|
||||
// OtherRelationship is to be used for a relationship which has not been defined in the formal SPDX specification. A description of the relationship should be included in the Relationship comments field.
|
||||
OtherRelationship RelationshipType = "OTHER"
|
||||
)
|
||||
@@ -0,0 +1,32 @@
|
||||
package spdx22
|
||||
|
||||
type StartPointer struct {
|
||||
Offset int `json:"offset,omitempty"`
|
||||
LineNumber int `json:"lineNumber,omitempty"`
|
||||
// SPDX ID for File
|
||||
Reference string `json:"reference"`
|
||||
}
|
||||
|
||||
type EndPointer struct {
|
||||
Offset int `json:"offset,omitempty"`
|
||||
LineNumber int `json:"lineNumber,omitempty"`
|
||||
// SPDX ID for File
|
||||
Reference string `json:"reference"`
|
||||
}
|
||||
|
||||
type Range struct {
|
||||
StartPointer StartPointer `json:"startPointer"`
|
||||
EndPointer EndPointer `json:"endPointer"`
|
||||
}
|
||||
|
||||
type Snippet struct {
|
||||
Item
|
||||
// Licensing information that was discovered directly in the subject snippet. This is also considered a declared
|
||||
// license for the snippet. (elements are license expressions)
|
||||
LicenseInfoInSnippets []string `json:"licenseInfoInSnippets"`
|
||||
// SPDX ID for File. File containing the SPDX element (e.g. the file contaning a snippet).
|
||||
SnippetFromFile string `json:"snippetFromFile"`
|
||||
// (At least 1 range is required). This field defines the byte range in the original host file (in X.2) that the
|
||||
// snippet information applies to.
|
||||
Ranges []Range `json:"ranges"`
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package spdx22
|
||||
|
||||
const Version = "SPDX-2.2"
|
||||
@@ -1,25 +0,0 @@
|
||||
/*
|
||||
Defines a Presenter interface for displaying catalog results to an io.Writer as well as a helper utility to obtain
|
||||
a specific Presenter implementation given user configuration.
|
||||
*/
|
||||
package packages
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/internal/presenter"
|
||||
)
|
||||
|
||||
// Presenter returns a presenter for images or directories
|
||||
func Presenter(option PresenterOption, config PresenterConfig) presenter.Presenter {
|
||||
switch option {
|
||||
case JSONPresenterOption:
|
||||
return NewJSONPresenter(config.Catalog, config.SourceMetadata, config.Distro, config.Scope)
|
||||
case TextPresenterOption:
|
||||
return NewTextPresenter(config.Catalog, config.SourceMetadata)
|
||||
case TablePresenterOption:
|
||||
return NewTablePresenter(config.Catalog)
|
||||
case CycloneDxPresenterOption:
|
||||
return NewCycloneDxPresenter(config.Catalog, config.SourceMetadata)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package packages
|
||||
|
||||
import "strings"
|
||||
|
||||
const (
|
||||
UnknownPresenterOption PresenterOption = "UnknownPresenterOption"
|
||||
JSONPresenterOption PresenterOption = "json"
|
||||
TextPresenterOption PresenterOption = "text"
|
||||
TablePresenterOption PresenterOption = "table"
|
||||
CycloneDxPresenterOption PresenterOption = "cyclonedx"
|
||||
)
|
||||
|
||||
var AllPresenters = []PresenterOption{
|
||||
JSONPresenterOption,
|
||||
TextPresenterOption,
|
||||
TablePresenterOption,
|
||||
CycloneDxPresenterOption,
|
||||
}
|
||||
|
||||
type PresenterOption string
|
||||
|
||||
func ParsePresenterOption(userStr string) PresenterOption {
|
||||
switch strings.ToLower(userStr) {
|
||||
case string(JSONPresenterOption):
|
||||
return JSONPresenterOption
|
||||
case string(TextPresenterOption):
|
||||
return TextPresenterOption
|
||||
case string(TablePresenterOption):
|
||||
return TablePresenterOption
|
||||
case string(CycloneDxPresenterOption), "cyclone", "cyclone-dx":
|
||||
return CycloneDxPresenterOption
|
||||
default:
|
||||
return UnknownPresenterOption
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
package packages
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/internal/presenter/packages/model/spdx22"
|
||||
"github.com/anchore/syft/internal/spdxlicense"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func getSPDXExternalRefs(p *pkg.Package) (externalRefs []spdx22.ExternalRef) {
|
||||
externalRefs = make([]spdx22.ExternalRef, 0)
|
||||
for _, c := range p.CPEs {
|
||||
externalRefs = append(externalRefs, spdx22.ExternalRef{
|
||||
ReferenceCategory: spdx22.SecurityReferenceCategory,
|
||||
ReferenceLocator: c.BindToFmtString(),
|
||||
ReferenceType: spdx22.Cpe23ExternalRefType,
|
||||
})
|
||||
}
|
||||
|
||||
if p.PURL != "" {
|
||||
externalRefs = append(externalRefs, spdx22.ExternalRef{
|
||||
ReferenceCategory: spdx22.PackageManagerReferenceCategory,
|
||||
ReferenceLocator: p.PURL,
|
||||
ReferenceType: spdx22.PurlExternalRefType,
|
||||
})
|
||||
}
|
||||
return externalRefs
|
||||
}
|
||||
|
||||
func getSPDXFiles(packageSpdxID string, p *pkg.Package) (files []spdx22.File, fileIDs []string, relationships []spdx22.Relationship) {
|
||||
files = make([]spdx22.File, 0)
|
||||
fileIDs = make([]string, 0)
|
||||
relationships = make([]spdx22.Relationship, 0)
|
||||
|
||||
pkgFileOwner, ok := p.Metadata.(pkg.FileOwner)
|
||||
if !ok {
|
||||
return files, fileIDs, relationships
|
||||
}
|
||||
|
||||
for _, ownedFilePath := range pkgFileOwner.OwnedFiles() {
|
||||
baseFileName := filepath.Base(ownedFilePath)
|
||||
pathHash := sha256.Sum256([]byte(ownedFilePath))
|
||||
fileSpdxID := spdx22.ElementID(fmt.Sprintf("File-%s-%x", p.Name, pathHash)).String()
|
||||
|
||||
fileIDs = append(fileIDs, fileSpdxID)
|
||||
|
||||
files = append(files, spdx22.File{
|
||||
FileName: ownedFilePath,
|
||||
Item: spdx22.Item{
|
||||
Element: spdx22.Element{
|
||||
SPDXID: fileSpdxID,
|
||||
Name: baseFileName,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
relationships = append(relationships, spdx22.Relationship{
|
||||
SpdxElementID: packageSpdxID,
|
||||
RelationshipType: spdx22.ContainsRelationship,
|
||||
RelatedSpdxElement: fileSpdxID,
|
||||
})
|
||||
}
|
||||
|
||||
return files, fileIDs, relationships
|
||||
}
|
||||
|
||||
func getSPDXLicense(p *pkg.Package) string {
|
||||
// source: https://spdx.github.io/spdx-spec/3-package-information/#313-concluded-license
|
||||
// The options to populate this field are limited to:
|
||||
// A valid SPDX License Expression as defined in Appendix IV;
|
||||
// NONE, if the SPDX file creator concludes there is no license available for this package; or
|
||||
// NOASSERTION if:
|
||||
// (i) the SPDX file creator has attempted to but cannot reach a reasonable objective determination;
|
||||
// (ii) the SPDX file creator has made no attempt to determine this field; or
|
||||
// (iii) the SPDX file creator has intentionally provided no information (no meaning should be implied by doing so).
|
||||
|
||||
if len(p.Licenses) == 0 {
|
||||
return "NONE"
|
||||
}
|
||||
|
||||
// take all licenses and assume an AND expression; for information about license expressions see https://spdx.github.io/spdx-spec/appendix-IV-SPDX-license-expressions/
|
||||
var parsedLicenses []string
|
||||
for _, l := range p.Licenses {
|
||||
if value, exists := spdxlicense.ID(l); exists {
|
||||
parsedLicenses = append(parsedLicenses, value)
|
||||
}
|
||||
}
|
||||
|
||||
if len(parsedLicenses) == 0 {
|
||||
return "NOASSERTION"
|
||||
}
|
||||
|
||||
return strings.Join(parsedLicenses, " AND ")
|
||||
}
|
||||
|
||||
func noneIfEmpty(value string) string {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return "NONE"
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func getSPDXDownloadLocation(p *pkg.Package) string {
|
||||
// 3.7: Package Download Location
|
||||
// Cardinality: mandatory, one
|
||||
// NONE if there is no download location whatsoever.
|
||||
// NOASSERTION if:
|
||||
// (i) the SPDX file creator has attempted to but cannot reach a reasonable objective determination;
|
||||
// (ii) the SPDX file creator has made no attempt to determine this field; or
|
||||
// (iii) the SPDX file creator has intentionally provided no information (no meaning should be implied by doing so).
|
||||
|
||||
switch metadata := p.Metadata.(type) {
|
||||
case pkg.ApkMetadata:
|
||||
return noneIfEmpty(metadata.URL)
|
||||
case pkg.NpmPackageJSONMetadata:
|
||||
return noneIfEmpty(metadata.URL)
|
||||
default:
|
||||
return "NOASSERTION"
|
||||
}
|
||||
}
|
||||
|
||||
func getSPDXHomepage(p *pkg.Package) string {
|
||||
switch metadata := p.Metadata.(type) {
|
||||
case pkg.GemMetadata:
|
||||
return metadata.Homepage
|
||||
case pkg.NpmPackageJSONMetadata:
|
||||
return metadata.Homepage
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func getSPDXSourceInfo(p *pkg.Package) string {
|
||||
var answer string
|
||||
switch p.Type {
|
||||
case pkg.RpmPkg:
|
||||
answer = "acquired package info from RPM DB"
|
||||
case pkg.ApkPkg:
|
||||
answer = "acquired package info from APK DB"
|
||||
case pkg.DebPkg:
|
||||
answer = "acquired package info from DPKG DB"
|
||||
case pkg.NpmPkg:
|
||||
answer = "acquired package info from installed node module manifest file"
|
||||
case pkg.PythonPkg:
|
||||
answer = "acquired package info from installed python package manifest file"
|
||||
case pkg.JavaPkg, pkg.JenkinsPluginPkg:
|
||||
answer = "acquired package info from installed java archive"
|
||||
case pkg.GemPkg:
|
||||
answer = "acquired package info from installed gem metadata file"
|
||||
case pkg.GoModulePkg:
|
||||
answer = "acquired package info from go module information"
|
||||
case pkg.RustPkg:
|
||||
answer = "acquired package info from rust cargo manifest"
|
||||
default:
|
||||
answer = "acquired package info from the following paths"
|
||||
}
|
||||
var paths []string
|
||||
for _, l := range p.Locations {
|
||||
paths = append(paths, l.RealPath)
|
||||
}
|
||||
|
||||
return answer + ": " + strings.Join(paths, ", ")
|
||||
}
|
||||
|
||||
func getSPDXOriginator(p *pkg.Package) string {
|
||||
switch metadata := p.Metadata.(type) {
|
||||
case pkg.ApkMetadata:
|
||||
return metadata.Maintainer
|
||||
case pkg.NpmPackageJSONMetadata:
|
||||
return metadata.Author
|
||||
case pkg.PythonPackageMetadata:
|
||||
author := metadata.Author
|
||||
if author == "" {
|
||||
return metadata.AuthorEmail
|
||||
}
|
||||
if metadata.AuthorEmail != "" {
|
||||
author += fmt.Sprintf(" <%s>", metadata.AuthorEmail)
|
||||
}
|
||||
return author
|
||||
case pkg.GemMetadata:
|
||||
if len(metadata.Authors) > 0 {
|
||||
return metadata.Authors[0]
|
||||
}
|
||||
return ""
|
||||
case pkg.RpmdbMetadata:
|
||||
return metadata.Vendor
|
||||
case pkg.DpkgMetadata:
|
||||
return metadata.Maintainer
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func getSPDXDescription(p *pkg.Package) string {
|
||||
switch metadata := p.Metadata.(type) {
|
||||
case pkg.ApkMetadata:
|
||||
return metadata.Description
|
||||
case pkg.NpmPackageJSONMetadata:
|
||||
return metadata.Description
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,527 @@
|
||||
package packages
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/syft/syft/source"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/anchore/syft/internal/presenter/packages/model/spdx22"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func Test_getSPDXExternalRefs(t *testing.T) {
|
||||
testCPE := must(pkg.NewCPE("cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*"))
|
||||
tests := []struct {
|
||||
name string
|
||||
input pkg.Package
|
||||
expected []spdx22.ExternalRef
|
||||
}{
|
||||
{
|
||||
name: "cpe + purl",
|
||||
input: pkg.Package{
|
||||
CPEs: []pkg.CPE{
|
||||
testCPE,
|
||||
},
|
||||
PURL: "a-purl",
|
||||
},
|
||||
expected: []spdx22.ExternalRef{
|
||||
{
|
||||
ReferenceCategory: spdx22.SecurityReferenceCategory,
|
||||
ReferenceLocator: testCPE.BindToFmtString(),
|
||||
ReferenceType: spdx22.Cpe23ExternalRefType,
|
||||
},
|
||||
{
|
||||
ReferenceCategory: spdx22.PackageManagerReferenceCategory,
|
||||
ReferenceLocator: "a-purl",
|
||||
ReferenceType: spdx22.PurlExternalRefType,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.ElementsMatch(t, test.expected, getSPDXExternalRefs(&test.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getSPDXLicense(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input pkg.Package
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "no licenses",
|
||||
input: pkg.Package{},
|
||||
expected: "NONE",
|
||||
},
|
||||
{
|
||||
name: "no SPDX licenses",
|
||||
input: pkg.Package{
|
||||
Licenses: []string{
|
||||
"made-up",
|
||||
},
|
||||
},
|
||||
expected: "NOASSERTION",
|
||||
},
|
||||
{
|
||||
name: "with SPDX license",
|
||||
input: pkg.Package{
|
||||
Licenses: []string{
|
||||
"MIT",
|
||||
},
|
||||
},
|
||||
expected: "MIT",
|
||||
},
|
||||
{
|
||||
name: "with SPDX license expression",
|
||||
input: pkg.Package{
|
||||
Licenses: []string{
|
||||
"MIT",
|
||||
"GPL-3.0",
|
||||
},
|
||||
},
|
||||
expected: "MIT AND GPL-3.0",
|
||||
},
|
||||
{
|
||||
name: "cap insensitive",
|
||||
input: pkg.Package{
|
||||
Licenses: []string{
|
||||
"gpl-3.0",
|
||||
},
|
||||
},
|
||||
expected: "GPL-3.0",
|
||||
},
|
||||
{
|
||||
name: "debian to spdx conversion",
|
||||
input: pkg.Package{
|
||||
Licenses: []string{
|
||||
"GPL-2",
|
||||
},
|
||||
},
|
||||
expected: "GPL-2.0",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, getSPDXLicense(&test.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_noneIfEmpty(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "non-zero value",
|
||||
value: "something",
|
||||
expected: "something",
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
value: "",
|
||||
expected: "NONE",
|
||||
},
|
||||
{
|
||||
name: "space",
|
||||
value: " ",
|
||||
expected: "NONE",
|
||||
},
|
||||
{
|
||||
name: "tab",
|
||||
value: "\t",
|
||||
expected: "NONE",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, noneIfEmpty(test.value))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getSPDXDownloadLocation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input pkg.Package
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "no metadata",
|
||||
input: pkg.Package{},
|
||||
expected: "NOASSERTION",
|
||||
},
|
||||
{
|
||||
name: "from apk",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.ApkMetadata{
|
||||
URL: "http://a-place.gov",
|
||||
},
|
||||
},
|
||||
expected: "http://a-place.gov",
|
||||
},
|
||||
{
|
||||
name: "from npm",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.NpmPackageJSONMetadata{
|
||||
URL: "http://a-place.gov",
|
||||
},
|
||||
},
|
||||
expected: "http://a-place.gov",
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.NpmPackageJSONMetadata{
|
||||
URL: "",
|
||||
},
|
||||
},
|
||||
expected: "NONE",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, getSPDXDownloadLocation(&test.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getSPDXHomepage(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input pkg.Package
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
// note: since this is an optional field, no value is preferred over NONE or NOASSERTION
|
||||
name: "no metadata",
|
||||
input: pkg.Package{},
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "from gem",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.GemMetadata{
|
||||
Homepage: "http://a-place.gov",
|
||||
},
|
||||
},
|
||||
expected: "http://a-place.gov",
|
||||
},
|
||||
{
|
||||
name: "from npm",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.NpmPackageJSONMetadata{
|
||||
Homepage: "http://a-place.gov",
|
||||
},
|
||||
},
|
||||
expected: "http://a-place.gov",
|
||||
},
|
||||
{
|
||||
// note: since this is an optional field, no value is preferred over NONE or NOASSERTION
|
||||
name: "empty",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.NpmPackageJSONMetadata{
|
||||
Homepage: "",
|
||||
},
|
||||
},
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, getSPDXHomepage(&test.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getSPDXSourceInfo(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input pkg.Package
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "locations are captured",
|
||||
input: pkg.Package{
|
||||
// note: no type given
|
||||
Locations: []source.Location{
|
||||
{
|
||||
RealPath: "/a-place",
|
||||
VirtualPath: "/b-place",
|
||||
},
|
||||
{
|
||||
RealPath: "/c-place",
|
||||
VirtualPath: "/d-place",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []string{
|
||||
"from the following paths",
|
||||
"/a-place",
|
||||
"/c-place",
|
||||
},
|
||||
},
|
||||
{
|
||||
// note: no specific support for this
|
||||
input: pkg.Package{
|
||||
Type: pkg.KbPkg,
|
||||
},
|
||||
expected: []string{
|
||||
"from the following paths",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: pkg.Package{
|
||||
Type: pkg.RpmPkg,
|
||||
},
|
||||
expected: []string{
|
||||
"from RPM DB",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: pkg.Package{
|
||||
Type: pkg.ApkPkg,
|
||||
},
|
||||
expected: []string{
|
||||
"from APK DB",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: pkg.Package{
|
||||
Type: pkg.DebPkg,
|
||||
},
|
||||
expected: []string{
|
||||
"from DPKG DB",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: pkg.Package{
|
||||
Type: pkg.NpmPkg,
|
||||
},
|
||||
expected: []string{
|
||||
"from installed node module manifest file",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: pkg.Package{
|
||||
Type: pkg.PythonPkg,
|
||||
},
|
||||
expected: []string{
|
||||
"from installed python package manifest file",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: pkg.Package{
|
||||
Type: pkg.JavaPkg,
|
||||
},
|
||||
expected: []string{
|
||||
"from installed java archive",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: pkg.Package{
|
||||
Type: pkg.JenkinsPluginPkg,
|
||||
},
|
||||
expected: []string{
|
||||
"from installed java archive",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: pkg.Package{
|
||||
Type: pkg.GemPkg,
|
||||
},
|
||||
expected: []string{
|
||||
"from installed gem metadata file",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: pkg.Package{
|
||||
Type: pkg.GoModulePkg,
|
||||
},
|
||||
expected: []string{
|
||||
"from go module information",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: pkg.Package{
|
||||
Type: pkg.RustPkg,
|
||||
},
|
||||
expected: []string{
|
||||
"from rust cargo manifest",
|
||||
},
|
||||
},
|
||||
}
|
||||
var pkgTypes []pkg.Type
|
||||
for _, test := range tests {
|
||||
t.Run(test.name+" "+string(test.input.Type), func(t *testing.T) {
|
||||
if test.input.Type != "" {
|
||||
pkgTypes = append(pkgTypes, test.input.Type)
|
||||
}
|
||||
actual := getSPDXSourceInfo(&test.input)
|
||||
for _, expected := range test.expected {
|
||||
assert.Contains(t, actual, expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
assert.ElementsMatch(t, pkg.AllPkgs, pkgTypes, "missing one or more package types to test against (maybe a package type was added?)")
|
||||
}
|
||||
|
||||
func Test_getSPDXOriginator(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input pkg.Package
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
// note: since this is an optional field, no value is preferred over NONE or NOASSERTION
|
||||
name: "no metadata",
|
||||
input: pkg.Package{},
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "from gem",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.GemMetadata{
|
||||
Authors: []string{
|
||||
"auth1",
|
||||
"auth2",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: "auth1",
|
||||
},
|
||||
{
|
||||
name: "from npm",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.NpmPackageJSONMetadata{
|
||||
Author: "auth",
|
||||
},
|
||||
},
|
||||
expected: "auth",
|
||||
},
|
||||
{
|
||||
name: "from apk",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.ApkMetadata{
|
||||
Maintainer: "auth",
|
||||
},
|
||||
},
|
||||
expected: "auth",
|
||||
},
|
||||
{
|
||||
name: "from python - just name",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.PythonPackageMetadata{
|
||||
Author: "auth",
|
||||
},
|
||||
},
|
||||
expected: "auth",
|
||||
},
|
||||
{
|
||||
name: "from python - just email",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.PythonPackageMetadata{
|
||||
AuthorEmail: "auth@auth.gov",
|
||||
},
|
||||
},
|
||||
expected: "auth@auth.gov",
|
||||
},
|
||||
{
|
||||
name: "from python - both name and email",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.PythonPackageMetadata{
|
||||
Author: "auth",
|
||||
AuthorEmail: "auth@auth.gov",
|
||||
},
|
||||
},
|
||||
expected: "auth <auth@auth.gov>",
|
||||
},
|
||||
{
|
||||
name: "from rpm",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.RpmdbMetadata{
|
||||
Vendor: "auth",
|
||||
},
|
||||
},
|
||||
expected: "auth",
|
||||
},
|
||||
{
|
||||
name: "from dpkg",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.DpkgMetadata{
|
||||
Maintainer: "auth",
|
||||
},
|
||||
},
|
||||
expected: "auth",
|
||||
},
|
||||
{
|
||||
// note: since this is an optional field, no value is preferred over NONE or NOASSERTION
|
||||
name: "empty",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.NpmPackageJSONMetadata{
|
||||
Author: "",
|
||||
},
|
||||
},
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, getSPDXOriginator(&test.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getSPDXDescription(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input pkg.Package
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
// note: since this is an optional field, no value is preferred over NONE or NOASSERTION
|
||||
name: "no metadata",
|
||||
input: pkg.Package{},
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "from apk",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.ApkMetadata{
|
||||
Description: "a description!",
|
||||
},
|
||||
},
|
||||
expected: "a description!",
|
||||
},
|
||||
{
|
||||
name: "from npm",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.NpmPackageJSONMetadata{
|
||||
Description: "a description!",
|
||||
},
|
||||
},
|
||||
expected: "a description!",
|
||||
},
|
||||
{
|
||||
// note: since this is an optional field, no value is preferred over NONE or NOASSERTION
|
||||
name: "empty",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.NpmPackageJSONMetadata{
|
||||
Homepage: "",
|
||||
},
|
||||
},
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, getSPDXDescription(&test.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package packages
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/presenter/packages/model/spdx22"
|
||||
"github.com/anchore/syft/internal/spdxlicense"
|
||||
"github.com/anchore/syft/internal/version"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const anchoreNamespace = "https://anchore.com/syft"
|
||||
|
||||
// SPDXJsonPresenter is a SPDX presentation object for the syft results (see https://github.com/spdx/spdx-spec)
|
||||
type SPDXJsonPresenter struct {
|
||||
catalog *pkg.Catalog
|
||||
srcMetadata source.Metadata
|
||||
}
|
||||
|
||||
// NewSPDXJSONPresenter creates a new JSON presenter object for the given cataloging results.
|
||||
func NewSPDXJSONPresenter(catalog *pkg.Catalog, srcMetadata source.Metadata) *SPDXJsonPresenter {
|
||||
return &SPDXJsonPresenter{
|
||||
catalog: catalog,
|
||||
srcMetadata: srcMetadata,
|
||||
}
|
||||
}
|
||||
|
||||
// Present the catalog results to the given writer.
|
||||
func (pres *SPDXJsonPresenter) Present(output io.Writer) error {
|
||||
doc := newSPDXJsonDocument(pres.catalog, pres.srcMetadata)
|
||||
|
||||
enc := json.NewEncoder(output)
|
||||
// prevent > and < from being escaped in the payload
|
||||
enc.SetEscapeHTML(false)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(&doc)
|
||||
}
|
||||
|
||||
// newSPDXJsonDocument creates and populates a new JSON document struct that follows the SPDX 2.2 spec from the given cataloging results.
|
||||
func newSPDXJsonDocument(catalog *pkg.Catalog, srcMetadata source.Metadata) spdx22.Document {
|
||||
uniqueID := uuid.Must(uuid.NewRandom())
|
||||
|
||||
var name, input, identifier string
|
||||
switch srcMetadata.Scheme {
|
||||
case source.ImageScheme:
|
||||
name = cleanSPDXName(srcMetadata.ImageMetadata.UserInput)
|
||||
input = "image"
|
||||
case source.DirectoryScheme:
|
||||
name = cleanSPDXName(srcMetadata.Path)
|
||||
input = "dir"
|
||||
}
|
||||
|
||||
if name != "." {
|
||||
identifier = path.Join(input, fmt.Sprintf("%s-%s", name, uniqueID.String()))
|
||||
} else {
|
||||
identifier = path.Join(input, uniqueID.String())
|
||||
}
|
||||
|
||||
namespace := path.Join(anchoreNamespace, identifier)
|
||||
packages, files, relationships := newSPDXJsonElements(catalog)
|
||||
|
||||
return spdx22.Document{
|
||||
Element: spdx22.Element{
|
||||
SPDXID: spdx22.ElementID("DOCUMENT").String(),
|
||||
Name: name,
|
||||
},
|
||||
SPDXVersion: spdx22.Version,
|
||||
CreationInfo: spdx22.CreationInfo{
|
||||
Created: time.Now().UTC(),
|
||||
Creators: []string{
|
||||
// note: key-value format derived from the JSON example document examples: https://github.com/spdx/spdx-spec/blob/v2.2/examples/SPDXJSONExample-v2.2.spdx.json
|
||||
"Organization: Anchore, Inc",
|
||||
"Tool: " + internal.ApplicationName + "-" + version.FromBuild().Version,
|
||||
},
|
||||
LicenseListVersion: spdxlicense.Version,
|
||||
},
|
||||
DataLicense: "CC0-1.0",
|
||||
DocumentNamespace: namespace,
|
||||
Packages: packages,
|
||||
Files: files,
|
||||
Relationships: relationships,
|
||||
}
|
||||
}
|
||||
|
||||
func newSPDXJsonElements(catalog *pkg.Catalog) ([]spdx22.Package, []spdx22.File, []spdx22.Relationship) {
|
||||
packages := make([]spdx22.Package, 0)
|
||||
relationships := make([]spdx22.Relationship, 0)
|
||||
files := make([]spdx22.File, 0)
|
||||
|
||||
for _, p := range catalog.Sorted() {
|
||||
license := getSPDXLicense(p)
|
||||
packageSpdxID := spdx22.ElementID(fmt.Sprintf("Package-%+v-%s-%s", p.Type, p.Name, p.Version)).String()
|
||||
|
||||
packageFiles, fileIDs, packageFileRelationships := getSPDXFiles(packageSpdxID, p)
|
||||
files = append(files, packageFiles...)
|
||||
|
||||
relationships = append(relationships, packageFileRelationships...)
|
||||
|
||||
// note: the license concluded and declared should be the same since we are collecting license information
|
||||
// from the project data itself (the installed package files).
|
||||
packages = append(packages, spdx22.Package{
|
||||
Description: getSPDXDescription(p),
|
||||
DownloadLocation: getSPDXDownloadLocation(p),
|
||||
ExternalRefs: getSPDXExternalRefs(p),
|
||||
FilesAnalyzed: false,
|
||||
HasFiles: fileIDs,
|
||||
Homepage: getSPDXHomepage(p),
|
||||
LicenseDeclared: license, // The Declared License is what the authors of a project believe govern the package
|
||||
Originator: getSPDXOriginator(p),
|
||||
SourceInfo: getSPDXSourceInfo(p),
|
||||
VersionInfo: p.Version,
|
||||
Item: spdx22.Item{
|
||||
LicenseConcluded: license, // The Concluded License field is the license the SPDX file creator believes governs the package
|
||||
Element: spdx22.Element{
|
||||
SPDXID: packageSpdxID,
|
||||
Name: p.Name,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return packages, files, relationships
|
||||
}
|
||||
|
||||
func cleanSPDXName(name string) string {
|
||||
// remove # according to specification
|
||||
name = strings.ReplaceAll(name, "#", "-")
|
||||
|
||||
// remove : for url construction
|
||||
name = strings.ReplaceAll(name, ":", "-")
|
||||
|
||||
// clean relative pathing
|
||||
return path.Clean(name)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package packages
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var updateSpdxJson = flag.Bool("update-spdx-json", false, "update the *.golden files for spdx-json presenters")
|
||||
|
||||
func TestSPDXJSONDirectoryPresenter(t *testing.T) {
|
||||
catalog, metadata, _ := presenterDirectoryInput(t)
|
||||
assertPresenterAgainstGoldenSnapshot(t,
|
||||
NewSPDXJSONPresenter(catalog, metadata),
|
||||
*updateSpdxJson,
|
||||
spdxJsonRedactor,
|
||||
)
|
||||
}
|
||||
|
||||
func TestSPDXJSONImagePresenter(t *testing.T) {
|
||||
testImage := "image-simple"
|
||||
catalog, metadata, _ := presenterImageInput(t, testImage)
|
||||
assertPresenterAgainstGoldenImageSnapshot(t,
|
||||
NewSPDXJSONPresenter(catalog, metadata),
|
||||
testImage,
|
||||
*updateSpdxJson,
|
||||
spdxJsonRedactor,
|
||||
)
|
||||
}
|
||||
|
||||
func spdxJsonRedactor(s []byte) []byte {
|
||||
// each SBOM reports the time it was generated, which is not useful during snapshot testing
|
||||
s = regexp.MustCompile(`"created": .*`).ReplaceAll(s, []byte("redacted"))
|
||||
|
||||
// each SBOM reports a unique documentNamespace when generated, this is not useful for snapshot testing
|
||||
s = regexp.MustCompile(`"documentNamespace": .*`).ReplaceAll(s, []byte("redacted"))
|
||||
|
||||
// the license list will be updated periodically, the value here should not be directly tested in snapshot tests
|
||||
return regexp.MustCompile(`"licenseListVersion": .*`).ReplaceAll(s, []byte("redacted"))
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
package packages
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/spdxlicense"
|
||||
"github.com/anchore/syft/internal/version"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/spdx/tools-golang/spdx"
|
||||
"github.com/spdx/tools-golang/tvsaver"
|
||||
)
|
||||
|
||||
// SPDXTagValuePresenter is a SPDX presentation object for the syft results (see https://github.com/spdx/spdx-spec)
|
||||
type SPDXTagValuePresenter struct {
|
||||
catalog *pkg.Catalog
|
||||
srcMetadata source.Metadata
|
||||
}
|
||||
|
||||
// NewJSONPresenter creates a new JSON presenter object for the given cataloging results.
|
||||
func NewSPDXTagValuePresenter(catalog *pkg.Catalog, srcMetadata source.Metadata) *SPDXTagValuePresenter {
|
||||
return &SPDXTagValuePresenter{
|
||||
catalog: catalog,
|
||||
srcMetadata: srcMetadata,
|
||||
}
|
||||
}
|
||||
|
||||
// Present the catalog results to the given writer.
|
||||
// nolint: funlen
|
||||
func (pres *SPDXTagValuePresenter) Present(output io.Writer) error {
|
||||
doc := spdx.Document2_2{
|
||||
CreationInfo: &spdx.CreationInfo2_2{
|
||||
// 2.1: SPDX Version; should be in the format "SPDX-2.2"
|
||||
// Cardinality: mandatory, one
|
||||
SPDXVersion: "SPDX-2.2",
|
||||
|
||||
// 2.2: Data License; should be "CC0-1.0"
|
||||
// Cardinality: mandatory, one
|
||||
DataLicense: "CC0-1.0",
|
||||
|
||||
// 2.3: SPDX Identifier; should be "DOCUMENT" to represent mandatory identifier of SPDXRef-DOCUMENT
|
||||
// Cardinality: mandatory, one
|
||||
SPDXIdentifier: spdx.ElementID("DOCUMENT"),
|
||||
|
||||
// 2.4: Document Name
|
||||
// Cardinality: mandatory, one
|
||||
DocumentName: pres.srcMetadata.ImageMetadata.UserInput,
|
||||
|
||||
// 2.5: Document Namespace
|
||||
// Cardinality: mandatory, one
|
||||
// Purpose: Provide an SPDX document specific namespace as a unique absolute Uniform Resource
|
||||
// Identifier (URI) as specified in RFC-3986, with the exception of the ‘#’ delimiter. The SPDX
|
||||
// Document URI cannot contain a URI "part" (e.g. the "#" character), since the ‘#’ is used in SPDX
|
||||
// element URIs (packages, files, snippets, etc) to separate the document namespace from the
|
||||
// element’s SPDX identifier. Additionally, a scheme (e.g. “https:”) is required.
|
||||
|
||||
// The URI must be unique for the SPDX document including the specific version of the SPDX document.
|
||||
// If the SPDX document is updated, thereby creating a new version, a new URI for the updated
|
||||
// document must be used. There can only be one URI for an SPDX document and only one SPDX document
|
||||
// for a given URI.
|
||||
|
||||
// Note that the URI does not have to be accessible. It is only intended to provide a unique ID.
|
||||
// In many cases, the URI will point to a web accessible document, but this should not be assumed
|
||||
// to be the case.
|
||||
|
||||
DocumentNamespace: fmt.Sprintf("https://anchore.com/syft/image/%s", pres.srcMetadata.ImageMetadata.UserInput),
|
||||
|
||||
// 2.6: External Document References
|
||||
// Cardinality: optional, one or many
|
||||
ExternalDocumentReferences: nil,
|
||||
|
||||
// 2.7: License List Version
|
||||
// Cardinality: optional, one
|
||||
LicenseListVersion: spdxlicense.Version,
|
||||
|
||||
// 2.8: Creators: may have multiple keys for Person, Organization
|
||||
// and/or Tool
|
||||
// Cardinality: mandatory, one or many
|
||||
CreatorPersons: nil,
|
||||
CreatorOrganizations: []string{"Anchore, Inc"},
|
||||
CreatorTools: []string{internal.ApplicationName + "-" + version.FromBuild().Version},
|
||||
|
||||
// 2.9: Created: data format YYYY-MM-DDThh:mm:ssZ
|
||||
// Cardinality: mandatory, one
|
||||
Created: time.Now().UTC().Format(time.RFC3339),
|
||||
|
||||
// 2.10: Creator Comment
|
||||
// Cardinality: optional, one
|
||||
CreatorComment: "",
|
||||
|
||||
// 2.11: Document Comment
|
||||
// Cardinality: optional, one
|
||||
DocumentComment: "",
|
||||
},
|
||||
Packages: pres.packages(),
|
||||
}
|
||||
|
||||
return tvsaver.Save2_2(&doc, output)
|
||||
}
|
||||
|
||||
// packages populates all Package Information from the package Catalog (see https://spdx.github.io/spdx-spec/3-package-information/)
|
||||
// nolint: funlen
|
||||
func (pres *SPDXTagValuePresenter) packages() map[spdx.ElementID]*spdx.Package2_2 {
|
||||
results := make(map[spdx.ElementID]*spdx.Package2_2)
|
||||
|
||||
for p := range pres.catalog.Enumerate() {
|
||||
// name should be guaranteed to be unique, but semantically useful and stable
|
||||
id := fmt.Sprintf("Package-%+v-%s", p.Type, p.Name)
|
||||
|
||||
// If the Concluded License is not the same as the Declared License, a written explanation should be provided
|
||||
// in the Comments on License field (section 3.16). With respect to NOASSERTION, a written explanation in
|
||||
// the Comments on License field (section 3.16) is preferred.
|
||||
license := getSPDXLicense(p)
|
||||
|
||||
results[spdx.ElementID(id)] = &spdx.Package2_2{
|
||||
|
||||
// NOT PART OF SPEC
|
||||
// flag: does this "package" contain files that were in fact "unpackaged",
|
||||
// e.g. included directly in the Document without being in a Package?
|
||||
IsUnpackaged: false,
|
||||
|
||||
// 3.1: Package Name
|
||||
// Cardinality: mandatory, one
|
||||
PackageName: p.Name,
|
||||
|
||||
// 3.2: Package SPDX Identifier: "SPDXRef-[idstring]"
|
||||
// Cardinality: mandatory, one
|
||||
PackageSPDXIdentifier: spdx.ElementID(id),
|
||||
|
||||
// 3.3: Package Version
|
||||
// Cardinality: optional, one
|
||||
PackageVersion: p.Version,
|
||||
|
||||
// 3.4: Package File Name
|
||||
// Cardinality: optional, one
|
||||
PackageFileName: "",
|
||||
|
||||
// 3.5: Package Supplier: may have single result for either Person or Organization,
|
||||
// or NOASSERTION
|
||||
// Cardinality: optional, one
|
||||
PackageSupplierPerson: "",
|
||||
PackageSupplierOrganization: "",
|
||||
PackageSupplierNOASSERTION: false,
|
||||
|
||||
// 3.6: Package Originator: may have single result for either Person or Organization,
|
||||
// or NOASSERTION
|
||||
// Cardinality: optional, one
|
||||
PackageOriginatorPerson: "",
|
||||
PackageOriginatorOrganization: "",
|
||||
PackageOriginatorNOASSERTION: false,
|
||||
|
||||
// 3.7: Package Download Location
|
||||
// Cardinality: mandatory, one
|
||||
// NONE if there is no download location whatsoever.
|
||||
// NOASSERTION if:
|
||||
// (i) the SPDX file creator has attempted to but cannot reach a reasonable objective determination;
|
||||
// (ii) the SPDX file creator has made no attempt to determine this field; or
|
||||
// (iii) the SPDX file creator has intentionally provided no information (no meaning should be implied by doing so).
|
||||
PackageDownloadLocation: "NOASSERTION",
|
||||
|
||||
// 3.8: FilesAnalyzed
|
||||
// Cardinality: optional, one; default value is "true" if omitted
|
||||
|
||||
// Purpose: Indicates whether the file content of this package has been available for or subjected to
|
||||
// analysis when creating the SPDX document. If false, indicates packages that represent metadata or
|
||||
// URI references to a project, product, artifact, distribution or a component. If false, the package
|
||||
// must not contain any files.
|
||||
|
||||
// Intent: A package can refer to a project, product, artifact, distribution or a component that is
|
||||
// external to the SPDX document.
|
||||
FilesAnalyzed: false,
|
||||
// NOT PART OF SPEC: did FilesAnalyzed tag appear?
|
||||
IsFilesAnalyzedTagPresent: true,
|
||||
|
||||
// 3.9: Package Verification Code
|
||||
// Cardinality: mandatory, one if filesAnalyzed is true / omitted;
|
||||
// zero (must be omitted) if filesAnalyzed is false
|
||||
PackageVerificationCode: "",
|
||||
// Spec also allows specifying a single file to exclude from the
|
||||
// verification code algorithm; intended to enable exclusion of
|
||||
// the SPDX document file itself.
|
||||
PackageVerificationCodeExcludedFile: "",
|
||||
|
||||
// 3.10: Package Checksum: may have keys for SHA1, SHA256 and/or MD5
|
||||
// Cardinality: optional, one or many
|
||||
|
||||
// 3.10.1 Purpose: Provide an independently reproducible mechanism that permits unique identification of
|
||||
// a specific package that correlates to the data in this SPDX file. This identifier enables a recipient
|
||||
// to determine if any file in the original package has been changed. If the SPDX file is to be included
|
||||
// in a package, this value should not be calculated. The SHA-1 algorithm will be used to provide the
|
||||
// checksum by default.
|
||||
|
||||
// note: based on the purpose above no discovered checksums should be provided, but instead, only
|
||||
// tool-derived checksums.
|
||||
PackageChecksumSHA1: "",
|
||||
PackageChecksumSHA256: "",
|
||||
PackageChecksumMD5: "",
|
||||
|
||||
// 3.11: Package Home Page
|
||||
// Cardinality: optional, one
|
||||
PackageHomePage: "",
|
||||
|
||||
// 3.12: Source Information
|
||||
// Cardinality: optional, one
|
||||
PackageSourceInfo: "",
|
||||
|
||||
// 3.13: Concluded License: SPDX License Expression, "NONE" or "NOASSERTION"
|
||||
// Cardinality: mandatory, one
|
||||
// Purpose: Contain the license the SPDX file creator has concluded as governing the
|
||||
// package or alternative values, if the governing license cannot be determined.
|
||||
PackageLicenseConcluded: license,
|
||||
|
||||
// 3.14: All Licenses Info from Files: SPDX License Expression, "NONE" or "NOASSERTION"
|
||||
// Cardinality: mandatory, one or many if filesAnalyzed is true / omitted;
|
||||
// zero (must be omitted) if filesAnalyzed is false
|
||||
PackageLicenseInfoFromFiles: nil,
|
||||
|
||||
// 3.15: Declared License: SPDX License Expression, "NONE" or "NOASSERTION"
|
||||
// Cardinality: mandatory, one
|
||||
// Purpose: List the licenses that have been declared by the authors of the package.
|
||||
// Any license information that does not originate from the package authors, e.g. license
|
||||
// information from a third party repository, should not be included in this field.
|
||||
PackageLicenseDeclared: license,
|
||||
|
||||
// 3.16: Comments on License
|
||||
// Cardinality: optional, one
|
||||
PackageLicenseComments: "",
|
||||
|
||||
// 3.17: Copyright Text: copyright notice(s) text, "NONE" or "NOASSERTION"
|
||||
// Cardinality: mandatory, one
|
||||
// Purpose: Identify the copyright holders of the package, as well as any dates present. This will be a free form text field extracted from package information files. The options to populate this field are limited to:
|
||||
//
|
||||
// Any text related to a copyright notice, even if not complete;
|
||||
// NONE if the package contains no copyright information whatsoever; or
|
||||
// NOASSERTION, if
|
||||
// (i) the SPDX document creator has made no attempt to determine this field; or
|
||||
// (ii) the SPDX document creator has intentionally provided no information (no meaning should be implied by doing so).
|
||||
//
|
||||
PackageCopyrightText: "NOASSERTION",
|
||||
|
||||
// 3.18: Package Summary Description
|
||||
// Cardinality: optional, one
|
||||
PackageSummary: "",
|
||||
|
||||
// 3.19: Package Detailed Description
|
||||
// Cardinality: optional, one
|
||||
PackageDescription: "",
|
||||
|
||||
// 3.20: Package Comment
|
||||
// Cardinality: optional, one
|
||||
PackageComment: "",
|
||||
|
||||
// 3.21: Package External Reference
|
||||
// Cardinality: optional, one or many
|
||||
PackageExternalReferences: formatSPDXExternalRefs(p),
|
||||
|
||||
// 3.22: Package External Reference Comment
|
||||
// Cardinality: conditional (optional, one) for each External Reference
|
||||
// contained within PackageExternalReference2_1 struct, if present
|
||||
|
||||
// 3.23: Package Attribution Text
|
||||
// Cardinality: optional, one or many
|
||||
PackageAttributionTexts: nil,
|
||||
|
||||
// Files contained in this Package
|
||||
Files: nil,
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func formatSPDXExternalRefs(p *pkg.Package) (refs []*spdx.PackageExternalReference2_2) {
|
||||
for _, ref := range getSPDXExternalRefs(p) {
|
||||
refs = append(refs, &spdx.PackageExternalReference2_2{
|
||||
Category: string(ref.ReferenceCategory),
|
||||
RefType: string(ref.ReferenceType),
|
||||
Locator: ref.ReferenceLocator,
|
||||
ExternalRefComment: ref.Comment,
|
||||
})
|
||||
}
|
||||
return refs
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package packages
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var updateSpdxTagValue = flag.Bool("update-spdx-tv", false, "update the *.golden files for spdx-tv presenters")
|
||||
|
||||
func TestSPDXTagValueDirectoryPresenter(t *testing.T) {
|
||||
catalog, metadata, _ := presenterDirectoryInput(t)
|
||||
assertPresenterAgainstGoldenSnapshot(t,
|
||||
NewSPDXTagValuePresenter(catalog, metadata),
|
||||
*updateSpdxTagValue,
|
||||
spdxTagValueRedactor,
|
||||
)
|
||||
}
|
||||
|
||||
func TestSPDXTagValueImagePresenter(t *testing.T) {
|
||||
testImage := "image-simple"
|
||||
catalog, metadata, _ := presenterImageInput(t, testImage)
|
||||
assertPresenterAgainstGoldenImageSnapshot(t,
|
||||
NewSPDXTagValuePresenter(catalog, metadata),
|
||||
testImage,
|
||||
*updateSpdxTagValue,
|
||||
spdxTagValueRedactor,
|
||||
)
|
||||
}
|
||||
|
||||
func spdxTagValueRedactor(s []byte) []byte {
|
||||
// each SBOM reports the time it was generated, which is not useful during snapshot testing
|
||||
s = regexp.MustCompile(`Created: .*`).ReplaceAll(s, []byte("redacted"))
|
||||
// the license list will be updated periodically, the value here should not be directly tested in snapshot tests
|
||||
return regexp.MustCompile(`LicenseListVersion: .*`).ReplaceAll(s, []byte("redacted"))
|
||||
}
|
||||
@@ -6,9 +6,8 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
)
|
||||
|
||||
type TablePresenter struct {
|
||||
|
||||
@@ -1,72 +1,22 @@
|
||||
package packages
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/filetree"
|
||||
|
||||
"github.com/go-test/deep"
|
||||
|
||||
"github.com/anchore/go-testutils"
|
||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/sergi/go-diff/diffmatchpatch"
|
||||
)
|
||||
|
||||
var updateTablePresenterGoldenFiles = flag.Bool("update-table", false, "update the *.golden files for table presenters")
|
||||
|
||||
func TestTablePresenter(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
|
||||
testImage := "image-simple"
|
||||
|
||||
catalog := pkg.NewCatalog()
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", testImage)
|
||||
|
||||
_, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks)
|
||||
_, ref2, _ := img.SquashedTree().File("/somefile-2.txt", filetree.FollowBasenameLinks)
|
||||
|
||||
// populate catalog with test data
|
||||
catalog.Add(pkg.Package{
|
||||
Name: "package-1",
|
||||
Version: "1.0.1",
|
||||
Locations: []source.Location{
|
||||
source.NewLocationFromImage(string(ref1.RealPath), *ref1, img),
|
||||
},
|
||||
Type: pkg.DebPkg,
|
||||
})
|
||||
catalog.Add(pkg.Package{
|
||||
Name: "package-2",
|
||||
Version: "2.0.1",
|
||||
Locations: []source.Location{
|
||||
source.NewLocationFromImage(string(ref2.RealPath), *ref2, img),
|
||||
},
|
||||
Type: pkg.DebPkg,
|
||||
})
|
||||
|
||||
pres := NewTablePresenter(catalog)
|
||||
|
||||
// run presenter
|
||||
err := pres.Present(&buffer)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
actual := buffer.Bytes()
|
||||
|
||||
if *updateTablePresenterGoldenFiles {
|
||||
testutils.UpdateGoldenFileContents(t, actual)
|
||||
}
|
||||
|
||||
var expected = testutils.GetGoldenFileContents(t)
|
||||
|
||||
if !bytes.Equal(expected, actual) {
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(string(actual), string(expected), true)
|
||||
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
|
||||
}
|
||||
catalog, _, _ := presenterImageInput(t, testImage)
|
||||
assertPresenterAgainstGoldenImageSnapshot(t,
|
||||
NewTablePresenter(catalog),
|
||||
testImage,
|
||||
*updateTablePresenterGoldenFiles,
|
||||
)
|
||||
}
|
||||
|
||||
func TestRemoveDuplicateRows(t *testing.T) {
|
||||
|
||||
+9
-10
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" version="1" serialNumber="urn:uuid:544b9f08-b39d-4d0c-8723-32609567e8ed">
|
||||
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" version="1" serialNumber="urn:uuid:5404937f-72d6-44a2-8e9b-954305ecb4f6">
|
||||
<metadata>
|
||||
<timestamp>2020-12-28T13:56:29-05:00</timestamp>
|
||||
<timestamp>2021-06-23T13:40:33-04:00</timestamp>
|
||||
<tools>
|
||||
<tool>
|
||||
<vendor>anchore</vendor>
|
||||
@@ -16,20 +16,19 @@
|
||||
</metadata>
|
||||
<components>
|
||||
<component type="library">
|
||||
<name>package1</name>
|
||||
<name>package-1</name>
|
||||
<version>1.0.1</version>
|
||||
</component>
|
||||
<component type="library">
|
||||
<name>package2</name>
|
||||
<version>2.0.1</version>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>MIT</name>
|
||||
</license>
|
||||
<license>
|
||||
<name>Apache-v2</name>
|
||||
</license>
|
||||
</licenses>
|
||||
<purl>a-purl-2</purl>
|
||||
</component>
|
||||
<component type="library">
|
||||
<name>package-2</name>
|
||||
<version>2.0.1</version>
|
||||
<purl>a-purl-2</purl>
|
||||
</component>
|
||||
</components>
|
||||
</bom>
|
||||
+9
-12
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" version="1" serialNumber="urn:uuid:e1515688-2fd6-422a-acb2-97405a44cdfe">
|
||||
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" version="1" serialNumber="urn:uuid:e34bad2e-cd27-483c-86dc-f4e26d6103b0">
|
||||
<metadata>
|
||||
<timestamp>2020-12-28T13:56:29-05:00</timestamp>
|
||||
<timestamp>2021-06-23T13:40:33-04:00</timestamp>
|
||||
<tools>
|
||||
<tool>
|
||||
<vendor>anchore</vendor>
|
||||
@@ -16,22 +16,19 @@
|
||||
</metadata>
|
||||
<components>
|
||||
<component type="library">
|
||||
<name>package1</name>
|
||||
<name>package-1</name>
|
||||
<version>1.0.1</version>
|
||||
<purl>the-purl-1</purl>
|
||||
</component>
|
||||
<component type="library">
|
||||
<name>package2</name>
|
||||
<version>2.0.1</version>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>MIT</name>
|
||||
</license>
|
||||
<license>
|
||||
<name>Apache-v2</name>
|
||||
</license>
|
||||
</licenses>
|
||||
<purl>the-purl-2</purl>
|
||||
<purl>a-purl-1</purl>
|
||||
</component>
|
||||
<component type="library">
|
||||
<name>package-2</name>
|
||||
<version>2.0.1</version>
|
||||
<purl>a-purl-2</purl>
|
||||
</component>
|
||||
</components>
|
||||
</bom>
|
||||
+8
-3
@@ -27,6 +27,11 @@
|
||||
"author": "",
|
||||
"authorEmail": "",
|
||||
"platform": "",
|
||||
"files": [
|
||||
{
|
||||
"path": "/some/path/pkg1/depedencies/foo"
|
||||
}
|
||||
],
|
||||
"sitePackagesRootPath": ""
|
||||
}
|
||||
},
|
||||
@@ -66,9 +71,9 @@
|
||||
"target": "/some/path"
|
||||
},
|
||||
"distro": {
|
||||
"name": "",
|
||||
"version": "",
|
||||
"idLike": ""
|
||||
"name": "debian",
|
||||
"version": "1.2.3",
|
||||
"idLike": "like!"
|
||||
},
|
||||
"descriptor": {
|
||||
"name": "syft",
|
||||
+10
-10
@@ -9,7 +9,7 @@
|
||||
"locations": [
|
||||
{
|
||||
"path": "/somefile-1.txt",
|
||||
"layerID": "sha256:fb6beecb75b39f4bb813dbf177e501edd5ddb3e69bb45cedeb78c676ee1b7a59"
|
||||
"layerID": "sha256:ffb5e9eaa453a002110719d12c294960117ca2903953d1faa40f01dc3f77045c"
|
||||
}
|
||||
],
|
||||
"licenses": [
|
||||
@@ -40,7 +40,7 @@
|
||||
"locations": [
|
||||
{
|
||||
"path": "/somefile-2.txt",
|
||||
"layerID": "sha256:319b588ce64253a87b533c8ed01cf0025e0eac98e7b516e12532957e1244fdec"
|
||||
"layerID": "sha256:8463854829fc53d47b9dcdf7ee79fe7eb4ca7933c910f67f8521412f7a2f5c21"
|
||||
}
|
||||
],
|
||||
"licenses": [],
|
||||
@@ -67,7 +67,7 @@
|
||||
"type": "image",
|
||||
"target": {
|
||||
"userInput": "user-image-input",
|
||||
"imageID": "sha256:5900c94a5bc1e083aa24ad1a223bf6eb9910dc8a6b01cb979ec306cb91709ea1",
|
||||
"imageID": "sha256:112851310e48e604f7379e2a3acddab50e91ce926edacb598a532e60ff6b776a",
|
||||
"manifestDigest": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368",
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"tags": [
|
||||
@@ -77,25 +77,25 @@
|
||||
"layers": [
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"digest": "sha256:fb6beecb75b39f4bb813dbf177e501edd5ddb3e69bb45cedeb78c676ee1b7a59",
|
||||
"digest": "sha256:ffb5e9eaa453a002110719d12c294960117ca2903953d1faa40f01dc3f77045c",
|
||||
"size": 22
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"digest": "sha256:319b588ce64253a87b533c8ed01cf0025e0eac98e7b516e12532957e1244fdec",
|
||||
"digest": "sha256:8463854829fc53d47b9dcdf7ee79fe7eb4ca7933c910f67f8521412f7a2f5c21",
|
||||
"size": 16
|
||||
}
|
||||
],
|
||||
"manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjo2NjUsImRpZ2VzdCI6InNoYTI1Njo1OTAwYzk0YTViYzFlMDgzYWEyNGFkMWEyMjNiZjZlYjk5MTBkYzhhNmIwMWNiOTc5ZWMzMDZjYjkxNzA5ZWExIn0sImxheWVycyI6W3sibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjIwNDgsImRpZ2VzdCI6InNoYTI1NjpmYjZiZWVjYjc1YjM5ZjRiYjgxM2RiZjE3N2U1MDFlZGQ1ZGRiM2U2OWJiNDVjZWRlYjc4YzY3NmVlMWI3YTU5In0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6MjA0OCwiZGlnZXN0Ijoic2hhMjU2OjMxOWI1ODhjZTY0MjUzYTg3YjUzM2M4ZWQwMWNmMDAyNWUwZWFjOThlN2I1MTZlMTI1MzI5NTdlMTI0NGZkZWMifV19",
|
||||
"config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiV29ya2luZ0RpciI6Ii8iLCJPbkJ1aWxkIjpudWxsfSwiY3JlYXRlZCI6IjIwMjEtMDQtMDZUMTk6MTM6NTIuNTI0Mzc4WiIsImhpc3RvcnkiOlt7ImNyZWF0ZWQiOiIyMDIxLTA0LTA2VDE5OjEzOjUyLjQ1ODIwNjFaIiwiY3JlYXRlZF9ieSI6IkFERCBmaWxlLTEudHh0IC9zb21lZmlsZS0xLnR4dCAjIGJ1aWxka2l0IiwiY29tbWVudCI6ImJ1aWxka2l0LmRvY2tlcmZpbGUudjAifSx7ImNyZWF0ZWQiOiIyMDIxLTA0LTA2VDE5OjEzOjUyLjUyNDM3OFoiLCJjcmVhdGVkX2J5IjoiQUREIGZpbGUtMi50eHQgL3NvbWVmaWxlLTIudHh0ICMgYnVpbGRraXQiLCJjb21tZW50IjoiYnVpbGRraXQuZG9ja2VyZmlsZS52MCJ9XSwib3MiOiJsaW51eCIsInJvb3RmcyI6eyJ0eXBlIjoibGF5ZXJzIiwiZGlmZl9pZHMiOlsic2hhMjU2OmZiNmJlZWNiNzViMzlmNGJiODEzZGJmMTc3ZTUwMWVkZDVkZGIzZTY5YmI0NWNlZGViNzhjNjc2ZWUxYjdhNTkiLCJzaGEyNTY6MzE5YjU4OGNlNjQyNTNhODdiNTMzYzhlZDAxY2YwMDI1ZTBlYWM5OGU3YjUxNmUxMjUzMjk1N2UxMjQ0ZmRlYyJdfX0=",
|
||||
"manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjo2NzMsImRpZ2VzdCI6InNoYTI1NjoxMTI4NTEzMTBlNDhlNjA0ZjczNzllMmEzYWNkZGFiNTBlOTFjZTkyNmVkYWNiNTk4YTUzMmU2MGZmNmI3NzZhIn0sImxheWVycyI6W3sibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjIwNDgsImRpZ2VzdCI6InNoYTI1NjpmZmI1ZTllYWE0NTNhMDAyMTEwNzE5ZDEyYzI5NDk2MDExN2NhMjkwMzk1M2QxZmFhNDBmMDFkYzNmNzcwNDVjIn0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6MjA0OCwiZGlnZXN0Ijoic2hhMjU2Ojg0NjM4NTQ4MjlmYzUzZDQ3YjlkY2RmN2VlNzlmZTdlYjRjYTc5MzNjOTEwZjY3Zjg1MjE0MTJmN2EyZjVjMjEifV19",
|
||||
"config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiV29ya2luZ0RpciI6Ii8iLCJPbkJ1aWxkIjpudWxsfSwiY3JlYXRlZCI6IjIwMjEtMDktMDhUMTc6MjE6NTguODk2NTI5MTkyWiIsImhpc3RvcnkiOlt7ImNyZWF0ZWQiOiIyMDIxLTA5LTA4VDE3OjIxOjU4Ljg3OTY5MDgyNFoiLCJjcmVhdGVkX2J5IjoiQUREIGZpbGUtMS50eHQgL3NvbWVmaWxlLTEudHh0ICMgYnVpbGRraXQiLCJjb21tZW50IjoiYnVpbGRraXQuZG9ja2VyZmlsZS52MCJ9LHsiY3JlYXRlZCI6IjIwMjEtMDktMDhUMTc6MjE6NTguODk2NTI5MTkyWiIsImNyZWF0ZWRfYnkiOiJBREQgZmlsZS0yLnR4dCAvc29tZWZpbGUtMi50eHQgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn1dLCJvcyI6ImxpbnV4Iiwicm9vdGZzIjp7InR5cGUiOiJsYXllcnMiLCJkaWZmX2lkcyI6WyJzaGEyNTY6ZmZiNWU5ZWFhNDUzYTAwMjExMDcxOWQxMmMyOTQ5NjAxMTdjYTI5MDM5NTNkMWZhYTQwZjAxZGMzZjc3MDQ1YyIsInNoYTI1Njo4NDYzODU0ODI5ZmM1M2Q0N2I5ZGNkZjdlZTc5ZmU3ZWI0Y2E3OTMzYzkxMGY2N2Y4NTIxNDEyZjdhMmY1YzIxIl19fQ==",
|
||||
"repoDigests": [],
|
||||
"scope": "Squashed"
|
||||
}
|
||||
},
|
||||
"distro": {
|
||||
"name": "",
|
||||
"version": "",
|
||||
"idLike": ""
|
||||
"name": "debian",
|
||||
"version": "1.2.3",
|
||||
"idLike": "like!"
|
||||
},
|
||||
"descriptor": {
|
||||
"name": "syft",
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"SPDXID": "SPDXRef-DOCUMENT",
|
||||
"name": "/some/path",
|
||||
"spdxVersion": "SPDX-2.2",
|
||||
"creationInfo": {
|
||||
"created": "2021-09-16T20:44:35.198887Z",
|
||||
"creators": [
|
||||
"Organization: Anchore, Inc",
|
||||
"Tool: syft-[not provided]"
|
||||
],
|
||||
"licenseListVersion": "3.14"
|
||||
},
|
||||
"dataLicense": "CC0-1.0",
|
||||
"documentNamespace": "https://anchore.com/syft/image/",
|
||||
"packages": [
|
||||
{
|
||||
"SPDXID": "SPDXRef-Package-python-package-1-1.0.1",
|
||||
"name": "package-1",
|
||||
"licenseConcluded": "MIT",
|
||||
"downloadLocation": "NOASSERTION",
|
||||
"externalRefs": [
|
||||
{
|
||||
"referenceCategory": "SECURITY",
|
||||
"referenceLocator": "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*",
|
||||
"referenceType": "cpe23Type"
|
||||
},
|
||||
{
|
||||
"referenceCategory": "PACKAGE_MANAGER",
|
||||
"referenceLocator": "a-purl-2",
|
||||
"referenceType": "purl"
|
||||
}
|
||||
],
|
||||
"filesAnalyzed": false,
|
||||
"hasFiles": [
|
||||
"SPDXRef-File-package-1-04cd22424378dcd6c77fce08beb52493b5494a60ea5e1f9bdf9b16dc0cacffe9"
|
||||
],
|
||||
"licenseDeclared": "MIT",
|
||||
"sourceInfo": "acquired package info from installed python package manifest file: /some/path/pkg1",
|
||||
"versionInfo": "1.0.1"
|
||||
},
|
||||
{
|
||||
"SPDXID": "SPDXRef-Package-deb-package-2-2.0.1",
|
||||
"name": "package-2",
|
||||
"licenseConcluded": "NONE",
|
||||
"downloadLocation": "NOASSERTION",
|
||||
"externalRefs": [
|
||||
{
|
||||
"referenceCategory": "SECURITY",
|
||||
"referenceLocator": "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*",
|
||||
"referenceType": "cpe23Type"
|
||||
},
|
||||
{
|
||||
"referenceCategory": "PACKAGE_MANAGER",
|
||||
"referenceLocator": "a-purl-2",
|
||||
"referenceType": "purl"
|
||||
}
|
||||
],
|
||||
"filesAnalyzed": false,
|
||||
"licenseDeclared": "NONE",
|
||||
"sourceInfo": "acquired package info from DPKG DB: /some/path/pkg1",
|
||||
"versionInfo": "2.0.1"
|
||||
}
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"SPDXID": "SPDXRef-File-package-1-04cd22424378dcd6c77fce08beb52493b5494a60ea5e1f9bdf9b16dc0cacffe9",
|
||||
"name": "foo",
|
||||
"licenseConcluded": "",
|
||||
"fileName": "/some/path/pkg1/depedencies/foo"
|
||||
}
|
||||
],
|
||||
"relationships": [
|
||||
{
|
||||
"spdxElementId": "SPDXRef-Package-python-package-1-1.0.1",
|
||||
"relationshipType": "CONTAINS",
|
||||
"relatedSpdxElement": "SPDXRef-File-package-1-04cd22424378dcd6c77fce08beb52493b5494a60ea5e1f9bdf9b16dc0cacffe9"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"SPDXID": "SPDXRef-DOCUMENT",
|
||||
"name": "user-image-input",
|
||||
"spdxVersion": "SPDX-2.2",
|
||||
"creationInfo": {
|
||||
"created": "2021-09-16T20:44:35.203911Z",
|
||||
"creators": [
|
||||
"Organization: Anchore, Inc",
|
||||
"Tool: syft-[not provided]"
|
||||
],
|
||||
"licenseListVersion": "3.14"
|
||||
},
|
||||
"dataLicense": "CC0-1.0",
|
||||
"documentNamespace": "https://anchore.com/syft/image/user-image-input",
|
||||
"packages": [
|
||||
{
|
||||
"SPDXID": "SPDXRef-Package-python-package-1-1.0.1",
|
||||
"name": "package-1",
|
||||
"licenseConcluded": "MIT",
|
||||
"downloadLocation": "NOASSERTION",
|
||||
"externalRefs": [
|
||||
{
|
||||
"referenceCategory": "SECURITY",
|
||||
"referenceLocator": "cpe:2.3:*:some:package:1:*:*:*:*:*:*:*",
|
||||
"referenceType": "cpe23Type"
|
||||
},
|
||||
{
|
||||
"referenceCategory": "PACKAGE_MANAGER",
|
||||
"referenceLocator": "a-purl-1",
|
||||
"referenceType": "purl"
|
||||
}
|
||||
],
|
||||
"filesAnalyzed": false,
|
||||
"licenseDeclared": "MIT",
|
||||
"sourceInfo": "acquired package info from installed python package manifest file: /somefile-1.txt",
|
||||
"versionInfo": "1.0.1"
|
||||
},
|
||||
{
|
||||
"SPDXID": "SPDXRef-Package-deb-package-2-2.0.1",
|
||||
"name": "package-2",
|
||||
"licenseConcluded": "NONE",
|
||||
"downloadLocation": "NOASSERTION",
|
||||
"externalRefs": [
|
||||
{
|
||||
"referenceCategory": "SECURITY",
|
||||
"referenceLocator": "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*",
|
||||
"referenceType": "cpe23Type"
|
||||
},
|
||||
{
|
||||
"referenceCategory": "PACKAGE_MANAGER",
|
||||
"referenceLocator": "a-purl-2",
|
||||
"referenceType": "purl"
|
||||
}
|
||||
],
|
||||
"filesAnalyzed": false,
|
||||
"licenseDeclared": "NONE",
|
||||
"sourceInfo": "acquired package info from DPKG DB: /somefile-2.txt",
|
||||
"versionInfo": "2.0.1"
|
||||
}
|
||||
]
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
SPDXVersion: SPDX-2.2
|
||||
DataLicense: CC0-1.0
|
||||
SPDXID: SPDXRef-DOCUMENT
|
||||
DocumentNamespace: https://anchore.com/syft/image/
|
||||
LicenseListVersion: 3.13
|
||||
Creator: Organization: Anchore, Inc
|
||||
Creator: Tool: syft-[not provided]
|
||||
Created: 2021-06-23T17:49:25Z
|
||||
|
||||
##### Package: package-2
|
||||
|
||||
PackageName: package-2
|
||||
SPDXID: SPDXRef-Package-deb-package-2
|
||||
PackageVersion: 2.0.1
|
||||
PackageDownloadLocation: NOASSERTION
|
||||
FilesAnalyzed: false
|
||||
PackageLicenseConcluded: NONE
|
||||
PackageLicenseDeclared: NONE
|
||||
PackageCopyrightText: NOASSERTION
|
||||
ExternalRef: SECURITY cpe23Type cpe:2.3:*:some:package:2:*:*:*:*:*:*:*
|
||||
ExternalRef: PACKAGE_MANAGER purl a-purl-2
|
||||
|
||||
##### Package: package-1
|
||||
|
||||
PackageName: package-1
|
||||
SPDXID: SPDXRef-Package-python-package-1
|
||||
PackageVersion: 1.0.1
|
||||
PackageDownloadLocation: NOASSERTION
|
||||
FilesAnalyzed: false
|
||||
PackageLicenseConcluded: MIT
|
||||
PackageLicenseDeclared: MIT
|
||||
PackageCopyrightText: NOASSERTION
|
||||
ExternalRef: SECURITY cpe23Type cpe:2.3:*:some:package:2:*:*:*:*:*:*:*
|
||||
ExternalRef: PACKAGE_MANAGER purl a-purl-2
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
SPDXVersion: SPDX-2.2
|
||||
DataLicense: CC0-1.0
|
||||
SPDXID: SPDXRef-DOCUMENT
|
||||
DocumentName: user-image-input
|
||||
DocumentNamespace: https://anchore.com/syft/image/user-image-input
|
||||
LicenseListVersion: 3.13
|
||||
Creator: Organization: Anchore, Inc
|
||||
Creator: Tool: syft-[not provided]
|
||||
Created: 2021-06-23T17:49:25Z
|
||||
|
||||
##### Package: package-2
|
||||
|
||||
PackageName: package-2
|
||||
SPDXID: SPDXRef-Package-deb-package-2
|
||||
PackageVersion: 2.0.1
|
||||
PackageDownloadLocation: NOASSERTION
|
||||
FilesAnalyzed: false
|
||||
PackageLicenseConcluded: NONE
|
||||
PackageLicenseDeclared: NONE
|
||||
PackageCopyrightText: NOASSERTION
|
||||
ExternalRef: SECURITY cpe23Type cpe:2.3:*:some:package:2:*:*:*:*:*:*:*
|
||||
ExternalRef: PACKAGE_MANAGER purl a-purl-2
|
||||
|
||||
##### Package: package-1
|
||||
|
||||
PackageName: package-1
|
||||
SPDXID: SPDXRef-Package-python-package-1
|
||||
PackageVersion: 1.0.1
|
||||
PackageDownloadLocation: NOASSERTION
|
||||
FilesAnalyzed: false
|
||||
PackageLicenseConcluded: MIT
|
||||
PackageLicenseDeclared: MIT
|
||||
PackageCopyrightText: NOASSERTION
|
||||
ExternalRef: SECURITY cpe23Type cpe:2.3:*:some:package:1:*:*:*:*:*:*:*
|
||||
ExternalRef: PACKAGE_MANAGER purl a-purl-1
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
NAME VERSION TYPE
|
||||
package-1 1.0.1 deb
|
||||
package-2 2.0.1 deb
|
||||
NAME VERSION TYPE
|
||||
package-1 1.0.1 python
|
||||
package-2 2.0.1 deb
|
||||
|
||||
+3
-3
@@ -1,11 +1,11 @@
|
||||
[Path: /some/path]
|
||||
[package-1]
|
||||
Version: 1.0.1
|
||||
Type: deb
|
||||
Found by:
|
||||
Type: python
|
||||
Found by: the-cataloger-1
|
||||
|
||||
[package-2]
|
||||
Version: 2.0.1
|
||||
Type: deb
|
||||
Found by:
|
||||
Found by: the-cataloger-2
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
[Image]
|
||||
Layer: 0
|
||||
Digest: sha256:ffb5e9eaa453a002110719d12c294960117ca2903953d1faa40f01dc3f77045c
|
||||
Size: 22
|
||||
MediaType: application/vnd.docker.image.rootfs.diff.tar.gzip
|
||||
|
||||
Layer: 1
|
||||
Digest: sha256:8463854829fc53d47b9dcdf7ee79fe7eb4ca7933c910f67f8521412f7a2f5c21
|
||||
Size: 16
|
||||
MediaType: application/vnd.docker.image.rootfs.diff.tar.gzip
|
||||
|
||||
[package-1]
|
||||
Version: 1.0.1
|
||||
Type: python
|
||||
Found by: the-cataloger-1
|
||||
|
||||
[package-2]
|
||||
Version: 2.0.1
|
||||
Type: deb
|
||||
Found by: the-cataloger-2
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
[Image]
|
||||
Layer: 0
|
||||
Digest: sha256:ad8ecdc058976c07e7e347cb89fa9ad86a294b5ceaae6d09713fb035f84115abf3c4a2388a4af3aa60f13b94f4c6846930bdf53
|
||||
Size: 22
|
||||
MediaType: application/vnd.docker.image.rootfs.diff.tar.gzip
|
||||
|
||||
Layer: 1
|
||||
Digest: sha256:ad8ecdc058976c07e7e347cb89fa9ad86a294b5ceaae6d09713fb035f84115abf3c4a2388a4af3aa60f13b94f4c6846930bdf53
|
||||
Size: 16
|
||||
MediaType: application/vnd.docker.image.rootfs.diff.tar.gzip
|
||||
|
||||
[package-1]
|
||||
Version: 1.0.1
|
||||
Type: deb
|
||||
Found by: dpkg
|
||||
|
||||
[package-2]
|
||||
Version: 2.0.1
|
||||
Type: deb
|
||||
Found by: dpkg
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
[Path: /some/path]
|
||||
[package-1]
|
||||
Version: 1.0.1
|
||||
Type: deb
|
||||
Found by:
|
||||
|
||||
[package-2]
|
||||
Version: 2.0.1
|
||||
Type: deb
|
||||
Found by:
|
||||
|
||||
BIN
Binary file not shown.
@@ -2,7 +2,6 @@ package packages
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"io"
|
||||
"text/tabwriter"
|
||||
|
||||
@@ -32,7 +31,7 @@ func (pres *TextPresenter) Present(output io.Writer) error {
|
||||
|
||||
switch pres.srcMetadata.Scheme {
|
||||
case source.DirectoryScheme:
|
||||
fmt.Fprintln(w, fmt.Sprintf("[Path: %s]", pres.srcMetadata.Path))
|
||||
fmt.Fprintf(w, "[Path: %s]\n", pres.srcMetadata.Path)
|
||||
case source.ImageScheme:
|
||||
fmt.Fprintln(w, "[Image]")
|
||||
|
||||
@@ -51,7 +50,7 @@ func (pres *TextPresenter) Present(output io.Writer) error {
|
||||
// populate artifacts...
|
||||
rows := 0
|
||||
for _, p := range pres.catalog.Sorted() {
|
||||
fmt.Fprintln(w, fmt.Sprintf("[%s]", p.Name))
|
||||
fmt.Fprintf(w, "[%s]\n", p.Name)
|
||||
fmt.Fprintln(w, " Version:\t", p.Version)
|
||||
fmt.Fprintln(w, " Type:\t", string(p.Type))
|
||||
fmt.Fprintln(w, " Found by:\t", p.FoundBy)
|
||||
|
||||
@@ -1,127 +1,26 @@
|
||||
package packages
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/filetree"
|
||||
|
||||
"github.com/anchore/go-testutils"
|
||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/sergi/go-diff/diffmatchpatch"
|
||||
)
|
||||
|
||||
var updateTextPresenterGoldenFiles = flag.Bool("update-text", false, "update the *.golden files for text presenters")
|
||||
|
||||
func TestTextDirPresenter(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
|
||||
catalog := pkg.NewCatalog()
|
||||
|
||||
// populate catalog with test data
|
||||
catalog.Add(pkg.Package{
|
||||
Name: "package-1",
|
||||
Version: "1.0.1",
|
||||
Type: pkg.DebPkg,
|
||||
})
|
||||
catalog.Add(pkg.Package{
|
||||
Name: "package-2",
|
||||
Version: "2.0.1",
|
||||
Type: pkg.DebPkg,
|
||||
})
|
||||
|
||||
s, err := source.NewFromDirectory("/some/path")
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create source: %+v", err)
|
||||
}
|
||||
pres := NewTextPresenter(catalog, s.Metadata)
|
||||
|
||||
// run presenter
|
||||
err = pres.Present(&buffer)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
actual := buffer.Bytes()
|
||||
|
||||
if *updateTextPresenterGoldenFiles {
|
||||
testutils.UpdateGoldenFileContents(t, actual)
|
||||
}
|
||||
|
||||
var expected = testutils.GetGoldenFileContents(t)
|
||||
|
||||
if !bytes.Equal(expected, actual) {
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(string(actual), string(expected), true)
|
||||
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
|
||||
}
|
||||
|
||||
func TestTextDirectoryPresenter(t *testing.T) {
|
||||
catalog, metadata, _ := presenterDirectoryInput(t)
|
||||
assertPresenterAgainstGoldenSnapshot(t,
|
||||
NewTextPresenter(catalog, metadata),
|
||||
*updateTextPresenterGoldenFiles,
|
||||
)
|
||||
}
|
||||
|
||||
type PackageInfo struct {
|
||||
Name string
|
||||
Version string
|
||||
}
|
||||
|
||||
func TestTextImgPresenter(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
|
||||
catalog := pkg.NewCatalog()
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", "image-simple")
|
||||
|
||||
_, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks)
|
||||
_, ref2, _ := img.SquashedTree().File("/somefile-2.txt", filetree.FollowBasenameLinks)
|
||||
|
||||
// populate catalog with test data
|
||||
catalog.Add(pkg.Package{
|
||||
Name: "package-1",
|
||||
Version: "1.0.1",
|
||||
Locations: []source.Location{
|
||||
source.NewLocationFromImage(string(ref1.RealPath), *ref1, img),
|
||||
},
|
||||
FoundBy: "dpkg",
|
||||
Type: pkg.DebPkg,
|
||||
})
|
||||
catalog.Add(pkg.Package{
|
||||
Name: "package-2",
|
||||
Version: "2.0.1",
|
||||
Locations: []source.Location{
|
||||
source.NewLocationFromImage(string(ref2.RealPath), *ref2, img),
|
||||
},
|
||||
FoundBy: "dpkg",
|
||||
Metadata: PackageInfo{Name: "package-2", Version: "1.0.2"},
|
||||
Type: pkg.DebPkg,
|
||||
})
|
||||
|
||||
// stub out all the digests so that they don't affect tests comparisons
|
||||
// TODO: update with stereoscope test utils feature when this issue is resolved: https://github.com/anchore/stereoscope/issues/43
|
||||
for _, l := range img.Layers {
|
||||
l.Metadata.Digest = "sha256:ad8ecdc058976c07e7e347cb89fa9ad86a294b5ceaae6d09713fb035f84115abf3c4a2388a4af3aa60f13b94f4c6846930bdf53"
|
||||
}
|
||||
|
||||
s, err := source.NewFromImage(img, "user-image-input")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pres := NewTextPresenter(catalog, s.Metadata)
|
||||
// run presenter
|
||||
err = pres.Present(&buffer)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
actual := buffer.Bytes()
|
||||
if *updateTextPresenterGoldenFiles {
|
||||
testutils.UpdateGoldenFileContents(t, actual)
|
||||
}
|
||||
|
||||
var expected = testutils.GetGoldenFileContents(t)
|
||||
|
||||
if !bytes.Equal(expected, actual) {
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(string(actual), string(expected), true)
|
||||
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
|
||||
}
|
||||
|
||||
func TestTextImagePresenter(t *testing.T) {
|
||||
testImage := "image-simple"
|
||||
catalog, metadata, _ := presenterImageInput(t, testImage)
|
||||
assertPresenterAgainstGoldenImageSnapshot(t,
|
||||
NewTextPresenter(catalog, metadata),
|
||||
testImage,
|
||||
*updateTextPresenterGoldenFiles,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
package packages
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/go-testutils"
|
||||
"github.com/anchore/stereoscope/pkg/filetree"
|
||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
"github.com/anchore/syft/syft/distro"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/presenter"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/sergi/go-diff/diffmatchpatch"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type redactor func(s []byte) []byte
|
||||
|
||||
func assertPresenterAgainstGoldenImageSnapshot(t *testing.T, pres presenter.Presenter, testImage string, updateSnapshot bool, redactors ...redactor) {
|
||||
var buffer bytes.Buffer
|
||||
|
||||
// grab the latest image contents and persist
|
||||
if updateSnapshot {
|
||||
imagetest.UpdateGoldenFixtureImage(t, testImage)
|
||||
}
|
||||
|
||||
err := pres.Present(&buffer)
|
||||
assert.NoError(t, err)
|
||||
actual := buffer.Bytes()
|
||||
|
||||
// replace the expected snapshot contents with the current presenter contents
|
||||
if updateSnapshot {
|
||||
testutils.UpdateGoldenFileContents(t, actual)
|
||||
}
|
||||
|
||||
var expected = testutils.GetGoldenFileContents(t)
|
||||
|
||||
// remove dynamic values, which should be tested independently
|
||||
for _, r := range redactors {
|
||||
actual = r(actual)
|
||||
expected = r(expected)
|
||||
}
|
||||
|
||||
// assert that the golden file snapshot matches the actual contents
|
||||
if !bytes.Equal(expected, actual) {
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(string(expected), string(actual), true)
|
||||
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
|
||||
}
|
||||
}
|
||||
|
||||
func assertPresenterAgainstGoldenSnapshot(t *testing.T, pres presenter.Presenter, updateSnapshot bool, redactors ...redactor) {
|
||||
var buffer bytes.Buffer
|
||||
|
||||
err := pres.Present(&buffer)
|
||||
assert.NoError(t, err)
|
||||
actual := buffer.Bytes()
|
||||
|
||||
// replace the expected snapshot contents with the current presenter contents
|
||||
if updateSnapshot {
|
||||
testutils.UpdateGoldenFileContents(t, actual)
|
||||
}
|
||||
|
||||
var expected = testutils.GetGoldenFileContents(t)
|
||||
|
||||
// remove dynamic values, which should be tested independently
|
||||
for _, r := range redactors {
|
||||
actual = r(actual)
|
||||
expected = r(expected)
|
||||
}
|
||||
|
||||
if !bytes.Equal(expected, actual) {
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(string(expected), string(actual), true)
|
||||
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
|
||||
}
|
||||
}
|
||||
|
||||
func presenterImageInput(t testing.TB, testImage string) (*pkg.Catalog, source.Metadata, *distro.Distro) {
|
||||
t.Helper()
|
||||
catalog := pkg.NewCatalog()
|
||||
img := imagetest.GetGoldenFixtureImage(t, testImage)
|
||||
|
||||
_, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks)
|
||||
_, ref2, _ := img.SquashedTree().File("/somefile-2.txt", filetree.FollowBasenameLinks)
|
||||
|
||||
// populate catalog with test data
|
||||
catalog.Add(pkg.Package{
|
||||
ID: "package-1-id",
|
||||
Name: "package-1",
|
||||
Version: "1.0.1",
|
||||
Locations: []source.Location{
|
||||
source.NewLocationFromImage(string(ref1.RealPath), *ref1, img),
|
||||
},
|
||||
Type: pkg.PythonPkg,
|
||||
FoundBy: "the-cataloger-1",
|
||||
Language: pkg.Python,
|
||||
MetadataType: pkg.PythonPackageMetadataType,
|
||||
Licenses: []string{"MIT"},
|
||||
Metadata: pkg.PythonPackageMetadata{
|
||||
Name: "package-1",
|
||||
Version: "1.0.1",
|
||||
},
|
||||
PURL: "a-purl-1",
|
||||
CPEs: []pkg.CPE{
|
||||
must(pkg.NewCPE("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*")),
|
||||
},
|
||||
})
|
||||
catalog.Add(pkg.Package{
|
||||
ID: "package-2-id",
|
||||
Name: "package-2",
|
||||
Version: "2.0.1",
|
||||
Locations: []source.Location{
|
||||
source.NewLocationFromImage(string(ref2.RealPath), *ref2, img),
|
||||
},
|
||||
Type: pkg.DebPkg,
|
||||
FoundBy: "the-cataloger-2",
|
||||
MetadataType: pkg.DpkgMetadataType,
|
||||
Metadata: pkg.DpkgMetadata{
|
||||
Package: "package-2",
|
||||
Version: "2.0.1",
|
||||
},
|
||||
PURL: "a-purl-2",
|
||||
CPEs: []pkg.CPE{
|
||||
must(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")),
|
||||
},
|
||||
})
|
||||
|
||||
// this is a hard coded value that is not given by the fixture helper and must be provided manually
|
||||
img.Metadata.ManifestDigest = "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368"
|
||||
|
||||
src, err := source.NewFromImage(img, "user-image-input")
|
||||
assert.NoError(t, err)
|
||||
|
||||
dist, err := distro.NewDistro(distro.Debian, "1.2.3", "like!")
|
||||
assert.NoError(t, err)
|
||||
|
||||
return catalog, src.Metadata, &dist
|
||||
}
|
||||
|
||||
func presenterDirectoryInput(t testing.TB) (*pkg.Catalog, source.Metadata, *distro.Distro) {
|
||||
catalog := pkg.NewCatalog()
|
||||
|
||||
// populate catalog with test data
|
||||
catalog.Add(pkg.Package{
|
||||
ID: "package-1-id",
|
||||
Name: "package-1",
|
||||
Version: "1.0.1",
|
||||
Type: pkg.PythonPkg,
|
||||
FoundBy: "the-cataloger-1",
|
||||
Locations: []source.Location{
|
||||
{RealPath: "/some/path/pkg1"},
|
||||
},
|
||||
Language: pkg.Python,
|
||||
MetadataType: pkg.PythonPackageMetadataType,
|
||||
Licenses: []string{"MIT"},
|
||||
Metadata: pkg.PythonPackageMetadata{
|
||||
Name: "package-1",
|
||||
Version: "1.0.1",
|
||||
Files: []pkg.PythonFileRecord{
|
||||
{
|
||||
Path: "/some/path/pkg1/depedencies/foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
PURL: "a-purl-2",
|
||||
CPEs: []pkg.CPE{
|
||||
must(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")),
|
||||
},
|
||||
})
|
||||
catalog.Add(pkg.Package{
|
||||
ID: "package-2-id",
|
||||
Name: "package-2",
|
||||
Version: "2.0.1",
|
||||
Type: pkg.DebPkg,
|
||||
FoundBy: "the-cataloger-2",
|
||||
Locations: []source.Location{
|
||||
{RealPath: "/some/path/pkg1"},
|
||||
},
|
||||
MetadataType: pkg.DpkgMetadataType,
|
||||
Metadata: pkg.DpkgMetadata{
|
||||
Package: "package-2",
|
||||
Version: "2.0.1",
|
||||
},
|
||||
PURL: "a-purl-2",
|
||||
CPEs: []pkg.CPE{
|
||||
must(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")),
|
||||
},
|
||||
})
|
||||
|
||||
dist, err := distro.NewDistro(distro.Debian, "1.2.3", "like!")
|
||||
assert.NoError(t, err)
|
||||
|
||||
src, err := source.NewFromDirectory("/some/path")
|
||||
assert.NoError(t, err)
|
||||
|
||||
return catalog, src.Metadata, &dist
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/anchore/syft/syft/file"
|
||||
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
@@ -22,6 +21,7 @@ type JSONFileMetadataEntry struct {
|
||||
UserID int `json:"userID"`
|
||||
GroupID int `json:"groupID"`
|
||||
Digests []file.Digest `json:"digests,omitempty"`
|
||||
MIMEType string `json:"mimeType"`
|
||||
}
|
||||
|
||||
func NewJSONFileMetadata(data map[source.Location]source.FileMetadata, digests map[source.Location][]file.Digest) ([]JSONFileMetadata, error) {
|
||||
@@ -46,6 +46,7 @@ func NewJSONFileMetadata(data map[source.Location]source.FileMetadata, digests m
|
||||
UserID: metadata.UserID,
|
||||
GroupID: metadata.GroupID,
|
||||
Digests: digestResults,
|
||||
MIMEType: metadata.MIMEType,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
"mode": 775,
|
||||
"type": "directory",
|
||||
"userID": 0,
|
||||
"groupID": 0
|
||||
"groupID": 0,
|
||||
"mimeType": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -33,7 +34,8 @@
|
||||
"algorithm": "sha256",
|
||||
"value": "366a3f5653e34673b875891b021647440d0127c2ef041e3b1a22da2a7d4f3703"
|
||||
}
|
||||
]
|
||||
],
|
||||
"mimeType": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -45,7 +47,8 @@
|
||||
"type": "symbolicLink",
|
||||
"linkDestination": "/c",
|
||||
"userID": 0,
|
||||
"groupID": 0
|
||||
"groupID": 0,
|
||||
"mimeType": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -62,7 +65,8 @@
|
||||
"algorithm": "sha256",
|
||||
"value": "1b3722da2a7d90d033b87581a2a3f12021647445653e34666ef041e3b4f3707c"
|
||||
}
|
||||
]
|
||||
],
|
||||
"mimeType": ""
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -168,6 +172,7 @@
|
||||
"configuration": {
|
||||
"configPath": "",
|
||||
"output": "",
|
||||
"file": "",
|
||||
"quiet": false,
|
||||
"check-for-app-update": false,
|
||||
"anchore": {
|
||||
@@ -227,6 +232,7 @@
|
||||
},
|
||||
"registry": {
|
||||
"insecure-skip-tls-verify": false,
|
||||
"insecure-use-http": false,
|
||||
"auth": null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/scylladb/go-set/strset"
|
||||
)
|
||||
|
||||
// This program generates license_list.go.
|
||||
const (
|
||||
source = "license_list.go"
|
||||
url = "https://spdx.org/licenses/licenses.json"
|
||||
)
|
||||
|
||||
var tmp = template.Must(template.New("").Parse(`// Code generated by go generate; DO NOT EDIT.
|
||||
// This file was generated by robots at {{ .Timestamp }}
|
||||
// using data from {{ .URL }}
|
||||
package spdxlicense
|
||||
|
||||
const Version = {{ printf "%q" .Version }}
|
||||
|
||||
var licenseIDs = map[string]string{
|
||||
{{- range $k, $v := .LicenseIDs }}
|
||||
{{ printf "%q" $k }}: {{ printf "%q" $v }},
|
||||
{{- end }}
|
||||
}
|
||||
`))
|
||||
|
||||
var versionMatch = regexp.MustCompile(`-([0-9]+)\.?([0-9]+)?\.?([0-9]+)?\.?`)
|
||||
|
||||
type LicenseList struct {
|
||||
Version string `json:"licenseListVersion"`
|
||||
Licenses []struct {
|
||||
ID string `json:"licenseId"`
|
||||
Name string `json:"name"`
|
||||
Text string `json:"licenseText"`
|
||||
Deprecated bool `json:"isDeprecatedLicenseId"`
|
||||
OSIApproved bool `json:"isOsiApproved"`
|
||||
SeeAlso []string `json:"seeAlso"`
|
||||
} `json:"licenses"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := run(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run() error {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get licenses list: %w", err)
|
||||
}
|
||||
|
||||
var result LicenseList
|
||||
if err = json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return fmt.Errorf("unable to decode license list: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
log.Fatalf("unable to close body: %+v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
f, err := os.Create(source)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create %q: %w", source, err)
|
||||
}
|
||||
defer func() {
|
||||
if err := f.Close(); err != nil {
|
||||
log.Fatalf("unable to close %q: %+v", source, err)
|
||||
}
|
||||
}()
|
||||
|
||||
licenseIDs := processSPDXLicense(result)
|
||||
|
||||
err = tmp.Execute(f, struct {
|
||||
Timestamp time.Time
|
||||
URL string
|
||||
Version string
|
||||
LicenseIDs map[string]string
|
||||
}{
|
||||
Timestamp: time.Now(),
|
||||
URL: url,
|
||||
Version: result.Version,
|
||||
LicenseIDs: licenseIDs,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to generate template: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parsing the provided SPDX license list necessitates a two pass approach.
|
||||
// The first pass is only related to what SPDX considers the truth. These K:V pairs will never be overwritten.
|
||||
// The second pass attempts to generate known short/long version listings for each key.
|
||||
// For info on some short name conventions see this document:
|
||||
// https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/#license-short-name.
|
||||
// The short long listing generation attempts to build all license permutations for a given key.
|
||||
// The new keys are then also associated with their relative SPDX value. If a key has already been entered
|
||||
// we know to ignore it since it came from the first pass which is considered the SPDX source of truth.
|
||||
// We also sort the licenses for the second pass so that cases like `GPL-1` associate to `GPL-1.0` and not `GPL-1.1`.
|
||||
func processSPDXLicense(result LicenseList) map[string]string {
|
||||
// first pass build map
|
||||
licenseIDs := make(map[string]string)
|
||||
for _, l := range result.Licenses {
|
||||
cleanID := strings.ToLower(l.ID)
|
||||
if _, exists := licenseIDs[cleanID]; exists {
|
||||
log.Fatalf("duplicate license ID found: %q", cleanID)
|
||||
}
|
||||
licenseIDs[cleanID] = l.ID
|
||||
}
|
||||
|
||||
sort.Slice(result.Licenses, func(i, j int) bool {
|
||||
return result.Licenses[i].ID < result.Licenses[j].ID
|
||||
})
|
||||
|
||||
// second pass build exceptions
|
||||
// do not overwrite if already exists
|
||||
for _, l := range result.Licenses {
|
||||
var multipleID []string
|
||||
cleanID := strings.ToLower(l.ID)
|
||||
multipleID = append(multipleID, buildLicensePermutations(cleanID)...)
|
||||
for _, id := range multipleID {
|
||||
if _, exists := licenseIDs[id]; !exists {
|
||||
licenseIDs[id] = l.ID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return licenseIDs
|
||||
}
|
||||
|
||||
func buildLicensePermutations(license string) (perms []string) {
|
||||
lv := findLicenseVersion(license)
|
||||
vp := versionPermutations(lv)
|
||||
|
||||
version := strings.Join(lv, ".")
|
||||
for _, p := range vp {
|
||||
perms = append(perms, strings.Replace(license, version, p, 1))
|
||||
}
|
||||
|
||||
return perms
|
||||
}
|
||||
|
||||
func findLicenseVersion(license string) (version []string) {
|
||||
versionList := versionMatch.FindAllStringSubmatch(license, -1)
|
||||
|
||||
if len(versionList) == 0 {
|
||||
return version
|
||||
}
|
||||
|
||||
for i, v := range versionList[0] {
|
||||
if v != "" && i != 0 {
|
||||
version = append(version, v)
|
||||
}
|
||||
}
|
||||
|
||||
return version
|
||||
}
|
||||
|
||||
func versionPermutations(version []string) []string {
|
||||
ver := append([]string(nil), version...)
|
||||
perms := strset.New()
|
||||
for i := 1; i <= 3; i++ {
|
||||
if len(ver) < i+1 {
|
||||
ver = append(ver, "0")
|
||||
}
|
||||
|
||||
perm := strings.Join(ver[:i], ".")
|
||||
badCount := strings.Count(perm, "0") + strings.Count(perm, ".")
|
||||
|
||||
if badCount != len(perm) {
|
||||
perms.Add(perm)
|
||||
}
|
||||
}
|
||||
|
||||
return perms.List()
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLicensePermutations(t *testing.T) {
|
||||
var tests = []struct {
|
||||
shortName string
|
||||
permutations []string
|
||||
}{
|
||||
{
|
||||
"GPL-1-only",
|
||||
[]string{
|
||||
"GPL-1-only",
|
||||
"GPL-1.0-only",
|
||||
"GPL-1.0.0-only",
|
||||
},
|
||||
},
|
||||
{
|
||||
"GPL-2",
|
||||
[]string{
|
||||
"GPL-2",
|
||||
"GPL-2.0",
|
||||
"GPL-2.0.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
"GPL-2.0+",
|
||||
[]string{
|
||||
"GPL-2+",
|
||||
"GPL-2.0+",
|
||||
"GPL-2.0.0+",
|
||||
},
|
||||
},
|
||||
{
|
||||
"GPL-3.0.0-or-later",
|
||||
[]string{
|
||||
"GPL-3-or-later",
|
||||
"GPL-3.0-or-later",
|
||||
"GPL-3.0.0-or-later",
|
||||
},
|
||||
},
|
||||
{
|
||||
"oldap-2.0",
|
||||
[]string{
|
||||
"oldap-2",
|
||||
"oldap-2.0",
|
||||
"oldap-2.0.0",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.shortName, func(t *testing.T) {
|
||||
perms := buildLicensePermutations(test.shortName)
|
||||
assert.ElementsMatch(t, test.permutations, perms)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionPermutations(t *testing.T) {
|
||||
var tests = []struct {
|
||||
version []string
|
||||
permutations []string
|
||||
}{
|
||||
{
|
||||
[]string{"1", "0"},
|
||||
[]string{"1", "1.0", "1.0.0"},
|
||||
},
|
||||
{
|
||||
[]string{"2"},
|
||||
[]string{"2", "2.0", "2.0.0"},
|
||||
},
|
||||
{
|
||||
[]string{"2", "0"},
|
||||
[]string{"2", "2.0", "2.0.0"},
|
||||
},
|
||||
|
||||
{
|
||||
[]string{"3", "0", "0"},
|
||||
[]string{"3", "3.0", "3.0.0"},
|
||||
},
|
||||
{
|
||||
[]string{"0", "3"},
|
||||
[]string{"0.3", "0.3.0"},
|
||||
},
|
||||
{
|
||||
[]string{"0", "0", "3"},
|
||||
[]string{"0.0.3"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(strings.Join(test.version, "."), func(t *testing.T) {
|
||||
got := versionPermutations(test.version)
|
||||
assert.ElementsMatch(t, test.permutations, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindLicenseVersion(t *testing.T) {
|
||||
var tests = []struct {
|
||||
license string
|
||||
version []string
|
||||
}{
|
||||
{
|
||||
"GPL-1.0-only",
|
||||
[]string{"1", "0"},
|
||||
},
|
||||
{
|
||||
"GPL-2.0",
|
||||
[]string{"2", "0"},
|
||||
},
|
||||
{
|
||||
"GPL-2.0.0",
|
||||
[]string{"2", "0", "0"},
|
||||
},
|
||||
{
|
||||
"GPL-2",
|
||||
[]string{"2"},
|
||||
},
|
||||
{
|
||||
"bzip2-1",
|
||||
[]string{"1"},
|
||||
},
|
||||
{
|
||||
"php-3.01",
|
||||
[]string{"3", "01"},
|
||||
},
|
||||
{
|
||||
"oldap-2.0",
|
||||
[]string{"2", "0"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.license, func(t *testing.T) {
|
||||
got := findLicenseVersion(test.license)
|
||||
assert.Equal(t, test.version, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package spdxlicense
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/#license-short-name
|
||||
// License generated in license_list.go uses a regular expression to help resolve cases where
|
||||
// x.0.0 and x are supplied as version numbers. For SPDX compatibility, versions with trailing
|
||||
// dot-zeroes are considered to be equivalent to versions without (e.g., “2.0.0” is considered equal to “2.0” and “2”).
|
||||
// EX: gpl-2+ ---> GPL-2.0+
|
||||
// EX: gpl-2.0.0-only ---> GPL-2.0-only
|
||||
// See the debian link for more details on the spdx license differences
|
||||
|
||||
//go:generate go run generate/generate_license_list.go
|
||||
|
||||
func ID(id string) (string, bool) {
|
||||
value, exists := licenseIDs[strings.ToLower(id)]
|
||||
return value, exists
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,15 @@
|
||||
package spdxlicense
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLicenceListIDs(t *testing.T) {
|
||||
// do a sanity check on the generated data
|
||||
assert.Equal(t, "0BSD", licenseIDs["0bsd"])
|
||||
assert.Equal(t, "ZPL-2.1", licenseIDs["zpl-2.1"])
|
||||
assert.Equal(t, "GPL-2.0", licenseIDs["gpl-2"])
|
||||
assert.NotEmpty(t, Version)
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package spdxlicense
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIDParse(t *testing.T) {
|
||||
var tests = []struct {
|
||||
shortName string
|
||||
spdx string
|
||||
}{
|
||||
{
|
||||
"GPL-1-only",
|
||||
"GPL-1.0-only",
|
||||
},
|
||||
{
|
||||
"GPL-2",
|
||||
"GPL-2.0",
|
||||
},
|
||||
{
|
||||
"GPL-2+",
|
||||
"GPL-2.0+",
|
||||
},
|
||||
{
|
||||
"GPL-3.0.0-or-later",
|
||||
"GPL-3.0-or-later",
|
||||
},
|
||||
{
|
||||
"GPL-3-with-autoconf-exception",
|
||||
"GPL-3.0-with-autoconf-exception",
|
||||
},
|
||||
{
|
||||
"CC-by-nc-3-de",
|
||||
"CC-BY-NC-3.0-DE",
|
||||
},
|
||||
// the below few cases are NOT expected, however, seem unavoidable given the current approach
|
||||
{
|
||||
"w3c-20150513.0.0",
|
||||
"W3C-20150513",
|
||||
},
|
||||
{
|
||||
"spencer-86.0.0",
|
||||
"Spencer-86",
|
||||
},
|
||||
{
|
||||
"unicode-dfs-2015.0.0",
|
||||
"Unicode-DFS-2015",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.shortName, func(t *testing.T) {
|
||||
got, exists := ID(test.shortName)
|
||||
assert.True(t, exists)
|
||||
assert.Equal(t, test.spdx, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -12,3 +12,10 @@ func HasAnyOfPrefixes(input string, prefixes ...string) bool {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func TruncateMiddleEllipsis(input string, maxLen int) string {
|
||||
if len(input) <= maxLen {
|
||||
return input
|
||||
}
|
||||
return input[:maxLen/2] + "..." + input[len(input)-(maxLen/2):]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -63,3 +64,43 @@ func TestHasAnyOfPrefixes(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTruncateMiddleEllipsis(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
len int
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
input: "nobody expects the spanish inquisition",
|
||||
len: 39,
|
||||
expected: "nobody expects the spanish inquisition",
|
||||
},
|
||||
{
|
||||
input: "nobody expects the spanish inquisition",
|
||||
len: 30,
|
||||
expected: "nobody expects ...ish inquisition",
|
||||
},
|
||||
{
|
||||
input: "nobody expects the spanish inquisition",
|
||||
len: 38,
|
||||
expected: "nobody expects the spanish inquisition",
|
||||
},
|
||||
{
|
||||
input: "",
|
||||
len: 30,
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
input: "",
|
||||
len: 0,
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.input+":"+strconv.Itoa(test.len), func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, TruncateMiddleEllipsis(test.input, test.len))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
)
|
||||
|
||||
// CatalogerPresenterReady is a UI function for processing the CatalogerFinished bus event, displaying the catalog
|
||||
// via the given presenter to stdout.
|
||||
func CatalogerPresenterReady(event partybus.Event) error {
|
||||
// show the report to stdout
|
||||
pres, err := syftEventParsers.ParsePresenterReady(event)
|
||||
if err != nil {
|
||||
return fmt.Errorf("bad CatalogerFinished event: %w", err)
|
||||
}
|
||||
|
||||
if err := pres.Present(os.Stdout); err != nil {
|
||||
return fmt.Errorf("unable to show package catalog report: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// TODO: move me to a common module (used in multiple repos)
|
||||
|
||||
const (
|
||||
SpinnerCircleOutlineSet = "◜◠◯◎◉●◉◎◯◡◞"
|
||||
SpinnerCircleSet = "◌◯◎◉●◉◎◯"
|
||||
SpinnerDotSet = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
|
||||
SpinnerHorizontalBarSet = "▉▊▋▌▍▎▏▎▍▌▋▊▉"
|
||||
SpinnerVerticalBarSet = "▁▃▄▅▆▇█▇▆▅▄▃▁"
|
||||
SpinnerDoubleBarSet = "▁▂▃▄▅▆▇█▉▊▋▌▍▎▏▏▎▍▌▋▊▉█▇▆▅▄▃▂▁"
|
||||
SpinnerArrowSet = "←↖↑↗→↘↓↙"
|
||||
)
|
||||
|
||||
var SpinnerCircleDotSet = []string{
|
||||
"⠈⠁",
|
||||
"⠈⠑",
|
||||
"⠈⠱",
|
||||
"⠈⡱",
|
||||
"⢀⡱",
|
||||
"⢄⡱",
|
||||
"⢄⡱",
|
||||
"⢆⡱",
|
||||
"⢎⡱",
|
||||
"⢎⡰",
|
||||
"⢎⡠",
|
||||
"⢎⡀",
|
||||
"⢎⠁",
|
||||
"⠎⠁",
|
||||
"⠊⠁",
|
||||
}
|
||||
|
||||
type Spinner struct {
|
||||
index int
|
||||
charset []string
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func NewSpinner(charset string) Spinner {
|
||||
return Spinner{
|
||||
charset: strings.Split(charset, ""),
|
||||
}
|
||||
}
|
||||
|
||||
func NewSpinnerFromSlice(charset []string) Spinner {
|
||||
return Spinner{
|
||||
charset: charset,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Spinner) Current() string {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
return s.charset[s.index]
|
||||
}
|
||||
|
||||
func (s *Spinner) Next() string {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
c := s.charset[s.index]
|
||||
s.index++
|
||||
if s.index >= len(s.charset) {
|
||||
s.index = 0
|
||||
}
|
||||
return c
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user