Compare commits

..
13 Commits
Author SHA1 Message Date
Alex Goodman b06a2d4f27 add task id to constructor
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2022-06-07 17:44:24 -04:00
Alex Goodman a5dd485672 add configurable task collection backend
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2022-06-07 10:57:44 -04:00
Alex Goodman 078dbedfb6 separate CPE definitions from capabilities
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2022-04-02 00:13:21 -04:00
Alex Goodman 9f60d32369 migrate source.FileResolver to the file package
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2022-03-22 20:50:05 -04:00
Alex Goodman a3dc0fa97d rename location.VirtualPath to AccessPath
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2022-03-22 18:24:50 -04:00
Alex Goodman f999604a29 migrate location and file metadata to the file package
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2022-03-22 18:21:35 -04:00
Alex Goodman a49c7e3c53 migrate filename glob helpers to internal
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2022-03-22 17:41:02 -04:00
Alex Goodman b3ca75646c keep file catalogers separate from file-related definitions
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2022-03-22 17:23:13 -04:00
Alex Goodman 3308079158 export event monitor structs but not behavior
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2022-03-22 16:05:45 -04:00
Alex Goodman 1279bd0b08 add additional package catalger append option
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2022-03-22 10:48:16 -04:00
Alex Goodman f89f58b825 change package cataloger option name
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2022-03-14 12:54:37 -04:00
Alex Goodman a70cfa6dd7 migrate cataloging config to separate source file
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2022-03-14 12:30:08 -04:00
Alex Goodman 3323ce2b6b [wip] api refactor
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2022-03-11 21:26:33 -05:00
601 changed files with 7252 additions and 22717 deletions
-30
View File
@@ -5,7 +5,6 @@ permit:
- MPL.*
- ISC
ignore-packages:
- .
# packageurl-go is released under the MIT license located in the root of the repo at /mit.LICENSE
- github.com/anchore/packageurl-go
@@ -21,32 +20,3 @@ ignore-packages:
# All these files have been put into the public domain.
# You can do whatever you want with these files.
- github.com/xi2/xz
# from: https://gitlab.com/cznic/sqlite/-/blob/v1.15.4/LICENSE
# This is a BSD-3-Clause license
- modernc.org/libc
- modernc.org/libc/errno
- modernc.org/libc/fcntl
- modernc.org/libc/fts
- modernc.org/libc/grp
- modernc.org/libc/langinfo
- modernc.org/libc/limits
- modernc.org/libc/netdb
- modernc.org/libc/netinet/in
- modernc.org/libc/poll
- modernc.org/libc/pthread
- modernc.org/libc/pwd
- modernc.org/libc/signal
- modernc.org/libc/stdio
- modernc.org/libc/stdlib
- modernc.org/libc/sys/socket
- modernc.org/libc/sys/stat
- modernc.org/libc/sys/types
- modernc.org/libc/termios
- modernc.org/libc/time
- modernc.org/libc/unistd
- modernc.org/libc/utime
- modernc.org/libc/uuid/uuid
- modernc.org/libc/wctype
- modernc.org/mathutil
- modernc.org/memory
+1 -1
View File
@@ -29,7 +29,7 @@ notarize() {
fi
# install gon
which gon || (go install github.com/mitchellh/gon/cmd/gon@latest)
which gon || (brew tap mitchellh/gon && brew install mitchellh/gon/gon)
# create config (note: json via stdin with gon is broken, can only use HCL from file)
hcl_file=$(mktemp).hcl
@@ -21,14 +21,6 @@ fi
if [ -z "$DOCKER_PASSWORD" ]; then
exit_with_error "DOCKER_PASSWORD not set"
fi
if [ -z "$GHCR_USERNAME" ]; then
exit_with_error "GHCR_USERNAME not set"
fi
if [ -z "$GHCR_PASSWORD" ]; then
exit_with_error "GHCR_PASSWORD not set"
fi
set -u
# setup_signing
@@ -49,5 +41,4 @@ setup_signing() {
commentary "log into docker -- required for publishing (since the default keychain has now been replaced)"
echo "${DOCKER_PASSWORD}" | docker login docker.io -u "${DOCKER_USERNAME}" --password-stdin
echo "${GHCR_PASSWORD}" | docker login ghcr.io -u "${GHCR_USERNAME}" --password-stdin
}
-17
View File
@@ -1,17 +0,0 @@
# Uses https://github.com/actions/add-to-project example to add issues to Anchore OSS project
name: Add issues to OSS project
on:
issues:
types:
- opened
jobs:
add-to-project:
name: Add issue to project
runs-on: ubuntu-latest
steps:
- uses: actions/add-to-project@main
with:
project-url: https://github.com/orgs/anchore/projects/22
github-token: ${{ secrets.CI_WRITE_GITHUB_TOKEN }}
-17
View File
@@ -1,17 +0,0 @@
# Uses https://github.com/actions/add-to-project example to add PR to Anchore OSS project
name: Add pr to OSS project
on:
pull_request:
types:
- opened
jobs:
add-to-project:
name: Add pr to project
runs-on: ubuntu-latest
steps:
- uses: actions/add-to-project@main
with:
project-url: https://github.com/orgs/anchore/projects/22
github-token: ${{ secrets.CI_WRITE_GITHUB_TOKEN }}
+1 -5
View File
@@ -9,7 +9,7 @@ on:
- "v*"
env:
GO_VERSION: "1.18.x"
GO_VERSION: "1.17.x"
jobs:
quality-gate:
@@ -94,8 +94,6 @@ jobs:
needs: [quality-gate]
# due to our code signing process, it's vital that we run our release steps on macOS
runs-on: macos-latest
permissions:
packages: write
steps:
- uses: docker-practice/actions-setup-docker@v1
@@ -130,8 +128,6 @@ jobs:
- name: Build & publish release artifacts
run: make release
env:
GHCR_USERNAME: ${{ github.actor }}
GHCR_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
DOCKER_USERNAME: ${{ secrets.TOOLBOX_DOCKER_USER }}
DOCKER_PASSWORD: ${{ secrets.TOOLBOX_DOCKER_PASS }}
# we use a different token than GITHUB_SECRETS to additionally allow updating the homebrew repos
@@ -1,51 +0,0 @@
name: PR for latest Stereoscope release
on:
schedule:
- cron: "0 8 * * *" # 3 AM EST
workflow_dispatch:
env:
GO_VERSION: "1.18.x"
GO_STABLE_VERSION: true
jobs:
upgrade-stereoscope:
runs-on: ubuntu-latest
if: github.repository == 'anchore/syft' # only run for main repo
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v2
with:
go-version: ${{ env.GO_VERSION }}
stable: ${{ env.GO_STABLE_VERSION }}
- run: |
LATEST_VERSION=$(git ls-remote https://github.com/anchore/stereoscope main | head -n1 | awk '{print $1;}')
# update go.mod
go get github.com/anchore/stereoscope@$LATEST_VERSION
go mod tidy
# export the version for use with create-pull-request
echo "::set-output name=LATEST_VERSION::$LATEST_VERSION"
id: latest-version
- uses: tibdex/github-app-token@v1
id: generate-token
with:
app_id: ${{ secrets.TOKEN_APP_ID }}
private_key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }}
- uses: peter-evans/create-pull-request@v4
with:
signoff: true
delete-branch: true
branch: auto/latest
labels: dependencies
commit-message: "Update Stereoscope to ${{ steps.latest-version.outputs.LATEST_VERSION }}"
title: "Update Stereoscope to ${{ steps.latest-version.outputs.LATEST_VERSION }}"
body: |
Update Stereoscope to ${{ steps.latest-version.outputs.LATEST_VERSION }}
token: ${{ steps.generate-token.outputs.token }}
+1 -18
View File
@@ -7,8 +7,7 @@ on:
pull_request:
env:
GO_VERSION: "1.18.x"
GO_STABLE_VERSION: true
GO_VERSION: "1.17.x"
jobs:
@@ -20,7 +19,6 @@ jobs:
- uses: actions/setup-go@v2
with:
go-version: ${{ env.GO_VERSION }}
stable: ${{ env.GO_STABLE_VERSION }}
- uses: actions/checkout@v2
@@ -58,7 +56,6 @@ jobs:
- uses: actions/setup-go@v2
with:
go-version: ${{ env.GO_VERSION }}
stable: ${{ env.GO_STABLE_VERSION }}
- uses: actions/checkout@v2
@@ -95,16 +92,6 @@ jobs:
path: syft/pkg/cataloger/java/test-fixtures/java-builds/packages
key: ${{ runner.os }}-unit-java-cache-${{ hashFiles( 'syft/pkg/cataloger/java/test-fixtures/java-builds/packages.fingerprint' ) }}
- name: Build cache key for go binary test-fixture blobs (for unit tests)
run: make go-binaries-fingerprint
- name: Restore Go binary test-fixture cache
id: unit-go-binary-cache
uses: actions/cache@v2.1.3
with:
path: syft/pkg/cataloger/golang/test-fixtures/archs/binaries
key: ${{ runner.os }}-unit-go-binaries-cache-${{ hashFiles( 'syft/pkg/cataloger/golang/test-fixtures/archs/binaries.fingerprint' ) }}
- name: Run unit tests
run: make unit
@@ -121,7 +108,6 @@ jobs:
- uses: actions/setup-go@v2
with:
go-version: ${{ env.GO_VERSION }}
stable: ${{ env.GO_STABLE_VERSION }}
- uses: actions/checkout@v2
@@ -173,7 +159,6 @@ jobs:
- uses: actions/setup-go@v2
with:
go-version: ${{ env.GO_VERSION }}
stable: ${{ env.GO_STABLE_VERSION }}
- uses: actions/checkout@v2
@@ -246,7 +231,6 @@ jobs:
- uses: actions/setup-go@v2
with:
go-version: ${{ env.GO_VERSION }}
stable: ${{ env.GO_STABLE_VERSION }}
- uses: actions/checkout@v2
@@ -354,7 +338,6 @@ jobs:
- uses: actions/setup-go@v2
with:
go-version: ${{ env.GO_VERSION }}
stable: ${{ env.GO_STABLE_VERSION }}
- uses: actions/checkout@v2
+1 -20
View File
@@ -1,6 +1,6 @@
release:
prerelease: auto
draft: false
draft: true
env:
# required to support multi architecture docker builds
@@ -12,7 +12,6 @@ before:
builds:
- id: linux-build
dir: ./cmd/syft
binary: syft
goos:
- linux
@@ -33,7 +32,6 @@ builds:
-X github.com/anchore/syft/internal/version.gitDescription={{.Summary}}
- id: darwin-build
dir: ./cmd/syft
binary: syft
goos:
- darwin
@@ -51,7 +49,6 @@ builds:
- ./.github/scripts/apple-signing/sign.sh "{{ .Path }}" "{{ .IsSnapshot }}" "{{ .Target }}"
- id: windows-build
dir: ./cmd/syft
binary: syft
goos:
- windows
@@ -102,11 +99,6 @@ dockers:
- "anchore/syft:{{ .Tag }}-amd64"
- "anchore/syft:v{{ .Major }}-amd64"
- "anchore/syft:v{{ .Major }}.{{ .Minor }}-amd64"
- "ghcr.io/anchore/syft:latest"
- "ghcr.io/anchore/syft:{{ .Tag }}-amd64"
- "ghcr.io/anchore/syft:v{{ .Major }}-amd64"
- "ghcr.io/anchore/syft:v{{ .Major }}.{{ .Minor }}-amd64"
goarch: amd64
dockerfile: Dockerfile
use: buildx
build_flag_templates:
@@ -120,9 +112,6 @@ dockers:
- "anchore/syft:{{ .Tag }}-arm64v8"
- "anchore/syft:v{{ .Major }}-arm64v8"
- "anchore/syft:v{{ .Major }}.{{ .Minor }}-arm64v8"
- "ghcr.io/anchore/syft:{{ .Tag }}-arm64v8"
- "ghcr.io/anchore/syft:v{{ .Major }}-arm64v8"
- "ghcr.io/anchore/syft:v{{ .Major }}.{{ .Minor }}-arm64v8"
goarch: arm64
dockerfile: Dockerfile
use: buildx
@@ -142,11 +131,3 @@ docker_manifests:
image_templates:
- anchore/syft:v{{ .Major }}.{{ .Minor }}-amd64
- anchore/syft:v{{ .Major }}.{{ .Minor }}-arm64v8
- name_template: ghcr.io/anchore/syft:{{ .Tag }}
image_templates:
- ghcr.io/anchore/syft:v{{ .Major }}.{{ .Minor }}-amd64
- ghcr.io/anchore/syft:v{{ .Major }}.{{ .Minor }}-arm64v8
- name_template: ghcr.io/anchore/syft:latest
image_templates:
- ghcr.io/anchore/syft:v{{ .Major }}.{{ .Minor }}-amd64
- ghcr.io/anchore/syft:v{{ .Major }}.{{ .Minor }}-arm64v8
+6
View File
@@ -82,6 +82,12 @@ 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
This project has a `Makefile` which includes many helpers running both unit and integration tests. Although PRs will have automatic checks for these, it is useful to run them locally, ensuring they pass before submitting changes. Ensure you've bootstrapped once before running tests:
+6 -12
View File
@@ -106,7 +106,7 @@ $(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.45.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.3.0
curl -sSfL https://raw.githubusercontent.com/anchore/chronicle/main/install.sh | sh -s -- -b $(TEMPDIR)/ v0.3.0
.github/scripts/goreleaser-install.sh -d -b $(TEMPDIR)/ v1.4.1
@@ -122,7 +122,7 @@ bootstrap: $(RESULTSDIR) bootstrap-go bootstrap-tools ## Download and install al
$(call title,Bootstrapping dependencies)
.PHONY: static-analysis
static-analysis: check-go-mod-tidy check-licenses lint
static-analysis: lint check-go-mod-tidy check-licenses
.PHONY: lint
lint: ## Run gofmt + golangci lint checks
@@ -147,7 +147,7 @@ lint-fix: ## Auto-format all source code + run golangci lint fixers
.PHONY: check-licenses
check-licenses: ## Ensure transitive dependencies are compliant with the current license policy
$(TEMPDIR)/bouncer check ./cmd/syft
$(TEMPDIR)/bouncer check
check-go-mod-tidy:
@ .github/scripts/go-mod-tidy-check.sh && echo "go.mod and go.sum are tidy!"
@@ -211,19 +211,13 @@ integration-fingerprint:
.PHONY: java-packages-fingerprint
java-packages-fingerprint:
$(call title,Java test fixture fingerprint)
cd syft/pkg/cataloger/java/test-fixtures/java-builds && \
cd syft/cataloger/packages/java/test-fixtures/java-builds && \
make packages.fingerprint
.PHONY: go-binaries-fingerprint
go-binaries-fingerprint:
$(call title,Go binaries test fixture fingerprint)
cd syft/pkg/cataloger/golang/test-fixtures/archs && \
make binaries.fingerprint
.PHONY: fixtures
fixtures:
$(call title,Generating test fixtures)
cd syft/pkg/cataloger/java/test-fixtures/java-builds && make
cd syft/cataloger/packages/java/test-fixtures/java-builds && make
.PHONY: generate-json-schema
generate-json-schema: ## Generate a new json schema
@@ -305,7 +299,7 @@ cli: $(SNAPSHOTDIR) ## Run CLI tests
chmod 755 "$(SNAPSHOT_BIN)"
$(SNAPSHOT_BIN) version
SYFT_BINARY_LOCATION='$(SNAPSHOT_BIN)' \
go test -count=1 -timeout=15m -v ./test/cli
go test -count=1 -v ./test/cli
.PHONY: changelog
changelog: clean-changelog CHANGELOG.md
+63 -134
View File
@@ -9,15 +9,7 @@
[![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/anchore/syft/blob/main/LICENSE)
[![Slack Invite](https://img.shields.io/badge/Slack-Join-blue?logo=slack)](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 like [Grype](https://github.com/anchore/grype).
### Join our Virtual OSS Meetup!
You are invited to join us on June 15th, 11AM-Noon PT for our virtual open source meetup.
Hosts Amy Bass from Docker Desktop and Christopher Phillips from Anchore OSS will explore how Docker Extensions for Docker Desktop is supporting open source projects and we’ll have the latest update on Syft: in-toto attestations.
[Register here ->](https://get.anchore.com/anchore-oss-meetup-jun-15-2022/)
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).
### Join our community meetings!
@@ -28,28 +20,15 @@ Hosts Amy Bass from Docker Desktop and Christopher Phillips from Anchore OSS wil
![syft-demo](https://user-images.githubusercontent.com/590471/90277200-2a253000-de33-11ea-893f-32c219eea11a.gif)
## Features
- Generates SBOMs for container images, filesystems, archives, and more to discover packages and libraries
- Supports OCI and Docker image formats
- Linux distribution identification
- Works seamlessly with [Grype](https://github.com/anchore/grype) (a fast, modern vulnerability scanner)
- Able to create signed SBOM attestations using the [in-toto specification](https://github.com/in-toto/attestation/blob/main/spec/README.md)
- Convert between SBOM formats, such as CycloneDX, SPDX, and Syft's own format.
- Catalog container images and filesystems to discover packages and libraries.
- Generate in-toto attestations where an SBOM is included as the payload.
- Supports packages and libraries from various ecosystems (APK, DEB, RPM, Ruby Bundles, Python Wheel/Egg/requirements.txt, JavaScript NPM/Yarn, Java JAR/EAR/WAR/PAR/SAR, Jenkins plugins JPI/HPI, Go modules, PHP Composer)
- Linux distribution identification (supports Alpine, BusyBox, CentOS/RedHat, Debian/Ubuntu flavored distributions)
- Supports Docker and OCI image formats
- Direct support for [Grype](https://github.com/anchore/grype), a fast and powerful vulnerability matcher.
### Supported Ecosystems
- Alpine (apk)
- Dart (pubs)
- Debian (dpkg)
- Dotnet (deps.json)
- Go (go.mod, Go binaries)
- Java (jar, ear, war, par, sar)
- JavaScript (npm, yarn)
- Jenkins Plugins (jpi, hpi)
- PHP (composer)
- Python (wheel, egg, poetry, requirements.txt)
- Red Hat (rpm)
- Ruby (gem)
- Rust (cargo.lock)
If you encounter an issue, please [let us know using the issue tracker](https://github.com/anchore/syft/issues).
## Installation
@@ -66,14 +45,6 @@ curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b <DESTINATION_DIR> <RELEASE_VERSION>
```
### Chocolatey
The chocolatey distribution of syft is community maintained and not distributed by the anchore team
```powershell
choco install syft -y
```
### Homebrew
```bash
brew tap anchore/syft
@@ -82,13 +53,15 @@ brew install syft
### Nix
**Note**: Nix packaging of Syft is [community maintained](https://github.com/NixOS/nixpkgs/blob/master/pkgs/tools/admin/syft/default.nix). Syft is currently only in the [unstable channel](https://nixos.wiki/wiki/Nix_channels#The_official_channels) awaiting the `22.05` release
**Note**: nix packaging of Syft is [community maintained](https://github.com/NixOS/nixpkgs/blob/master/pkgs/tools/admin/syft/default.nix)
Also syft is currently only in the [unstable channel](https://nixos.wiki/wiki/Nix_channels#The_official_channels) awaiting the `22.05` release
```bash
nix-env -i syft
```
... or, just try it out in an ephemeral nix shell:
... or, just try it out in an ephemeral nix shell
```bash
nix-shell -p syft
@@ -97,78 +70,33 @@ nix-shell -p syft
## Getting started
#### SBOM
To generate an SBOM for a container image:
To generate an SBOM for an OCI image:
```
syft <image>
```
The above output includes only software that is visible in the container (i.e., the squashed representation of the image). To include software from all image layers in the SBOM, regardless of its presence in the final image, provide `--scope all-layers`:
**Note**: This is equivalent to specifying the `packages` subcommand:
```
syft <image> --scope all-layers
syft packages <image>
```
#### Format conversion (experimental)
The ability to convert existing SBOMs means you can create SBOMs in different formats quickly, without the need to regenerate the SBOM from scratch, which may take significantly more time.
The above output includes only software that is visible in the container (i.e., the squashed representation of the image).
To include software from all image layers in the SBOM, regardless of its presence in the final image, provide `--scope all-layers`:
```
syft convert <ORIGINAL-SBOM-FILE> -o <NEW-SBOM-FORMAT>[=<NEW-SBOM-FILE>]
syft packages <image> --scope all-layers
```
This feature is experimental and data might be lost when converting formats. Packages are the main SBOM component easily transferable across formats, whereas files and relationships, as well as other information Syft doesn't support, are more likely to be lost.
We support formats with wide community usage AND good encode/decode support by Syft. The supported formats are:
- Syft JSON
- SPDX 2.2 JSON
- SPDX 2.2 tag-value
- CycloneDX 1.4 JSON
- CycloneDX 1.4 XML
Conversion example:
```sh
syft alpine:latest -o syft-json=sbom.syft.json # generate a syft SBOM
syft convert sbom.syft.json -o cyclonedx-json=sbom.cdx.json # convert it to CycloneDX
```
#### SBOM attestation
### Keyless support
Syft supports generating attestations using cosign's [keyless](https://github.com/sigstore/cosign/blob/main/KEYLESS.md) signatures.
To use this feature with a format like CycloneDX json simply run:
```
syft attest --output cyclonedx-json <IMAGE WITH OCI WRITE ACCESS>
```
This command will open a web browser and allow the user to authenticate their OIDC identity as the root of trust for the attestation (Github, Google, Microsoft).
After authenticating, Syft will upload the attestation to the OCI registry specified by the image that the user has write access to.
You will need to make sure your credentials are configured for the OCI registry you are uploading to so that the attestation can write successfully.
Users can then verify the attestation(or any image with attestations) by running:
```
COSIGN_EXPERIMENTAL=1 cosign verify-attestation <IMAGE_WITH_ATTESTATIONS>
```
Users should see that the uploaded attestation claims are validated, the claims exist within the transparency log, and certificates on the attestations were verified against [fulcio](https://github.com/SigStore/fulcio).
There will also be a printout of the certificates subject `<user identity>` and the certificate issuer URL: `<provider of user identity (Github, Google, Microsoft)>`:
```
Certificate subject: test.email@testdomain.com
Certificate issuer URL: https://accounts.google.com
```
### Local private key support
To generate an SBOM attestation for a container image using a local private key:
#### SBOM Attestation
To generate an attested SBOM for an OCI image as the predicate of an in-toto attestation
```
syft attest --output [FORMAT] --key [KEY] [SOURCE] [flags]
```
The above output is in the form of the [DSSE envelope](https://github.com/secure-systems-lab/dsse/blob/master/envelope.md#dsse-envelope).
The payload is a base64 encoded `in-toto` statement with the generated SBOM as the predicate. For details on workflows using this command see [here](#adding-an-sbom-to-an-image-as-an-attestation-using-syft).
The payload is a base64 encoded `in-toto` statement with the SBOM as the predicate, the payload type is `application/vnd.in-toto+json`, and the signatures array is populated
with the contents needed for public key verification. For details on workflows using this command see [here](#adding-an-sbom-to-an-image-as-an-attestation-using-syft).
### Supported sources
@@ -176,10 +104,10 @@ 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 path/to/image.tar
syft packages path/to/image.tar
# catalog a directory
syft path/to/dir
syft packages path/to/dir
```
Sources can be explicitly provided with a scheme:
@@ -218,17 +146,16 @@ The output format for Syft is configurable as well using the
`-o` (or `--output`) option:
```
syft <image> -o <format>
syft packages <image> -o <format>
```
Where the `formats` 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-xml`: A XML report conforming to the [CycloneDX 1.4 specification](https://cyclonedx.org/specification/overview/).
- `cyclonedx-json`: A JSON report conforming to the [CycloneDX 1.4 specification](https://cyclonedx.org/specification/overview/).
- `spdx-tag-value`: A tag-value formatted report conforming to the [SPDX 2.2 specification](https://spdx.github.io/spdx-spec/).
- `cyclonedx`: A XML report conforming to the [CycloneDX 1.3 specification](https://cyclonedx.org/specification/overview/).
- `cyclonedx-json`: A JSON report conforming to the [CycloneDX 1.3 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).
- `github`: A JSON report conforming to GitHub's dependency snapshot format.
- `table`: A columnar summary (default).
#### Multiple outputs
@@ -237,16 +164,20 @@ Syft can also output _multiple_ files in differing formats by appending
`=<file>` to the option, for example to output Syft JSON and SPDX JSON:
```shell
syft <image> -o json=sbom.syft.json -o spdx-json=sbom.spdx.json
syft packages <image> -o json=sbom.syft.json -o spdx-json=sbom.spdx.json
```
## Private Registry Authentication
### Local Docker Credentials
When a container runtime is not present, Syft can still utilize credentials configured in common credential sources (such as `~/.docker/config.json`). It will pull images from private registries using these credentials. The config file is where your credentials are stored when authenticating with private registries via some command like `docker login`. For more information see the `go-containerregistry` [documentation](https://github.com/google/go-containerregistry/tree/main/pkg/authn).
When a container runtime is not present, Syft can still utilize credentials configured in common credential sources (such as `~/.docker/config.json`).
It will pull images from private registries using these credentials. The config file is where your credentials are stored when authenticating with private registries via some command like `docker login`.
For more information see the `go-containerregistry` [documentation](https://github.com/google/go-containerregistry/tree/main/pkg/authn).
An example `config.json` looks something like this:
```json
```
// config.json
{
"auths": {
"registry.example.com": {
@@ -259,19 +190,17 @@ An example `config.json` looks something like this:
You can run the following command as an example. It details the mount/environment configuration a container needs to access a private registry:
```
docker run -v ./config.json:/config/config.json -e "DOCKER_CONFIG=/config" anchore/syft:latest <private_image>
```
`docker run -v ./config.json:/config/config.json -e "DOCKER_CONFIG=/config" anchore/syft:latest <private_image>`
### Docker Credentials in Kubernetes
Here's a simple workflow to mount this config file as a secret into a container on Kubernetes.
1. Create a secret. The value of `config.json` is important. It refers to the specification detailed [here](https://github.com/google/go-containerregistry/tree/main/pkg/authn#the-config-file). Below this section is the `secret.yaml` file that the pod configuration will consume as a volume. The key `config.json` is important. It will end up being the name of the file when mounted into the pod.
```yaml
The below section shows a simple workflow on how to mount this config file as a secret into a container on kubernetes.
1. Create a secret. The value of `config.json` is important. It refers to the specification detailed [here](https://github.com/google/go-containerregistry/tree/main/pkg/authn#the-config-file).
Below this section is the `secret.yaml` file that the pod configuration will consume as a volume.
The key `config.json` is important. It will end up being the name of the file when mounted into the pod.
```
# secret.yaml
apiVersion: v1
kind: Secret
metadata:
@@ -284,11 +213,13 @@ Here's a simple workflow to mount this config file as a secret into a container
`kubectl apply -f secret.yaml`
2. Create your pod running syft. The env `DOCKER_CONFIG` is important because it advertises where to look for the credential file. In the below example, setting `DOCKER_CONFIG=/config` informs syft that credentials can be found at `/config/config.json`. This is why we used `config.json` as the key for our secret. When mounted into containers the secrets' key is used as the filename. The `volumeMounts` section mounts our secret to `/config`. The `volumes` section names our volume and leverages the secret we created in step one.
```yaml
2. Create your pod running syft. The env `DOCKER_CONFIG` is important because it advertises where to look for the credential file.
In the below example, setting `DOCKER_CONFIG=/config` informs syft that credentials can be found at `/config/config.json`.
This is why we used `config.json` as the key for our secret. When mounted into containers the secrets' key is used as the filename.
The `volumeMounts` section mounts our secret to `/config`. The `volumes` section names our volume and leverages the secret we created in step one.
```
# pod.yaml
apiVersion: v1
kind: Pod
metadata:
@@ -315,9 +246,10 @@ Here's a simple workflow to mount this config file as a secret into a container
`kubectl apply -f pod.yaml`
3. The user can now run `kubectl logs syft-private-registry-demo`. The logs should show the Syft analysis for the `<private_image>` provided in the pod configuration.
3. The user can now run `kubectl logs syft-private-registry-demo`. The logs should show the syft analysis for the `<private_image>` provided in the pod configuration.
Using the above information, users should be able to configure private registry access without having to do so in the `grype` or `syft` configuration files. They will also not be dependent on a Docker daemon, (or some other runtime software) for registry configuration and access.
Using the above information, users should be able to configure private registry access without having to do so in the `grype` or `syft` configuration files.
They will also not be dependent on a docker daemon, (or some other runtime software) for registry configuration and access.
## Configuration
@@ -354,11 +286,7 @@ check-for-app-update: true
# exclude:
# - "/etc/**"
# - "./out/**/*.json"
exclude: []
# os and/or architecture to use when referencing container images (e.g. "windows/armv6" or "arm64")
# same as --platform; SYFT_PLATFORM env var
platform: ""
exclude:
# cataloging packages is exposed through the packages and power-user subcommands
package:
@@ -532,25 +460,26 @@ anchore:
`syft attest --output [FORMAT] --key [KEY] [SOURCE] [flags]`
SBOMs themselves can serve as input to different analysis tools. [Grype](https://github.com/anchore/grype), a vulnerability scanner CLI tool from Anchore, is one such tool. Publishers of container images can use attestations to enable their consumers to trust Syft-generated SBOM descriptions of those container images. To create and provide these attestations, image publishers can run `syft attest` in conjunction with the [cosign](https://github.com/sigstore/cosign) tool to attach SBOM attestations to their images.
SBOMs themselves can serve as input to different analysis tools. The Anchore organization offers the vulnerability scanner
[grype](https://github.com/anchore/grype) as one such tool.
One of the foundational approaches to "trust" between tools is for producers to use the artifacts generated by syft as attestations to their images.
The DSSE output of `syft attest` can be used with the [cosign](https://github.com/sigstore/cosign) tool to attach an attestation to an image.
#### Example attestation
Note for the following example replace `docker.io/image:latest` with an image you own. You should also have push access to
its remote reference. Replace `$MY_PRIVATE_KEY` with a private key you own or have generated with cosign.
#### Example attest
Note for the following example replace `docker.io/image:latest` with an image you own. You should also have push access to
its remote reference. Replace $MY_PRIVATE_KEY with a private key you own or have generated with cosign.
```bash
syft attest --key $MY_PRIVATE_KEY docker.io/image:latest > image_latest_sbom_attestation.json
cosign attach attestation --attestation image_latest_sbom_attestation.json docker.io/image:latest
```
Verify the new attestation exists on your image.
Verify the new attestation exists on your image
```bash
cosign verify-attestation -key $MY_PUBLIC_KEY docker.io/image:latest | jq '.payload | @base64d | .payload | fromjson | .predicate'
```
You should see this output along with the attached SBOM:
You should see this output along with the attached SBOM.
```
Verification for docker.io/image:latest --
The following checks were performed on each of these signatures:
+333
View File
@@ -0,0 +1,333 @@
package cmd
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"os"
"strings"
"github.com/anchore/syft/internal/formats/cyclonedxjson"
"github.com/anchore/syft/internal/formats/spdx22json"
"github.com/anchore/syft/internal/formats/syftjson"
"github.com/anchore/stereoscope"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/ui"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
"github.com/in-toto/in-toto-golang/in_toto"
"github.com/pkg/errors"
"github.com/pkg/profile"
"github.com/sigstore/cosign/cmd/cosign/cli/sign"
"github.com/sigstore/cosign/pkg/cosign"
"github.com/sigstore/cosign/pkg/cosign/attestation"
"github.com/sigstore/sigstore/pkg/signature/dsse"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
"github.com/wagoodman/go-partybus"
signatureoptions "github.com/sigstore/sigstore/pkg/signature/options"
)
const (
attestExample = ` {{.appName}} {{.command}} --output [FORMAT] --key [KEY] alpine:latest
Supports the following image sources:
{{.appName}} {{.command}} --key [KEY] 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}} --key [KEY] path/to/a/file/or/dir only for OCI tar or OCI directory
`
attestSchemeHelp = "\n" + indent + schemeHelpHeader + "\n" + imageSchemeHelp
attestHelp = attestExample + attestSchemeHelp
intotoJSONDsseType = `application/vnd.in-toto+json`
)
var attestFormats = []sbom.FormatID{
syftjson.ID,
spdx22json.ID,
cyclonedxjson.ID,
}
var (
attestCmd = &cobra.Command{
Use: "attest --output [FORMAT] --key [KEY] [SOURCE]",
Short: "Generate a package SBOM as an attestation for the given [SOURCE] container image",
Long: "Generate a packaged-based Software Bill Of Materials (SBOM) from a container image as the predicate of an in-toto attestation",
Example: internal.Tprintf(attestHelp, map[string]interface{}{
"appName": internal.ApplicationName,
"command": "attest",
}),
Args: validateInputArgs,
SilenceUsage: true,
SilenceErrors: true,
PreRunE: func(cmd *cobra.Command, args []string) (err error) {
if appConfig.Dev.ProfileCPU && appConfig.Dev.ProfileMem {
return fmt.Errorf("cannot profile CPU and memory simultaneously")
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
if appConfig.Dev.ProfileCPU {
defer profile.Start(profile.CPUProfile).Stop()
} else if appConfig.Dev.ProfileMem {
defer profile.Start(profile.MemProfile).Stop()
}
return attestExec(cmd.Context(), cmd, args)
},
}
)
func fetchPassword(_ bool) (b []byte, err error) {
potentiallyPipedInput, err := internal.IsPipedInput()
if err != nil {
log.Warnf("unable to determine if there is piped input: %+v", err)
}
switch {
case appConfig.Attest.Password != "":
return []byte(appConfig.Attest.Password), nil
case potentiallyPipedInput:
// handle piped in passwords
pwBytes, err := io.ReadAll(os.Stdin)
if err != nil {
return nil, fmt.Errorf("unable to get password from stdin: %w", err)
}
// be resilient to input that may have newline characters (in case someone is using echo without -n)
cleanPw := strings.TrimRight(string(pwBytes), "\n")
return []byte(cleanPw), nil
case internal.IsTerminal():
return cosign.GetPassFromTerm(false)
}
return nil, errors.New("no method available to fetch password")
}
func selectPassFunc(keypath string) (cosign.PassFunc, error) {
keyContents, err := os.ReadFile(keypath)
if err != nil {
return nil, err
}
var fn cosign.PassFunc = func(bool) (b []byte, err error) { return nil, nil }
_, err = cosign.LoadPrivateKey(keyContents, nil)
if err != nil {
fn = fetchPassword
}
return fn, nil
}
func attestExec(ctx context.Context, _ *cobra.Command, args []string) error {
// can only be an image for attestation or OCI DIR
userInput := args[0]
si, err := source.ParseInput(userInput, appConfig.Platform, false)
if err != nil {
return fmt.Errorf("could not generate source input for attest command: %w", err)
}
switch si.Type {
case source.ImageType, source.UnknownType:
// at this point we know that it cannot be dir: or file: schemes, so we will assume that the unknown scheme could represent an image
si.Type = source.ImageType
default:
return fmt.Errorf("attest command can only be used with image sources but discovered %q when given %q", si.Type, userInput)
}
// if the original detection was from a local daemon we want to short circuit
// that and attempt to generate the image source from a registry source instead
switch si.ImageSource {
case image.UnknownSource, image.OciRegistrySource:
si.ImageSource = image.OciRegistrySource
default:
return fmt.Errorf("attest command can only be used with image sources fetch directly from the registry, but discovered an image source of %q when given %q", si.ImageSource, userInput)
}
if len(appConfig.Outputs) > 1 {
return fmt.Errorf("unable to generate attestation for more than one output")
}
format := syft.FormatByName(appConfig.Outputs[0])
predicateType := formatPredicateType(format)
if predicateType == "" {
return fmt.Errorf("could not produce attestation predicate for given format: %q. Available formats: %+v", formatAliases(format.ID()), formatAliases(attestFormats...))
}
passFunc, err := selectPassFunc(appConfig.Attest.Key)
if err != nil {
return err
}
ko := sign.KeyOpts{
KeyRef: appConfig.Attest.Key,
PassFunc: passFunc,
}
sv, err := sign.SignerFromKeyOpts(ctx, "", ko)
if err != nil {
return err
}
defer sv.Close()
return eventLoop(
attestationExecWorker(*si, format, predicateType, sv),
setupSignals(),
eventSubscription,
stereoscope.Cleanup,
ui.Select(isVerbose(), appConfig.Quiet)...,
)
}
func attestationExecWorker(si source.Input, format sbom.Format, predicateType string, sv *sign.SignerVerifier) <-chan error {
errs := make(chan error)
go func() {
defer close(errs)
catalogingOpts, err := appConfig.ToCatalogingOptions()
if err != nil {
errs <- err
return
}
src, cleanup, err := source.NewFromRegistry(si, appConfig.Registry.ToOptions(), appConfig.Exclusions)
if cleanup != nil {
defer cleanup()
}
if err != nil {
errs <- fmt.Errorf("failed to construct source from user input %q: %w", si.UserInput, err)
return
}
s, err := syft.Catalog(src, catalogingOpts...)
if err != nil {
errs <- err
return
}
if s == nil {
errs <- fmt.Errorf("no SBOM produced for %q", si.UserInput)
return
}
sbomBytes, err := syft.Encode(*s, format)
if err != nil {
errs <- err
return
}
err = generateAttestation(sbomBytes, src, sv, predicateType)
if err != nil {
errs <- err
return
}
}()
return errs
}
func formatPredicateType(format sbom.Format) string {
switch format.ID() {
case spdx22json.ID:
return in_toto.PredicateSPDX
case cyclonedxjson.ID:
// Tentative see https://github.com/in-toto/attestation/issues/82
return "https://cyclonedx.org/bom"
case syftjson.ID:
return "https://syft.dev/bom"
default:
return ""
}
}
func findValidDigest(digests []string) string {
// since we are only using the OCI repo provider for this source we are safe that this is only 1 value
// see https://github.com/anchore/stereoscope/blob/25ebd49a842b5ac0a20c2e2b4b81335b64ad248c/pkg/image/oci/registry_provider.go#L57-L63
split := strings.Split(digests[0], "sha256:")
return split[1]
}
func generateAttestation(predicate []byte, src *source.Source, sv *sign.SignerVerifier, predicateType string) error {
switch len(src.Image.Metadata.RepoDigests) {
case 0:
return fmt.Errorf("cannot generate attestation since no repo digests were found; make sure you're passing an OCI registry source for the attest command")
case 1:
default:
return fmt.Errorf("cannot generate attestation since multiple repo digests were found for the image: %+v", src.Image.Metadata.RepoDigests)
}
wrapped := dsse.WrapSigner(sv, intotoJSONDsseType)
sh, err := attestation.GenerateStatement(attestation.GenerateOpts{
Predicate: bytes.NewBuffer(predicate),
Type: predicateType,
Digest: findValidDigest(src.Image.Metadata.RepoDigests),
})
if err != nil {
return err
}
payload, err := json.Marshal(sh)
if err != nil {
return err
}
signedPayload, err := wrapped.SignMessage(bytes.NewReader(payload), signatureoptions.WithContext(context.Background()))
if err != nil {
return errors.Wrap(err, "unable to sign SBOM")
}
bus.Publish(partybus.Event{
Type: event.Exit,
Value: func() error {
_, err := os.Stdout.Write(signedPayload)
return err
},
})
return nil
}
func init() {
setAttestFlags(attestCmd.Flags())
if err := bindAttestConfigOptions(attestCmd.Flags()); err != nil {
panic(err)
}
rootCmd.AddCommand(attestCmd)
}
func setAttestFlags(flags *pflag.FlagSet) {
// key options
flags.StringP("key", "", "cosign.key",
"path to the private key file to use for attestation",
)
// in-toto attestations only support JSON predicates, so not all SBOM formats that syft can output are supported
flags.StringP(
"output", "o", formatAliases(syftjson.ID)[0],
fmt.Sprintf("the SBOM format encapsulated within the attestation, available options=%v", formatAliases(attestFormats...)),
)
flags.StringP(
"platform", "", "",
"an optional platform specifier for container image sources (e.g. 'linux/arm64', 'linux/arm64/v8', 'arm64', 'linux')",
)
}
func bindAttestConfigOptions(flags *pflag.FlagSet) error {
// note: output is not included since this configuration option is shared between multiple subcommands
if err := viper.BindPFlag("attest.key", flags.Lookup("key")); err != nil {
return err
}
return nil
}
+31
View File
@@ -0,0 +1,31 @@
package cmd
import (
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft/event"
"github.com/wagoodman/go-partybus"
)
func checkForApplicationUpdate() {
if appConfig.CheckForAppUpdate {
log.Debugf("checking if new vesion of %s is available", internal.ApplicationName)
isAvailable, newVersion, err := version.IsUpdateAvailable()
if err != nil {
// this should never stop the application
log.Errorf(err.Error())
}
if isAvailable {
log.Infof("new version of %s is available: %s (current version is %s)", internal.ApplicationName, newVersion, version.FromBuild().Version)
bus.Publish(partybus.Event{
Type: event.AppUpdateAvailable,
Value: newVersion,
})
} else {
log.Debugf("no new %s update available", internal.ApplicationName)
}
}
}
+171
View File
@@ -0,0 +1,171 @@
package cmd
import (
"encoding/json"
"fmt"
"os"
"sort"
"github.com/spf13/pflag"
"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/internal/version"
"github.com/anchore/syft/syft"
"github.com/gookit/color"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/wagoodman/go-partybus"
)
var (
appConfig *config.Application
eventBus *partybus.Bus
eventSubscription *partybus.Subscription
)
func init() {
cobra.OnInitialize(
initCmdAliasBindings,
initAppConfig,
initLogging,
logAppConfig,
checkForApplicationUpdate,
logAppVersion,
initEventBus,
)
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, color.Red.Sprint(err.Error()))
os.Exit(1)
}
}
// we must setup the config-cli bindings first before the application configuration is parsed. However, this cannot
// be done without determining what the primary command that the config options should be bound to since there are
// shared concerns (the root-packages alias).
func initCmdAliasBindings() {
activeCmd, _, err := rootCmd.Find(os.Args[1:])
if err != nil {
panic(err)
}
// enable all cataloger by default if power-user command is run
if activeCmd == powerUserCmd {
config.PowerUserCatalogerEnabledDefault()
}
// set bindings based on the packages alias
switch activeCmd {
case packagesCmd, rootCmd:
// note: we need to lazily bind config options since they are shared between both the root command
// and the packages command. Otherwise there will be global viper state that is in contention.
// See for more details: https://github.com/spf13/viper/issues/233 . Additionally, the bindings must occur BEFORE
// reading the application configuration, which implies that it must be an initializer (or rewrite the command
// initialization structure against typical patterns used with cobra, which is somewhat extreme for a
// temporary alias)
if err = bindPackagesConfigOptions(activeCmd.Flags()); err != nil {
panic(err)
}
case attestCmd:
// the --output and --platform options are independently defined flags, but a shared config option
if err = bindSharedConfigOption(attestCmd.Flags()); err != nil {
panic(err)
}
// even though the root command or packages command is NOT being run, we still need default bindings
// such that application config parsing passes.
if err = bindExclusivePackagesConfigOptions(packagesCmd.Flags()); err != nil {
panic(err)
}
default:
// even though the root command or packages command is NOT being run, we still need default bindings
// such that application config parsing passes.
if err = bindPackagesConfigOptions(packagesCmd.Flags()); err != nil {
panic(err)
}
}
}
func bindSharedConfigOption(flags *pflag.FlagSet) error {
if err := viper.BindPFlag("output", flags.Lookup("output")); err != nil {
return err
}
if err := viper.BindPFlag("platform", flags.Lookup("platform")); err != nil {
return err
}
return nil
}
func initAppConfig() {
cfg, err := config.LoadApplicationConfig(viper.GetViper(), persistentOpts)
if err != nil {
fmt.Printf("failed to load application config: \n\t%+v\n", err)
os.Exit(1)
}
appConfig = cfg
}
func initLogging() {
cfg := logger.LogrusConfig{
EnableConsole: (appConfig.Log.FileLocation == "" || appConfig.CliOptions.Verbosity > 0) && !appConfig.Quiet,
EnableFile: appConfig.Log.FileLocation != "",
Level: appConfig.Log.LevelOpt,
Structured: appConfig.Log.Structured,
FileLocation: appConfig.Log.FileLocation,
}
logWrapper := logger.NewLogrusLogger(cfg)
syft.SetLogger(logWrapper)
stereoscope.SetLogger(&logger.LogrusNestedLogger{
Logger: logWrapper.Logger.WithField("from-lib", "stereoscope"),
})
}
func logAppConfig() {
log.Debugf("application config:\n%+v", color.Magenta.Sprint(appConfig.String()))
}
func initEventBus() {
eventBus = partybus.NewBus()
eventSubscription = eventBus.Subscribe()
stereoscope.SetBus(eventBus)
syft.SetBus(eventBus)
}
func logAppVersion() {
versionInfo := version.FromBuild()
log.Infof("syft version: %s", versionInfo.Version)
var fields map[string]interface{}
bytes, err := json.Marshal(versionInfo)
if err != nil {
return
}
err = json.Unmarshal(bytes, &fields)
if err != nil {
return
}
keys := make([]string, 0, len(fields))
for k := range fields {
keys = append(keys, k)
}
sort.Strings(keys)
for idx, field := range keys {
value := fields[field]
branch := "├──"
if idx == len(fields)-1 {
branch = "└──"
}
log.Debugf(" %s %s: %s", branch, field, value)
}
}
+113
View File
@@ -0,0 +1,113 @@
package cmd
import (
"context"
"os"
"strings"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
"github.com/spf13/cobra"
)
// completionCmd represents the completion command
var completionCmd = &cobra.Command{
Use: "completion [bash|zsh|fish]",
Short: "Generate a shell completion for Syft (listing local docker images)",
Long: `To load completions (docker image list):
Bash:
$ source <(syft completion bash)
# To load completions for each session, execute once:
Linux:
$ syft completion bash > /etc/bash_completion.d/syft
MacOS:
$ syft completion bash > /usr/local/etc/bash_completion.d/syft
Zsh:
# If shell completion is not already enabled in your environment you will need
# to enable it. You can execute the following once:
$ echo "autoload -U compinit; compinit" >> ~/.zshrc
# To load completions for each session, execute once:
$ syft completion zsh > "${fpath[1]}/_syft"
# You will need to start a new shell for this setup to take effect.
Fish:
$ syft completion fish | source
# To load completions for each session, execute once:
$ syft completion fish > ~/.config/fish/completions/syft.fish
`,
DisableFlagsInUseLine: true,
ValidArgs: []string{"bash", "zsh", "fish"},
Args: cobra.ExactValidArgs(1),
Run: func(cmd *cobra.Command, args []string) {
var err error
switch args[0] {
case "bash":
err = cmd.Root().GenBashCompletion(os.Stdout)
case "zsh":
err = cmd.Root().GenZshCompletion(os.Stdout)
case "fish":
err = cmd.Root().GenFishCompletion(os.Stdout, true)
}
if err != nil {
panic(err)
}
},
}
func init() {
rootCmd.AddCommand(completionCmd)
}
func dockerImageValidArgsFunction(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
// Since we use ValidArgsFunction, Cobra will call this AFTER having parsed all flags and arguments provided
dockerImageRepoTags, err := listLocalDockerImages(toComplete)
if err != nil {
// Indicates that an error occurred and completions should be ignored
return []string{"completion failed"}, cobra.ShellCompDirectiveError
}
if len(dockerImageRepoTags) == 0 {
return []string{"no docker images found"}, cobra.ShellCompDirectiveError
}
// ShellCompDirectiveDefault indicates that the shell will perform its default behavior after completions have
// been provided (without implying other possible directives)
return dockerImageRepoTags, cobra.ShellCompDirectiveDefault
}
func listLocalDockerImages(prefix string) ([]string, error) {
var repoTags = make([]string, 0)
ctx := context.Background()
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return repoTags, err
}
// Only want to return tagged images
imageListArgs := filters.NewArgs()
imageListArgs.Add("dangling", "false")
images, err := cli.ImageList(ctx, types.ImageListOptions{All: false, Filters: imageListArgs})
if err != nil {
return repoTags, err
}
for _, image := range images {
// image may have multiple tags
for _, tag := range image.RepoTags {
if strings.HasPrefix(tag, prefix) {
repoTags = append(repoTags, tag)
}
}
}
return repoTags, nil
}
@@ -1,4 +1,4 @@
package eventloop
package cmd
import (
"errors"
@@ -15,7 +15,7 @@ import (
// signal interrupts. Is responsible for handling each event relative to a given UI an to coordinate eventing until
// an eventual graceful exit.
// nolint:funlen
func EventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription *partybus.Subscription, cleanupFn func(), uxs ...ui.UI) error {
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
@@ -1,4 +1,4 @@
package eventloop
package cmd
import (
"fmt"
@@ -42,7 +42,7 @@ func (u *uiMock) Teardown(_ bool) error {
return u.Called().Error(0)
}
func Test_EventLoop_gracefulExit(t *testing.T) {
func Test_eventLoop_gracefulExit(t *testing.T) {
test := func(t *testing.T) {
testBus := partybus.NewBus()
@@ -92,7 +92,7 @@ func Test_EventLoop_gracefulExit(t *testing.T) {
}
assert.NoError(t,
EventLoop(
eventLoop(
worker(),
signaler(),
subscription,
@@ -109,7 +109,7 @@ func Test_EventLoop_gracefulExit(t *testing.T) {
testWithTimeout(t, 5*time.Second, test)
}
func Test_EventLoop_workerError(t *testing.T) {
func Test_eventLoop_workerError(t *testing.T) {
test := func(t *testing.T) {
testBus := partybus.NewBus()
@@ -155,7 +155,7 @@ func Test_EventLoop_workerError(t *testing.T) {
// ensure we see an error returned
assert.ErrorIs(t,
EventLoop(
eventLoop(
worker(),
signaler(),
subscription,
@@ -174,7 +174,7 @@ func Test_EventLoop_workerError(t *testing.T) {
testWithTimeout(t, 5*time.Second, test)
}
func Test_EventLoop_unsubscribeError(t *testing.T) {
func Test_eventLoop_unsubscribeError(t *testing.T) {
test := func(t *testing.T) {
testBus := partybus.NewBus()
@@ -226,7 +226,7 @@ func Test_EventLoop_unsubscribeError(t *testing.T) {
// 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(
eventLoop(
worker(),
signaler(),
subscription,
@@ -243,7 +243,7 @@ func Test_EventLoop_unsubscribeError(t *testing.T) {
testWithTimeout(t, 5*time.Second, test)
}
func Test_EventLoop_handlerError(t *testing.T) {
func Test_eventLoop_handlerError(t *testing.T) {
test := func(t *testing.T) {
testBus := partybus.NewBus()
@@ -296,7 +296,7 @@ func Test_EventLoop_handlerError(t *testing.T) {
// 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(
eventLoop(
worker(),
signaler(),
subscription,
@@ -315,7 +315,7 @@ func Test_EventLoop_handlerError(t *testing.T) {
testWithTimeout(t, 5*time.Second, test)
}
func Test_EventLoop_signalsStopExecution(t *testing.T) {
func Test_eventLoop_signalsStopExecution(t *testing.T) {
test := func(t *testing.T) {
testBus := partybus.NewBus()
@@ -351,7 +351,7 @@ func Test_EventLoop_signalsStopExecution(t *testing.T) {
}
assert.NoError(t,
EventLoop(
eventLoop(
worker(),
signaler(),
subscription,
@@ -368,7 +368,7 @@ func Test_EventLoop_signalsStopExecution(t *testing.T) {
testWithTimeout(t, 5*time.Second, test)
}
func Test_EventLoop_uiTeardownError(t *testing.T) {
func Test_eventLoop_uiTeardownError(t *testing.T) {
test := func(t *testing.T) {
testBus := partybus.NewBus()
@@ -421,7 +421,7 @@ func Test_EventLoop_uiTeardownError(t *testing.T) {
// ensure we see an error returned
assert.ErrorIs(t,
EventLoop(
eventLoop(
worker(),
signaler(),
subscription,
@@ -1,11 +1,11 @@
package options
package cmd
import (
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/sbom"
)
func FormatAliases(ids ...sbom.FormatID) (aliases []string) {
func formatAliases(ids ...sbom.FormatID) (aliases []string) {
for _, id := range ids {
switch id {
case syft.JSONFormatID:
@@ -22,8 +22,6 @@ func FormatAliases(ids ...sbom.FormatID) (aliases []string) {
aliases = append(aliases, "cyclonedx-xml")
case syft.CycloneDxJSONFormatID:
aliases = append(aliases, "cyclonedx-json")
case syft.GitHubID:
aliases = append(aliases, "github", "github-json")
default:
aliases = append(aliases, string(id))
}
@@ -1,19 +1,21 @@
package options
package cmd
import (
"fmt"
"strings"
"github.com/anchore/syft/internal/formats/table"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/sbom"
"github.com/hashicorp/go-multierror"
)
// makeWriter creates a sbom.Writer for output or returns an error. this will either return a valid writer
// or an error but neither both and if there is no error, sbom.Writer.Close() should be called
func MakeWriter(outputs []string, defaultFile string) (sbom.Writer, error) {
outputOptions, err := parseOutputs(outputs, defaultFile)
func makeWriter(outputs []string, defaultFile string) (sbom.Writer, error) {
outputOptions, err := parseOptions(outputs, defaultFile)
if err != nil {
return nil, err
}
@@ -27,7 +29,7 @@ func MakeWriter(outputs []string, defaultFile string) (sbom.Writer, error) {
}
// parseOptions utility to parse command-line option strings and retain the existing behavior of default format and file
func parseOutputs(outputs []string, defaultFile string) (out []sbom.WriterOption, errs error) {
func parseOptions(outputs []string, defaultFile string) (out []sbom.WriterOption, errs error) {
// always should have one option -- we generally get the default of "table", but just make sure
if len(outputs) == 0 {
outputs = append(outputs, string(table.ID))
+78
View File
@@ -0,0 +1,78 @@
package cmd
import (
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestOutputWriterConfig(t *testing.T) {
tmp := t.TempDir() + "/"
tests := []struct {
outputs []string
file string
err bool
expected []string
}{
{
outputs: []string{},
expected: []string{""},
},
{
outputs: []string{"json"},
expected: []string{""},
},
{
file: "test-1.json",
expected: []string{"test-1.json"},
},
{
outputs: []string{"json=test-2.json"},
expected: []string{"test-2.json"},
},
{
outputs: []string{"json=test-3-1.json", "spdx-json=test-3-2.json"},
expected: []string{"test-3-1.json", "test-3-2.json"},
},
{
outputs: []string{"text", "json=test-4.json"},
expected: []string{"", "test-4.json"},
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%s/%s", test.outputs, test.file), func(t *testing.T) {
outputs := test.outputs
for i, val := range outputs {
outputs[i] = strings.Replace(val, "=", "="+tmp, 1)
}
file := test.file
if file != "" {
file = tmp + file
}
_, err := makeWriter(test.outputs, file)
if test.err {
assert.Error(t, err)
return
} else {
assert.NoError(t, err)
}
for _, expected := range test.expected {
if expected != "" {
assert.FileExists(t, tmp+expected)
} else if file != "" {
assert.FileExists(t, file)
} else {
assert.NoFileExists(t, expected)
}
}
})
}
}
+362
View File
@@ -0,0 +1,362 @@
package cmd
import (
"context"
"fmt"
"io/ioutil"
"os"
"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/formats/table"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/ui"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/sbom"
"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 formatted SBOM
{{.appName}} {{.command}} alpine:latest -o cyclonedx-json show a CycloneDX JSON formatted SBOM
{{.appName}} {{.command}} alpine:latest -o spdx show a SPDX 2.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
`
schemeHelpHeader = "You can also explicitly specify the scheme to use:"
imageSchemeHelp = ` {{.appName}} {{.command}} docker:yourrepo/yourimage:tag explicitly use the Docker daemon
{{.appName}} {{.command}} podman:yourrepo/yourimage:tag explicitly use the Podman daemon
{{.appName}} {{.command}} registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required)
{{.appName}} {{.command}} docker-archive:path/to/yourimage.tar use a tarball from disk for archives created from "docker save"
{{.appName}} {{.command}} oci-archive:path/to/yourimage.tar use a tarball from disk for OCI archives (from Skopeo or otherwise)
{{.appName}} {{.command}} oci-dir:path/to/yourimage read directly from a path on disk for OCI layout directories (from Skopeo or otherwise)
`
nonImageSchemeHelp = ` {{.appName}} {{.command}} dir:path/to/yourproject read directly from a path on disk (any directory)
{{.appName}} {{.command}} file:path/to/yourproject/file read directly from a path on disk (any single file)
`
packagesSchemeHelp = "\n" + indent + schemeHelpHeader + "\n" + imageSchemeHelp + nonImageSchemeHelp
packagesHelp = packagesExample + packagesSchemeHelp
)
var (
packagesCmd = &cobra.Command{
Use: "packages [SOURCE]",
Short: "Generate a package SBOM",
Long: "Generate a packaged-based Software Bill Of Materials (SBOM) from container images and filesystems",
Example: internal.Tprintf(packagesHelp, map[string]interface{}{
"appName": internal.ApplicationName,
"command": "packages",
}),
Args: validateInputArgs,
SilenceUsage: true,
SilenceErrors: true,
PreRunE: func(cmd *cobra.Command, args []string) (err error) {
if appConfig.Dev.ProfileCPU && appConfig.Dev.ProfileMem {
return fmt.Errorf("cannot profile CPU and memory simultaneously")
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
if appConfig.Dev.ProfileCPU {
defer profile.Start(profile.CPUProfile).Stop()
} else if appConfig.Dev.ProfileMem {
defer profile.Start(profile.MemProfile).Stop()
}
return packagesExec(cmd, args)
},
ValidArgsFunction: dockerImageValidArgsFunction,
}
)
func init() {
setPackageFlags(packagesCmd.Flags())
rootCmd.AddCommand(packagesCmd)
}
func setPackageFlags(flags *pflag.FlagSet) {
// Formatting & Input options //////////////////////////////////////////////
flags.StringP(
"scope", "s", syft.DefaultCatalogingConfig().DefaultScope.String(),
fmt.Sprintf("selection of layers to catalog, options=%v", source.AllScopes))
flags.StringArrayP(
"output", "o", formatAliases(table.ID),
fmt.Sprintf("report output format, options=%v", formatAliases(syft.FormatIDs()...)),
)
flags.StringP(
"file", "", "",
"file to write the default report output to (default is STDOUT)",
)
flags.StringP(
"platform", "", "",
"an optional platform specifier for container image sources (e.g. 'linux/arm64', 'linux/arm64/v8', 'arm64', 'linux')",
)
flags.StringArrayP(
"cataloger", "", nil,
// TODO: have a nice way for a user to get the set of catalogers that exist
"set which catalogers to use when generating an SBOM",
)
// Upload options //////////////////////////////////////////////////////////
flags.StringP(
"host", "H", "",
"the hostname or URL of the Anchore Enterprise instance to upload to",
)
flags.StringP(
"username", "u", "",
"the username to authenticate against Anchore Enterprise",
)
flags.StringP(
"password", "p", "",
"the password to authenticate against Anchore Enterprise",
)
flags.StringP(
"dockerfile", "d", "",
"include dockerfile for upload to Anchore Enterprise",
)
flags.StringArrayP(
"exclude", "", nil,
"exclude paths from being scanned using a glob expression",
)
flags.Bool(
"overwrite-existing-image", false,
"overwrite an existing image during the upload to Anchore Enterprise",
)
flags.Uint(
"import-timeout", 30,
"set a timeout duration (in seconds) for the upload to Anchore Enterprise",
)
}
func bindPackagesConfigOptions(flags *pflag.FlagSet) error {
if err := bindExclusivePackagesConfigOptions(flags); err != nil {
return err
}
if err := bindSharedConfigOption(flags); err != nil {
return err
}
return nil
}
// NOTE(alex): Write a helper for the binding operation, which can be used to perform the binding but also double check that the intended effect was had or else return an error. Another thought is to somehow provide zero-valued defaults for all values in our config struct (maybe with reflection?). There may be a mechanism that already exists in viper that protects against this that I'm not aware of. ref: https://github.com/anchore/syft/pull/805#discussion_r801931192
func bindExclusivePackagesConfigOptions(flags *pflag.FlagSet) error {
// Formatting & Input options //////////////////////////////////////////////
// note: output is not included since this configuration option is shared between multiple subcommands
if err := viper.BindPFlag("scope", flags.Lookup("scope")); err != nil {
return err
}
if err := viper.BindPFlag("catalogers", flags.Lookup("cataloger")); err != nil {
return err
}
if err := viper.BindPFlag("file", flags.Lookup("file")); err != nil {
return err
}
if err := viper.BindPFlag("exclude", flags.Lookup("exclude")); err != nil {
return err
}
// Upload options //////////////////////////////////////////////////////////
if err := viper.BindPFlag("anchore.host", flags.Lookup("host")); err != nil {
return err
}
if err := viper.BindPFlag("anchore.username", flags.Lookup("username")); err != nil {
return err
}
if err := viper.BindPFlag("anchore.password", flags.Lookup("password")); err != nil {
return err
}
if err := viper.BindPFlag("anchore.dockerfile", flags.Lookup("dockerfile")); err != nil {
return err
}
if err := viper.BindPFlag("anchore.overwrite-existing-image", flags.Lookup("overwrite-existing-image")); err != nil {
return err
}
if err := viper.BindPFlag("anchore.import-timeout", flags.Lookup("import-timeout")); err != nil {
return err
}
return nil
}
func validateInputArgs(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
// in the case that no arguments are given we want to show the help text and return with a non-0 return code.
if err := cmd.Help(); err != nil {
return fmt.Errorf("unable to display help: %w", err)
}
return fmt.Errorf("an image/directory argument is required")
}
return cobra.MaximumNArgs(1)(cmd, args)
}
func packagesExec(_ *cobra.Command, args []string) error {
writer, err := makeWriter(appConfig.Outputs, appConfig.File)
if err != nil {
return err
}
defer func() {
if err := writer.Close(); err != nil {
log.Warnf("unable to write to report destination: %w", err)
}
}()
// could be an image or a directory, with or without a scheme
userInput := args[0]
si, err := source.ParseInput(userInput, appConfig.Platform, true)
if err != nil {
return fmt.Errorf("could not generate source input for packages command: %w", err)
}
return eventLoop(
packagesExecWorker(*si, writer),
setupSignals(),
eventSubscription,
stereoscope.Cleanup,
ui.Select(isVerbose(), appConfig.Quiet)...,
)
}
func isVerbose() (result bool) {
isPipedInput, err := internal.IsPipedInput()
if err != nil {
// since we can't tell if there was piped input we assume that there could be to disable the ETUI
log.Warnf("unable to determine if there is piped input: %+v", err)
return true
}
// verbosity should consider if there is piped input (in which case we should not show the ETUI)
return appConfig.CliOptions.Verbosity > 0 || isPipedInput
}
func packagesExecWorker(si source.Input, writer sbom.Writer) <-chan error {
errs := make(chan error)
go func() {
defer close(errs)
catalogingOpts, err := appConfig.ToCatalogingOptions()
if err != nil {
errs <- err
return
}
src, cleanup, err := source.New(si, appConfig.Registry.ToOptions(), appConfig.Exclusions)
if cleanup != nil {
defer cleanup()
}
if err != nil {
errs <- fmt.Errorf("failed to construct source from user input %q: %w", si.UserInput, err)
return
}
s, err := syft.Catalog(src, catalogingOpts...)
if err != nil {
errs <- err
return
}
if s == nil {
errs <- fmt.Errorf("no SBOM produced for %q", si.UserInput)
return
}
if appConfig.Anchore.Host != "" {
if err := runPackageSbomUpload(src, *s); err != nil {
errs <- err
return
}
}
bus.Publish(partybus.Event{
Type: event.Exit,
Value: func() error { return writer.Write(*s) },
})
}()
return errs
}
func runPackageSbomUpload(src *source.Source, s sbom.SBOM) error {
log.Infof("uploading results to %s", appConfig.Anchore.Host)
if src.Metadata.Scheme != source.ImageType {
return fmt.Errorf("unable to upload results: only images are supported")
}
var dockerfileContents []byte
if appConfig.Anchore.Dockerfile != "" {
if _, err := os.Stat(appConfig.Anchore.Dockerfile); os.IsNotExist(err) {
return fmt.Errorf("unable dockerfile=%q does not exist: %w", appConfig.Anchore.Dockerfile, err)
}
fh, err := os.Open(appConfig.Anchore.Dockerfile)
if err != nil {
return fmt.Errorf("unable to open dockerfile=%q: %w", appConfig.Anchore.Dockerfile, err)
}
dockerfileContents, err = ioutil.ReadAll(fh)
if err != nil {
return fmt.Errorf("unable to read dockerfile=%q: %w", appConfig.Anchore.Dockerfile, err)
}
}
c, err := anchore.NewClient(anchore.Configuration{
BaseURL: appConfig.Anchore.Host,
Username: appConfig.Anchore.Username,
Password: appConfig.Anchore.Password,
})
if err != nil {
return fmt.Errorf("failed to create anchore client: %w", err)
}
importCfg := anchore.ImportConfig{
ImageMetadata: src.Image.Metadata,
SBOM: s,
Dockerfile: dockerfileContents,
OverwriteExistingUpload: appConfig.Anchore.OverwriteExistingImage,
Timeout: appConfig.Anchore.ImportTimeout,
}
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 nil
}
+153
View File
@@ -0,0 +1,153 @@
package cmd
import (
"fmt"
"github.com/anchore/syft/syft"
"os"
"github.com/anchore/stereoscope"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/formats/syftjson"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/ui"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
"github.com/gookit/color"
"github.com/pkg/profile"
"github.com/spf13/cobra"
"github.com/wagoodman/go-partybus"
)
const powerUserExample = ` {{.appName}} {{.command}} <image>
DEPRECATED - THIS COMMAND WILL BE REMOVED in v1.0.0
Only image sources are supported (e.g. docker: , podman: , docker-archive: , oci: , etc.), the directory source (dir:) is not supported.
All behavior is controlled via application configuration and environment variables (see https://github.com/anchore/syft#configuration)
`
var powerUserOpts = struct {
configPath string
}{}
var powerUserCmd = &cobra.Command{
Use: "power-user [IMAGE]",
Short: "Run bulk operations on container images",
Example: internal.Tprintf(powerUserExample, map[string]interface{}{
"appName": internal.ApplicationName,
"command": "power-user",
}),
Args: validateInputArgs,
Hidden: true,
SilenceUsage: true,
SilenceErrors: true,
PreRunE: func(cmd *cobra.Command, args []string) error {
if appConfig.Dev.ProfileCPU && appConfig.Dev.ProfileMem {
return fmt.Errorf("cannot profile CPU and memory simultaneously")
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
if appConfig.Dev.ProfileCPU {
defer profile.Start(profile.CPUProfile).Stop()
} else if appConfig.Dev.ProfileMem {
defer profile.Start(profile.MemProfile).Stop()
}
return powerUserExec(cmd, args)
},
ValidArgsFunction: dockerImageValidArgsFunction,
}
func init() {
powerUserCmd.Flags().StringVarP(&powerUserOpts.configPath, "config", "c", "", "config file path with all power-user options")
rootCmd.AddCommand(powerUserCmd)
}
func powerUserExec(_ *cobra.Command, args []string) error {
// could be an image or a directory, with or without a scheme
userInput := args[0]
writer, err := sbom.NewWriter(
sbom.NewWriterOption(
syftjson.Format(),
appConfig.File,
),
)
if err != nil {
return err
}
defer func() {
if err := writer.Close(); err != nil {
log.Warnf("unable to write to report destination: %+v", err)
}
// inform user at end of run that command will be removed
deprecated := color.Style{color.Red, color.OpBold}.Sprint("DEPRECATED: This command will be removed in v1.0.0")
fmt.Fprintln(os.Stderr, deprecated)
}()
return eventLoop(
powerUserExecWorker(userInput, writer),
setupSignals(),
eventSubscription,
stereoscope.Cleanup,
ui.Select(isVerbose(), appConfig.Quiet)...,
)
}
func powerUserExecWorker(userInput string, writer sbom.Writer) <-chan error {
errs := make(chan error)
go func() {
defer close(errs)
// TODO: replace
//appConfig.Secrets.Cataloger.Enabled = true
//appConfig.FileMetadata.Cataloger.Enabled = true
//appConfig.FileContents.Cataloger.Enabled = true
//appConfig.FileClassification.Cataloger.Enabled = true
catalogingOpts, err := appConfig.ToCatalogingOptions()
if err != nil {
errs <- err
return
}
si, err := source.ParseInput(userInput, appConfig.Platform, true)
if err != nil {
errs <- err
return
}
src, cleanup, err := source.New(*si, appConfig.Registry.ToOptions(), appConfig.Exclusions)
if err != nil {
errs <- err
return
}
if cleanup != nil {
defer cleanup()
}
s, err := syft.Catalog(src, catalogingOpts...)
if err != nil {
errs <- err
return
}
if s == nil {
errs <- fmt.Errorf("no SBOM produced for %q", si.UserInput)
return
}
bus.Publish(partybus.Event{
Type: event.Exit,
Value: func() error { return writer.Write(*s) },
})
}()
return errs
}
+52
View File
@@ -0,0 +1,52 @@
package cmd
import (
"fmt"
"os"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/config"
"github.com/anchore/syft/internal/version"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var persistentOpts = config.CliOnlyOptions{}
// rootCmd is currently an alias for the packages command
var rootCmd = &cobra.Command{
Short: packagesCmd.Short,
Long: packagesCmd.Long,
Args: packagesCmd.Args,
Example: packagesCmd.Example,
SilenceUsage: true,
SilenceErrors: true,
PreRunE: packagesCmd.PreRunE,
RunE: packagesCmd.RunE,
ValidArgsFunction: packagesCmd.ValidArgsFunction,
Version: version.FromBuild().Version,
}
const indent = " "
func init() {
// set universal flags
rootCmd.PersistentFlags().StringVarP(&persistentOpts.ConfigPath, "config", "c", "", "application config file")
// setting the version template to just print out the string since we already have a templatized version string
rootCmd.SetVersionTemplate(fmt.Sprintf("%s {{.Version}}\n", internal.ApplicationName))
flag := "quiet"
rootCmd.PersistentFlags().BoolP(
flag, "q", false,
"suppress all logging output",
)
if err := viper.BindPFlag(flag, rootCmd.PersistentFlags().Lookup(flag)); err != nil {
fmt.Printf("unable to bind flag '%s': %+v", flag, err)
os.Exit(1)
}
rootCmd.PersistentFlags().CountVarP(&persistentOpts.Verbosity, "verbose", "v", "increase verbosity (-v = info, -vv = debug)")
// set common options that are not universal (package subcommand-alias specific)
setPackageFlags(rootCmd.Flags())
}
@@ -1,4 +1,4 @@
package eventloop
package cmd
import (
"os"
@@ -6,7 +6,7 @@ import (
"syscall"
)
func SetupSignals() <-chan os.Signal {
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{
-80
View File
@@ -1,80 +0,0 @@
package cli
import (
"fmt"
"log"
"github.com/anchore/syft/cmd/syft/cli/attest"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/config"
"github.com/spf13/cobra"
"github.com/spf13/viper"
sigopts "github.com/sigstore/cosign/cmd/cosign/cli/options"
)
const (
attestExample = ` {{.appName}} {{.command}} --output [FORMAT] --key [KEY] alpine:latest
Supports the following image sources:
{{.appName}} {{.command}} --key [KEY] 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}} --key [KEY] path/to/a/file/or/dir only for OCI tar or OCI directory
`
attestSchemeHelp = "\n" + indent + schemeHelpHeader + "\n" + imageSchemeHelp
attestHelp = attestExample + attestSchemeHelp
)
func Attest(v *viper.Viper, app *config.Application, ro *options.RootOptions) *cobra.Command {
ao := options.AttestOptions{}
cmd := &cobra.Command{
Use: "attest --output [FORMAT] --key [KEY] [SOURCE]",
Short: "Generate a package SBOM as an attestation for the given [SOURCE] container image",
Long: "Generate a packaged-based Software Bill Of Materials (SBOM) from a container image as the predicate of an in-toto attestation",
Example: internal.Tprintf(attestHelp, map[string]interface{}{
"appName": internal.ApplicationName,
"command": "attest",
}),
Args: func(cmd *cobra.Command, args []string) error {
// run to unmarshal viper object onto app config
// the viper object correctly
if err := app.LoadAllValues(v, ro.Config); err != nil {
return fmt.Errorf("invalid application config: %v", err)
}
// configure logging for command
newLogWrapper(app)
logApplicationConfig(app)
return validateArgs(cmd, args)
},
SilenceUsage: true,
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
// this MUST be called first to make sure app config decodes
// the viper object correctly
if app.CheckForAppUpdate {
checkForApplicationUpdate()
}
// build cosign key options for attestation
ko := sigopts.KeyOpts{
KeyRef: app.Attest.KeyRef,
FulcioURL: app.Attest.FulcioURL,
IDToken: app.Attest.FulcioIdentityToken,
InsecureSkipFulcioVerify: app.Attest.InsecureSkipFulcioVerify,
RekorURL: app.Attest.RekorURL,
OIDCIssuer: app.Attest.OIDCIssuer,
OIDCClientID: app.Attest.OIDCClientID,
OIDCRedirectURL: app.Attest.OIDCRedirectURL,
}
return attest.Run(cmd.Context(), app, ko, args)
},
}
err := ao.AddFlags(cmd, v)
if err != nil {
log.Fatal(err)
}
return cmd
}
-359
View File
@@ -1,359 +0,0 @@
package attest
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"github.com/wagoodman/go-progress"
"github.com/anchore/stereoscope"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/syft/cmd/syft/cli/eventloop"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/cmd/syft/cli/packages"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/config"
"github.com/anchore/syft/internal/formats/cyclonedxjson"
"github.com/anchore/syft/internal/formats/spdx22json"
"github.com/anchore/syft/internal/formats/syftjson"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/ui"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/in-toto/in-toto-golang/in_toto"
"github.com/pkg/errors"
sigopts "github.com/sigstore/cosign/cmd/cosign/cli/options"
"github.com/sigstore/cosign/cmd/cosign/cli/rekor"
"github.com/sigstore/cosign/cmd/cosign/cli/sign"
"github.com/sigstore/cosign/pkg/cosign"
"github.com/sigstore/cosign/pkg/cosign/attestation"
cbundle "github.com/sigstore/cosign/pkg/cosign/bundle"
"github.com/sigstore/cosign/pkg/oci/mutate"
ociremote "github.com/sigstore/cosign/pkg/oci/remote"
"github.com/sigstore/cosign/pkg/oci/static"
sigs "github.com/sigstore/cosign/pkg/signature"
"github.com/sigstore/cosign/pkg/types"
"github.com/sigstore/rekor/pkg/generated/client"
"github.com/sigstore/rekor/pkg/generated/models"
"github.com/sigstore/sigstore/pkg/signature/dsse"
"github.com/wagoodman/go-partybus"
signatureoptions "github.com/sigstore/sigstore/pkg/signature/options"
)
var (
allowedAttestFormats = []sbom.FormatID{
syftjson.ID,
spdx22json.ID,
cyclonedxjson.ID,
}
intotoJSONDsseType = `application/vnd.in-toto+json`
)
func Run(ctx context.Context, app *config.Application, ko sigopts.KeyOpts, args []string) error {
// We cannot generate an attestation for more than one output
if len(app.Outputs) > 1 {
return fmt.Errorf("unable to generate attestation for more than one output")
}
// can only be an image for attestation or OCI DIR
userInput := args[0]
si, err := parseImageSource(userInput, app)
if err != nil {
return err
}
format := syft.FormatByName(app.Outputs[0])
predicateType := formatPredicateType(format)
if predicateType == "" {
return fmt.Errorf(
"could not produce attestation predicate for given format: %q. Available formats: %+v",
options.FormatAliases(format.ID()),
options.FormatAliases(allowedAttestFormats...),
)
}
if app.Attest.KeyRef != "" {
passFunc, err := selectPassFunc(app.Attest.KeyRef, app.Attest.Password)
if err != nil {
return err
}
ko.PassFunc = passFunc
}
sv, err := sign.SignerFromKeyOpts(ctx, "", "", ko)
if err != nil {
return err
}
defer sv.Close()
eventBus := partybus.NewBus()
stereoscope.SetBus(eventBus)
syft.SetBus(eventBus)
subscription := eventBus.Subscribe()
return eventloop.EventLoop(
execWorker(app, *si, format, predicateType, sv),
eventloop.SetupSignals(),
subscription,
stereoscope.Cleanup,
ui.Select(options.IsVerbose(app), app.Quiet)...,
)
}
func parseImageSource(userInput string, app *config.Application) (s *source.Input, err error) {
si, err := source.ParseInput(userInput, app.Platform, false)
if err != nil {
return nil, fmt.Errorf("could not generate source input for attest command: %w", err)
}
switch si.Scheme {
case source.ImageScheme, source.UnknownScheme:
// at this point we know that it cannot be dir: or file: schemes;
// we will assume that the unknown scheme could represent an image;
si.Scheme = source.ImageScheme
default:
return nil, fmt.Errorf("attest command can only be used with image sources but discovered %q when given %q", si.Scheme, userInput)
}
// if the original detection was from the local daemon we want to short circuit
// that and attempt to generate the image source from its current registry source instead
switch si.ImageSource {
case image.UnknownSource, image.OciRegistrySource:
si.ImageSource = image.OciRegistrySource
default:
return nil, fmt.Errorf("attest command can only be used with image sources fetch directly from the registry, but discovered an image source of %q when given %q", si.ImageSource, userInput)
}
return si, nil
}
func execWorker(app *config.Application, sourceInput source.Input, format sbom.Format, predicateType string, sv *sign.SignerVerifier) <-chan error {
errs := make(chan error)
go func() {
defer close(errs)
src, cleanup, err := source.NewFromRegistry(sourceInput, app.Registry.ToOptions(), app.Exclusions)
if cleanup != nil {
defer cleanup()
}
if err != nil {
errs <- fmt.Errorf("failed to construct source from user input %q: %w", sourceInput.UserInput, err)
return
}
s, err := packages.GenerateSBOM(src, errs, app)
if err != nil {
errs <- err
return
}
sbomBytes, err := syft.Encode(*s, format)
if err != nil {
errs <- err
return
}
err = generateAttestation(app, sbomBytes, src, sv, predicateType)
if err != nil {
errs <- err
return
}
}()
return errs
}
func generateAttestation(app *config.Application, predicate []byte, src *source.Source, sv *sign.SignerVerifier, predicateType string) error {
switch len(src.Image.Metadata.RepoDigests) {
case 0:
return fmt.Errorf("cannot generate attestation since no repo digests were found; make sure you're passing an OCI registry source for the attest command")
case 1:
default:
return fmt.Errorf("cannot generate attestation since multiple repo digests were found for the image: %+v", src.Image.Metadata.RepoDigests)
}
wrapped := dsse.WrapSigner(sv, intotoJSONDsseType)
ref, err := name.ParseReference(src.Metadata.ImageMetadata.UserInput)
if err != nil {
return err
}
digest, err := ociremote.ResolveDigest(ref)
if err != nil {
return err
}
h, _ := v1.NewHash(digest.Identifier())
sh, err := attestation.GenerateStatement(attestation.GenerateOpts{
Predicate: bytes.NewBuffer(predicate),
Type: predicateType,
Digest: h.Hex,
})
if err != nil {
return err
}
payload, err := json.Marshal(sh)
if err != nil {
return err
}
signedPayload, err := wrapped.SignMessage(bytes.NewReader(payload), signatureoptions.WithContext(context.Background()))
if err != nil {
return errors.Wrap(err, "unable to sign SBOM")
}
// We want to give the option to not upload the generated attestation
// if passed or if the user is using local PKI
if app.Attest.NoUpload || app.Attest.KeyRef != "" {
bus.Publish(partybus.Event{
Type: event.Exit,
Value: func() error {
_, err := os.Stdout.Write(signedPayload)
return err
},
})
return nil
}
return uploadAttestation(app, signedPayload, digest, sv)
}
func trackUploadAttestation() (*progress.Stage, *progress.Manual) {
stage := &progress.Stage{}
prog := &progress.Manual{}
bus.Publish(partybus.Event{
Type: event.UploadAttestation,
Value: progress.StagedProgressable(&struct {
progress.Stager
progress.Progressable
}{
Stager: stage,
Progressable: prog,
}),
})
return stage, prog
}
// uploads signed SBOM payload to Rekor transparency log along with key information;
// returns a bundle for attestation annotations
// rekor bundle includes a signed payload and rekor timestamp;
// the bundle is then wrapped onto an OCI signed entity and uploaded to
// the user's image's OCI registry repository as *.att
func uploadAttestation(app *config.Application, signedPayload []byte, digest name.Digest, sv *sign.SignerVerifier) error {
// add application/vnd.dsse.envelope.v1+json as media type for other applications to decode attestation
opts := []static.Option{static.WithLayerMediaType(types.DssePayloadType)}
if sv.Cert != nil {
opts = append(opts, static.WithCertChain(sv.Cert, sv.Chain))
}
stage, prog := trackUploadAttestation()
defer prog.SetCompleted() // just in case we return early
prog.Total = 2
stage.Current = "uploading signing information to transparency log"
// uploads payload to Rekor transparency log along with key information;
// returns bundle for attesation annotations
// rekor bundle includes a signed payload and rekor timestamp;
// the bundle is then wrapped onto an OCI signed entity and uploaded to
// the user's image's OCI registry repository as *.att
bundle, err := uploadToTlog(context.TODO(), sv, app.Attest.RekorURL, func(r *client.Rekor, b []byte) (*models.LogEntryAnon, error) {
return cosign.TLogUploadInTotoAttestation(context.TODO(), r, signedPayload, b)
})
if err != nil {
return err
}
prog.N = 1
stage.Current = "uploading attestation to OCI registry"
// add bundle OCI attestation that is uploaded to
opts = append(opts, static.WithBundle(bundle))
sig, err := static.NewAttestation(signedPayload, opts...)
if err != nil {
return err
}
se, err := ociremote.SignedEntity(digest)
if err != nil {
return err
}
newSE, err := mutate.AttachAttestationToEntity(se, sig)
if err != nil {
return err
}
// Publish the attestations associated with this entity
err = ociremote.WriteAttestations(digest.Repository, newSE)
if err != nil {
return err
}
prog.SetCompleted()
bus.Publish(partybus.Event{
Type: event.Exit,
Value: func() error {
return nil
},
})
return nil
}
func formatPredicateType(format sbom.Format) string {
switch format.ID() {
case spdx22json.ID:
return in_toto.PredicateSPDX
case cyclonedxjson.ID:
// Tentative see https://github.com/in-toto/attestation/issues/82
return "https://cyclonedx.org/bom"
case syftjson.ID:
return "https://syft.dev/bom"
default:
return ""
}
}
type tlogUploadFn func(*client.Rekor, []byte) (*models.LogEntryAnon, error)
func uploadToTlog(ctx context.Context, sv *sign.SignerVerifier, rekorURL string, upload tlogUploadFn) (*cbundle.RekorBundle, error) {
var rekorBytes []byte
// Upload the cert or the public key, depending on what we have
if sv.Cert != nil {
rekorBytes = sv.Cert
} else {
pemBytes, err := sigs.PublicKeyPem(sv, signatureoptions.WithContext(ctx))
if err != nil {
return nil, err
}
rekorBytes = pemBytes
}
rekorClient, err := rekor.NewClient(rekorURL)
if err != nil {
return nil, err
}
entry, err := upload(rekorClient, rekorBytes)
if err != nil {
return nil, err
}
if entry.LogIndex != nil {
log.Debugf("transparency log entry created with index: %v", *entry.LogIndex)
}
return cbundle.EntryToBundle(entry), nil
}
-56
View File
@@ -1,56 +0,0 @@
package attest
import (
"errors"
"fmt"
"io"
"os"
"strings"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/sigstore/cosign/pkg/cosign"
)
func selectPassFunc(keypath, password string) (cosign.PassFunc, error) {
keyContents, err := os.ReadFile(keypath)
if err != nil {
return nil, err
}
var fn cosign.PassFunc = func(bool) (b []byte, err error) { return nil, nil }
_, err = cosign.LoadPrivateKey(keyContents, nil)
if err != nil {
fn = func(bool) (b []byte, err error) {
return fetchPassword(password)
}
}
return fn, nil
}
func fetchPassword(password string) (b []byte, err error) {
potentiallyPipedInput, err := internal.IsPipedInput()
if err != nil {
log.Warnf("unable to determine if there is piped input: %+v", err)
}
switch {
case password != "":
return []byte(password), nil
case potentiallyPipedInput:
// handle piped in passwords
pwBytes, err := io.ReadAll(os.Stdin)
if err != nil {
return nil, fmt.Errorf("unable to get password from stdin: %w", err)
}
// be resilient to input that may have newline characters (in case someone is using echo without -n)
cleanPw := strings.TrimRight(string(pwBytes), "\n")
return []byte(cleanPw), nil
case internal.IsTerminal():
return cosign.GetPassFromTerm(false)
}
return nil, errors.New("no method available to fetch password")
}
-147
View File
@@ -1,147 +0,0 @@
package cli
import (
"fmt"
"strings"
"github.com/anchore/stereoscope"
"github.com/anchore/syft/internal/logger"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/config"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft/event"
"github.com/gookit/color"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/wagoodman/go-partybus"
)
const indent = " "
// New constructs the `syft packages` command, aliases the root command to `syft packages`,
// and constructs the `syft power-user` and `syft attest` commands. It is also responsible for
// organizing flag usage and injecting the application config for each command.
// Because of how the `cobra` library behaves, the application's configuration is initialized
// at this level. Values from the config should only be used after `app.LoadAllValues` has been called.
// Cobra does not have knowledge of the user provided flags until the `RunE` block of each command.
// `RunE` is the earliest that the complete application configuration can be loaded.
func New() (*cobra.Command, error) {
app := &config.Application{}
// allow for nested options to be specified via environment variables
// e.g. pod.context = APPNAME_POD_CONTEXT
v := viper.NewWithOptions(viper.EnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")))
// since root is aliased as the packages cmd we need to construct this command first
// we also need the command to have information about the `root` options because of this alias
ro := &options.RootOptions{}
po := &options.PackagesOptions{}
packagesCmd := Packages(v, app, ro, po)
// root options are also passed to the attestCmd so that a user provided config location can be discovered
attestCmd := Attest(v, app, ro)
poweruserCmd := PowerUser(v, app, ro)
convertCmd := Convert(v, app, ro)
// rootCmd is currently an alias for the packages command
rootCmd := &cobra.Command{
Short: packagesCmd.Short,
Long: packagesCmd.Long,
Args: packagesCmd.Args,
Example: packagesCmd.Example,
SilenceUsage: true,
SilenceErrors: true,
RunE: packagesCmd.RunE,
Version: version.FromBuild().Version,
}
rootCmd.SetVersionTemplate(fmt.Sprintf("%s {{.Version}}\n", internal.ApplicationName))
// start adding flags to all the commands
err := ro.AddFlags(rootCmd, v)
if err != nil {
return nil, err
}
// package flags need to be decorated onto the rootCmd so that rootCmd can function as a packages alias
err = po.AddFlags(rootCmd, v)
if err != nil {
return nil, err
}
// attest also uses flags from the packagesCmd since it generates an sbom
err = po.AddFlags(attestCmd, v)
if err != nil {
return nil, err
}
// poweruser also uses the packagesCmd flags since it is a specialized version of the command
err = po.AddFlags(poweruserCmd, v)
if err != nil {
return nil, err
}
// Add sub-commands.
rootCmd.AddCommand(packagesCmd)
rootCmd.AddCommand(attestCmd)
rootCmd.AddCommand(convertCmd)
rootCmd.AddCommand(poweruserCmd)
rootCmd.AddCommand(Completion())
rootCmd.AddCommand(Version(v, app))
return rootCmd, err
}
func validateArgs(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
// in the case that no arguments are given we want to show the help text and return with a non-0 return code.
if err := cmd.Help(); err != nil {
return fmt.Errorf("unable to display help: %w", err)
}
return fmt.Errorf("an image/directory argument is required")
}
return cobra.MaximumNArgs(1)(cmd, args)
}
func checkForApplicationUpdate() {
log.Debugf("checking if new vesion of %s is available", internal.ApplicationName)
isAvailable, newVersion, err := version.IsUpdateAvailable()
if err != nil {
// this should never stop the application
log.Errorf(err.Error())
}
if isAvailable {
log.Infof("new version of %s is available: %s (current version is %s)", internal.ApplicationName, newVersion, version.FromBuild().Version)
bus.Publish(partybus.Event{
Type: event.AppUpdateAvailable,
Value: newVersion,
})
} else {
log.Debugf("no new %s update available", internal.ApplicationName)
}
}
func logApplicationConfig(app *config.Application) {
versionInfo := version.FromBuild()
log.Infof("syft version: %+v", versionInfo.Version)
log.Debugf("application config:\n%+v", color.Magenta.Sprint(app.String()))
}
func newLogWrapper(app *config.Application) {
cfg := logger.LogrusConfig{
EnableConsole: (app.Log.FileLocation == "" || app.Verbosity > 0) && !app.Quiet,
EnableFile: app.Log.FileLocation != "",
Level: app.Log.LevelOpt,
Structured: app.Log.Structured,
FileLocation: app.Log.FileLocation,
}
logWrapper := logger.NewLogrusLogger(cfg)
syft.SetLogger(logWrapper)
stereoscope.SetLogger(&logger.LogrusNestedLogger{
Logger: logWrapper.Logger.WithField("from-lib", "stereoscope"),
})
}
-54
View File
@@ -1,54 +0,0 @@
package cli
import (
"os"
"github.com/spf13/cobra"
)
func Completion() *cobra.Command {
cmd := &cobra.Command{
Use: "completion [bash|zsh|fish]",
Short: "Generate a shell completion for Syft (listing local docker images)",
Long: `To load completions (docker image list):
Bash:
$ source <(syft completion bash)
# To load completions for each session, execute once:
Linux:
$ syft completion bash > /etc/bash_completion.d/syft
MacOS:
$ syft completion bash > /usr/local/etc/bash_completion.d/syft
Zsh:
# If shell completion is not already enabled in your environment you will need
# to enable it. You can execute the following once:
$ echo "autoload -U compinit; compinit" >> ~/.zshrc
# To load completions for each session, execute once:
$ syft completion zsh > "${fpath[1]}/_syft"
# You will need to start a new shell for this setup to take effect.
Fish:
$ syft completion fish | source
# To load completions for each session, execute once:
$ syft completion fish > ~/.config/fish/completions/syft.fish
`,
DisableFlagsInUseLine: true,
ValidArgs: []string{"bash", "zsh", "fish"},
Args: cobra.ExactValidArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
var err error
switch args[0] {
case "bash":
err = cmd.Root().GenBashCompletion(os.Stdout)
case "zsh":
err = cmd.Root().GenZshCompletion(os.Stdout)
case "fish":
err = cmd.Root().GenFishCompletion(os.Stdout, true)
}
if err != nil {
return err
}
return nil
},
}
return cmd
}
-47
View File
@@ -1,47 +0,0 @@
package cli
import (
"fmt"
"github.com/anchore/syft/cmd/syft/cli/convert"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/config"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
const (
convertExample = ` {{.appName}} {{.command}} img.syft.json -o spdx-json convert a syft SBOM to spdx-json, output goes to stdout in table format, by default
{{.appName}} {{.command}} img.syft.json -o cyclonedx-json=img.cdx.json convert a syft SBOM to CycloneDX, output goes to a file named img.cdx.json
`
)
func Convert(v *viper.Viper, app *config.Application, ro *options.RootOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "convert [SOURCE-SBOM] -o [FORMAT]",
Short: "Convert between SBOM formats",
Long: "[Experimental] Convert SBOM files to, and from, SPDX, CycloneDX and Syft's format. For more info about data loss between formats see https://github.com/anchore/syft#format-conversion-experimental",
Example: internal.Tprintf(convertExample, map[string]interface{}{
"appName": internal.ApplicationName,
"command": "convert",
}),
Args: func(cmd *cobra.Command, args []string) error {
if err := app.LoadAllValues(v, ro.Config); err != nil {
return fmt.Errorf("invalid application config: %w", err)
}
newLogWrapper(app)
logApplicationConfig(app)
return validateArgs(cmd, args)
},
SilenceUsage: true,
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
if app.CheckForAppUpdate {
checkForApplicationUpdate()
}
return convert.Run(cmd.Context(), app, args)
},
}
return cmd
}
-41
View File
@@ -1,41 +0,0 @@
package convert
import (
"context"
"fmt"
"os"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/internal/config"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft"
)
func Run(ctx context.Context, app *config.Application, args []string) error {
log.Warn("convert is an experimental feature, run `syft convert -h` for help")
writer, err := options.MakeWriter(app.Outputs, app.File)
if err != nil {
return err
}
defer func() {
if err := writer.Close(); err != nil {
log.Warnf("unable to write to report destination: %w", err)
}
}()
// this can only be a SBOM file
userInput := args[0]
f, err := os.Open(userInput)
if err != nil {
return fmt.Errorf("failed to open SBOM file: %w", err)
}
defer f.Close()
sbom, _, err := syft.Decode(f)
if err != nil {
return fmt.Errorf("failed to decode SBOM: %w", err)
}
return writer.Write(*sbom)
}
-232
View File
@@ -1,232 +0,0 @@
package eventloop
import (
"crypto"
"fmt"
"github.com/anchore/syft/internal/config"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
type Task func(*sbom.Artifacts, *source.Source) ([]artifact.Relationship, error)
func Tasks(app *config.Application) ([]Task, error) {
var tasks []Task
generators := []func(app *config.Application) (Task, error){
generateCatalogPackagesTask,
generateCatalogFileMetadataTask,
generateCatalogFileDigestsTask,
generateCatalogSecretsTask,
generateCatalogFileClassificationsTask,
generateCatalogContentsTask,
}
for _, generator := range generators {
task, err := generator(app)
if err != nil {
return nil, err
}
if task != nil {
tasks = append(tasks, task)
}
}
return tasks, nil
}
func generateCatalogPackagesTask(app *config.Application) (Task, error) {
if !app.Package.Cataloger.Enabled {
return nil, nil
}
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
packageCatalog, relationships, theDistro, err := syft.CatalogPackages(src, app.Package.ToConfig())
if err != nil {
return nil, err
}
results.PackageCatalog = packageCatalog
results.LinuxDistribution = theDistro
return relationships, nil
}
return task, nil
}
func generateCatalogFileMetadataTask(app *config.Application) (Task, error) {
if !app.FileMetadata.Cataloger.Enabled {
return nil, nil
}
metadataCataloger := file.NewMetadataCataloger()
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
resolver, err := src.FileResolver(app.FileMetadata.Cataloger.ScopeOpt)
if err != nil {
return nil, err
}
result, err := metadataCataloger.Catalog(resolver)
if err != nil {
return nil, err
}
results.FileMetadata = result
return nil, nil
}
return task, nil
}
func generateCatalogFileDigestsTask(app *config.Application) (Task, error) {
if !app.FileMetadata.Cataloger.Enabled {
return nil, nil
}
supportedHashAlgorithms := make(map[string]crypto.Hash)
for _, h := range []crypto.Hash{
crypto.MD5,
crypto.SHA1,
crypto.SHA256,
} {
supportedHashAlgorithms[file.DigestAlgorithmName(h)] = h
}
var hashes []crypto.Hash
for _, hashStr := range app.FileMetadata.Digests {
name := file.CleanDigestAlgorithmName(hashStr)
hashObj, ok := supportedHashAlgorithms[name]
if !ok {
return nil, fmt.Errorf("unsupported hash algorithm: %s", hashStr)
}
hashes = append(hashes, hashObj)
}
digestsCataloger, err := file.NewDigestsCataloger(hashes)
if err != nil {
return nil, err
}
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
resolver, err := src.FileResolver(app.FileMetadata.Cataloger.ScopeOpt)
if err != nil {
return nil, err
}
result, err := digestsCataloger.Catalog(resolver)
if err != nil {
return nil, err
}
results.FileDigests = result
return nil, nil
}
return task, nil
}
func generateCatalogSecretsTask(app *config.Application) (Task, error) {
if !app.Secrets.Cataloger.Enabled {
return nil, nil
}
patterns, err := file.GenerateSearchPatterns(file.DefaultSecretsPatterns, app.Secrets.AdditionalPatterns, app.Secrets.ExcludePatternNames)
if err != nil {
return nil, err
}
secretsCataloger, err := file.NewSecretsCataloger(patterns, app.Secrets.RevealValues, app.Secrets.SkipFilesAboveSize)
if err != nil {
return nil, err
}
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
resolver, err := src.FileResolver(app.Secrets.Cataloger.ScopeOpt)
if err != nil {
return nil, err
}
result, err := secretsCataloger.Catalog(resolver)
if err != nil {
return nil, err
}
results.Secrets = result
return nil, nil
}
return task, nil
}
func generateCatalogFileClassificationsTask(app *config.Application) (Task, error) {
if !app.FileClassification.Cataloger.Enabled {
return nil, nil
}
// TODO: in the future we could expose out the classifiers via configuration
classifierCataloger, err := file.NewClassificationCataloger(file.DefaultClassifiers)
if err != nil {
return nil, err
}
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
resolver, err := src.FileResolver(app.FileClassification.Cataloger.ScopeOpt)
if err != nil {
return nil, err
}
result, err := classifierCataloger.Catalog(resolver)
if err != nil {
return nil, err
}
results.FileClassifications = result
return nil, nil
}
return task, nil
}
func generateCatalogContentsTask(app *config.Application) (Task, error) {
if !app.FileContents.Cataloger.Enabled {
return nil, nil
}
contentsCataloger, err := file.NewContentsCataloger(app.FileContents.Globs, app.FileContents.SkipFilesAboveSize)
if err != nil {
return nil, err
}
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
resolver, err := src.FileResolver(app.FileContents.Cataloger.ScopeOpt)
if err != nil {
return nil, err
}
result, err := contentsCataloger.Catalog(resolver)
if err != nil {
return nil, err
}
results.FileContents = result
return nil, nil
}
return task, nil
}
func RunTask(t Task, a *sbom.Artifacts, src *source.Source, c chan<- artifact.Relationship, errs chan<- error) {
defer close(c)
relationships, err := t(a, src)
if err != nil {
errs <- err
return
}
for _, relationship := range relationships {
c <- relationship
}
}
-77
View File
@@ -1,77 +0,0 @@
package options
import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
const defaultKeyFileName = "cosign.key"
type AttestOptions struct {
Key string
Cert string
CertChain string
NoUpload bool
Force bool
Recursive bool
Rekor RekorOptions
Fulcio FulcioOptions
OIDC OIDCOptions
}
var _ Interface = (*AttestOptions)(nil)
func (o *AttestOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error {
if err := o.Rekor.AddFlags(cmd, v); err != nil {
return err
}
if err := o.Fulcio.AddFlags(cmd, v); err != nil {
return err
}
if err := o.OIDC.AddFlags(cmd, v); err != nil {
return err
}
cmd.Flags().StringVarP(&o.Key, "key", "", defaultKeyFileName,
"path to the private key file to use for attestation")
cmd.Flags().StringVarP(&o.Cert, "cert", "", "",
"path to the x.509 certificate in PEM format to include in the OCI Signature")
cmd.Flags().BoolVarP(&o.NoUpload, "no-upload", "", false,
"do not upload the generated attestation")
cmd.Flags().BoolVarP(&o.Force, "force", "", false,
"skip warnings and confirmations")
cmd.Flags().BoolVarP(&o.Recursive, "recursive", "", false,
"if a multi-arch image is specified, additionally sign each discrete image")
return bindAttestationConfigOptions(cmd.Flags(), v)
}
func bindAttestationConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error {
if err := v.BindPFlag("attest.key", flags.Lookup("key")); err != nil {
return err
}
if err := v.BindPFlag("attest.cert", flags.Lookup("cert")); err != nil {
return err
}
if err := v.BindPFlag("attest.no-upload", flags.Lookup("no-upload")); err != nil {
return err
}
if err := v.BindPFlag("attest.force", flags.Lookup("force")); err != nil {
return err
}
if err := v.BindPFlag("attest.recursive", flags.Lookup("recursive")); err != nil {
return err
}
return nil
}
-48
View File
@@ -1,48 +0,0 @@
package options
import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
const defaultFulcioURL = "https://fulcio.sigstore.dev"
// FulcioOptions is the wrapper for Fulcio related options.
type FulcioOptions struct {
URL string
IdentityToken string
InsecureSkipFulcioVerify bool
}
var _ Interface = (*FulcioOptions)(nil)
// AddFlags implements Interface
func (o *FulcioOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error {
// TODO: change this back to api.SigstorePublicServerURL after the v1 migration is complete.
cmd.Flags().StringVar(&o.URL, "fulcio-url", defaultFulcioURL,
"address of sigstore PKI server")
cmd.Flags().StringVar(&o.IdentityToken, "identity-token", "",
"identity token to use for certificate from fulcio")
cmd.Flags().BoolVar(&o.InsecureSkipFulcioVerify, "insecure-skip-verify", false,
"skip verifying fulcio certificat and the SCT (Signed Certificate Timestamp) (this should only be used for testing).")
return bindFulcioConfigOptions(cmd.Flags(), v)
}
func bindFulcioConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error {
if err := v.BindPFlag("attest.fulcio-url", flags.Lookup("fulcio-url")); err != nil {
return err
}
if err := v.BindPFlag("attest.fulcio-identity-token", flags.Lookup("identity-token")); err != nil {
return err
}
if err := v.BindPFlag("attest.insecure-skip-verify", flags.Lookup("insecure-skip-verify")); err != nil {
return err
}
return nil
}
-48
View File
@@ -1,48 +0,0 @@
package options
import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
const DefaultOIDCIssuerURL = "https://oauth2.sigstore.dev/auth"
// OIDCOptions is the wrapper for OIDC related options.
type OIDCOptions struct {
Issuer string
ClientID string
RedirectURL string
}
var _ Interface = (*OIDCOptions)(nil)
// AddFlags implements Interface
func (o *OIDCOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error {
cmd.Flags().StringVar(&o.Issuer, "oidc-issuer", DefaultOIDCIssuerURL,
"OIDC provider to be used to issue ID token")
cmd.Flags().StringVar(&o.ClientID, "oidc-client-id", "sigstore",
"OIDC client ID for application")
cmd.Flags().StringVar(&o.RedirectURL, "oidc-redirect-url", "",
"OIDC redirect URL (Optional)")
return bindOIDCConfigOptions(cmd.Flags(), v)
}
func bindOIDCConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error {
if err := v.BindPFlag("attest.oidc-issuer", flags.Lookup("oidc-issuer")); err != nil {
return err
}
if err := v.BindPFlag("attest.oidc-client-id", flags.Lookup("oidc-client-id")); err != nil {
return err
}
if err := v.BindPFlag("attest.oidc-redirect-url", flags.Lookup("oidc-redirect-url")); err != nil {
return err
}
return nil
}
-11
View File
@@ -1,11 +0,0 @@
package options
import (
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
type Interface interface {
// AddFlags adds this options' flags to the cobra command.
AddFlags(cmd *cobra.Command, v *viper.Viper) error
}
-118
View File
@@ -1,118 +0,0 @@
package options
import (
"fmt"
"github.com/anchore/syft/internal/formats/table"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/pkg/cataloger"
"github.com/anchore/syft/syft/source"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
type PackagesOptions struct {
Scope string
Output []string
File string
Platform string
Host string
Username string
Password string
Dockerfile string
Exclude []string
OverwriteExistingImage bool
ImportTimeout uint
}
var _ Interface = (*PackagesOptions)(nil)
func (o *PackagesOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error {
cmd.PersistentFlags().StringVarP(&o.Scope, "scope", "s", cataloger.DefaultSearchConfig().Scope.String(),
fmt.Sprintf("selection of layers to catalog, options=%v", source.AllScopes))
cmd.PersistentFlags().StringArrayVarP(&o.Output, "output", "o", FormatAliases(table.ID),
fmt.Sprintf("report output format, options=%v", FormatAliases(syft.FormatIDs()...)))
cmd.PersistentFlags().StringVarP(&o.File, "file", "", "",
"file to write the default report output to (default is STDOUT)")
cmd.PersistentFlags().StringVarP(&o.Platform, "platform", "", "",
"an optional platform specifier for container image sources (e.g. 'linux/arm64', 'linux/arm64/v8', 'arm64', 'linux')")
cmd.PersistentFlags().StringVarP(&o.Host, "host", "H", "",
"the hostname or URL of the Anchore Enterprise instance to upload to")
cmd.PersistentFlags().StringVarP(&o.Username, "username", "u", "",
"the username to authenticate against Anchore Enterprise")
cmd.PersistentFlags().StringVarP(&o.Password, "password", "p", "",
"the password to authenticate against Anchore Enterprise")
cmd.PersistentFlags().StringVarP(&o.Dockerfile, "dockerfile", "d", "",
"include dockerfile for upload to Anchore Enterprise")
cmd.PersistentFlags().StringArrayVarP(&o.Exclude, "exclude", "", nil,
"exclude paths from being scanned using a glob expression")
cmd.PersistentFlags().BoolVarP(&o.OverwriteExistingImage, "overwrite-existing-image", "", false,
"overwrite an existing image during the upload to Anchore Enterprise")
cmd.PersistentFlags().UintVarP(&o.ImportTimeout, "import-timeout", "", 30,
"set a timeout duration (in seconds) for the upload to Anchore Enterprise")
return bindPackageConfigOptions(cmd.PersistentFlags(), v)
}
func bindPackageConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error {
// Formatting & Input options //////////////////////////////////////////////
if err := v.BindPFlag("package.cataloger.scope", flags.Lookup("scope")); err != nil {
return err
}
if err := v.BindPFlag("file", flags.Lookup("file")); err != nil {
return err
}
if err := v.BindPFlag("exclude", flags.Lookup("exclude")); err != nil {
return err
}
if err := v.BindPFlag("output", flags.Lookup("output")); err != nil {
return err
}
if err := v.BindPFlag("platform", flags.Lookup("platform")); err != nil {
return err
}
// Upload options //////////////////////////////////////////////////////////
if err := v.BindPFlag("anchore.host", flags.Lookup("host")); err != nil {
return err
}
if err := v.BindPFlag("anchore.username", flags.Lookup("username")); err != nil {
return err
}
if err := v.BindPFlag("anchore.password", flags.Lookup("password")); err != nil {
return err
}
if err := v.BindPFlag("anchore.dockerfile", flags.Lookup("dockerfile")); err != nil {
return err
}
if err := v.BindPFlag("anchore.overwrite-existing-image", flags.Lookup("overwrite-existing-image")); err != nil {
return err
}
if err := v.BindPFlag("anchore.import-timeout", flags.Lookup("import-timeout")); err != nil {
return err
}
return nil
}
-32
View File
@@ -1,32 +0,0 @@
package options
import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
const DefaultRekorURL = "https://rekor.sigstore.dev"
// RekorOptions is the wrapper for Rekor related options.
type RekorOptions struct {
URL string
}
var _ Interface = (*RekorOptions)(nil)
// AddFlags implements Interface
func (o *RekorOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error {
cmd.Flags().StringVar(&o.URL, "rekor-url", DefaultRekorURL,
"address of rekor STL server")
return bindRekorConfigOptions(cmd.Flags(), v)
}
func bindRekorConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error {
// TODO: config re-design
if err := v.BindPFlag("attest.rekor-url", flags.Lookup("rekor-url")); err != nil {
return err
}
return nil
}
-36
View File
@@ -1,36 +0,0 @@
package options
import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
type RootOptions struct {
Config string
Quiet bool
Verbose int
}
var _ Interface = (*RootOptions)(nil)
func (o *RootOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error {
cmd.PersistentFlags().StringVarP(&o.Config, "config", "c", "", "application config file")
cmd.PersistentFlags().CountVarP(&o.Verbose, "verbose", "v", "increase verbosity (-v = info, -vv = debug)")
cmd.PersistentFlags().BoolVarP(&o.Quiet, "quiet", "q", false, "suppress all logging output")
return bindRootConfigOptions(cmd.PersistentFlags(), v)
}
func bindRootConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error {
if err := v.BindPFlag("config", flags.Lookup("config")); err != nil {
return err
}
if err := v.BindPFlag("verbosity", flags.Lookup("verbose")); err != nil {
return err
}
if err := v.BindPFlag("quiet", flags.Lookup("quiet")); err != nil {
return err
}
return nil
}
-18
View File
@@ -1,18 +0,0 @@
package options
import (
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/config"
"github.com/anchore/syft/internal/log"
)
func IsVerbose(app *config.Application) (result bool) {
isPipedInput, err := internal.IsPipedInput()
if err != nil {
// since we can't tell if there was piped input we assume that there could be to disable the ETUI
log.Warnf("unable to determine if there is piped input: %+v", err)
return true
}
// verbosity should consider if there is piped input (in which case we should not show the ETUI)
return app.Verbosity > 0 || isPipedInput
}
-17
View File
@@ -1,17 +0,0 @@
package options
import (
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
type VersionOptions struct {
Output string
}
var _ Interface = (*PackagesOptions)(nil)
func (o *VersionOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error {
cmd.Flags().StringVarP(&o.Output, "output", "o", "text", "format to show version information (available=[text, json])")
return nil
}
-34
View File
@@ -1,34 +0,0 @@
package options
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestIsSupportedFormat(t *testing.T) {
tests := []struct {
outputs []string
wantErr assert.ErrorAssertionFunc
}{
{
outputs: []string{"json"},
wantErr: assert.NoError,
},
{
outputs: []string{"table", "json"},
wantErr: assert.NoError,
},
{
outputs: []string{"unknown"},
wantErr: func(t assert.TestingT, err error, bla ...interface{}) bool {
return assert.ErrorContains(t, err, "bad output format: 'unknown'")
},
},
}
for _, tt := range tests {
_, err := MakeWriter(tt.outputs, "")
tt.wantErr(t, err)
}
}
-79
View File
@@ -1,79 +0,0 @@
package cli
import (
"fmt"
"log"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/cmd/syft/cli/packages"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/config"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
const (
packagesExample = ` {{.appName}} {{.command}} alpine:latest a summary of discovered packages
{{.appName}} {{.command}} alpine:latest -o json show all possible cataloging details
{{.appName}} {{.command}} alpine:latest -o cyclonedx show a CycloneDX formatted SBOM
{{.appName}} {{.command}} alpine:latest -o cyclonedx-json show a CycloneDX JSON formatted SBOM
{{.appName}} {{.command}} alpine:latest -o spdx show a SPDX 2.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
`
schemeHelpHeader = "You can also explicitly specify the scheme to use:"
imageSchemeHelp = ` {{.appName}} {{.command}} docker:yourrepo/yourimage:tag explicitly use the Docker daemon
{{.appName}} {{.command}} podman:yourrepo/yourimage:tag explicitly use the Podman daemon
{{.appName}} {{.command}} registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required)
{{.appName}} {{.command}} docker-archive:path/to/yourimage.tar use a tarball from disk for archives created from "docker save"
{{.appName}} {{.command}} oci-archive:path/to/yourimage.tar use a tarball from disk for OCI archives (from Skopeo or otherwise)
{{.appName}} {{.command}} oci-dir:path/to/yourimage read directly from a path on disk for OCI layout directories (from Skopeo or otherwise)
`
nonImageSchemeHelp = ` {{.appName}} {{.command}} dir:path/to/yourproject read directly from a path on disk (any directory)
{{.appName}} {{.command}} file:path/to/yourproject/file read directly from a path on disk (any single file)
`
packagesSchemeHelp = "\n" + indent + schemeHelpHeader + "\n" + imageSchemeHelp + nonImageSchemeHelp
packagesHelp = packagesExample + packagesSchemeHelp
)
func Packages(v *viper.Viper, app *config.Application, ro *options.RootOptions, po *options.PackagesOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "packages [SOURCE]",
Short: "Generate a package SBOM",
Long: "Generate a packaged-based Software Bill Of Materials (SBOM) from container images and filesystems",
Example: internal.Tprintf(packagesHelp, map[string]interface{}{
"appName": internal.ApplicationName,
"command": "packages",
}),
Args: func(cmd *cobra.Command, args []string) error {
if err := app.LoadAllValues(v, ro.Config); err != nil {
return fmt.Errorf("invalid application config: %v", err)
}
// configure logging for command
newLogWrapper(app)
logApplicationConfig(app)
return validateArgs(cmd, args)
},
SilenceUsage: true,
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
if app.CheckForAppUpdate {
checkForApplicationUpdate()
}
return packages.Run(cmd.Context(), app, args)
},
}
err := po.AddFlags(cmd, v)
if err != nil {
log.Fatal(err)
}
return cmd
}
-187
View File
@@ -1,187 +0,0 @@
package packages
import (
"context"
"fmt"
"io/ioutil"
"os"
"github.com/anchore/stereoscope"
"github.com/anchore/syft/cmd/syft/cli/eventloop"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/anchore"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/config"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/ui"
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
"github.com/wagoodman/go-partybus"
)
func Run(ctx context.Context, app *config.Application, args []string) error {
writer, err := options.MakeWriter(app.Outputs, app.File)
if err != nil {
return err
}
defer func() {
if err := writer.Close(); err != nil {
log.Warnf("unable to write to report destination: %w", err)
}
}()
// could be an image or a directory, with or without a scheme
userInput := args[0]
si, err := source.ParseInput(userInput, app.Platform, true)
if err != nil {
return fmt.Errorf("could not generate source input for packages command: %w", err)
}
eventBus := partybus.NewBus()
stereoscope.SetBus(eventBus)
syft.SetBus(eventBus)
subscription := eventBus.Subscribe()
return eventloop.EventLoop(
execWorker(app, *si, writer),
eventloop.SetupSignals(),
subscription,
stereoscope.Cleanup,
ui.Select(options.IsVerbose(app), app.Quiet)...,
)
}
func execWorker(app *config.Application, si source.Input, writer sbom.Writer) <-chan error {
errs := make(chan error)
go func() {
defer close(errs)
src, cleanup, err := source.New(si, app.Registry.ToOptions(), app.Exclusions)
if cleanup != nil {
defer cleanup()
}
if err != nil {
errs <- fmt.Errorf("failed to construct source from user input %q: %w", si.UserInput, err)
return
}
s, err := GenerateSBOM(src, errs, app)
if err != nil {
errs <- err
return
}
if s == nil {
errs <- fmt.Errorf("no SBOM produced for %q", si.UserInput)
}
if app.Anchore.Host != "" {
if err := runPackageSbomUpload(src, *s, app); err != nil {
errs <- err
return
}
}
bus.Publish(partybus.Event{
Type: event.Exit,
Value: func() error { return writer.Write(*s) },
})
}()
return errs
}
func GenerateSBOM(src *source.Source, errs chan error, app *config.Application) (*sbom.SBOM, error) {
tasks, err := eventloop.Tasks(app)
if err != nil {
return nil, err
}
s := sbom.SBOM{
Source: src.Metadata,
Descriptor: sbom.Descriptor{
Name: internal.ApplicationName,
Version: version.FromBuild().Version,
Configuration: app,
},
}
buildRelationships(&s, src, tasks, errs)
return &s, nil
}
func buildRelationships(s *sbom.SBOM, src *source.Source, tasks []eventloop.Task, errs chan error) {
var relationships []<-chan artifact.Relationship
for _, task := range tasks {
c := make(chan artifact.Relationship)
relationships = append(relationships, c)
go eventloop.RunTask(task, &s.Artifacts, src, c, errs)
}
s.Relationships = append(s.Relationships, MergeRelationships(relationships...)...)
}
func MergeRelationships(cs ...<-chan artifact.Relationship) (relationships []artifact.Relationship) {
for _, c := range cs {
for n := range c {
relationships = append(relationships, n)
}
}
return relationships
}
func runPackageSbomUpload(src *source.Source, s sbom.SBOM, app *config.Application) error {
log.Infof("uploading results to %s", app.Anchore.Host)
if src.Metadata.Scheme != source.ImageScheme {
return fmt.Errorf("unable to upload results: only images are supported")
}
var dockerfileContents []byte
if app.Anchore.Dockerfile != "" {
if _, err := os.Stat(app.Anchore.Dockerfile); os.IsNotExist(err) {
return fmt.Errorf("unable dockerfile=%q does not exist: %w", app.Anchore.Dockerfile, err)
}
fh, err := os.Open(app.Anchore.Dockerfile)
if err != nil {
return fmt.Errorf("unable to open dockerfile=%q: %w", app.Anchore.Dockerfile, err)
}
dockerfileContents, err = ioutil.ReadAll(fh)
if err != nil {
return fmt.Errorf("unable to read dockerfile=%q: %w", app.Anchore.Dockerfile, err)
}
}
c, err := anchore.NewClient(anchore.Configuration{
BaseURL: app.Anchore.Host,
Username: app.Anchore.Username,
Password: app.Anchore.Password,
})
if err != nil {
return fmt.Errorf("failed to create anchore client: %w", err)
}
importCfg := anchore.ImportConfig{
ImageMetadata: src.Image.Metadata,
SBOM: s,
Dockerfile: dockerfileContents,
OverwriteExistingUpload: app.Anchore.OverwriteExistingImage,
Timeout: app.Anchore.ImportTimeout,
}
if err := c.Import(context.Background(), importCfg); err != nil {
return fmt.Errorf("failed to upload results to host=%s: %+v", app.Anchore.Host, err)
}
return nil
}
-49
View File
@@ -1,49 +0,0 @@
package cli
import (
"fmt"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/cmd/syft/cli/poweruser"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/config"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
const powerUserExample = ` {{.appName}} {{.command}} <image>
DEPRECATED - THIS COMMAND WILL BE REMOVED in v1.0.0
Only image sources are supported (e.g. docker: , podman: , docker-archive: , oci: , etc.), the directory source (dir:) is not supported.
All behavior is controlled via application configuration and environment variables (see https://github.com/anchore/syft#configuration)
`
func PowerUser(v *viper.Viper, app *config.Application, ro *options.RootOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "power-user [IMAGE]",
Short: "Run bulk operations on container images",
Example: internal.Tprintf(powerUserExample, map[string]interface{}{
"appName": internal.ApplicationName,
"command": "power-user",
}),
Args: func(cmd *cobra.Command, args []string) error {
if err := app.LoadAllValues(v, ro.Config); err != nil {
return fmt.Errorf("invalid application config: %v", err)
}
// configure logging for command
newLogWrapper(app)
logApplicationConfig(app)
return validateArgs(cmd, args)
},
Hidden: true,
SilenceUsage: true,
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
if app.CheckForAppUpdate {
checkForApplicationUpdate()
}
return poweruser.Run(cmd.Context(), app, args)
},
}
return cmd
}
-117
View File
@@ -1,117 +0,0 @@
package poweruser
import (
"context"
"fmt"
"os"
"github.com/anchore/stereoscope"
"github.com/anchore/syft/cmd/syft/cli/eventloop"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/cmd/syft/cli/packages"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/config"
"github.com/anchore/syft/internal/formats/syftjson"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/ui"
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
"github.com/gookit/color"
"github.com/wagoodman/go-partybus"
)
func Run(ctx context.Context, app *config.Application, args []string) error {
writer, err := sbom.NewWriter(sbom.WriterOption{
Format: syftjson.Format(),
Path: app.File,
})
if err != nil {
return err
}
defer func() {
if err := writer.Close(); err != nil {
log.Warnf("unable to write to report destination: %+v", err)
}
// inform user at end of run that command will be removed
deprecated := color.Style{color.Red, color.OpBold}.Sprint("DEPRECATED: This command will be removed in v1.0.0")
fmt.Fprintln(os.Stderr, deprecated)
}()
userInput := args[0]
si, err := source.ParseInput(userInput, app.Platform, true)
if err != nil {
return fmt.Errorf("could not generate source input for packages command: %w", err)
}
eventBus := partybus.NewBus()
stereoscope.SetBus(eventBus)
syft.SetBus(eventBus)
subscription := eventBus.Subscribe()
return eventloop.EventLoop(
execWorker(app, *si, writer),
eventloop.SetupSignals(),
subscription,
stereoscope.Cleanup,
ui.Select(options.IsVerbose(app), app.Quiet)...,
)
}
func execWorker(app *config.Application, si source.Input, writer sbom.Writer) <-chan error {
errs := make(chan error)
go func() {
defer close(errs)
app.Secrets.Cataloger.Enabled = true
app.FileMetadata.Cataloger.Enabled = true
app.FileContents.Cataloger.Enabled = true
app.FileClassification.Cataloger.Enabled = true
tasks, err := eventloop.Tasks(app)
if err != nil {
errs <- err
return
}
src, cleanup, err := source.New(si, app.Registry.ToOptions(), app.Exclusions)
if err != nil {
errs <- err
return
}
if cleanup != nil {
defer cleanup()
}
s := sbom.SBOM{
Source: src.Metadata,
Descriptor: sbom.Descriptor{
Name: internal.ApplicationName,
Version: version.FromBuild().Version,
Configuration: app,
},
}
var relationships []<-chan artifact.Relationship
for _, task := range tasks {
c := make(chan artifact.Relationship)
relationships = append(relationships, c)
go eventloop.RunTask(task, &s.Artifacts, src, c, errs)
}
s.Relationships = append(s.Relationships, packages.MergeRelationships(relationships...)...)
bus.Publish(partybus.Event{
Type: event.Exit,
Value: func() error { return writer.Write(s) },
})
}()
return errs
}
-18
View File
@@ -1,18 +0,0 @@
package main
import (
"log"
"github.com/anchore/syft/cmd/syft/cli"
)
func main() {
cli, err := cli.New()
if err != nil {
log.Fatalf("error during command construction: %v", err)
}
if err := cli.Execute(); err != nil {
log.Fatalf("error during command execution: %v", err)
}
}
+15 -25
View File
@@ -1,41 +1,33 @@
package cli
package cmd
import (
"encoding/json"
"fmt"
"log"
"os"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/config"
"github.com/anchore/syft/internal/version"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func Version(v *viper.Viper, app *config.Application) *cobra.Command {
o := &options.VersionOptions{}
cmd := &cobra.Command{
Use: "version",
Short: "show the version",
RunE: func(cmd *cobra.Command, args []string) error {
return printVersion(o.Output)
},
}
var versionCmdOutputFormat string
err := o.AddFlags(cmd, v)
if err != nil {
log.Fatal(err)
}
return cmd
var versionCmd = &cobra.Command{
Use: "version",
Short: "show the version",
Run: printVersion,
}
func printVersion(output string) error {
func init() {
versionCmd.Flags().StringVarP(&versionCmdOutputFormat, "output", "o", "text", "format to show version information (available=[text, json])")
rootCmd.AddCommand(versionCmd)
}
func printVersion(_ *cobra.Command, _ []string) {
versionInfo := version.FromBuild()
switch output {
switch versionCmdOutputFormat {
case "text":
fmt.Println("Application: ", internal.ApplicationName)
fmt.Println("Version: ", versionInfo.Version)
@@ -63,9 +55,7 @@ func printVersion(output string) error {
os.Exit(1)
}
default:
fmt.Printf("unsupported output format: %s\n", output)
fmt.Printf("unsupported output format: %s\n", versionCmdOutputFormat)
os.Exit(1)
}
return nil
}
+145 -136
View File
@@ -1,136 +1,129 @@
module github.com/anchore/syft
go 1.18
go 1.17
require (
github.com/CycloneDX/cyclonedx-go v0.5.2
github.com/CycloneDX/cyclonedx-go v0.5.0
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
github.com/acobaugh/osrelease v0.1.0
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-macholibre v0.0.0-20220308212642-53e6d0aaf6fb
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/packageurl-go v0.1.1-0.20220428202044-a072fa3cb6d7
github.com/anchore/stereoscope v0.0.0-20220616165231-b0fd10fdee06
github.com/anchore/packageurl-go v0.0.0-20210922164639-b3fa992ebd29
github.com/anchore/stereoscope v0.0.0-20220307154759-8a5a70c227d3
github.com/antihax/optional v1.0.0
github.com/bmatcuk/doublestar/v4 v4.0.2
github.com/dustin/go-humanize v1.0.0
github.com/facebookincubator/nvdtools v0.1.4
github.com/go-test/deep v1.0.8
github.com/google/go-cmp v0.5.8
github.com/google/go-cmp v0.5.7
github.com/google/uuid v1.3.0
github.com/gookit/color v1.4.2
github.com/hashicorp/go-multierror v1.1.1
github.com/in-toto/in-toto-golang v0.3.4-0.20211211042327-af1f9fb822bf
github.com/jinzhu/copier v0.3.2
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mholt/archiver/v3 v3.5.1
github.com/mitchellh/go-homedir v1.1.0
github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/mitchellh/mapstructure v1.5.0
github.com/mitchellh/mapstructure v1.4.3
github.com/olekukonko/tablewriter v0.0.5
github.com/pelletier/go-toml v1.9.5
// pinned to pull in 386 arch fix: https://github.com/scylladb/go-set/commit/cc7b2070d91ebf40d233207b633e28f5bd8f03a5
github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e
github.com/pelletier/go-toml v1.9.4
github.com/pkg/profile v1.5.0
github.com/scylladb/go-set v1.0.2
github.com/sergi/go-diff v1.2.0
github.com/sigstore/sigstore v1.1.1-0.20220217212907-e48ca03a5ba7
github.com/sirupsen/logrus v1.8.1
github.com/spdx/tools-golang v0.2.0
github.com/spf13/afero v1.8.2
github.com/spf13/cobra v1.4.0
github.com/spf13/afero v1.8.0
github.com/spf13/cobra v1.3.0
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.12.0
github.com/stretchr/testify v1.7.1
github.com/spf13/viper v1.10.1
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-20211129225309-56b0d0a4aebb
github.com/x-cray/logrus-prefixed-formatter v0.5.2
github.com/xeipuuv/gojsonschema v1.2.0
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2
golang.org/x/mod v0.5.1
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
gopkg.in/yaml.v2 v2.4.0
)
require (
github.com/docker/docker v20.10.12+incompatible
github.com/google/go-containerregistry v0.8.1-0.20220209165246-a44adc326839
github.com/in-toto/in-toto-golang v0.3.4-0.20211211042327-af1f9fb822bf
github.com/knqyf263/go-rpmdb v0.0.0-20220530130908-5101b033fa0d
github.com/sigstore/cosign v1.9.0
github.com/sigstore/rekor v0.4.1-0.20220114213500-23f583409af3
github.com/sigstore/sigstore v1.2.1-0.20220424143412-3d41663116d5
github.com/vbatts/go-mtree v0.5.0
github.com/sigstore/cosign v1.6.0
)
require (
bitbucket.org/creachadair/shell v0.0.6 // indirect
cloud.google.com/go v0.100.2 // indirect
cloud.google.com/go/compute v1.6.1 // indirect
cloud.google.com/go/iam v0.3.0 // indirect
cloud.google.com/go/storage v1.22.1 // indirect
github.com/Azure/azure-sdk-for-go v63.3.0+incompatible // indirect
cloud.google.com/go/compute v1.3.0 // indirect
cloud.google.com/go/iam v0.1.1 // indirect
cloud.google.com/go/kms v1.3.0 // indirect
cloud.google.com/go/storage v1.21.0 // indirect
github.com/Azure/azure-sdk-for-go v61.5.0+incompatible // indirect
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
github.com/Azure/go-autorest/autorest v0.11.27 // indirect
github.com/Azure/go-autorest/autorest v0.11.24 // indirect
github.com/Azure/go-autorest/autorest/adal v0.9.18 // indirect
github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 // indirect
github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 // indirect
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect
github.com/Azure/go-autorest/logger v0.2.1 // indirect
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/Microsoft/go-winio v0.5.2 // indirect
github.com/PaesslerAG/gval v1.0.0 // indirect
github.com/PaesslerAG/jsonpath v0.1.1 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/ReneKroon/ttlcache/v2 v2.11.0 // indirect
github.com/ThalesIgnite/crypto11 v1.2.5 // indirect
github.com/armon/go-metrics v0.3.10 // indirect
github.com/armon/go-radix v1.0.0 // indirect
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
github.com/aws/aws-sdk-go-v2 v1.16.4 // indirect
github.com/aws/aws-sdk-go-v2/config v1.14.0 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.9.0 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.11.0 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.5 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.3.0 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.6 // indirect
github.com/aws/aws-sdk-go-v2/service/ecr v1.15.0 // indirect
github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.12.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.8.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.10.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.15.0 // indirect
github.com/aws/smithy-go v1.11.2 // indirect
github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20220228164355-396b2034c795 // indirect
github.com/aws/aws-sdk-go v1.43.0 // indirect
github.com/aws/aws-sdk-go-v2 v1.13.0 // indirect
github.com/aws/aws-sdk-go-v2/config v1.13.1 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.8.0 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.10.0 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.2.0 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.5 // indirect
github.com/aws/aws-sdk-go-v2/service/ecr v1.14.0 // indirect
github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.11.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.7.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.9.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.14.0 // indirect
github.com/aws/smithy-go v1.10.0 // indirect
github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20220216180153-3d7835abdf40 // indirect
github.com/benbjohnson/clock v1.1.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bgentry/speakeasy v0.1.0 // indirect
github.com/blang/semver v3.5.1+incompatible // indirect
github.com/cenkalti/backoff/v3 v3.2.2 // indirect
github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/chrismellard/docker-credential-acr-env v0.0.0-20220119192733-fe33c00cee21 // indirect
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 // indirect
github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490 // indirect
github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be // indirect
github.com/containerd/containerd v1.5.13 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.10.1 // indirect
github.com/coreos/go-oidc/v3 v3.1.0 // indirect
github.com/coreos/go-semver v0.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
github.com/cyberphone/json-canonicalization v0.0.0-20210823021906-dc406ceaf94b // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dimchansky/utfbom v1.1.1 // indirect
github.com/docker/cli v20.10.12+incompatible // indirect
github.com/docker/distribution v2.8.0+incompatible // indirect
github.com/docker/docker-credential-helpers v0.6.4 // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1 // indirect
github.com/envoyproxy/go-control-plane v0.10.1 // indirect
github.com/envoyproxy/protoc-gen-validate v0.6.2 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/form3tech-oss/jwt-go v3.2.5+incompatible // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/fullstorydev/grpcurl v1.8.2 // indirect
github.com/gabriel-vasile/mimetype v1.4.0 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-chi/chi v4.1.2+incompatible // indirect
github.com/go-logr/logr v1.2.2 // indirect
@@ -139,7 +132,7 @@ require (
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.6 // indirect
github.com/go-openapi/loads v0.21.1 // indirect
github.com/go-openapi/runtime v0.24.1 // indirect
github.com/go-openapi/runtime v0.23.1 // indirect
github.com/go-openapi/spec v0.20.4 // indirect
github.com/go-openapi/strfmt v0.21.2 // indirect
github.com/go-openapi/swag v0.21.1 // indirect
@@ -148,106 +141,90 @@ require (
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.10.0 // indirect
github.com/go-restruct/restruct v1.2.0-alpha // indirect
github.com/go-stack/stack v1.8.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.3.0 // indirect
github.com/golang-jwt/jwt/v4 v4.2.0 // indirect
github.com/golang/glog v1.0.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/btree v1.0.1 // indirect
github.com/google/certificate-transparency-go v1.1.2 // indirect
github.com/google/go-github/v42 v42.0.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/trillian v1.4.0 // indirect
github.com/googleapis/gax-go/v2 v2.4.0 // indirect
github.com/googleapis/gax-go/v2 v2.1.1 // indirect
github.com/googleapis/gnostic v0.5.5 // indirect
github.com/googleapis/go-type-adapters v1.0.0 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.1 // indirect
github.com/hashicorp/go-hclog v1.1.0 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-plugin v1.4.3 // indirect
github.com/hashicorp/go-retryablehttp v0.7.0 // indirect
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
github.com/hashicorp/go-secure-stdlib/mlock v0.1.2 // indirect
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.2 // indirect
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
github.com/hashicorp/go-sockaddr v1.0.2 // indirect
github.com/hashicorp/go-uuid v1.0.2 // indirect
github.com/hashicorp/go-version v1.4.0 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 // indirect
github.com/hashicorp/vault/api v1.3.1 // indirect
github.com/hashicorp/vault/sdk v0.4.1 // indirect
github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jedisct1/go-minisign v0.0.0-20210703085342-c1f07ee84431 // indirect
github.com/jhump/protoreflect v1.9.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jonboulle/clockwork v0.2.2 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/klauspost/compress v1.14.2 // indirect
github.com/klauspost/pgzip v1.2.5 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/letsencrypt/boulder v0.0.0-20220331220046-b23ab962616e // indirect
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/miekg/pkcs11 v1.1.1 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/nwaples/rardecode v1.1.0 // indirect
github.com/oklog/run v1.1.0 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.1 // indirect
github.com/pierrec/lz4/v4 v4.1.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.12.1 // indirect
github.com/pierrec/lz4 v2.6.1+incompatible // indirect
github.com/prometheus/client_golang v1.11.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect
github.com/sassoftware/relic v0.0.0-20210427151427-dfb082b79b74 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.4.0 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.3.1 // indirect
github.com/segmentio/ksuid v1.0.4 // indirect
github.com/shibumi/go-pathspec v1.3.0 // indirect
github.com/sigstore/fulcio v0.1.2-0.20220114150912-86a2036f9bc7 // indirect
github.com/sigstore/rekor v0.4.1-0.20220114213500-23f583409af3 // indirect
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect
github.com/soheilhy/cmux v0.1.5 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spiffe/go-spiffe/v2 v2.1.0 // indirect
github.com/stretchr/objx v0.3.0 // indirect
github.com/subosito/gotenv v1.3.0 // indirect
github.com/spiffe/go-spiffe/v2 v2.0.0-beta.12 // indirect
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect
github.com/tent/canonical-json-go v0.0.0-20130607151641-96e4ba3a7613 // indirect
github.com/thales-e-security/pool v0.0.2 // indirect
github.com/theupdateframework/go-tuf v0.3.0 // indirect
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect
github.com/theupdateframework/go-tuf v0.0.0-20220211205608-f0c3294f63b9 // indirect
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect
github.com/transparency-dev/merkle v0.0.1 // indirect
github.com/ulikunitz/xz v0.5.10 // indirect
github.com/urfave/cli v1.22.5 // indirect
github.com/vbatts/tar-split v0.11.2 // indirect
github.com/xanzy/go-gitlab v0.68.0 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
github.com/xanzy/go-gitlab v0.56.0 // indirect
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
github.com/zeebo/errs v1.2.2 // indirect
go.etcd.io/bbolt v1.3.6 // indirect
go.etcd.io/etcd/api/v3 v3.5.4 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.4 // indirect
go.etcd.io/etcd/client/v2 v2.305.4 // indirect
go.etcd.io/etcd/client/v3 v3.5.4 // indirect
go.etcd.io/etcd/api/v3 v3.5.1 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.1 // indirect
go.etcd.io/etcd/client/v2 v2.305.1 // indirect
go.etcd.io/etcd/client/v3 v3.5.0 // indirect
go.etcd.io/etcd/etcdctl/v3 v3.5.0 // indirect
go.etcd.io/etcd/etcdutl/v3 v3.5.0 // indirect
go.etcd.io/etcd/pkg/v3 v3.5.0 // indirect
@@ -270,51 +247,83 @@ require (
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.7.0 // indirect
go.uber.org/zap v1.21.0 // indirect
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 // indirect
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 // indirect
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect
golang.org/x/tools v0.1.10 // indirect
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df // indirect
google.golang.org/api v0.81.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd // indirect
google.golang.org/grpc v1.46.2 // indirect
google.golang.org/protobuf v1.28.0 // indirect
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect
golang.org/x/tools v0.1.8 // indirect
google.golang.org/api v0.70.0 // indirect
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/cheggaaa/pb.v1 v1.0.28 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.66.4 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
gopkg.in/yaml.v3 v3.0.0 // indirect
k8s.io/api v0.23.5 // indirect
k8s.io/apimachinery v0.23.5 // indirect
k8s.io/client-go v0.23.5 // indirect
k8s.io/klog/v2 v2.60.1-0.20220317184644-43cc75f9ae89 // indirect
k8s.io/api v0.23.3 // indirect
k8s.io/apimachinery v0.23.3 // indirect
k8s.io/client-go v0.23.3 // indirect
k8s.io/klog/v2 v2.40.1 // indirect
k8s.io/kube-openapi v0.0.0-20220124234850-424119656bbf // indirect
k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect
knative.dev/pkg v0.0.0-20220325200448-1f7514acd0c2 // indirect
lukechampine.com/uint128 v1.1.1 // indirect
modernc.org/cc/v3 v3.35.22 // indirect
modernc.org/ccgo/v3 v3.15.1 // indirect
modernc.org/libc v1.14.1 // indirect
modernc.org/mathutil v1.4.1 // indirect
modernc.org/memory v1.0.5 // indirect
modernc.org/opt v0.1.1 // indirect
modernc.org/sqlite v1.14.5 // indirect
modernc.org/strutil v1.1.1 // indirect
modernc.org/token v1.0.0 // indirect
k8s.io/utils v0.0.0-20220127004650-9b3446523e65 // indirect
knative.dev/pkg v0.0.0-20220202132633-df430fa0dd96 // indirect
sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect
sigs.k8s.io/release-utils v0.6.0 // indirect
sigs.k8s.io/release-utils v0.4.1-0.20220207182343-6dadf2228617 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)
require (
github.com/Microsoft/go-winio v0.5.1 // indirect
// we are hinting brotli to latest due to warning when installing archiver v3:
// go: warning: github.com/andybalholm/brotli@v1.0.1: retracted by module author: occasional panics and data corruption
github.com/andybalholm/brotli v1.0.4 // indirect
github.com/containerd/containerd v1.5.10 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.10.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/cli v20.10.12+incompatible // indirect
github.com/docker/distribution v2.8.0+incompatible // indirect
github.com/docker/docker-credential-helpers v0.6.4 // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.0 // indirect
github.com/go-restruct/restruct v1.2.0-alpha // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-containerregistry v0.8.1-0.20220209165246-a44adc326839 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/klauspost/compress v1.14.2 // indirect
github.com/klauspost/pgzip v1.2.5 // indirect
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 // indirect
github.com/magiconair/properties v1.8.5 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/nwaples/rardecode v1.1.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198 // indirect
github.com/pierrec/lz4/v4 v4.1.2 // indirect
github.com/pkg/errors v0.9.1
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/cast v1.4.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/stretchr/objx v0.3.0 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
github.com/ulikunitz/xz v0.5.10 // indirect
github.com/vbatts/tar-split v0.11.2 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
golang.org/x/crypto v0.0.0-20220213190939-1e6e3497d506 // indirect
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c // indirect
google.golang.org/grpc v1.44.0 // indirect
gopkg.in/ini.v1 v1.66.2 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
)
+317 -366
View File
File diff suppressed because it is too large Load Diff
+7 -10
View File
@@ -6,14 +6,15 @@ import (
"fmt"
"time"
"github.com/anchore/client-go/pkg/external"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/sbom"
"github.com/antihax/optional"
"github.com/wagoodman/go-partybus"
"github.com/wagoodman/go-progress"
"github.com/anchore/client-go/pkg/external"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/syft/syft/sbom"
"github.com/antihax/optional"
)
type ImportConfig struct {
@@ -56,7 +57,7 @@ func (c *Client) Import(ctx context.Context, cfg ImportConfig) error {
authedCtx := c.newRequestContext(ctxWithTimeout)
stage.Current = "starting session"
startOperation, createResponse, err := c.client.ImportsApi.CreateOperation(authedCtx)
startOperation, _, err := c.client.ImportsApi.CreateOperation(authedCtx)
if err != nil {
var detail = "no details given"
var openAPIErr external.GenericOpenAPIError
@@ -65,8 +66,6 @@ func (c *Client) Import(ctx context.Context, cfg ImportConfig) error {
}
return fmt.Errorf("unable to start import session: %w: %s", err, detail)
}
defer createResponse.Body.Close()
prog.N++
sessionID := startOperation.Uuid
@@ -100,7 +99,7 @@ func (c *Client) Import(ctx context.Context, cfg ImportConfig) error {
Force: optional.NewBool(cfg.OverwriteExistingUpload),
}
_, addResponse, err := c.client.ImagesApi.AddImage(authedCtx, imageModel, &opts)
_, _, err = c.client.ImagesApi.AddImage(authedCtx, imageModel, &opts)
if err != nil {
var detail = "no details given"
var openAPIErr external.GenericOpenAPIError
@@ -109,8 +108,6 @@ func (c *Client) Import(ctx context.Context, cfg ImportConfig) error {
}
return fmt.Errorf("unable to complete import session=%q: %w: %s", sessionID, err, detail)
}
defer addResponse.Body.Close()
prog.N++
stage.Current = ""
+13 -11
View File
@@ -4,6 +4,8 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/file"
"net/http"
"strings"
"testing"
@@ -22,7 +24,7 @@ import (
"github.com/wagoodman/go-progress"
)
func must(c pkg.CPE, e error) pkg.CPE {
func must(c cpe.CPE, e error) cpe.CPE {
if e != nil {
panic(e)
}
@@ -52,23 +54,23 @@ func (m *mockPackageSBOMImportAPI) ImportImagePackages(ctx context.Context, sess
func sbomFixture() sbom.SBOM {
return sbom.SBOM{
Artifacts: sbom.Artifacts{
PackageCatalog: pkg.NewCatalog(pkg.Package{
Packages: pkg.NewCollection(pkg.Package{
Name: "name",
Version: "version",
FoundBy: "foundBy",
Locations: source.NewLocationSet(
source.Location{
Coordinates: source.Coordinates{
Locations: []file.Location{
{
Coordinates: file.Coordinates{
RealPath: "path",
FileSystemID: "layerID",
},
},
),
},
Licenses: []string{"license"},
Language: pkg.Python,
Type: pkg.PythonPkg,
CPEs: []pkg.CPE{
must(pkg.NewCPE("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*")),
CPEs: []cpe.CPE{
must(cpe.New("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*")),
},
PURL: "purl",
MetadataType: pkg.PythonPackageMetadataType,
@@ -102,13 +104,13 @@ func sbomFixture() sbom.SBOM {
},
Relationships: []artifact.Relationship{
{
From: source.NewLocation("/place1"),
To: source.NewLocation("/place2"),
From: file.NewLocation("/place1"),
To: file.NewLocation("/place2"),
Type: artifact.ContainsRelationship,
},
},
Source: source.Metadata{
Scheme: source.ImageScheme,
Scheme: source.ImageType,
ImageMetadata: source.ImageMetadata{
UserInput: "user-in",
Layers: nil,
@@ -1,12 +1,14 @@
package file
package archive
import (
"errors"
"fmt"
"io"
"github.com/anchore/syft/syft/file"
)
const perFileReadLimit = 2 * GB
const perFileReadLimit = 2 * file.GB
// safeCopy limits the copy from the reader. This is useful when extracting files from archives to
// protect against decompression bomb attacks.
@@ -1,38 +1,39 @@
package file
package archive
import (
"fmt"
"io/ioutil"
"path/filepath"
"github.com/anchore/syft/syft/file"
"github.com/bmatcuk/doublestar/v4"
"github.com/mholt/archiver/v3"
)
// ExtractGlobsFromTarToUniqueTempFile extracts paths matching the given globs within the given archive to a temporary directory, returning file openers for each file extracted.
func ExtractGlobsFromTarToUniqueTempFile(archivePath, dir string, globs ...string) (map[string]Opener, error) {
results := make(map[string]Opener)
func ExtractGlobsFromTarToUniqueTempFile(archivePath, dir string, globs ...string) (map[string]file.Opener, error) {
results := make(map[string]file.Opener)
// don't allow for full traversal, only select traversal from given paths
if len(globs) == 0 {
return results, nil
}
visitor := func(file archiver.File) error {
defer file.Close()
visitor := func(f archiver.File) error {
defer f.Close()
// ignore directories
if file.FileInfo.IsDir() {
if f.FileInfo.IsDir() {
return nil
}
// ignore any filename that doesn't match the given globs...
if !matchesAnyGlob(file.Name(), globs...) {
if !matchesAnyGlob(f.Name(), globs...) {
return nil
}
// we have a file we want to extract....
tempfilePrefix := filepath.Base(filepath.Clean(file.Name())) + "-"
tempfilePrefix := filepath.Base(filepath.Clean(f.Name())) + "-"
tempFile, err := ioutil.TempFile(dir, tempfilePrefix)
if err != nil {
return fmt.Errorf("unable to create temp file: %w", err)
@@ -42,11 +43,11 @@ func ExtractGlobsFromTarToUniqueTempFile(archivePath, dir string, globs ...strin
// provides a ReadCloser. It is up to the caller to handle closing the file explicitly.
defer tempFile.Close()
if err := safeCopy(tempFile, file.ReadCloser); err != nil {
return fmt.Errorf("unable to copy source=%q for tar=%q: %w", file.Name(), archivePath, err)
if err := safeCopy(tempFile, f.ReadCloser); err != nil {
return fmt.Errorf("unable to copy source=%q for tar=%q: %w", f.Name(), archivePath, err)
}
results[file.Name()] = Opener{path: tempFile.Name()}
results[f.Name()] = file.NewOpener(tempFile.Name())
return nil
}
@@ -3,10 +3,5 @@ set -eux
# $1 —— absolute path to destination file, should end with .zip, ideally
# $2 —— absolute path to directory from which to add entries to the archive
# $3 —— if files should be zip64 or not
if [[$3]]; then
pushd "$2" && find . -print | zip -fz "$1" -@ && popd
else
pushd "$2" && find . -print | zip "$1" -@ && popd
fi
pushd "$2" && find . -print | zip "$1" -@ && popd
@@ -1,4 +1,4 @@
package file
package archive
import (
"io/ioutil"
@@ -20,20 +20,16 @@ var expectedZipArchiveEntries = []string{
}
// createZipArchive creates a new ZIP archive file at destinationArchivePath based on the directory found at
// sourceDirPath. It forces a zip64 archive if zip64 is "0".
func createZipArchive(t testing.TB, sourceDirPath, destinationArchivePath string, zip64 bool) {
// sourceDirPath.
func createZipArchive(t testing.TB, sourceDirPath, destinationArchivePath string) {
t.Helper()
cwd, err := os.Getwd()
if err != nil {
t.Fatalf("unable to get cwd: %+v", err)
}
zip64Arg := "0"
if zip64 {
zip64Arg = "1"
}
cmd := exec.Command("./generate-zip-fixture-from-source-dir.sh", destinationArchivePath, path.Base(sourceDirPath), zip64Arg)
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 {
@@ -70,7 +66,7 @@ func assertNoError(t testing.TB, fn func() error) func() {
// 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.TB, sourceDirPath string, zip64 bool) string {
func setupZipFileTest(t testing.TB, sourceDirPath string) string {
t.Helper()
archivePrefix, err := ioutil.TempFile("", "syft-ziputil-archive-TEST-")
@@ -88,7 +84,7 @@ func setupZipFileTest(t testing.TB, sourceDirPath string, zip64 bool) string {
destinationArchiveFilePath := archivePrefix.Name() + ".zip"
t.Logf("archive path: %s", destinationArchiveFilePath)
createZipArchive(t, sourceDirPath, destinationArchiveFilePath, zip64)
createZipArchive(t, sourceDirPath, destinationArchiveFilePath)
t.Cleanup(
assertNoError(t,
@@ -113,7 +109,7 @@ func ensureNestedZipExists(t *testing.T, sourceDirPath string) error {
t.Helper()
nestedArchiveFilePath := path.Join(sourceDirPath, "nested.zip")
createZipArchive(t, sourceDirPath, nestedArchiveFilePath, false)
createZipArchive(t, sourceDirPath, nestedArchiveFilePath)
return nil
}
@@ -1,4 +1,4 @@
package file
package archive
import (
"fmt"
@@ -48,7 +48,7 @@ func (z ZipFileManifest) GlobMatch(patterns ...string) []string {
// so that glob logic is consistent inside and outside of ZIP archives
normalizedEntry := normalizeZipEntryName(entry)
if GlobMatch(pattern, normalizedEntry) {
if internal.FileNameGlobMatch(pattern, normalizedEntry) {
uniqueMatches.Add(entry)
}
}
@@ -1,7 +1,7 @@
//go:build !windows
// +build !windows
package file
package archive
import (
"encoding/json"
@@ -22,42 +22,7 @@ func TestNewZipFileManifest(t *testing.T) {
t.Fatal(err)
}
archiveFilePath := setupZipFileTest(t, sourceDirPath, false)
actual, err := NewZipFileManifest(archiveFilePath)
if err != nil {
t.Fatalf("unable to extract from unzip archive: %+v", err)
}
if len(expectedZipArchiveEntries) != len(actual) {
t.Fatalf("mismatched manifest: %d != %d", len(actual), len(expectedZipArchiveEntries))
}
for _, e := range expectedZipArchiveEntries {
_, ok := actual[e]
if !ok {
t.Errorf("missing path: %s", e)
}
}
if t.Failed() {
b, err := json.MarshalIndent(actual, "", " ")
if err != nil {
t.Fatalf("can't show results: %+v", err)
}
t.Errorf("full result: %s", string(b))
}
}
func TestNewZip64FileManifest(t *testing.T) {
cwd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
sourceDirPath := path.Join(cwd, "test-fixtures", "zip-source")
archiveFilePath := setupZipFileTest(t, sourceDirPath, true)
archiveFilePath := setupZipFileTest(t, sourceDirPath)
actual, err := NewZipFileManifest(archiveFilePath)
if err != nil {
@@ -97,7 +62,7 @@ func TestZipFileManifest_GlobMatch(t *testing.T) {
t.Fatal(err)
}
archiveFilePath := setupZipFileTest(t, sourceDirPath, false)
archiveFilePath := setupZipFileTest(t, sourceDirPath)
z, err := NewZipFileManifest(archiveFilePath)
if err != nil {
@@ -1,4 +1,4 @@
package file
package archive
import (
"archive/zip"
@@ -9,15 +9,9 @@ import (
"path/filepath"
"strings"
"github.com/anchore/syft/internal/log"
)
"github.com/anchore/syft/syft/file"
const (
// represents the order of bytes
_ = iota
KB = 1 << (10 * iota)
MB
GB
"github.com/anchore/syft/internal/log"
)
type errZipSlipDetected struct {
@@ -71,16 +65,16 @@ func TraverseFilesInZip(archivePath string, visitor func(*zip.File) error, paths
}
// ExtractFromZipToUniqueTempFile extracts select paths for the given archive to a temporary directory, returning file openers for each file extracted.
func ExtractFromZipToUniqueTempFile(archivePath, dir string, paths ...string) (map[string]Opener, error) {
results := make(map[string]Opener)
func ExtractFromZipToUniqueTempFile(archivePath, dir string, paths ...string) (map[string]file.Opener, error) {
results := make(map[string]file.Opener)
// don't allow for full traversal, only select traversal from given paths
if len(paths) == 0 {
return results, nil
}
visitor := func(file *zip.File) error {
tempfilePrefix := filepath.Base(filepath.Clean(file.Name)) + "-"
visitor := func(f *zip.File) error {
tempfilePrefix := filepath.Base(filepath.Clean(f.Name)) + "-"
tempFile, err := ioutil.TempFile(dir, tempfilePrefix)
if err != nil {
@@ -91,26 +85,26 @@ func ExtractFromZipToUniqueTempFile(archivePath, dir string, paths ...string) (m
// provides a ReadCloser. It is up to the caller to handle closing the file explicitly.
defer tempFile.Close()
zippedFile, err := file.Open()
zippedFile, err := f.Open()
if err != nil {
return fmt.Errorf("unable to read file=%q from zip=%q: %w", file.Name, archivePath, err)
return fmt.Errorf("unable to read file=%q from zip=%q: %w", f.Name, archivePath, err)
}
defer func() {
err := zippedFile.Close()
if err != nil {
log.Errorf("unable to close source file=%q from zip=%q: %+v", file.Name, archivePath, err)
log.Errorf("unable to close source file=%q from zip=%q: %+v", f.Name, archivePath, err)
}
}()
if file.FileInfo().IsDir() {
return fmt.Errorf("unable to extract directories, only files: %s", file.Name)
if f.FileInfo().IsDir() {
return fmt.Errorf("unable to extract directories, only files: %s", f.Name)
}
if err := safeCopy(tempFile, zippedFile); err != nil {
return fmt.Errorf("unable to copy source=%q for zip=%q: %w", file.Name, archivePath, err)
return fmt.Errorf("unable to copy source=%q for zip=%q: %w", f.Name, archivePath, err)
}
results[file.Name] = Opener{path: tempFile.Name()}
results[f.Name] = file.NewOpener(tempFile.Name())
return nil
}
@@ -1,7 +1,7 @@
//go:build !windows
// +build !windows
package file
package archive
import (
"crypto/sha256"
@@ -47,7 +47,7 @@ func TestUnzipToDir(t *testing.T) {
goldenRootDir := filepath.Join(cwd, "test-fixtures")
sourceDirPath := path.Join(goldenRootDir, "zip-source")
archiveFilePath := setupZipFileTest(t, sourceDirPath, false)
archiveFilePath := setupZipFileTest(t, sourceDirPath)
unzipDestinationDir, err := ioutil.TempDir("", "syft-ziputil-contents-TEST-")
t.Cleanup(assertNoError(t, func() error {
@@ -227,7 +227,7 @@ func prepZipSourceFixture(t testing.TB) string {
t.Logf("archive path: %s", archivePath)
createZipArchive(t, "zip-source", archivePrefix.Name(), false)
createZipArchive(t, "zip-source", archivePrefix.Name())
return archivePath
}
@@ -1,9 +1,8 @@
package file
package archive
import (
"archive/zip"
"encoding/binary"
"errors"
"fmt"
"io"
"os"
@@ -14,13 +13,7 @@ import (
// - 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
directory64LocLen = 20
directory64EndLen = 56
directory64LocSignature = 0x07064b50
directory64EndSignature = 0x06064b50
)
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).
@@ -79,12 +72,6 @@ func (b *readBuf) uint32() uint32 {
return v
}
func (b *readBuf) uint64() uint64 {
v := binary.LittleEndian.Uint64(*b)
*b = (*b)[8:]
return v
}
type directoryEnd struct {
diskNbr uint32 // unused
dirDiskNbr uint32 // unused
@@ -95,7 +82,6 @@ type directoryEnd struct {
}
// note: this is derived from readDirectoryEnd within the archive/zip package
// nolint:gocognit
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
@@ -134,22 +120,13 @@ func findArchiveStartOffset(r io.ReaderAt, size int64) (startOfArchive uint64, e
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 {
p, err := findDirectory64End(r, directoryEndOffset)
if err == nil && p >= 0 {
directoryEndOffset = p
err = readDirectory64End(r, p, d)
}
if err != nil {
return 0, err
}
startOfArchive = 0 // Prefixed data not supported
}
startOfArchive = uint64(directoryEndOffset) - d.directorySize - d.directoryOffset
// Make sure directoryOffset points to somewhere in our file.
if o := int64(d.directoryOffset); o < 0 || o >= size {
return 0, zip.ErrFormat
@@ -157,56 +134,6 @@ func findArchiveStartOffset(r io.ReaderAt, size int64) (startOfArchive uint64, e
return startOfArchive, nil
}
// findDirectory64End tries to read the zip64 locator just before the
// directory end and returns the offset of the zip64 directory end if
// found.
func findDirectory64End(r io.ReaderAt, directoryEndOffset int64) (int64, error) {
locOffset := directoryEndOffset - directory64LocLen
if locOffset < 0 {
return -1, nil // no need to look for a header outside the file
}
buf := make([]byte, directory64LocLen)
if _, err := r.ReadAt(buf, locOffset); err != nil {
return -1, err
}
b := readBuf(buf)
if sig := b.uint32(); sig != directory64LocSignature {
return -1, nil
}
if b.uint32() != 0 { // number of the disk with the start of the zip64 end of central directory
return -1, nil // the file is not a valid zip64-file
}
p := b.uint64() // relative offset of the zip64 end of central directory record
if b.uint32() != 1 { // total number of disks
return -1, nil // the file is not a valid zip64-file
}
return int64(p), nil
}
// readDirectory64End reads the zip64 directory end and updates the
// directory end with the zip64 directory end values.
func readDirectory64End(r io.ReaderAt, offset int64, d *directoryEnd) (err error) {
buf := make([]byte, directory64EndLen)
if _, err := r.ReadAt(buf, offset); err != nil {
return err
}
b := readBuf(buf)
if sig := b.uint32(); sig != directory64EndSignature {
return errors.New("could not read directory64End")
}
b = b[12:] // skip dir size, version and version needed (uint64 + 2x uint16)
d.diskNbr = b.uint32() // number of this disk
d.dirDiskNbr = b.uint32() // number of the disk with the start of the central directory
d.dirRecordsThisDisk = b.uint64() // total number of entries in the central directory on this disk
d.directoryRecords = b.uint64() // total number of entries in the central directory
d.directorySize = b.uint64() // size of the central directory
d.directoryOffset = b.uint64() // offset of start of central directory with respect to the starting disk number
return nil
}
func findSignatureInBlock(b []byte) int {
for i := len(b) - directoryEndLen; i >= 0; i-- {
// defined from directoryEndSignature
@@ -1,7 +1,7 @@
//go:build !windows
// +build !windows
package file
package archive
import (
"os"
+163 -101
View File
@@ -3,24 +3,26 @@ package config
import (
"errors"
"fmt"
"github.com/anchore/syft/syft/source"
"path"
"reflect"
"strings"
"github.com/sirupsen/logrus"
"github.com/adrg/xdg"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/file"
"github.com/mitchellh/go-homedir"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
"gopkg.in/yaml.v2"
)
var (
ErrApplicationConfigNotFound = fmt.Errorf("application config not found")
catalogerEnabledDefault = false
)
var ErrApplicationConfigNotFound = fmt.Errorf("application config not found")
// TODO: set all catalogers when this is set
var catalogerEnabledDefault = false
type defaultValueLoader interface {
loadDefaultValues(*viper.Viper)
@@ -32,59 +34,128 @@ type parser interface {
// Application is the main syft application configuration.
type Application struct {
// the location where the application config was read from (either from -c or discovered while loading); default .syft.yaml
ConfigPath string `yaml:"configPath,omitempty" json:"configPath" mapstructure:"config"`
Verbosity uint `yaml:"verbosity,omitempty" json:"verbosity" mapstructure:"verbosity"`
// -q, indicates to not show any status output to stderr (ETUI or logging UI)
Quiet bool `yaml:"quiet" json:"quiet" mapstructure:"quiet"`
Outputs []string `yaml:"output" json:"output" mapstructure:"output"` // -o, the format to use for output
File string `yaml:"file" json:"file" mapstructure:"file"` // --file, the file to write report output to
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
Dev development `yaml:"dev" json:"dev" mapstructure:"dev"`
Log logging `yaml:"log" json:"log" mapstructure:"log"` // all logging-related options
Package pkg `yaml:"package" json:"package" mapstructure:"package"`
FileMetadata FileMetadata `yaml:"file-metadata" json:"file-metadata" mapstructure:"file-metadata"`
FileClassification fileClassification `yaml:"file-classification" json:"file-classification" mapstructure:"file-classification"`
FileContents fileContents `yaml:"file-contents" json:"file-contents" mapstructure:"file-contents"`
Secrets secrets `yaml:"secrets" json:"secrets" mapstructure:"secrets"`
Registry registry `yaml:"registry" json:"registry" mapstructure:"registry"`
Exclusions []string `yaml:"exclude" json:"exclude" mapstructure:"exclude"`
Attest attest `yaml:"attest" json:"attest" mapstructure:"attest"`
Platform string `yaml:"platform" json:"platform" mapstructure:"platform"`
ConfigPath string `yaml:",omitempty" json:"configPath"` // the location where the application config was read from (either from -c or discovered while loading)
Outputs []string `yaml:"output" json:"output" mapstructure:"output"` // -o, the format to use for output
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
CliOptions CliOnlyOptions `yaml:"-" json:"-"` // all options only available through the CLI (not via env vars or config)
Catalogers []string `yaml:"catalogers" json:"catalogers" mapstructure:"catalogers"`
Scope string `yaml:"scope" json:"scope" mapstructure:"scope"`
Dev development `yaml:"dev" json:"dev" mapstructure:"dev"`
Log logging `yaml:"log" json:"log" mapstructure:"log"` // all logging-related options
Package pkg `yaml:"package" json:"package" mapstructure:"package"`
FileMetadata fileMetadata `yaml:"file-metadata" json:"file-metadata" mapstructure:"file-metadata"`
FileContents fileContents `yaml:"file-contents" json:"file-contents" mapstructure:"file-contents"`
Secrets secretsCfg `yaml:"secrets" json:"secrets" mapstructure:"secrets"`
Registry registry `yaml:"registry" json:"registry" mapstructure:"registry"`
Exclusions []string `yaml:"exclude" json:"exclude" mapstructure:"exclude"`
Attest attest `yaml:"attest" json:"attest" mapstructure:"attest"`
Platform string `yaml:"platform" json:"platform" mapstructure:"platform"`
}
func (cfg *Application) LoadAllValues(v *viper.Viper, configPath string) error {
// priority order: viper.Set, flag, env, config, kv, defaults
// flags have already been loaded into viper by command construction
func (cfg Application) ToCatalogingOptions() ([]syft.CatalogingOption, error) {
// check if user specified config; otherwise read all possible paths
if err := loadConfig(v, configPath); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
// Not Found; ignore this error
log.Debug("no config file found; proceeding with defaults")
digests, err := file.DigestHashesByName(cfg.FileMetadata.Digests...)
if err != nil {
return nil, fmt.Errorf("unable to parse config item 'file-metadata.digests': %w", err)
}
scopeOption := source.ParseScope(cfg.Scope)
if scopeOption == source.UnknownScope {
return nil, fmt.Errorf("bad scope value %q", cfg.Scope)
}
secretsConfig, err := cfg.Secrets.ToConfig()
if err != nil {
return nil, err
}
secretsScopeOption := source.ParseScope(cfg.Secrets.Scope)
if secretsScopeOption == source.UnknownScope {
return nil, fmt.Errorf("bad scope value %q", cfg.Secrets.Scope)
}
c := syft.DefaultCatalogingConfig()
c.ToolVersion = version.FromBuild().Version
c.ToolConfiguration = &c
c.DefaultScope = scopeOption
c.ProcessTasksInSerial = false
c.DigestHashes = digests
c.SecretsSearch = *secretsConfig
c.SecretsScope = secretsScopeOption
c.ContentsSearch = cfg.FileContents.ToConfig()
opts := []syft.CatalogingOption{
syft.WithConfig(c),
syft.WithCatalogers(cfg.Catalogers...),
}
return opts, nil
}
// PowerUserCatalogerEnabledDefault switches all catalogers to be enabled when running power-user command
func PowerUserCatalogerEnabledDefault() {
catalogerEnabledDefault = true
}
func newApplicationConfig(v *viper.Viper, cliOpts CliOnlyOptions) *Application {
config := &Application{
CliOptions: cliOpts,
}
config.loadDefaultValues(v)
return config
}
// LoadApplicationConfig populates the given viper object with application configuration discovered on disk
func LoadApplicationConfig(v *viper.Viper, cliOpts CliOnlyOptions) (*Application, error) {
// the user may not have a config, and this is OK, we can use the default config + default cobra cli values instead
config := newApplicationConfig(v, cliOpts)
if err := readConfig(v, cliOpts.ConfigPath); err != nil && !errors.Is(err, ErrApplicationConfigNotFound) {
return nil, err
}
if err := v.Unmarshal(config); err != nil {
return nil, fmt.Errorf("unable to parse config: %w", err)
}
config.ConfigPath = v.ConfigFileUsed()
if err := config.parseConfigValues(); err != nil {
return nil, fmt.Errorf("invalid application config: %w", err)
}
return config, nil
}
// init loads the default configuration values into the viper instance (before the config values are read and parsed).
func (cfg Application) loadDefaultValues(v *viper.Viper) {
// set the default values for primitive fields in this struct
v.SetDefault("check-for-app-update", true)
// for each field in the configuration struct, see if the field implements the defaultValueLoader interface and invoke it if it does
value := reflect.ValueOf(cfg)
for i := 0; i < value.NumField(); i++ {
// note: the defaultValueLoader method receiver is NOT a pointer receiver.
if loadable, ok := value.Field(i).Interface().(defaultValueLoader); ok {
// the field implements defaultValueLoader, call it
loadable.loadDefaultValues(v)
}
}
// load default config values into viper
loadDefaultValues(v)
// load environment variables
v.SetEnvPrefix(internal.ApplicationName)
v.AllowEmptyEnv(true)
v.AutomaticEnv()
// unmarshal fully populated viper object onto config
err := v.Unmarshal(cfg)
if err != nil {
return err
}
// Convert all populated config options to their internal application values ex: scope string => scopeOpt source.Scope
return cfg.parseConfigValues()
}
func (cfg *Application) parseConfigValues() error {
// parse fields on application directyy
var catalogers []string
for _, c := range cfg.Catalogers {
for _, f := range strings.Split(c, ",") {
catalogers = append(catalogers, strings.TrimSpace(f))
}
}
cfg.Catalogers = catalogers
// parse application config options
for _, optionFn := range []func() error{
cfg.parseUploadOptions,
@@ -95,6 +166,7 @@ func (cfg *Application) parseConfigValues() error {
return err
}
}
// parse nested config options
// for each field in the configuration struct, see if the field implements the parser interface
// note: the app config is a pointer, so we need to grab the elements explicitly (to traverse the address)
@@ -111,6 +183,17 @@ func (cfg *Application) parseConfigValues() error {
return nil
}
func (cfg *Application) parseFile() error {
if cfg.File != "" {
expandedPath, err := homedir.Expand(cfg.File)
if err != nil {
return fmt.Errorf("unable to expand file path=%q: %w", cfg.File, err)
}
cfg.File = expandedPath
}
return nil
}
func (cfg *Application) parseUploadOptions() error {
if cfg.Anchore.Host == "" && cfg.Anchore.Dockerfile != "" {
return fmt.Errorf("cannot provide dockerfile option without enabling upload")
@@ -125,17 +208,11 @@ func (cfg *Application) parseLogLevelOption() error {
// we should be able to quiet the console logging and leave file logging alone...
// ... this will be an enhancement for later
cfg.Log.LevelOpt = logrus.PanicLevel
case cfg.Verbosity > 0:
switch v := cfg.Verbosity; {
case v == 1:
cfg.Log.LevelOpt = logrus.InfoLevel
case v >= 2:
cfg.Log.LevelOpt = logrus.DebugLevel
default:
cfg.Log.LevelOpt = logrus.ErrorLevel
}
case cfg.Log.Level != "":
if cfg.CliOptions.Verbosity > 0 {
return fmt.Errorf("cannot explicitly set log level (cfg file or env var) and use -v flag together")
}
lvl, err := logrus.ParseLevel(strings.ToLower(cfg.Log.Level))
if err != nil {
return fmt.Errorf("bad log level configured (%q): %w", cfg.Log.Level, err)
@@ -143,10 +220,18 @@ func (cfg *Application) parseLogLevelOption() error {
cfg.Log.LevelOpt = lvl
if cfg.Log.LevelOpt >= logrus.InfoLevel {
cfg.Verbosity = 1
cfg.CliOptions.Verbosity = 1
}
default:
cfg.Log.LevelOpt = logrus.WarnLevel
switch v := cfg.CliOptions.Verbosity; {
case v == 1:
cfg.Log.LevelOpt = logrus.InfoLevel
case v >= 2:
cfg.Log.LevelOpt = logrus.DebugLevel
default:
cfg.Log.LevelOpt = logrus.ErrorLevel
}
}
if cfg.Log.Level == "" {
@@ -156,64 +241,43 @@ func (cfg *Application) parseLogLevelOption() error {
return nil
}
func (cfg *Application) parseFile() error {
if cfg.File != "" {
expandedPath, err := homedir.Expand(cfg.File)
if err != nil {
return fmt.Errorf("unable to expand file path=%q: %w", cfg.File, err)
}
cfg.File = expandedPath
}
return nil
}
// init loads the default configuration values into the viper instance (before the config values are read and parsed).
func loadDefaultValues(v *viper.Viper) {
// set the default values for primitive fields in this struct
v.SetDefault("quiet", false)
v.SetDefault("check-for-app-update", true)
// for each field in the configuration struct, see if the field implements the defaultValueLoader interface and invoke it if it does
value := reflect.ValueOf(Application{})
for i := 0; i < value.NumField(); i++ {
// note: the defaultValueLoader method receiver is NOT a pointer receiver.
if loadable, ok := value.Field(i).Interface().(defaultValueLoader); ok {
// the field implements defaultValueLoader, call it
loadable.loadDefaultValues(v)
}
}
}
func (cfg Application) String() string {
// yaml is pretty human friendly (at least when compared to json)
appaStr, err := yaml.Marshal(&cfg)
appCfgStr, err := yaml.Marshal(&cfg)
if err != nil {
return err.Error()
}
return string(appaStr)
return string(appCfgStr)
}
func loadConfig(v *viper.Viper, configPath string) error {
// readConfig attempts to read the given config path from disk or discover an alternate store location
// nolint:funlen
func readConfig(v *viper.Viper, configPath string) error {
var err error
v.AutomaticEnv()
v.SetEnvPrefix(internal.ApplicationName)
// allow for nested options to be specified via environment variables
// e.g. pod.context = APPNAME_POD_CONTEXT
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
// use explicitly the given user config
if configPath != "" {
v.SetConfigFile(configPath)
if err := v.ReadInConfig(); err != nil {
return fmt.Errorf("unable to read application config=%q : %w", configPath, err)
}
v.Set("config", v.ConfigFileUsed())
// don't fall through to other options if the config path was explicitly provided
return nil
}
// start searching for valid configs in order...
// 1. look for .<appname>.yaml (in the current directory)
v.AddConfigPath(".")
v.SetConfigName("." + internal.ApplicationName)
if err = v.ReadInConfig(); err == nil {
v.Set("config", v.ConfigFileUsed())
return nil
} else if !errors.As(err, &viper.ConfigFileNotFoundError{}) {
return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err)
@@ -223,7 +287,6 @@ func loadConfig(v *viper.Viper, configPath string) error {
v.AddConfigPath("." + internal.ApplicationName)
v.SetConfigName("config")
if err = v.ReadInConfig(); err == nil {
v.Set("config", v.ConfigFileUsed())
return nil
} else if !errors.As(err, &viper.ConfigFileNotFoundError{}) {
return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err)
@@ -235,7 +298,6 @@ func loadConfig(v *viper.Viper, configPath string) error {
v.AddConfigPath(home)
v.SetConfigName("." + internal.ApplicationName)
if err = v.ReadInConfig(); err == nil {
v.Set("config", v.ConfigFileUsed())
return nil
} else if !errors.As(err, &viper.ConfigFileNotFoundError{}) {
return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err)
@@ -249,10 +311,10 @@ func loadConfig(v *viper.Viper, configPath string) error {
}
v.SetConfigName("config")
if err = v.ReadInConfig(); err == nil {
v.Set("config", v.ConfigFileUsed())
return nil
} else if !errors.As(err, &viper.ConfigFileNotFoundError{}) {
return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err)
}
return nil
return ErrApplicationConfigNotFound
}
+52
View File
@@ -0,0 +1,52 @@
package config
import (
"github.com/docker/docker/pkg/homedir"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"path/filepath"
"testing"
)
func TestApplication_parseFile(t *testing.T) {
tests := []struct {
name string
config Application
expected string
wantErr require.ErrorAssertionFunc
}{
{
name: "expand home dir",
config: Application{
File: "~/place.txt",
},
expected: filepath.Join(homedir.Get(), "place.txt"),
},
{
name: "passthrough other paths",
config: Application{
File: "/other/place.txt",
},
expected: "/other/place.txt",
},
{
name: "no path",
config: Application{
File: "",
},
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := tt.config
if tt.wantErr == nil {
tt.wantErr = require.NoError
}
tt.wantErr(t, cfg.parseFile())
assert.Equal(t, tt.expected, cfg.File)
})
}
}
+7 -25
View File
@@ -5,35 +5,22 @@ import (
"os"
"github.com/mitchellh/go-homedir"
"github.com/sigstore/cosign/cmd/cosign/cli/options"
"github.com/spf13/viper"
)
// IMPORTANT: do not show the password in any YAML/JSON output (sensitive information)
type attest struct {
KeyRef string `yaml:"key" json:"key" mapstructure:"key"` // same as --key, file path to the private key
Cert string `yaml:"cert" json:"cert" mapstructure:"cert"`
NoUpload bool `yaml:"no_upload" json:"noUpload" mapstructure:"no_upload"`
Force bool `yaml:"force" json:"force" mapstructure:"force"`
Recursive bool `yaml:"recursive" json:"recursive" mapstructure:"recursive"`
Replace bool `yaml:"replace" json:"replace" mapstructure:"replace"`
Password string `yaml:"-" json:"-" mapstructure:"password"` // password for the private key
FulcioURL string `yaml:"fulcio_url" json:"fulcioUrl" mapstructure:"fulcio_url"`
FulcioIdentityToken string `yaml:"fulcio_identity_token" json:"fulcio_identity_token" mapstructure:"fulcio_identity_token"`
InsecureSkipFulcioVerify bool `yaml:"insecure_skip_verify" json:"insecure_skip_verify" mapstructure:"insecure_skip_verify"`
RekorURL string `yaml:"rekor_url" json:"rekorUrl" mapstructure:"rekor_url"`
OIDCIssuer string `yaml:"oidc_issuer" json:"oidcIssuer" mapstructure:"oidc_issuer"`
OIDCClientID string `yaml:"oidc_client_id" json:"oidcClientId" mapstructure:"oidc_client_id"`
OIDCRedirectURL string `yaml:"oidc_redirect_url" json:"OIDCRedirectURL" mapstructure:"oidc_redirect_url"`
Key string `yaml:"key" json:"key" mapstructure:"key"` // same as --key, file path to the private key
// IMPORTANT: do not show the password in any YAML/JSON output (sensitive information)
Password string `yaml:"-" json:"-" mapstructure:"password"` // password for the private key
}
func (cfg *attest) parseConfigValues() error {
if cfg.KeyRef != "" {
expandedPath, err := homedir.Expand(cfg.KeyRef)
if cfg.Key != "" {
expandedPath, err := homedir.Expand(cfg.Key)
if err != nil {
return fmt.Errorf("unable to expand key path=%q: %w", cfg.KeyRef, err)
return fmt.Errorf("unable to expand key path=%q: %w", cfg.Key, err)
}
cfg.KeyRef = expandedPath
cfg.Key = expandedPath
}
if cfg.Password == "" {
@@ -47,10 +34,5 @@ func (cfg *attest) parseConfigValues() error {
}
func (cfg attest) loadDefaultValues(v *viper.Viper) {
v.SetDefault("attest.key", "")
v.SetDefault("attest.password", "")
v.SetDefault("attest.fulcio_url", options.DefaultFulcioURL)
v.SetDefault("attest.rekor_url", options.DefaultRekorURL)
v.SetDefault("attest.oidc_issuer", options.DefaultOIDCIssuerURL)
v.SetDefault("attest.oidc_client_id", "sigstore")
}
-29
View File
@@ -1,29 +0,0 @@
package config
import (
"fmt"
"github.com/spf13/viper"
"github.com/anchore/syft/syft/source"
)
type catalogerOptions struct {
Enabled bool `yaml:"enabled" json:"enabled" mapstructure:"enabled"`
Scope string `yaml:"scope" json:"scope" mapstructure:"scope"`
ScopeOpt source.Scope `yaml:"-" json:"-"`
}
func (cfg catalogerOptions) loadDefaultValues(v *viper.Viper) {
v.SetDefault("package.cataloger.enabled", true)
}
func (cfg *catalogerOptions) parseConfigValues() error {
scopeOption := source.ParseScope(cfg.Scope)
if scopeOption == source.UnknownScope {
return fmt.Errorf("bad scope value %q", cfg.Scope)
}
cfg.ScopeOpt = scopeOption
return nil
}
+7
View File
@@ -0,0 +1,7 @@
package config
// CliOnlyOptions are options that are in the application config in memory, but are only exposed via CLI switches (not from unmarshaling a config file)
type CliOnlyOptions struct {
ConfigPath string // -c. where the read config is on disk
Verbosity int // -v or -vv , controlling which UI (ETUI vs logging) and what the log level should be
}
-1
View File
@@ -1 +0,0 @@
package config
-19
View File
@@ -1,19 +0,0 @@
package config
import (
"github.com/anchore/syft/syft/source"
"github.com/spf13/viper"
)
type fileClassification struct {
Cataloger catalogerOptions `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"`
}
func (cfg fileClassification) loadDefaultValues(v *viper.Viper) {
v.SetDefault("file-classification.cataloger.enabled", catalogerEnabledDefault)
v.SetDefault("file-classification.cataloger.scope", source.SquashedScope)
}
func (cfg *fileClassification) parseConfigValues() error {
return cfg.Cataloger.parseConfigValues()
}
+9 -9
View File
@@ -1,24 +1,24 @@
package config
import (
"github.com/anchore/syft/internal/file"
"github.com/anchore/syft/syft/source"
"github.com/anchore/syft/syft/cataloger/files/filecontents"
"github.com/anchore/syft/syft/file"
"github.com/spf13/viper"
)
type fileContents struct {
Cataloger catalogerOptions `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"`
SkipFilesAboveSize int64 `yaml:"skip-files-above-size" json:"skip-files-above-size" mapstructure:"skip-files-above-size"`
Globs []string `yaml:"globs" json:"globs" mapstructure:"globs"`
SkipFilesAboveSize int64 `yaml:"skip-files-above-size" json:"skip-files-above-size" mapstructure:"skip-files-above-size"`
Globs []string `yaml:"globs" json:"globs" mapstructure:"globs"`
}
func (cfg fileContents) loadDefaultValues(v *viper.Viper) {
v.SetDefault("file-contents.cataloger.enabled", catalogerEnabledDefault)
v.SetDefault("file-contents.cataloger.scope", source.SquashedScope)
v.SetDefault("file-contents.skip-files-above-size", 1*file.MB)
v.SetDefault("file-contents.globs", []string{})
}
func (cfg *fileContents) parseConfigValues() error {
return cfg.Cataloger.parseConfigValues()
func (cfg fileContents) ToConfig() filecontents.Config {
return filecontents.Config{
Globs: cfg.Globs,
SkipFilesAboveSizeInBytes: cfg.SkipFilesAboveSize,
}
}
+5 -9
View File
@@ -1,21 +1,17 @@
package config
import (
"github.com/anchore/syft/syft/source"
"github.com/spf13/viper"
)
type FileMetadata struct {
Cataloger catalogerOptions `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"`
Digests []string `yaml:"digests" json:"digests" mapstructure:"digests"`
type fileMetadata struct {
Digests []string `yaml:"digests" json:"digests" mapstructure:"digests"`
}
func (cfg FileMetadata) loadDefaultValues(v *viper.Viper) {
v.SetDefault("file-metadata.cataloger.enabled", catalogerEnabledDefault)
v.SetDefault("file-metadata.cataloger.scope", source.SquashedScope)
func (cfg fileMetadata) loadDefaultValues(v *viper.Viper) {
v.SetDefault("file-metadata.digests", []string{"sha256"})
}
func (cfg *FileMetadata) parseConfigValues() error {
return cfg.Cataloger.parseConfigValues()
func (cfg *fileMetadata) parseConfigValues() error {
return nil
}
+8 -17
View File
@@ -1,33 +1,24 @@
package config
import (
"github.com/anchore/syft/syft/pkg/cataloger"
"github.com/anchore/syft/syft/cataloger/packages"
"github.com/spf13/viper"
)
type pkg struct {
Cataloger catalogerOptions `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"`
SearchUnindexedArchives bool `yaml:"search-unindexed-archives" json:"search-unindexed-archives" mapstructure:"search-unindexed-archives"`
SearchIndexedArchives bool `yaml:"search-indexed-archives" json:"search-indexed-archives" mapstructure:"search-indexed-archives"`
SearchUnindexedArchives bool `yaml:"search-unindexed-archives" json:"search-unindexed-archives" mapstructure:"search-unindexed-archives"`
SearchIndexedArchives bool `yaml:"search-indexed-archives" json:"search-indexed-archives" mapstructure:"search-indexed-archives"`
}
func (cfg pkg) loadDefaultValues(v *viper.Viper) {
cfg.Cataloger.loadDefaultValues(v)
c := cataloger.DefaultSearchConfig()
c := packages.DefaultSearchConfig()
v.SetDefault("package.search-unindexed-archives", c.IncludeUnindexedArchives)
v.SetDefault("package.search-indexed-archives", c.IncludeIndexedArchives)
}
func (cfg *pkg) parseConfigValues() error {
return cfg.Cataloger.parseConfigValues()
}
func (cfg pkg) ToConfig() cataloger.Config {
return cataloger.Config{
Search: cataloger.SearchConfig{
IncludeIndexedArchives: cfg.SearchIndexedArchives,
IncludeUnindexedArchives: cfg.SearchUnindexedArchives,
Scope: cfg.Cataloger.ScopeOpt,
},
func (cfg pkg) ToConfig() packages.SearchConfig {
return packages.SearchConfig{
IncludeIndexedArchives: cfg.SearchIndexedArchives,
IncludeUnindexedArchives: cfg.SearchUnindexedArchives,
}
}
+18 -8
View File
@@ -1,28 +1,38 @@
package config
import (
"github.com/anchore/syft/internal/file"
"fmt"
"github.com/anchore/syft/syft/cataloger/files/secrets"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/source"
"github.com/spf13/viper"
)
type secrets struct {
Cataloger catalogerOptions `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"`
type secretsCfg struct {
AdditionalPatterns map[string]string `yaml:"additional-patterns" json:"additional-patterns" mapstructure:"additional-patterns"`
ExcludePatternNames []string `yaml:"exclude-pattern-names" json:"exclude-pattern-names" mapstructure:"exclude-pattern-names"`
RevealValues bool `yaml:"reveal-values" json:"reveal-values" mapstructure:"reveal-values"`
SkipFilesAboveSize int64 `yaml:"skip-files-above-size" json:"skip-files-above-size" mapstructure:"skip-files-above-size"`
Scope string `yaml:"scope" json:"scope" mapstructure:"scope"`
}
func (cfg secrets) loadDefaultValues(v *viper.Viper) {
v.SetDefault("secrets.cataloger.enabled", catalogerEnabledDefault)
v.SetDefault("secrets.cataloger.scope", source.AllLayersScope)
func (cfg secretsCfg) loadDefaultValues(v *viper.Viper) {
v.SetDefault("secrets.scope", source.AllLayersScope)
v.SetDefault("secrets.reveal-values", false)
v.SetDefault("secrets.skip-files-above-size", 1*file.MB)
v.SetDefault("secrets.additional-patterns", map[string]string{})
v.SetDefault("secrets.exclude-pattern-names", []string{})
}
func (cfg *secrets) parseConfigValues() error {
return cfg.Cataloger.parseConfigValues()
func (cfg secretsCfg) ToConfig() (*secrets.Config, error) {
patterns, err := file.GenerateSearchPatterns(secrets.DefaultSecretsPatterns, cfg.AdditionalPatterns, cfg.ExcludePatternNames)
if err != nil {
return nil, fmt.Errorf("unable to process secrets config patterns: %w", err)
}
return &secrets.Config{
Patterns: patterns,
RevealValues: cfg.RevealValues,
MaxFileSize: cfg.SkipFilesAboveSize,
}, nil
}
+1 -1
View File
@@ -6,5 +6,5 @@ const (
// JSONSchemaVersion is the current schema version output by the JSON encoder
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
JSONSchemaVersion = "3.3.0"
JSONSchemaVersion = "3.0.1"
)
@@ -1,8 +1,8 @@
package file
package internal
// GlobMatch evaluates the given glob pattern against the given "name" string, indicating if there is a match or not.
// FileNameGlobMatch evaluates the given glob pattern against the given "name" string, indicating if there is a match or not.
// Source: https://research.swtch.com/glob.go
func GlobMatch(pattern, name string) bool {
func FileNameGlobMatch(pattern, name string) bool {
px := 0
nx := 0
nextPx := 0
@@ -1,4 +1,4 @@
package file
package internal
import (
"strings"
@@ -32,7 +32,7 @@ func TestGlobMatch(t *testing.T) {
}
for _, test := range tests {
if GlobMatch(test.pattern, test.data) != test.ok {
if FileNameGlobMatch(test.pattern, test.data) != test.ok {
t.Errorf("failed glob='%s' data='%s'", test.pattern, test.data)
}
}
@@ -3,20 +3,19 @@ package cyclonedxhelpers
import (
"reflect"
"github.com/anchore/syft/syft/file"
"github.com/CycloneDX/cyclonedx-go"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/internal/formats/common"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
func encodeComponent(p pkg.Package) cyclonedx.Component {
props := encodeProperties(p, "syft:package")
props = append(props, encodeCPEs(p)...)
locations := p.Locations.ToSlice()
if len(locations) > 0 {
props = append(props, encodeProperties(locations, "syft:location")...)
if len(p.Locations) > 0 {
props = append(props, encodeProperties(p.Locations, "syft:location")...)
}
if hasMetadata(p) {
props = append(props, encodeProperties(p.Metadata, "syft:metadata")...)
@@ -40,33 +39,17 @@ func encodeComponent(p pkg.Package) cyclonedx.Component {
Description: encodeDescription(p),
ExternalReferences: encodeExternalReferences(p),
Properties: properties,
BOMRef: deriveBomRef(p),
}
}
func deriveBomRef(p pkg.Package) string {
// try and parse the PURL if possible and append syft id to it, to make
// the purl unique in the BOM.
// TODO: In the future we may want to dedupe by PURL and combine components with
// the same PURL while preserving their unique metadata.
if parsedPURL, err := packageurl.FromString(p.PURL); err == nil {
parsedPURL.Qualifiers = append(parsedPURL.Qualifiers, packageurl.Qualifier{Key: "package-id", Value: string(p.ID())})
return parsedPURL.ToString()
}
// fallback is to use strictly the ID if there is no valid pURL
return string(p.ID())
}
func hasMetadata(p pkg.Package) bool {
return p.Metadata != nil
}
func decodeComponent(c *cyclonedx.Component) *pkg.Package {
values := map[string]string{}
if c.Properties != nil {
for _, p := range *c.Properties {
values[p.Name] = p.Value
}
for _, p := range *c.Properties {
values[p.Name] = p.Value
}
p := &pkg.Package{
@@ -86,20 +69,13 @@ func decodeComponent(c *cyclonedx.Component) *pkg.Package {
p.Type = pkg.TypeFromPURL(p.PURL)
}
if p.Language == "" {
p.Language = pkg.LanguageFromPURL(p.PURL)
}
return p
}
func decodeLocations(vals map[string]string) source.LocationSet {
v := common.Decode(reflect.TypeOf([]source.Location{}), vals, "syft:location", CycloneDXFields)
out, ok := v.([]source.Location)
if !ok {
out = nil
}
return source.NewLocationSet(out...)
func decodeLocations(vals map[string]string) []file.Location {
v := common.Decode(reflect.TypeOf([]file.Location{}), vals, "syft:location", CycloneDXFields)
out, _ := v.([]file.Location)
return out
}
func decodePackageMetadata(vals map[string]string, c *cyclonedx.Component, typ pkg.MetadataType) interface{} {
@@ -1,14 +1,13 @@
package cyclonedxhelpers
import (
"fmt"
"github.com/anchore/syft/syft/file"
"testing"
"github.com/CycloneDX/cyclonedx-go"
"github.com/stretchr/testify/assert"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
func Test_encodeComponentProperties(t *testing.T) {
@@ -27,9 +26,9 @@ func Test_encodeComponentProperties(t *testing.T) {
name: "from apk",
input: pkg.Package{
FoundBy: "cataloger",
Locations: source.NewLocationSet(
source.Location{Coordinates: source.Coordinates{RealPath: "test"}},
),
Locations: []file.Location{
{Coordinates: file.Coordinates{RealPath: "test"}},
},
Metadata: pkg.ApkMetadata{
Package: "libc-utils",
OriginPackage: "libc-dev",
@@ -140,82 +139,3 @@ func Test_encodeComponentProperties(t *testing.T) {
})
}
}
func Test_deriveBomRef(t *testing.T) {
pkgWithPurl := pkg.Package{
Name: "django",
Version: "1.11.1",
PURL: "pkg:pypi/django@1.11.1",
}
pkgWithPurl.SetID()
pkgWithOutPurl := pkg.Package{
Name: "django",
Version: "1.11.1",
PURL: "",
}
pkgWithOutPurl.SetID()
pkgWithBadPurl := pkg.Package{
Name: "django",
Version: "1.11.1",
PURL: "pkg:pyjango@1.11.1",
}
pkgWithBadPurl.SetID()
tests := []struct {
name string
pkg pkg.Package
want string
}{
{
name: "use pURL-id hybrid",
pkg: pkgWithPurl,
want: fmt.Sprintf("pkg:pypi/django@1.11.1?package-id=%s", pkgWithPurl.ID()),
},
{
name: "fallback to ID when pURL is invalid",
pkg: pkgWithBadPurl,
want: string(pkgWithBadPurl.ID()),
},
{
name: "fallback to ID when pURL is missing",
pkg: pkgWithOutPurl,
want: string(pkgWithOutPurl.ID()),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.pkg.ID()
assert.Equal(t, tt.want, deriveBomRef(tt.pkg))
})
}
}
func Test_decodeComponent(t *testing.T) {
javaComponentWithNoSyftProperties := cyclonedx.Component{
Name: "ch.qos.logback/logback-classic",
Version: "1.2.3",
PackageURL: "pkg:maven/ch.qos.logback/logback-classic@1.2.3",
Type: "library",
BOMRef: "pkg:maven/ch.qos.logback/logback-classic@1.2.3",
}
tests := []struct {
name string
component cyclonedx.Component
want pkg.Language
}{
{
name: "derive language from pURL if missing",
component: javaComponentWithNoSyftProperties,
want: pkg.Java,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, decodeComponent(&tt.component).Language)
})
}
}
@@ -2,6 +2,7 @@ package cyclonedxhelpers
import (
"github.com/CycloneDX/cyclonedx-go"
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/pkg"
@@ -11,7 +12,7 @@ func encodeSingleCPE(p pkg.Package) string {
// Since the CPEs in a package are sorted by specificity
// we can extract the first CPE as the one to output in cyclonedx
if len(p.CPEs) > 0 {
return pkg.CPEString(p.CPEs[0])
return cpe.String(p.CPEs[0])
}
return ""
}
@@ -24,15 +25,15 @@ func encodeCPEs(p pkg.Package) (out []cyclonedx.Property) {
}
out = append(out, cyclonedx.Property{
Name: "syft:cpe23",
Value: pkg.CPEString(c),
Value: cpe.String(c),
})
}
return
}
func decodeCPEs(c *cyclonedx.Component) (out []pkg.CPE) {
func decodeCPEs(c *cyclonedx.Component) (out []cpe.CPE) {
if c.CPE != "" {
cp, err := pkg.NewCPE(c.CPE)
cp, err := cpe.New(c.CPE)
if err != nil {
log.Warnf("invalid CPE: %s", c.CPE)
} else {
@@ -43,7 +44,7 @@ func decodeCPEs(c *cyclonedx.Component) (out []pkg.CPE) {
if c.Properties != nil {
for _, p := range *c.Properties {
if p.Name == "syft:cpe23" {
cp, err := pkg.NewCPE(p.Value)
cp, err := cpe.New(p.Value)
if err != nil {
log.Warnf("invalid CPE: %s", p.Value)
} else {
@@ -1,6 +1,7 @@
package cyclonedxhelpers
import (
"github.com/anchore/syft/syft/cpe"
"testing"
"github.com/stretchr/testify/assert"
@@ -9,8 +10,8 @@ import (
)
func Test_encodeCPE(t *testing.T) {
testCPE := pkg.MustCPE("cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*")
testCPE2 := pkg.MustCPE("cpe:2.3:a:name:name2:3.2:*:*:*:*:*:*:*")
testCPE := cpe.Must("cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*")
testCPE2 := cpe.Must("cpe:2.3:a:name:name2:3.2:*:*:*:*:*:*:*")
tests := []struct {
name string
input pkg.Package
@@ -20,14 +21,14 @@ func Test_encodeCPE(t *testing.T) {
// note: since this is an optional field, no value is preferred over NONE or NOASSERTION
name: "no metadata",
input: pkg.Package{
CPEs: []pkg.CPE{},
CPEs: []cpe.CPE{},
},
expected: "",
},
{
name: "single CPE",
input: pkg.Package{
CPEs: []pkg.CPE{
CPEs: []cpe.CPE{
testCPE,
},
},
@@ -36,7 +37,7 @@ func Test_encodeCPE(t *testing.T) {
{
name: "multiple CPEs",
input: pkg.Package{
CPEs: []pkg.CPE{
CPEs: []cpe.CPE{
testCPE2,
testCPE,
},
@@ -5,8 +5,6 @@ import (
"io"
"github.com/CycloneDX/cyclonedx-go"
"github.com/anchore/syft/internal/formats/common"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/pkg"
@@ -22,7 +20,7 @@ func GetValidator(format cyclonedx.BOMFileFormat) sbom.Validator {
return err
}
// random JSON does not necessarily cause an error (e.g. SPDX)
if (cyclonedx.BOM{} == *bom || bom.Components == nil) {
if (cyclonedx.BOM{} == *bom) {
return fmt.Errorf("not a valid CycloneDX document")
}
return nil
@@ -31,9 +29,7 @@ func GetValidator(format cyclonedx.BOMFileFormat) sbom.Validator {
func GetDecoder(format cyclonedx.BOMFileFormat) sbom.Decoder {
return func(reader io.Reader) (*sbom.SBOM, error) {
bom := &cyclonedx.BOM{
Components: &[]cyclonedx.Component{},
}
bom := &cyclonedx.BOM{}
err := cyclonedx.NewBOMDecoder(reader, format).Decode(bom)
if err != nil {
return nil, err
@@ -47,17 +43,17 @@ func GetDecoder(format cyclonedx.BOMFileFormat) sbom.Decoder {
}
func toSyftModel(bom *cyclonedx.BOM) (*sbom.SBOM, error) {
if bom == nil {
return nil, fmt.Errorf("no content defined in CycloneDX BOM")
meta := source.Metadata{}
if bom.Metadata != nil {
meta = decodeMetadata(bom.Metadata.Component)
}
s := &sbom.SBOM{
Artifacts: sbom.Artifacts{
PackageCatalog: pkg.NewCatalog(),
Packages: pkg.NewCollection(),
LinuxDistribution: linuxReleaseFromComponents(*bom.Components),
},
Source: extractComponents(bom.Metadata),
Descriptor: extractDescriptor(bom.Metadata),
Source: meta,
//Descriptor: sbom.Descriptor{},
}
idMap := make(map[string]interface{})
@@ -90,7 +86,7 @@ func collectPackages(component *cyclonedx.Component, s *sbom.SBOM, idMap map[str
idMap[component.BOMRef] = p
// TODO there must be a better way than needing to call this manually:
p.SetID()
s.Artifacts.PackageCatalog.Add(*p)
s.Artifacts.Packages.Add(*p)
}
if component.Components != nil {
@@ -161,14 +157,6 @@ func linuxReleaseFromOSComponent(component *cyclonedx.Component) *linux.Release
}
}
if component.Properties != nil {
values := map[string]string{}
for _, p := range *component.Properties {
values[p.Name] = p.Value
}
common.DecodeInto(&rel, values, "syft:distro", CycloneDXFields)
}
return rel
}
@@ -207,45 +195,27 @@ func collectRelationships(bom *cyclonedx.BOM, s *sbom.SBOM, idMap map[string]int
}
}
func extractComponents(meta *cyclonedx.Metadata) source.Metadata {
if meta == nil || meta.Component == nil {
return source.Metadata{}
}
c := meta.Component
image := source.ImageMetadata{
UserInput: c.Name,
ID: c.BOMRef,
ManifestDigest: c.Version,
}
switch c.Type {
func decodeMetadata(component *cyclonedx.Component) source.Metadata {
switch component.Type {
case cyclonedx.ComponentTypeContainer:
return source.Metadata{
Scheme: source.ImageScheme,
ImageMetadata: image,
Scheme: source.ImageType,
ImageMetadata: source.ImageMetadata{
UserInput: component.Name,
ID: component.BOMRef,
ManifestDigest: component.Version,
},
}
case cyclonedx.ComponentTypeFile:
return source.Metadata{
Scheme: source.FileScheme, // or source.DirectoryScheme
Path: c.Name,
ImageMetadata: image,
Scheme: source.FileType, // or source.DirectoryType
Path: component.Name,
ImageMetadata: source.ImageMetadata{
UserInput: component.Name,
ID: component.BOMRef,
ManifestDigest: component.Version,
},
}
}
return source.Metadata{}
}
// if there is more than one tool in meta.Tools' list the last item will be used
// as descriptor. If there is a way to know which tool to use here please fix it.
func extractDescriptor(meta *cyclonedx.Metadata) (desc sbom.Descriptor) {
if meta == nil || meta.Tools == nil {
return
}
for _, t := range *meta.Tools {
desc.Name = t.Name
desc.Version = t.Version
}
return
}
@@ -1,8 +1,6 @@
package cyclonedxhelpers
import (
"bytes"
"encoding/json"
"fmt"
"testing"
@@ -214,7 +212,7 @@ func Test_decode(t *testing.T) {
assert.Equal(t, e.ver, sbom.Artifacts.LinuxDistribution.VersionID)
}
if e.pkg != "" {
for p := range sbom.Artifacts.PackageCatalog.Enumerate() {
for p := range sbom.Artifacts.Packages.Enumerate() {
if e.pkg != p.Name {
continue
}
@@ -242,7 +240,7 @@ func Test_decode(t *testing.T) {
if e.relation != "" {
foundRelation := false
for _, r := range sbom.Relationships {
p := sbom.Artifacts.PackageCatalog.Package(r.To.ID())
p := sbom.Artifacts.Packages.Package(r.To.ID())
if e.relation == p.Name {
foundRelation = true
break
@@ -260,38 +258,3 @@ func Test_decode(t *testing.T) {
})
}
}
func Test_missingDataDecode(t *testing.T) {
bom := &cyclonedx.BOM{
Metadata: nil,
Components: &[]cyclonedx.Component{},
}
_, err := toSyftModel(bom)
assert.NoError(t, err)
bom.Metadata = &cyclonedx.Metadata{}
_, err = toSyftModel(bom)
assert.NoError(t, err)
pkg := decodeComponent(&cyclonedx.Component{
Licenses: &cyclonedx.Licenses{
{
License: nil,
},
},
})
assert.Len(t, pkg.Licenses, 0)
}
func Test_missingComponentsDecode(t *testing.T) {
bom := &cyclonedx.BOM{}
bomBytes, _ := json.Marshal(&bom)
decode := GetDecoder(cyclonedx.BOMFileFormatJSON)
_, err := decode(bytes.NewReader(bomBytes))
assert.NoError(t, err)
}
@@ -4,15 +4,12 @@ import (
"fmt"
"strings"
syftFile "github.com/anchore/syft/syft/file"
"github.com/CycloneDX/cyclonedx-go"
"github.com/anchore/syft/syft/pkg"
)
//nolint:funlen, gocognit
func encodeExternalReferences(p pkg.Package) *[]cyclonedx.ExternalReference {
var refs []cyclonedx.ExternalReference
refs := []cyclonedx.ExternalReference{}
if hasMetadata(p) {
switch metadata := p.Metadata.(type) {
case pkg.ApkMetadata:
@@ -49,19 +46,6 @@ func encodeExternalReferences(p pkg.Package) *[]cyclonedx.ExternalReference {
Type: cyclonedx.ERTypeWebsite,
})
}
case pkg.JavaMetadata:
if len(metadata.ArchiveDigests) > 0 {
for _, digest := range metadata.ArchiveDigests {
refs = append(refs, cyclonedx.ExternalReference{
URL: "",
Type: cyclonedx.ERTypeBuildMeta,
Hashes: &[]cyclonedx.Hash{{
Algorithm: toCycloneDXAlgorithm(digest.Algorithm),
Value: digest.Value,
}},
})
}
}
case pkg.PythonPackageMetadata:
if metadata.DirectURLOrigin != nil && metadata.DirectURLOrigin.URL != "" {
ref := cyclonedx.ExternalReference{
@@ -81,21 +65,6 @@ func encodeExternalReferences(p pkg.Package) *[]cyclonedx.ExternalReference {
return nil
}
// supported algorithm in cycloneDX as of 1.4
// "MD5", "SHA-1", "SHA-256", "SHA-384", "SHA-512",
// "SHA3-256", "SHA3-384", "SHA3-512", "BLAKE2b-256", "BLAKE2b-384", "BLAKE2b-512", "BLAKE3"
// syft supported digests: cmd/syft/cli/eventloop/tasks.go
// MD5, SHA1, SHA256
func toCycloneDXAlgorithm(algorithm string) cyclonedx.HashAlgorithm {
validMap := map[string]cyclonedx.HashAlgorithm{
"sha1": cyclonedx.HashAlgorithm("SHA-1"),
"md5": cyclonedx.HashAlgorithm("MD5"),
"sha256": cyclonedx.HashAlgorithm("SHA-256"),
}
return validMap[algorithm]
}
func decodeExternalReferences(c *cyclonedx.Component, metadata interface{}) {
if c.ExternalReferences == nil {
return
@@ -110,20 +79,6 @@ func decodeExternalReferences(c *cyclonedx.Component, metadata interface{}) {
meta.Homepage = refURL(c, cyclonedx.ERTypeWebsite)
case *pkg.GemMetadata:
meta.Homepage = refURL(c, cyclonedx.ERTypeWebsite)
case *pkg.JavaMetadata:
var digests []syftFile.Digest
if ref := findExternalRef(c, cyclonedx.ERTypeBuildMeta); ref != nil {
if ref.Hashes != nil {
for _, hash := range *ref.Hashes {
digests = append(digests, syftFile.Digest{
Algorithm: syftFile.CleanDigestAlgorithmName(string(hash.Algorithm)),
Value: hash.Value,
})
}
}
}
meta.ArchiveDigests = digests
case *pkg.PythonPackageMetadata:
if meta.DirectURLOrigin == nil {
meta.DirectURLOrigin = &pkg.PythonDirectURLOriginInfo{}
@@ -8,6 +8,7 @@ import (
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/sbom"
@@ -16,14 +17,15 @@ import (
func ToFormatModel(s sbom.SBOM) *cyclonedx.BOM {
cdxBOM := cyclonedx.NewBOM()
versionInfo := version.FromBuild()
// NOTE(jonasagx): cycloneDX requires URN uuids (URN returns the RFC 2141 URN form of uuid):
// https://github.com/CycloneDX/specification/blob/master/schema/bom-1.3-strict.schema.json#L36
// "pattern": "^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
cdxBOM.SerialNumber = uuid.New().URN()
cdxBOM.Metadata = toBomDescriptor(internal.ApplicationName, s.Descriptor.Version, s.Source)
cdxBOM.Metadata = toBomDescriptor(internal.ApplicationName, versionInfo.Version, s.Source)
packages := s.Artifacts.PackageCatalog.Sorted()
packages := s.Artifacts.Packages.Sorted()
components := make([]cyclonedx.Component, len(packages))
for i, p := range packages {
components[i] = encodeComponent(p)
@@ -137,7 +139,7 @@ func toDependencies(relationships []artifact.Relationship) []cyclonedx.Dependenc
for _, r := range relationships {
exists := isExpressiblePackageRelationship(r.Type)
if !exists {
log.Debugf("unable to convert relationship from CycloneDX 1.4 JSON, dropping: %+v", r)
log.Warnf("unable to convert relationship from CycloneDX 1.3 JSON, dropping: %+v", r)
continue
}
@@ -153,7 +155,7 @@ func toDependencies(relationships []artifact.Relationship) []cyclonedx.Dependenc
func toBomDescriptorComponent(srcMetadata source.Metadata) *cyclonedx.Component {
switch srcMetadata.Scheme {
case source.ImageScheme:
case source.ImageType:
bomRef, err := artifact.IDByHash(srcMetadata.ImageMetadata.ID)
if err != nil {
log.Warnf("unable to get fingerprint of image metadata=%s: %+v", srcMetadata.ImageMetadata.ID, err)
@@ -164,7 +166,7 @@ func toBomDescriptorComponent(srcMetadata source.Metadata) *cyclonedx.Component
Name: srcMetadata.ImageMetadata.UserInput,
Version: srcMetadata.ImageMetadata.ManifestDigest,
}
case source.DirectoryScheme, source.FileScheme:
case source.DirectoryType, source.FileType:
bomRef, err := artifact.IDByHash(srcMetadata.Path)
if err != nil {
log.Warnf("unable to get fingerprint of source metadata path=%s: %+v", srcMetadata.Path, err)
@@ -26,9 +26,7 @@ func encodeLicenses(p pkg.Package) *cyclonedx.Licenses {
func decodeLicenses(c *cyclonedx.Component) (out []string) {
if c.Licenses != nil {
for _, l := range *c.Licenses {
if l.License != nil {
out = append(out, l.License.ID)
}
out = append(out, l.License.ID)
}
}
return
@@ -1,21 +1,22 @@
package spdxhelpers
import (
"fmt"
"path"
"strings"
"github.com/anchore/syft/syft/source"
)
func DocumentName(srcMetadata source.Metadata) string {
func DocumentName(srcMetadata source.Metadata) (string, error) {
switch srcMetadata.Scheme {
case source.ImageScheme:
return cleanName(srcMetadata.ImageMetadata.UserInput)
case source.DirectoryScheme, source.FileScheme:
return cleanName(srcMetadata.Path)
default:
return "unknown"
case source.ImageType:
return cleanName(srcMetadata.ImageMetadata.UserInput), nil
case source.DirectoryType, source.FileType:
return cleanName(srcMetadata.Path), nil
}
return "", fmt.Errorf("unable to determine document name from scheme=%q", srcMetadata.Scheme)
}
func cleanName(name string) string {
@@ -5,6 +5,8 @@ import (
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/source"
"github.com/scylladb/go-set/strset"
"github.com/stretchr/testify/assert"
@@ -12,7 +14,7 @@ import (
func Test_DocumentName(t *testing.T) {
allSchemes := strset.New()
for _, s := range source.AllSchemes {
for _, s := range source.AllTypes {
allSchemes.Add(string(s))
}
testedSchemes := strset.New()
@@ -27,7 +29,7 @@ func Test_DocumentName(t *testing.T) {
name: "image",
inputName: "my-name",
srcMetadata: source.Metadata{
Scheme: source.ImageScheme,
Scheme: source.ImageType,
ImageMetadata: source.ImageMetadata{
UserInput: "image-repo/name:tag",
ID: "id",
@@ -40,7 +42,7 @@ func Test_DocumentName(t *testing.T) {
name: "directory",
inputName: "my-name",
srcMetadata: source.Metadata{
Scheme: source.DirectoryScheme,
Scheme: source.DirectoryType,
Path: "some/path/to/place",
},
expected: "some/path/to/place",
@@ -49,7 +51,7 @@ func Test_DocumentName(t *testing.T) {
name: "file",
inputName: "my-name",
srcMetadata: source.Metadata{
Scheme: source.FileScheme,
Scheme: source.FileType,
Path: "some/path/to/place",
},
expected: "some/path/to/place",
@@ -57,7 +59,8 @@ func Test_DocumentName(t *testing.T) {
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actual := DocumentName(test.srcMetadata)
actual, err := DocumentName(test.srcMetadata)
require.NoError(t, err)
assert.True(t, strings.HasPrefix(actual, test.expected), fmt.Sprintf("actual name %q", actual))
// track each scheme tested (passed or not)
@@ -10,19 +10,22 @@ import (
"github.com/google/uuid"
)
func DocumentNameAndNamespace(srcMetadata source.Metadata) (string, string) {
name := DocumentName(srcMetadata)
return name, DocumentNamespace(name, srcMetadata)
func DocumentNameAndNamespace(srcMetadata source.Metadata) (string, string, error) {
name, err := DocumentName(srcMetadata)
if err != nil {
return "", "", err
}
return name, DocumentNamespace(name, srcMetadata), nil
}
func DocumentNamespace(name string, srcMetadata source.Metadata) string {
input := "unknown-source-type"
switch srcMetadata.Scheme {
case source.ImageScheme:
case source.ImageType:
input = "image"
case source.DirectoryScheme:
case source.DirectoryType:
input = "dir"
case source.FileScheme:
case source.FileType:
input = "file"
}
@@ -12,7 +12,7 @@ import (
func Test_documentNamespace(t *testing.T) {
allSchemes := strset.New()
for _, s := range source.AllSchemes {
for _, s := range source.AllTypes {
allSchemes.Add(string(s))
}
testedSchemes := strset.New()
@@ -27,7 +27,7 @@ func Test_documentNamespace(t *testing.T) {
name: "image",
inputName: "my-name",
srcMetadata: source.Metadata{
Scheme: source.ImageScheme,
Scheme: source.ImageType,
ImageMetadata: source.ImageMetadata{
UserInput: "image-repo/name:tag",
ID: "id",
@@ -40,7 +40,7 @@ func Test_documentNamespace(t *testing.T) {
name: "directory",
inputName: "my-name",
srcMetadata: source.Metadata{
Scheme: source.DirectoryScheme,
Scheme: source.DirectoryType,
Path: "some/path/to/place",
},
expected: "https://anchore.com/syft/dir/my-name-",
@@ -49,7 +49,7 @@ func Test_documentNamespace(t *testing.T) {
name: "file",
inputName: "my-name",
srcMetadata: source.Metadata{
Scheme: source.FileScheme,
Scheme: source.FileType,
Path: "some/path/to/place",
},
expected: "https://anchore.com/syft/file/my-name-",
@@ -1,6 +1,7 @@
package spdxhelpers
import (
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/pkg"
)
@@ -10,7 +11,7 @@ func ExternalRefs(p pkg.Package) (externalRefs []ExternalRef) {
for _, c := range p.CPEs {
externalRefs = append(externalRefs, ExternalRef{
ReferenceCategory: SecurityReferenceCategory,
ReferenceLocator: pkg.CPEString(c),
ReferenceLocator: cpe.String(c),
ReferenceType: Cpe23ExternalRefType,
})
}
@@ -22,6 +23,5 @@ func ExternalRefs(p pkg.Package) (externalRefs []ExternalRef) {
ReferenceType: PurlExternalRefType,
})
}
return externalRefs
}
@@ -1,6 +1,7 @@
package spdxhelpers
import (
"github.com/anchore/syft/syft/cpe"
"testing"
"github.com/anchore/syft/syft/pkg"
@@ -8,7 +9,7 @@ import (
)
func Test_ExternalRefs(t *testing.T) {
testCPE := pkg.MustCPE("cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*")
testCPE := cpe.Must("cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*")
tests := []struct {
name string
input pkg.Package
@@ -17,7 +18,7 @@ func Test_ExternalRefs(t *testing.T) {
{
name: "cpe + purl",
input: pkg.Package{
CPEs: []pkg.CPE{
CPEs: []cpe.CPE{
testCPE,
},
PURL: "a-purl",
@@ -25,7 +26,7 @@ func Test_ExternalRefs(t *testing.T) {
expected: []ExternalRef{
{
ReferenceCategory: SecurityReferenceCategory,
ReferenceLocator: pkg.CPEString(testCPE),
ReferenceLocator: cpe.String(testCPE),
ReferenceType: Cpe23ExternalRefType,
},
{
@@ -9,18 +9,12 @@ import (
func SourceInfo(p pkg.Package) string {
answer := ""
switch p.Type {
case pkg.AlpmPkg:
answer = "acquired package info from ALPM DB"
case pkg.RpmPkg:
answer = "acquired package info from RPM DB"
case pkg.ApkPkg:
answer = "acquired package info from APK DB"
case pkg.DartPubPkg:
answer = "acquired package info from pubspec manifest"
case pkg.DebPkg:
answer = "acquired package info from DPKG DB"
case pkg.DotnetPkg:
answer = "acquired package info from dotnet project assets file"
case pkg.NpmPkg:
answer = "acquired package info from installed node module manifest file"
case pkg.PythonPkg:
@@ -39,7 +33,7 @@ func SourceInfo(p pkg.Package) string {
answer = "acquired package info from the following paths"
}
var paths []string
for _, l := range p.Locations.ToSlice() {
for _, l := range p.Locations {
paths = append(paths, l.RealPath)
}
@@ -1,10 +1,10 @@
package spdxhelpers
import (
"github.com/anchore/syft/syft/file"
"testing"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
"github.com/stretchr/testify/assert"
)
@@ -18,10 +18,10 @@ func Test_SourceInfo(t *testing.T) {
name: "locations are captured",
input: pkg.Package{
// note: no type given
Locations: source.NewLocationSet(
source.NewVirtualLocation("/a-place", "/b-place"),
source.NewVirtualLocation("/c-place", "/d-place"),
),
Locations: []file.Location{
file.NewVirtualLocation("/a-place", "/b-place"),
file.NewVirtualLocation("/c-place", "/d-place"),
},
},
expected: []string{
"from the following paths",
@@ -126,30 +126,6 @@ func Test_SourceInfo(t *testing.T) {
"from PHP composer manifest",
},
},
{
input: pkg.Package{
Type: pkg.DartPubPkg,
},
expected: []string{
"from pubspec manifest",
},
},
{
input: pkg.Package{
Type: pkg.DotnetPkg,
},
expected: []string{
"from dotnet project assets file",
},
},
{
input: pkg.Package{
Type: pkg.AlpmPkg,
},
expected: []string{
"from ALPM DB",
},
},
}
var pkgTypes []pkg.Type
for _, test := range tests {
@@ -1,13 +0,0 @@
package spdxhelpers
import (
"regexp"
)
var expr = regexp.MustCompile("[^a-zA-Z0-9.-]")
// SPDX spec says SPDXID must be:
// "SPDXRef-"[idstring] where [idstring] is a unique string containing letters, numbers, ., and/or -
func SanitizeElementID(id string) string {
return expr.ReplaceAllString(id, "-")
}
@@ -1,39 +0,0 @@
package spdxhelpers
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_SanitizeElementID(t *testing.T) {
tests := []struct {
input string
expected string
}{
{
input: "letters",
expected: "letters",
},
{
input: "ssl-client",
expected: "ssl-client",
},
{
input: "ssl_client",
expected: "ssl-client",
},
{
input: "go-module-sigs.k8s.io/structured-merge-diff/v3",
expected: "go-module-sigs.k8s.io-structured-merge-diff-v3",
},
}
for _, test := range tests {
t.Run(test.input, func(t *testing.T) {
actual := SanitizeElementID(test.input)
assert.Equal(t, test.expected, actual)
})
}
}
@@ -1,8 +1,7 @@
package spdxhelpers
import (
"errors"
"net/url"
"github.com/anchore/syft/syft/cpe"
"strconv"
"strings"
@@ -15,27 +14,16 @@ import (
"github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
func ToSyftModel(doc *spdx.Document2_2) (*sbom.SBOM, error) {
if doc == nil {
return nil, errors.New("cannot convert SPDX document to Syft model because document is nil")
}
spdxIDMap := make(map[string]interface{})
src := source.Metadata{Scheme: source.UnknownScheme}
if doc.CreationInfo != nil {
src.Scheme = extractSchemeFromNamespace(doc.CreationInfo.DocumentNamespace)
}
s := &sbom.SBOM{
Source: src,
Artifacts: sbom.Artifacts{
PackageCatalog: pkg.NewCatalog(),
FileMetadata: map[source.Coordinates]source.FileMetadata{},
FileDigests: map[source.Coordinates][]file.Digest{},
Packages: pkg.NewCollection(),
FileMetadata: map[file.Coordinates]file.Metadata{},
FileDigests: map[file.Coordinates][]file.Digest{},
LinuxDistribution: findLinuxReleaseByPURL(doc),
},
}
@@ -49,30 +37,6 @@ func ToSyftModel(doc *spdx.Document2_2) (*sbom.SBOM, error) {
return s, nil
}
// NOTE(jonas): SPDX doesn't inform what an SBOM is about,
// image, directory, for example. This is our best effort to determine
// the scheme. Syft-generated SBOMs have in the namespace
// field a type encoded, which we try to identify here.
func extractSchemeFromNamespace(ns string) source.Scheme {
u, err := url.Parse(ns)
if err != nil {
return source.UnknownScheme
}
parts := strings.Split(u.Path, "/")
for _, p := range parts {
switch p {
case "file":
return source.FileScheme
case "image":
return source.ImageScheme
case "dir":
return source.DirectoryScheme
}
}
return source.UnknownScheme
}
func findLinuxReleaseByPURL(doc *spdx.Document2_2) *linux.Release {
for _, p := range doc.Packages {
purlValue := findPURLValue(p)
@@ -110,7 +74,7 @@ func collectSyftPackages(s *sbom.SBOM, spdxIDMap map[string]interface{}, doc *sp
for _, p := range doc.Packages {
syftPkg := toSyftPackage(p)
spdxIDMap[string(p.PackageSPDXIdentifier)] = syftPkg
s.Artifacts.PackageCatalog.Add(*syftPkg)
s.Artifacts.Packages.Add(*syftPkg)
}
}
@@ -134,7 +98,7 @@ func toFileDigests(f *spdx.File2_2) (digests []file.Digest) {
return digests
}
func toFileMetadata(f *spdx.File2_2) (meta source.FileMetadata) {
func toFileMetadata(f *spdx.File2_2) (meta file.Metadata) {
// FIXME Syft is currently lossy due to the SPDX 2.2.1 spec not supporting arbitrary mimetypes
for _, typ := range f.FileType {
switch FileType(typ) {
@@ -168,7 +132,7 @@ func toSyftRelationships(spdxIDMap map[string]interface{}, doc *spdx.Document2_2
b := spdxIDMap[string(r.RefB.ElementRefID)]
from, fromOk := a.(*pkg.Package)
toPackage, toPackageOk := b.(*pkg.Package)
toLocation, toLocationOk := b.(*source.Location)
toLocation, toLocationOk := b.(*file.Location)
if !fromOk || !(toPackageOk || toLocationOk) {
log.Debugf("unable to find valid relationship mapping from SPDX 2.2 JSON, ignoring: (from: %+v) (to: %+v)", a, b)
continue
@@ -210,7 +174,7 @@ func toSyftRelationships(spdxIDMap map[string]interface{}, doc *spdx.Document2_2
return out
}
func toSyftCoordinates(f *spdx.File2_2) source.Coordinates {
func toSyftCoordinates(f *spdx.File2_2) file.Coordinates {
const layerIDPrefix = "layerID: "
var fileSystemID string
if strings.Index(f.FileComment, layerIDPrefix) == 0 {
@@ -219,16 +183,16 @@ func toSyftCoordinates(f *spdx.File2_2) source.Coordinates {
if strings.Index(string(f.FileSPDXIdentifier), layerIDPrefix) == 0 {
fileSystemID = strings.TrimPrefix(string(f.FileSPDXIdentifier), layerIDPrefix)
}
return source.Coordinates{
return file.Coordinates{
RealPath: f.FileName,
FileSystemID: fileSystemID,
}
}
func toSyftLocation(f *spdx.File2_2) *source.Location {
return &source.Location{
func toSyftLocation(f *spdx.File2_2) *file.Location {
return &file.Location{
Coordinates: toSyftCoordinates(f),
VirtualPath: f.FileName,
AccessPath: f.FileName,
}
}
@@ -340,14 +304,6 @@ func extractMetadata(p *spdx.Package2_2, info pkgInfo) (pkg.MetadataType, interf
Architecture: arch,
Maintainer: p.PackageOriginatorPerson,
}
case pkg.JavaPkg:
var digests []file.Digest
for algorithm, value := range p.PackageChecksums {
digests = append(digests, file.Digest{Algorithm: string(algorithm), Value: value.Value})
}
return pkg.JavaMetadataType, pkg.JavaMetadata{
ArchiveDigests: digests,
}
}
return pkg.UnknownMetadataType, nil
}
@@ -361,10 +317,10 @@ func findPURLValue(p *spdx.Package2_2) string {
return ""
}
func extractCPEs(p *spdx.Package2_2) (cpes []pkg.CPE) {
func extractCPEs(p *spdx.Package2_2) (cpes []cpe.CPE) {
for _, r := range p.PackageExternalReferences {
if r.RefType == string(Cpe23ExternalRefType) {
cpe, err := pkg.NewCPE(r.Locator)
cpe, err := cpe.New(r.Locator)
if err != nil {
log.Warnf("unable to extract SPDX CPE=%q: %+v", r.Locator, err)
continue
@@ -4,10 +4,8 @@ import (
"testing"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
"github.com/spdx/tools-golang/spdx"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestToSyftModel(t *testing.T) {
@@ -96,7 +94,7 @@ func TestToSyftModel(t *testing.T) {
assert.NotNil(t, sbom)
pkgs := sbom.Artifacts.PackageCatalog.Sorted()
pkgs := sbom.Artifacts.Packages.Sorted()
assert.Len(t, pkgs, 2)
@@ -197,39 +195,3 @@ func Test_extractMetadata(t *testing.T) {
})
}
}
func TestExtractSourceFromNamespaces(t *testing.T) {
tests := []struct {
namespace string
expected source.Scheme
}{
{
namespace: "https://anchore.com/syft/file/d42b01d0-7325-409b-b03f-74082935c4d3",
expected: source.FileScheme,
},
{
namespace: "https://anchore.com/syft/image/d42b01d0-7325-409b-b03f-74082935c4d3",
expected: source.ImageScheme,
},
{
namespace: "https://anchore.com/syft/dir/d42b01d0-7325-409b-b03f-74082935c4d3",
expected: source.DirectoryScheme,
},
{
namespace: "https://another-host/blob/123",
expected: source.UnknownScheme,
},
{
namespace: "bla bla",
expected: source.UnknownScheme,
},
{
namespace: "",
expected: source.UnknownScheme,
},
}
for _, tt := range tests {
require.Equal(t, tt.expected, extractSchemeFromNamespace(tt.namespace))
}
}

Some files were not shown because too many files have changed in this diff Show More