├── .bouncer.yaml ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── scripts │ ├── go-mod-tidy-check.sh │ ├── goreleaser-install.sh │ └── syft-released-version-check.sh └── workflows │ ├── release.yaml │ └── validations.yaml ├── .gitignore ├── .golangci.yaml ├── .goreleaser.yaml ├── .syft.yaml ├── DEVELOPING.md ├── LICENSE ├── Makefile ├── README.md ├── cmd ├── clean_image_reference.go ├── clean_image_reference_test.go ├── cmd.go ├── event_loop.go ├── event_loop_test.go ├── format_aliases.go ├── output_writer.go ├── output_writer_test.go ├── root.go ├── signals.go └── version.go ├── go.mod ├── go.sum ├── install.sh ├── internal ├── bus │ └── bus.go ├── config │ ├── application.go │ ├── cataloger_options.go │ ├── logging.go │ └── pkg.go ├── constants.go ├── input.go ├── log │ ├── log.go │ └── nop.go ├── logger │ └── logrus.go ├── schema.go ├── ui │ ├── common_event_handlers.go │ ├── ephemeral_terminal_ui.go │ ├── logger_ui.go │ ├── select.go │ ├── select_windows.go │ └── ui.go └── version │ └── build.go ├── main.go └── test ├── cli ├── all_formats_expressible_test.go ├── sbom_cmd_test.go ├── test-fixtures │ ├── image-hidden-packages │ │ └── Dockerfile │ └── image-pkg-coverage │ │ ├── Dockerfile │ │ ├── composer │ │ └── composer.lock │ │ ├── etc │ │ └── os-release │ │ ├── lib │ │ └── apk │ │ │ └── db │ │ │ └── installed │ │ └── pkgs │ │ ├── go │ │ └── go.mod │ │ ├── java │ │ ├── example-java-app-maven-0.1.0.jar │ │ ├── example-jenkins-plugin.hpi │ │ └── generate-fixtures.md │ │ ├── javascript │ │ ├── package-json │ │ │ └── package.json │ │ ├── package-lock │ │ │ └── package-lock.json │ │ └── yarn │ │ │ └── yarn.lock │ │ ├── lib │ │ └── apk │ │ │ └── db │ │ │ └── installed │ │ ├── php │ │ └── vendor │ │ │ └── composer │ │ │ └── installed.json │ │ ├── python │ │ ├── dist-info │ │ │ ├── METADATA │ │ │ ├── RECORD │ │ │ └── top_level.txt │ │ ├── egg-info │ │ │ ├── PKG-INFO │ │ │ └── top_level.txt │ │ ├── requires │ │ │ ├── requirements-dev.txt │ │ │ ├── requirements.txt │ │ │ └── test-requirements.txt │ │ ├── setup │ │ │ └── setup.py │ │ ├── someotherpkg-3.19.0-py3.8.egg-info │ │ │ ├── PKG-INFO │ │ │ └── top_level.txt │ │ └── somerequests-3.22.0.dist-info │ │ │ ├── METADATA │ │ │ └── top_level.txt │ │ ├── ruby │ │ ├── Gemfile.lock │ │ └── specifications │ │ │ ├── bundler.gemspec │ │ │ └── default │ │ │ └── unbundler.gemspec │ │ ├── rust │ │ └── Cargo.lock │ │ └── var │ │ └── lib │ │ ├── dpkg │ │ ├── status │ │ └── status.d │ │ │ ├── dash │ │ │ └── netbase │ │ └── rpm │ │ ├── Packages │ │ └── generate-fixture.sh ├── trait_assertions_test.go └── utils_test.go └── install ├── .dockerignore ├── .gitignore ├── 0_search_for_asset_test.sh ├── 1_download_snapshot_asset_test.sh ├── 2_download_release_asset_test.sh ├── 3_install_asset_test.sh ├── Makefile ├── environments ├── Dockerfile-alpine-3.6 └── Dockerfile-ubuntu-20.04 ├── github_test.sh ├── test-fixtures ├── github-api-docker-sbom-cli-plugin-v0.1.0-release.json └── sbom-cli-plugin_0.0.0-SNAPSHOT-ac27dcf_checksums.txt └── test_harness.sh /.bouncer.yaml: -------------------------------------------------------------------------------- 1 | permit: 2 | - BSD.* 3 | - MIT.* 4 | - Apache.* 5 | - MPL.* 6 | - ISC 7 | ignore-packages: 8 | # packageurl-go is released under the MIT license located in the root of the repo at /mit.LICENSE 9 | - github.com/anchore/packageurl-go 10 | 11 | # from: https://github.com/spdx/tools-golang/blob/main/LICENSE.code 12 | # The tools-golang source code is provided and may be used, at your option, 13 | # under either: 14 | # * Apache License, version 2.0 (Apache-2.0), OR 15 | # * GNU General Public License, version 2.0 or later (GPL-2.0-or-later). 16 | # (we choose Apache-2.0) 17 | - github.com/spdx/tools-golang 18 | 19 | # from: https://github.com/xi2/xz/blob/master/LICENSE 20 | # All these files have been put into the public domain. 21 | # You can do whatever you want with these files. 22 | - github.com/xi2/xz 23 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | **/test-fixtures/**/*.jpi filter=lfs diff=lfs merge=lfs -text 2 | **/test-fixtures/**/*.hpi filter=lfs diff=lfs merge=lfs -text 3 | **/test-fixtures/**/*.jar filter=lfs diff=lfs merge=lfs -text 4 | **/test-fixtures/**/*.war filter=lfs diff=lfs merge=lfs -text 5 | **/test-fixtures/**/*.ear filter=lfs diff=lfs merge=lfs -text 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **What happened**: 11 | 12 | **What you expected to happen**: 13 | 14 | **How to reproduce it (as minimally and precisely as possible)**: 15 | 16 | **Anything else we need to know?**: 17 | 18 | **Environment**: 19 | - Output of `docker version`: 20 | - Output of `docker sbom version`: 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **What would you like to be added**: 11 | 12 | **Why is this needed**: 13 | 14 | **Additional context**: 15 | 16 | -------------------------------------------------------------------------------- /.github/scripts/go-mod-tidy-check.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eu 3 | 4 | ORIGINAL_STATE_DIR=$(mktemp -d "TEMP-original-state-XXXXXXXXX") 5 | TIDY_STATE_DIR=$(mktemp -d "TEMP-tidy-state-XXXXXXXXX") 6 | 7 | trap "cp ${ORIGINAL_STATE_DIR}/* ./ && rm -fR ${ORIGINAL_STATE_DIR} ${TIDY_STATE_DIR}" EXIT 8 | 9 | echo "Capturing original state of files..." 10 | cp go.mod go.sum "${ORIGINAL_STATE_DIR}" 11 | 12 | echo "Capturing state of go.mod and go.sum after running go mod tidy..." 13 | go mod tidy 14 | cp go.mod go.sum "${TIDY_STATE_DIR}" 15 | echo "" 16 | 17 | set +e 18 | 19 | # Detect difference between the git HEAD state and the go mod tidy state 20 | DIFF_MOD=$(diff -u "${ORIGINAL_STATE_DIR}/go.mod" "${TIDY_STATE_DIR}/go.mod") 21 | DIFF_SUM=$(diff -u "${ORIGINAL_STATE_DIR}/go.sum" "${TIDY_STATE_DIR}/go.sum") 22 | 23 | if [[ -n "${DIFF_MOD}" || -n "${DIFF_SUM}" ]]; then 24 | echo "go.mod diff:" 25 | echo "${DIFF_MOD}" 26 | echo "go.sum diff:" 27 | echo "${DIFF_SUM}" 28 | echo "" 29 | printf "FAILED! go.mod and/or go.sum are NOT tidy; please run 'go mod tidy'.\n\n" 30 | exit 1 31 | fi 32 | -------------------------------------------------------------------------------- /.github/scripts/goreleaser-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | # Code generated by godownloader on 2019-12-25T12:47:14Z. DO NOT EDIT. 4 | # 5 | 6 | usage() { 7 | this=$1 8 | cat </dev/null 138 | } 139 | echoerr() { 140 | echo "$@" 1>&2 141 | } 142 | log_prefix() { 143 | echo "$0" 144 | } 145 | _logp=6 146 | log_set_priority() { 147 | _logp="$1" 148 | } 149 | log_priority() { 150 | if test -z "$1"; then 151 | echo "$_logp" 152 | return 153 | fi 154 | [ "$1" -le "$_logp" ] 155 | } 156 | log_tag() { 157 | case $1 in 158 | 0) echo "emerg" ;; 159 | 1) echo "alert" ;; 160 | 2) echo "crit" ;; 161 | 3) echo "err" ;; 162 | 4) echo "warning" ;; 163 | 5) echo "notice" ;; 164 | 6) echo "info" ;; 165 | 7) echo "debug" ;; 166 | *) echo "$1" ;; 167 | esac 168 | } 169 | log_debug() { 170 | log_priority 7 || return 0 171 | echoerr "$(log_prefix)" "$(log_tag 7)" "$@" 172 | } 173 | log_info() { 174 | log_priority 6 || return 0 175 | echoerr "$(log_prefix)" "$(log_tag 6)" "$@" 176 | } 177 | log_err() { 178 | log_priority 3 || return 0 179 | echoerr "$(log_prefix)" "$(log_tag 3)" "$@" 180 | } 181 | log_crit() { 182 | log_priority 2 || return 0 183 | echoerr "$(log_prefix)" "$(log_tag 2)" "$@" 184 | } 185 | uname_os() { 186 | os=$(uname -s | tr '[:upper:]' '[:lower:]') 187 | case "$os" in 188 | cygwin_nt*) os="windows" ;; 189 | mingw*) os="windows" ;; 190 | msys_nt*) os="windows" ;; 191 | esac 192 | echo "$os" 193 | } 194 | uname_arch() { 195 | arch=$(uname -m) 196 | case $arch in 197 | x86_64) arch="amd64" ;; 198 | x86) arch="386" ;; 199 | i686) arch="386" ;; 200 | i386) arch="386" ;; 201 | aarch64) arch="arm64" ;; 202 | armv5*) arch="armv5" ;; 203 | armv6*) arch="armv6" ;; 204 | armv7*) arch="armv7" ;; 205 | esac 206 | echo ${arch} 207 | } 208 | uname_os_check() { 209 | os=$(uname_os) 210 | case "$os" in 211 | darwin) return 0 ;; 212 | dragonfly) return 0 ;; 213 | freebsd) return 0 ;; 214 | linux) return 0 ;; 215 | android) return 0 ;; 216 | nacl) return 0 ;; 217 | netbsd) return 0 ;; 218 | openbsd) return 0 ;; 219 | plan9) return 0 ;; 220 | solaris) return 0 ;; 221 | windows) return 0 ;; 222 | esac 223 | log_crit "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib" 224 | return 1 225 | } 226 | uname_arch_check() { 227 | arch=$(uname_arch) 228 | case "$arch" in 229 | 386) return 0 ;; 230 | amd64) return 0 ;; 231 | arm64) return 0 ;; 232 | armv5) return 0 ;; 233 | armv6) return 0 ;; 234 | armv7) return 0 ;; 235 | ppc64) return 0 ;; 236 | ppc64le) return 0 ;; 237 | mips) return 0 ;; 238 | mipsle) return 0 ;; 239 | mips64) return 0 ;; 240 | mips64le) return 0 ;; 241 | s390x) return 0 ;; 242 | amd64p32) return 0 ;; 243 | esac 244 | log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib" 245 | return 1 246 | } 247 | untar() { 248 | tarball=$1 249 | case "${tarball}" in 250 | *.tar.gz | *.tgz) tar --no-same-owner -xzf "${tarball}" ;; 251 | *.tar) tar --no-same-owner -xf "${tarball}" ;; 252 | *.zip) unzip "${tarball}" ;; 253 | *) 254 | log_err "untar unknown archive format for ${tarball}" 255 | return 1 256 | ;; 257 | esac 258 | } 259 | http_download_curl() { 260 | local_file=$1 261 | source_url=$2 262 | header=$3 263 | if [ -z "$header" ]; then 264 | code=$(curl -w '%{http_code}' -sL -o "$local_file" "$source_url") 265 | else 266 | code=$(curl -w '%{http_code}' -sL -H "$header" -o "$local_file" "$source_url") 267 | fi 268 | if [ "$code" != "200" ]; then 269 | log_debug "http_download_curl received HTTP status $code" 270 | return 1 271 | fi 272 | return 0 273 | } 274 | http_download_wget() { 275 | local_file=$1 276 | source_url=$2 277 | header=$3 278 | if [ -z "$header" ]; then 279 | wget -q -O "$local_file" "$source_url" 280 | else 281 | wget -q --header "$header" -O "$local_file" "$source_url" 282 | fi 283 | } 284 | http_download() { 285 | log_debug "http_download $2" 286 | if is_command curl; then 287 | http_download_curl "$@" 288 | return 289 | elif is_command wget; then 290 | http_download_wget "$@" 291 | return 292 | fi 293 | log_crit "http_download unable to find wget or curl" 294 | return 1 295 | } 296 | http_copy() { 297 | tmp=$(mktemp) 298 | http_download "${tmp}" "$1" "$2" || return 1 299 | body=$(cat "$tmp") 300 | rm -f "${tmp}" 301 | echo "$body" 302 | } 303 | github_release() { 304 | owner_repo=$1 305 | version=$2 306 | test -z "$version" && version="latest" 307 | giturl="https://github.com/${owner_repo}/releases/${version}" 308 | json=$(http_copy "$giturl" "Accept:application/json") 309 | test -z "$json" && return 1 310 | version=$(echo "$json" | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//') 311 | test -z "$version" && return 1 312 | echo "$version" 313 | } 314 | hash_sha256() { 315 | TARGET=${1:-/dev/stdin} 316 | if is_command gsha256sum; then 317 | hash=$(gsha256sum "$TARGET") || return 1 318 | echo "$hash" | cut -d ' ' -f 1 319 | elif is_command sha256sum; then 320 | hash=$(sha256sum "$TARGET") || return 1 321 | echo "$hash" | cut -d ' ' -f 1 322 | elif is_command shasum; then 323 | hash=$(shasum -a 256 "$TARGET" 2>/dev/null) || return 1 324 | echo "$hash" | cut -d ' ' -f 1 325 | elif is_command openssl; then 326 | hash=$(openssl -dst openssl dgst -sha256 "$TARGET") || return 1 327 | echo "$hash" | cut -d ' ' -f a 328 | else 329 | log_crit "hash_sha256 unable to find command to compute sha-256 hash" 330 | return 1 331 | fi 332 | } 333 | hash_sha256_verify() { 334 | TARGET=$1 335 | checksums=$2 336 | if [ -z "$checksums" ]; then 337 | log_err "hash_sha256_verify checksum file not specified in arg2" 338 | return 1 339 | fi 340 | BASENAME=${TARGET##*/} 341 | want=$(grep "${BASENAME}$" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1) 342 | if [ -z "$want" ]; then 343 | log_err "hash_sha256_verify unable to find checksum for '${TARGET}' in '${checksums}'" 344 | return 1 345 | fi 346 | got=$(hash_sha256 "$TARGET") 347 | if [ "$want" != "$got" ]; then 348 | log_err "hash_sha256_verify checksum for '$TARGET' did not verify ${want} vs $got" 349 | return 1 350 | fi 351 | } 352 | cat /dev/null < $(COVER_TOTAL) 124 | @echo "Coverage: $$(cat $(COVER_TOTAL))" 125 | 126 | # note: this is used by CI to determine if the install test fixture cache (docker image tars) should be busted 127 | install-fingerprint: 128 | cd test/install && \ 129 | make cache.fingerprint 130 | 131 | install-test: 132 | cd test/install && \ 133 | make 134 | 135 | install-test-cache-save: 136 | cd test/install && \ 137 | make save 138 | 139 | install-test-cache-load: 140 | cd test/install && \ 141 | make load 142 | 143 | install-test-ci-mac: 144 | cd test/install && \ 145 | make ci-test-mac 146 | 147 | # note: this is used by CI to determine if the integration test fixture cache (docker image tars) should be busted 148 | cli-fingerprint: 149 | $(call title,CLI test fixture fingerprint) 150 | find test/cli/test-fixtures/image-* -type f -exec md5sum {} + | awk '{print $1}' | sort | md5sum | tee test/cli/test-fixtures/cache.fingerprint && echo "$(CLI_CACHE_BUSTER)" >> test/cli/test-fixtures/cache.fingerprint 151 | 152 | .PHONY: cli 153 | cli: $(SNAPSHOT_DIR) ## Run CLI tests 154 | chmod 755 "$(SNAPSHOT_BIN)" 155 | SYFT_BINARY_LOCATION='$(SNAPSHOT_BIN)' \ 156 | go test -count=1 -v ./test/cli 157 | 158 | $(SNAPSHOT_DIR): $(TEMP_DIR) ## Build snapshot release binaries and packages 159 | $(call title,Building snapshot artifacts) 160 | # create a config with the dist dir overridden 161 | echo "dist: $(SNAPSHOT_DIR)" > $(TEMP_DIR)/goreleaser.yaml 162 | cat .goreleaser.yaml >> $(TEMP_DIR)/goreleaser.yaml 163 | 164 | $(SNAPSHOT_CMD) --config $(TEMP_DIR)/goreleaser.yaml 165 | 166 | 167 | .PHONY: install-snapshot 168 | install-snapshot: 169 | cp $(SNAPSHOT_BIN) ~/.docker/cli-plugins/ 170 | 171 | .PHONY: changelog 172 | changelog: clean-changelog CHANGELOG.md 173 | @docker run -it --rm \ 174 | -v $(shell pwd)/CHANGELOG.md:/CHANGELOG.md \ 175 | rawkode/mdv \ 176 | -t 748.5989 \ 177 | /CHANGELOG.md 178 | 179 | CHANGELOG.md: 180 | $(TEMP_DIR)/chronicle -vv > CHANGELOG.md 181 | 182 | .PHONY: validate-syft-release-version 183 | validate-syft-release-version: 184 | @./.github/scripts/syft-released-version-check.sh 185 | 186 | .PHONY: release 187 | release: clean-dist CHANGELOG.md 188 | $(call title,Publishing release artifacts) 189 | bash -c "$(RELEASE_CMD) --release-notes <(cat CHANGELOG.md)" 190 | 191 | .PHONY: clean 192 | clean: clean-dist clean-snapshot ## Remove previous builds, result reports, and test cache 193 | $(call safe_rm_rf_children,$(RESULTS_DIR)) 194 | 195 | .PHONY: clean-snapshot 196 | clean-snapshot: 197 | $(call safe_rm_rf,$(SNAPSHOT_DIR)) 198 | rm -f $(TEMP_DIR)/goreleaser.yaml 199 | 200 | .PHONY: clean-dist 201 | clean-dist: clean-changelog 202 | $(call safe_rm_rf,$(DIST_DIR)) 203 | rm -f $(TEMP_DIR)/goreleaser.yaml 204 | 205 | .PHONY: clean-changelog 206 | clean-changelog: 207 | rm -f CHANGELOG.md 208 | 209 | 210 | .PHONY: clean-tmp 211 | clean-tmp: 212 | rm -rf $(TEMP_DIR) 213 | 214 | .PHONY: help 215 | help: 216 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "$(BOLD)$(CYAN)%-25s$(RESET)%s\n", $$1, $$2}' 217 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sbom-cli-plugin 2 | 3 | Plugin for Docker CLI to support viewing and creating SBOMs for Docker images using Syft. 4 | 5 | ## Getting started 6 | 7 | ``` 8 | # install the docker-sbom plugin 9 | curl -sSfL https://raw.githubusercontent.com/docker/sbom-cli-plugin/main/install.sh | sh -s -- 10 | 11 | # use the sbom plugin 12 | docker sbom 13 | ``` 14 | -------------------------------------------------------------------------------- /cmd/clean_image_reference.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/google/go-containerregistry/pkg/name" 8 | ) 9 | 10 | func cleanImageReference(userInput string) (string, error) { 11 | ref, err := name.ParseReference(userInput, name.WeakValidation, name.WithDefaultTag("latest")) 12 | if err != nil { 13 | return "", fmt.Errorf("unable to parse image reference: %w", err) 14 | } 15 | 16 | if t, ok := ref.(name.Tag); ok { 17 | if !strings.HasSuffix(userInput, t.Identifier()) { 18 | return userInput + ":" + t.Identifier(), nil 19 | } 20 | return userInput, nil 21 | } 22 | 23 | if d, ok := ref.(name.Digest); ok { 24 | if !strings.HasSuffix(userInput, d.Identifier()) { 25 | return userInput + "@" + d.Identifier(), nil 26 | } 27 | return userInput, nil 28 | } 29 | 30 | return ref.Name(), nil 31 | } 32 | -------------------------------------------------------------------------------- /cmd/clean_image_reference_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func Test_cleanImageReference(t *testing.T) { 11 | tests := []struct { 12 | input string 13 | want string 14 | wantErr require.ErrorAssertionFunc 15 | }{ 16 | { 17 | input: "alpine:latest", 18 | want: "alpine:latest", 19 | }, 20 | { 21 | input: "alpine", 22 | want: "alpine:latest", 23 | }, 24 | { 25 | input: "docker", 26 | want: "docker:latest", 27 | }, 28 | { 29 | input: "anchore/syft:latest", 30 | want: "anchore/syft:latest", 31 | }, 32 | { 33 | input: "anchore/syft:v1.4.5", 34 | want: "anchore/syft:v1.4.5", 35 | }, 36 | { 37 | input: "anchore/syft", 38 | want: "anchore/syft:latest", 39 | }, 40 | { 41 | input: "docker.io/anchore/syft", 42 | want: "docker.io/anchore/syft:latest", 43 | }, 44 | { 45 | input: "registry.upbound.io/crossplane/provider-gcp:stable", 46 | want: "registry.upbound.io/crossplane/provider-gcp:stable", 47 | }, 48 | { 49 | input: "registry.upbound.io/crossplane/provider-gcp", 50 | want: "registry.upbound.io/crossplane/provider-gcp:latest", 51 | }, 52 | { 53 | input: "anchore/syft@sha256:dba09c285770f58d6685b25a0606d72420b0a7525a2338080807d138a258c671", 54 | want: "anchore/syft@sha256:dba09c285770f58d6685b25a0606d72420b0a7525a2338080807d138a258c671", 55 | }, 56 | { 57 | // mix tag and digest 58 | input: "anchore/syft:latest@sha256:8bbaebbd4bfc3fed46227eba1d49643fc1bb79b23378956f96cff4c5d69dd42b", 59 | want: "anchore/syft:latest@sha256:8bbaebbd4bfc3fed46227eba1d49643fc1bb79b23378956f96cff4c5d69dd42b", 60 | }, 61 | { 62 | // mix tag and digest 63 | input: "registry.upbound.io/crossplane/provider-gcp:v0.2.0@sha256:8bbaebbd4bfc3fed46227eba1d49643fc1bb79b23378956f96cff4c5d69dd42b", 64 | want: "registry.upbound.io/crossplane/provider-gcp:v0.2.0@sha256:8bbaebbd4bfc3fed46227eba1d49643fc1bb79b23378956f96cff4c5d69dd42b", 65 | }, 66 | } 67 | for _, tt := range tests { 68 | t.Run(tt.input, func(t *testing.T) { 69 | if tt.wantErr == nil { 70 | tt.wantErr = require.NoError 71 | } 72 | got, err := cleanImageReference(tt.input) 73 | tt.wantErr(t, err) 74 | assert.Equal(t, tt.want, got) 75 | }) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /cmd/cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "sort" 8 | 9 | "github.com/docker/cli/cli-plugins/manager" 10 | "github.com/docker/cli/cli-plugins/plugin" 11 | "github.com/docker/sbom-cli-plugin/internal" 12 | "github.com/docker/sbom-cli-plugin/internal/bus" 13 | "github.com/docker/sbom-cli-plugin/internal/config" 14 | "github.com/docker/sbom-cli-plugin/internal/log" 15 | "github.com/docker/sbom-cli-plugin/internal/logger" 16 | "github.com/docker/sbom-cli-plugin/internal/version" 17 | "github.com/gookit/color" 18 | "github.com/spf13/cobra" 19 | "github.com/spf13/viper" 20 | "github.com/wagoodman/go-partybus" 21 | 22 | "github.com/anchore/stereoscope" 23 | "github.com/anchore/syft/syft" 24 | ) 25 | 26 | var ( 27 | appConfig *config.Application 28 | eventBus *partybus.Bus 29 | eventSubscription *partybus.Subscription 30 | ) 31 | 32 | func init() { 33 | cobra.OnInitialize( 34 | initAppConfig, 35 | initLogging, 36 | logAppConfig, 37 | logAppVersion, 38 | initEventBus, 39 | ) 40 | } 41 | 42 | func Execute() { 43 | plugin.Run( 44 | cmd, 45 | manager.Metadata{ 46 | SchemaVersion: internal.SchemaVersion, 47 | Vendor: "Anchore Inc.", 48 | Version: version.FromBuild().Version, 49 | ShortDescription: shortDescription, 50 | URL: "https://github.com/docker/sbom-cli-plugin", 51 | }, 52 | ) 53 | } 54 | 55 | func initAppConfig() { 56 | cfg, err := config.LoadApplicationConfig(viper.GetViper()) 57 | if err != nil { 58 | fmt.Printf("failed to load application config: \n\t%+v\n", err) 59 | os.Exit(1) 60 | } 61 | 62 | appConfig = cfg 63 | } 64 | 65 | func initLogging() { 66 | cfg := logger.LogrusConfig{ 67 | EnableConsole: (appConfig.Log.FileLocation == "" || appConfig.Debug) && !appConfig.Quiet, 68 | EnableFile: appConfig.Log.FileLocation != "", 69 | Level: appConfig.Log.LevelOpt, 70 | Structured: appConfig.Log.Structured, 71 | FileLocation: appConfig.Log.FileLocation, 72 | } 73 | 74 | logWrapper := logger.NewLogrusLogger(cfg) 75 | syft.SetLogger(logWrapper) 76 | stereoscope.SetLogger(&logger.LogrusNestedLogger{ 77 | Logger: logWrapper.Logger.WithField("from-lib", "stereoscope"), 78 | }) 79 | log.Log = logWrapper 80 | } 81 | 82 | func logAppConfig() { 83 | log.Debugf("application config:\n%+v", color.Magenta.Sprint(appConfig.String())) 84 | } 85 | 86 | func initEventBus() { 87 | eventBus = partybus.NewBus() 88 | eventSubscription = eventBus.Subscribe() 89 | 90 | stereoscope.SetBus(eventBus) 91 | syft.SetBus(eventBus) 92 | bus.SetPublisher(eventBus) 93 | } 94 | 95 | func logAppVersion() { 96 | versionInfo := version.FromBuild() 97 | log.Infof("%s version: %s", internal.SyftName, versionInfo.SyftVersion) 98 | 99 | var fields map[string]interface{} 100 | bytes, err := json.Marshal(versionInfo) 101 | if err != nil { 102 | return 103 | } 104 | err = json.Unmarshal(bytes, &fields) 105 | if err != nil { 106 | return 107 | } 108 | 109 | keys := make([]string, 0, len(fields)) 110 | for k := range fields { 111 | keys = append(keys, k) 112 | } 113 | sort.Strings(keys) 114 | 115 | for idx, field := range keys { 116 | value := fields[field] 117 | branch := "├──" 118 | if idx == len(fields)-1 { 119 | branch = "└──" 120 | } 121 | log.Debugf(" %s %s: %s", branch, field, value) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /cmd/event_loop.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/docker/sbom-cli-plugin/internal/log" 9 | "github.com/docker/sbom-cli-plugin/internal/ui" 10 | "github.com/hashicorp/go-multierror" 11 | "github.com/wagoodman/go-partybus" 12 | ) 13 | 14 | // eventLoop listens to worker errors (from execution path), worker events (from a partybus subscription), and 15 | // signal interrupts. Is responsible for handling each event relative to a given UI an to coordinate eventing until 16 | // an eventual graceful exit. 17 | // nolint:funlen 18 | func eventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription *partybus.Subscription, cleanupFn func(), uxs ...ui.UI) error { 19 | defer cleanupFn() 20 | events := subscription.Events() 21 | var err error 22 | var ux ui.UI 23 | 24 | if ux, err = setupUI(subscription.Unsubscribe, uxs...); err != nil { 25 | return err 26 | } 27 | 28 | var retErr error 29 | var forceTeardown bool 30 | 31 | for { 32 | if workerErrs == nil && events == nil { 33 | break 34 | } 35 | select { 36 | case err, isOpen := <-workerErrs: 37 | if !isOpen { 38 | workerErrs = nil 39 | continue 40 | } 41 | if err != nil { 42 | // capture the error from the worker and unsubscribe to complete a graceful shutdown 43 | retErr = multierror.Append(retErr, err) 44 | _ = subscription.Unsubscribe() 45 | // the worker has exited, we may have been mid-handling events for the UI which should now be 46 | // ignored, in which case forcing a teardown of the UI irregardless of the state is required. 47 | forceTeardown = true 48 | } 49 | case e, isOpen := <-events: 50 | if !isOpen { 51 | events = nil 52 | continue 53 | } 54 | 55 | if err := ux.Handle(e); err != nil { 56 | if errors.Is(err, partybus.ErrUnsubscribe) { 57 | events = nil 58 | } else { 59 | retErr = multierror.Append(retErr, err) 60 | // TODO: should we unsubscribe? should we try to halt execution? or continue? 61 | } 62 | } 63 | case <-signals: 64 | // ignore further results from any event source and exit ASAP, but ensure that all cache is cleaned up. 65 | // we ignore further errors since cleaning up the tmp directories will affect running catalogers that are 66 | // reading/writing from/to their nested temp dirs. This is acceptable since we are bailing without result. 67 | 68 | // TODO: potential future improvement would be to pass context into workers with a cancel function that is 69 | // to the event loop. In this way we can have a more controlled shutdown even at the most nested levels 70 | // of processing. 71 | events = nil 72 | workerErrs = nil 73 | forceTeardown = true 74 | } 75 | } 76 | 77 | if err := ux.Teardown(forceTeardown); err != nil { 78 | retErr = multierror.Append(retErr, err) 79 | } 80 | 81 | return retErr 82 | } 83 | 84 | // setupUI takes one or more UIs that responds to events and takes a event bus unsubscribe function for use 85 | // during teardown. With the given UIs, the first UI which the ui.Setup() function does not return an error 86 | // will be utilized in execution. Providing a set of UIs allows for the caller to provide graceful fallbacks 87 | // when there are environmental problem (e.g. unable to setup a TUI with the current TTY). 88 | func setupUI(unsubscribe func() error, uis ...ui.UI) (ui.UI, error) { 89 | for _, ux := range uis { 90 | if err := ux.Setup(unsubscribe); err != nil { 91 | log.Warnf("unable to setup given UI, falling back to alternative UI: %+v", err) 92 | continue 93 | } 94 | 95 | return ux, nil 96 | } 97 | return nil, fmt.Errorf("unable to setup any UI") 98 | } 99 | -------------------------------------------------------------------------------- /cmd/format_aliases.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/anchore/syft/syft" 5 | "github.com/anchore/syft/syft/sbom" 6 | ) 7 | 8 | func formatAliases(ids ...sbom.FormatID) (aliases []string) { 9 | for _, id := range ids { 10 | switch id { 11 | case syft.JSONFormatID: 12 | aliases = append(aliases, "syft-json") 13 | case syft.TextFormatID: 14 | aliases = append(aliases, "text") 15 | case syft.TableFormatID: 16 | aliases = append(aliases, "table") 17 | case syft.SPDXJSONFormatID: 18 | aliases = append(aliases, "spdx-json") 19 | case syft.SPDXTagValueFormatID: 20 | aliases = append(aliases, "spdx-tag-value") 21 | case syft.CycloneDxXMLFormatID: 22 | aliases = append(aliases, "cyclonedx-xml") 23 | case syft.CycloneDxJSONFormatID: 24 | aliases = append(aliases, "cyclonedx-json") 25 | case syft.GitHubID: 26 | aliases = append(aliases, "github-json") 27 | default: 28 | aliases = append(aliases, string(id)) 29 | } 30 | } 31 | return aliases 32 | } 33 | -------------------------------------------------------------------------------- /cmd/output_writer.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/hashicorp/go-multierror" 8 | 9 | "github.com/anchore/syft/syft" 10 | "github.com/anchore/syft/syft/sbom" 11 | ) 12 | 13 | // makeWriter creates a sbom.Writer for output or returns an error. this will either return a valid writer 14 | // or an error but neither both and if there is no error, sbom.Writer.Close() should be called 15 | func makeWriter(outputs []string, defaultFile string) (sbom.Writer, error) { 16 | outputOptions, err := parseOptions(outputs, defaultFile) 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | writer, err := sbom.NewWriter(outputOptions...) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | return writer, nil 27 | } 28 | 29 | // parseOptions utility to parse command-line option strings and retain the existing behavior of default format and file 30 | func parseOptions(outputs []string, defaultFile string) (out []sbom.WriterOption, errs error) { 31 | // always should have one option -- we generally get the default of "table", but just make sure 32 | if len(outputs) == 0 { 33 | outputs = append(outputs, string(syft.TableFormatID)) 34 | } 35 | 36 | for _, name := range outputs { 37 | name = strings.TrimSpace(name) 38 | 39 | // split to at most two parts for = 40 | parts := strings.SplitN(name, "=", 2) 41 | 42 | // the format name is the first part 43 | name = parts[0] 44 | 45 | // default to the --file or empty string if not specified 46 | file := defaultFile 47 | 48 | // If a file is specified as part of the output formatName, use that 49 | if len(parts) > 1 { 50 | file = parts[1] 51 | } 52 | 53 | format := syft.FormatByName(name) 54 | if format == nil { 55 | errs = multierror.Append(errs, fmt.Errorf("bad output format: '%s'", name)) 56 | continue 57 | } 58 | 59 | out = append(out, sbom.NewWriterOption(format, file)) 60 | } 61 | return out, errs 62 | } 63 | -------------------------------------------------------------------------------- /cmd/output_writer_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestOutputWriterConfig(t *testing.T) { 12 | tmp := t.TempDir() + "/" 13 | 14 | tests := []struct { 15 | outputs []string 16 | file string 17 | err bool 18 | expected []string 19 | }{ 20 | { 21 | outputs: []string{}, 22 | expected: []string{""}, 23 | }, 24 | { 25 | outputs: []string{"json"}, 26 | expected: []string{""}, 27 | }, 28 | { 29 | file: "test-1.json", 30 | expected: []string{"test-1.json"}, 31 | }, 32 | { 33 | outputs: []string{"json=test-2.json"}, 34 | expected: []string{"test-2.json"}, 35 | }, 36 | { 37 | outputs: []string{"json=test-3-1.json", "spdx-json=test-3-2.json"}, 38 | expected: []string{"test-3-1.json", "test-3-2.json"}, 39 | }, 40 | { 41 | outputs: []string{"text", "json=test-4.json"}, 42 | expected: []string{"", "test-4.json"}, 43 | }, 44 | } 45 | 46 | for _, test := range tests { 47 | t.Run(fmt.Sprintf("%s/%s", test.outputs, test.file), func(t *testing.T) { 48 | outputs := test.outputs 49 | for i, val := range outputs { 50 | outputs[i] = strings.Replace(val, "=", "="+tmp, 1) 51 | } 52 | 53 | file := test.file 54 | if file != "" { 55 | file = tmp + file 56 | } 57 | 58 | _, err := makeWriter(test.outputs, file) 59 | 60 | if test.err { 61 | assert.Error(t, err) 62 | return 63 | } else { 64 | assert.NoError(t, err) 65 | } 66 | 67 | for _, expected := range test.expected { 68 | if expected != "" { 69 | assert.FileExists(t, tmp+expected) 70 | } else if file != "" { 71 | assert.FileExists(t, file) 72 | } else { 73 | assert.NoFileExists(t, expected) 74 | } 75 | } 76 | }) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "strings" 9 | "text/template" 10 | 11 | "github.com/docker/cli/cli/command" 12 | "github.com/docker/sbom-cli-plugin/internal" 13 | "github.com/docker/sbom-cli-plugin/internal/bus" 14 | "github.com/docker/sbom-cli-plugin/internal/log" 15 | "github.com/docker/sbom-cli-plugin/internal/ui" 16 | "github.com/docker/sbom-cli-plugin/internal/version" 17 | "github.com/spf13/cobra" 18 | "github.com/spf13/pflag" 19 | "github.com/spf13/viper" 20 | "github.com/wagoodman/go-partybus" 21 | 22 | "github.com/anchore/stereoscope" 23 | "github.com/anchore/stereoscope/pkg/file" 24 | "github.com/anchore/stereoscope/pkg/image" 25 | stereoscopeDocker "github.com/anchore/stereoscope/pkg/image/docker" 26 | "github.com/anchore/syft/syft" 27 | "github.com/anchore/syft/syft/event" 28 | "github.com/anchore/syft/syft/pkg/cataloger" 29 | "github.com/anchore/syft/syft/sbom" 30 | "github.com/anchore/syft/syft/source" 31 | ) 32 | 33 | const ( 34 | helpExample = ` 35 | docker sbom alpine:latest a summary of discovered packages 36 | docker sbom alpine:latest --format syft-json show all possible cataloging details 37 | docker sbom alpine:latest --output sbom.txt write report output to a file 38 | docker sbom alpine:latest --exclude /lib --exclude '**/*.db' ignore one or more paths/globs in the image 39 | ` 40 | shortDescription = "View the packaged-based Software Bill Of Materials (SBOM) for an image" 41 | ) 42 | 43 | func cmd(dockerCli command.Cli) *cobra.Command { 44 | c := &cobra.Command{ 45 | Use: "sbom", 46 | Short: shortDescription, 47 | Long: shortDescription + ".\n\nEXPERIMENTAL: The flags and outputs of this command may change. Leave feedback on https://github.com/docker/sbom-cli-plugin.", 48 | Example: helpExample, 49 | Args: validateInputArgs, 50 | SilenceUsage: true, 51 | SilenceErrors: true, 52 | Version: version.FromBuild().Version, 53 | RunE: newRunner(dockerCli).run, 54 | } 55 | 56 | c.SetVersionTemplate(fmt.Sprintf("%s {{.Version}}, build %s\n", internal.ApplicationName, version.FromBuild().GitCommit)) 57 | 58 | setPackageFlags(c.Flags()) 59 | 60 | if err := bindConfigOptions(c.Flags()); err != nil { 61 | panic(fmt.Errorf("unable to bind config options: %w", err)) 62 | } 63 | 64 | c.AddCommand(versionCmd()) 65 | 66 | return c 67 | } 68 | 69 | func tprintf(tmpl string, data interface{}) string { 70 | t := template.Must(template.New("").Parse(tmpl)) 71 | buf := &bytes.Buffer{} 72 | if err := t.Execute(buf, data); err != nil { 73 | return "" 74 | } 75 | return buf.String() 76 | } 77 | 78 | func allScopes() (result []string) { 79 | for _, s := range source.AllScopes { 80 | result = append(result, cleanScope(s)) 81 | } 82 | return result 83 | } 84 | 85 | func cleanScope(s source.Scope) string { 86 | var opt string 87 | switch s { 88 | case source.AllLayersScope: 89 | opt = "all" 90 | case source.SquashedScope: 91 | opt = "squashed" 92 | default: 93 | opt = strings.ToLower(string(s)) 94 | } 95 | return opt 96 | } 97 | 98 | func setPackageFlags(flags *pflag.FlagSet) { 99 | flags.BoolP( 100 | "quiet", "q", false, 101 | "suppress all non-report output", 102 | ) 103 | 104 | flags.StringP( 105 | "layers", "", cleanScope(cataloger.DefaultSearchConfig().Scope), 106 | fmt.Sprintf("[experimental] selection of layers to catalog, options=%v", allScopes()), 107 | ) 108 | 109 | flags.StringP( 110 | "format", "", formatAliases(syft.TableFormatID)[0], 111 | fmt.Sprintf("report output format, options=%v", formatAliases(syft.FormatIDs()...)), 112 | ) 113 | 114 | flags.StringP( 115 | "output", "o", "", 116 | "file to write the default report output to (default is STDOUT)", 117 | ) 118 | 119 | flags.StringArrayP( 120 | "exclude", "", nil, 121 | "exclude paths from being scanned using a glob expression", 122 | ) 123 | 124 | flags.StringP( 125 | "platform", "", "", 126 | "an optional platform specifier for container image sources (e.g. 'linux/arm64', 'linux/arm64/v8', 'arm64', 'linux')", 127 | ) 128 | 129 | flags.BoolP( 130 | "debug", "D", false, 131 | "show debug logging", 132 | ) 133 | } 134 | 135 | func bindConfigOptions(flags *pflag.FlagSet) error { 136 | if err := viper.BindPFlag("quiet", flags.Lookup("quiet")); err != nil { 137 | return err 138 | } 139 | 140 | if err := viper.BindPFlag("output", flags.Lookup("output")); err != nil { 141 | return err 142 | } 143 | 144 | if err := viper.BindPFlag("package.cataloger.scope", flags.Lookup("layers")); err != nil { 145 | return err 146 | } 147 | 148 | if err := viper.BindPFlag("format", flags.Lookup("format")); err != nil { 149 | return err 150 | } 151 | 152 | if err := viper.BindPFlag("exclude", flags.Lookup("exclude")); err != nil { 153 | return err 154 | } 155 | 156 | if err := viper.BindPFlag("platform", flags.Lookup("platform")); err != nil { 157 | return err 158 | } 159 | 160 | if err := viper.BindPFlag("debug", flags.Lookup("debug")); err != nil { 161 | return err 162 | } 163 | 164 | return nil 165 | } 166 | 167 | func validateInputArgs(cmd *cobra.Command, args []string) error { 168 | if len(args) == 0 { 169 | // in the case that no arguments are given we want to show the help text and return with a non-0 return code. 170 | if err := cmd.Help(); err != nil { 171 | return fmt.Errorf("unable to display help: %w", err) 172 | } 173 | return fmt.Errorf("an image argument is required") 174 | } 175 | 176 | return cobra.ExactArgs(1)(cmd, args) 177 | } 178 | 179 | type runner struct { 180 | client command.Cli 181 | } 182 | 183 | func newRunner(client command.Cli) runner { 184 | return runner{ 185 | client: client, 186 | } 187 | } 188 | 189 | func (r runner) run(_ *cobra.Command, args []string) error { 190 | writer, err := makeWriter([]string{appConfig.Format}, appConfig.Output) 191 | if err != nil { 192 | return err 193 | } 194 | 195 | defer func() { 196 | if err := writer.Close(); err != nil { 197 | log.Warnf("unable to write to report destination: %+v", err) 198 | } 199 | }() 200 | 201 | var platform *image.Platform 202 | if appConfig.Platform != "" { 203 | platform, err = image.NewPlatform(appConfig.Platform) 204 | if err != nil { 205 | return fmt.Errorf("invalid platform provided: %w", err) 206 | } 207 | } 208 | 209 | cleanImageName, err := cleanImageReference(args[0]) 210 | if err != nil { 211 | return nil 212 | } 213 | 214 | return eventLoop( 215 | sbomExecWorker(cleanImageName, r.client, platform, writer), 216 | setupSignals(), 217 | eventSubscription, 218 | stereoscope.Cleanup, 219 | ui.Select(isVerbose(), appConfig.Quiet)..., 220 | ) 221 | } 222 | 223 | func isVerbose() (result bool) { 224 | isPipedInput, err := internal.IsPipedInput() 225 | if err != nil { 226 | // since we can't tell if there was piped input we assume that there could be to disable the ETUI 227 | log.Warnf("unable to determine if there is piped input: %+v", err) 228 | return true 229 | } 230 | // verbosity should consider if there is piped input (in which case we should not show the ETUI) 231 | return appConfig.Debug || isPipedInput 232 | } 233 | 234 | func generateSBOM(src *source.Source) (*sbom.SBOM, error) { 235 | s := sbom.SBOM{ 236 | Source: src.Metadata, 237 | Descriptor: sbom.Descriptor{ 238 | Name: internal.SyftName, 239 | Version: version.FromBuild().SyftVersion, 240 | Configuration: appConfig, 241 | }, 242 | } 243 | 244 | packageCatalog, relationships, theDistro, err := syft.CatalogPackages(src, appConfig.Package.ToConfig()) 245 | if err != nil { 246 | return nil, fmt.Errorf("unable to catalog packages: %w", err) 247 | } 248 | 249 | s.Artifacts.PackageCatalog = packageCatalog 250 | s.Artifacts.LinuxDistribution = theDistro 251 | s.Relationships = relationships 252 | 253 | return &s, nil 254 | } 255 | 256 | func sbomExecWorker(imageName string, dockerCli command.Cli, platform *image.Platform, writer sbom.Writer) <-chan error { 257 | errs := make(chan error) 258 | go func() { 259 | defer close(errs) 260 | 261 | tempGen := file.NewTempDirGenerator(internal.ApplicationName) 262 | 263 | provider := stereoscopeDocker.NewProviderFromDaemon( 264 | imageName, 265 | tempGen, 266 | dockerCli.Client(), 267 | platform, 268 | ) 269 | img, err := provider.Provide(context.Background()) 270 | defer func() { 271 | if err := tempGen.Cleanup(); err != nil { 272 | log.Warnf("failed to clean up image: %+v", err) 273 | } 274 | }() 275 | if err != nil { 276 | errs <- fmt.Errorf("failed to fetch the image %q: %w", imageName, err) 277 | return 278 | } 279 | 280 | err = img.Read() 281 | if err != nil { 282 | errs <- fmt.Errorf("failed to read the image %q: %w", imageName, err) 283 | return 284 | } 285 | 286 | src, err := source.NewFromImage(img, imageName) 287 | if err != nil { 288 | errs <- fmt.Errorf("failed to construct source from user input %q: %w", imageName, err) 289 | return 290 | } 291 | src.Exclusions = appConfig.Exclusions 292 | 293 | s, err := generateSBOM(&src) 294 | if err != nil { 295 | errs <- err 296 | return 297 | } 298 | 299 | if err != nil { 300 | errs <- errors.New("could not produce an sbom") 301 | return 302 | } 303 | 304 | bus.Publish(partybus.Event{ 305 | Type: event.Exit, 306 | Value: func() error { return writer.Write(*s) }, 307 | }) 308 | }() 309 | return errs 310 | } 311 | -------------------------------------------------------------------------------- /cmd/signals.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | "syscall" 7 | ) 8 | 9 | func setupSignals() <-chan os.Signal { 10 | c := make(chan os.Signal, 1) // Note: A buffered channel is recommended for this; see https://golang.org/pkg/os/signal/#Notify 11 | 12 | interruptions := []os.Signal{ 13 | syscall.SIGINT, 14 | syscall.SIGTERM, 15 | } 16 | 17 | signal.Notify(c, interruptions...) 18 | 19 | return c 20 | } 21 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/docker/sbom-cli-plugin/internal" 7 | "github.com/docker/sbom-cli-plugin/internal/version" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func versionCmd() *cobra.Command { 12 | c := &cobra.Command{ 13 | Use: "version", 14 | Short: "Show Docker sbom version information", 15 | Args: cobra.NoArgs, 16 | SilenceUsage: true, 17 | SilenceErrors: true, 18 | RunE: func(_ *cobra.Command, args []string) error { 19 | report := tprintf(`Application: {{ .Name }} ({{ .Version.Version }}) 20 | Provider: {{ .SyftName }} ({{ .SyftVersion }}) 21 | GitCommit: {{ .GitCommit }} 22 | GitDescription: {{ .GitDescription }} 23 | Platform: {{ .Platform }} 24 | `, struct { 25 | Name string 26 | SyftName string 27 | version.Version 28 | }{ 29 | Name: internal.BinaryName, 30 | SyftName: internal.SyftName, 31 | Version: version.FromBuild(), 32 | }) 33 | 34 | fmt.Print(report) 35 | return nil 36 | }, 37 | } 38 | 39 | return c 40 | } 41 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/docker/sbom-cli-plugin 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/Microsoft/hcsshim v0.9.2 // indirect 7 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d 8 | github.com/anchore/stereoscope v0.0.0-20220518185348-c97a3c6ffc67 9 | github.com/anchore/syft v0.46.3 10 | github.com/containerd/containerd v1.5.10 // indirect 11 | github.com/containerd/continuity v0.2.2 // indirect 12 | github.com/docker/cli v20.10.12+incompatible 13 | github.com/docker/docker v20.10.12+incompatible // indirect 14 | github.com/docker/docker-credential-helpers v0.6.4 // indirect 15 | github.com/fvbommel/sortorder v1.0.2 // indirect 16 | github.com/gookit/color v1.4.2 17 | github.com/hashicorp/go-multierror v1.1.1 18 | github.com/moby/sys/mount v0.3.1 // indirect 19 | github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect 20 | github.com/sirupsen/logrus v1.8.1 21 | github.com/spf13/cobra v1.4.0 22 | github.com/spf13/pflag v1.0.5 23 | github.com/spf13/viper v1.11.0 24 | github.com/stretchr/testify v1.7.1 25 | github.com/theupdateframework/notary v0.7.0 // indirect 26 | github.com/wagoodman/go-partybus v0.0.0-20210627031916-db1f5573bbc5 27 | github.com/wagoodman/jotframe v0.0.0-20211129225309-56b0d0a4aebb 28 | github.com/x-cray/logrus-prefixed-formatter v0.5.2 29 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 30 | gopkg.in/yaml.v2 v2.4.0 31 | gotest.tools/v3 v3.1.0 // indirect 32 | ) 33 | 34 | require github.com/google/go-containerregistry v0.8.1-0.20220209165246-a44adc326839 35 | 36 | require ( 37 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect 38 | github.com/CycloneDX/cyclonedx-go v0.5.2 // indirect 39 | github.com/Microsoft/go-winio v0.5.1 // indirect 40 | github.com/acobaugh/osrelease v0.1.0 // indirect 41 | github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb // indirect 42 | github.com/anchore/go-rpmdb v0.0.0-20210914181456-a9c52348da63 // indirect 43 | github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 // indirect 44 | github.com/anchore/packageurl-go v0.1.1-0.20220428202044-a072fa3cb6d7 // indirect 45 | github.com/andybalholm/brotli v1.0.4 // indirect 46 | github.com/beorn7/perks v1.0.1 // indirect 47 | github.com/bmatcuk/doublestar/v4 v4.0.2 // indirect 48 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 49 | github.com/containerd/stargz-snapshotter/estargz v0.10.1 // indirect 50 | github.com/davecgh/go-spew v1.1.1 // indirect 51 | github.com/docker/distribution v2.8.0+incompatible // indirect 52 | github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect 53 | github.com/docker/go-connections v0.4.0 // indirect 54 | github.com/docker/go-metrics v0.0.1 // indirect 55 | github.com/docker/go-units v0.4.0 // indirect 56 | github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect 57 | github.com/dustin/go-humanize v1.0.0 // indirect 58 | github.com/facebookincubator/nvdtools v0.1.4 // indirect 59 | github.com/fsnotify/fsnotify v1.5.1 // indirect 60 | github.com/gabriel-vasile/mimetype v1.4.0 // indirect 61 | github.com/go-restruct/restruct v1.2.0-alpha // indirect 62 | github.com/go-sql-driver/mysql v1.6.0 // indirect 63 | github.com/gogo/protobuf v1.3.2 // indirect 64 | github.com/golang/protobuf v1.5.2 // indirect 65 | github.com/golang/snappy v0.0.4 // indirect 66 | github.com/google/go-cmp v0.5.7 // indirect 67 | github.com/google/uuid v1.3.0 // indirect 68 | github.com/gorilla/mux v1.8.0 // indirect 69 | github.com/hashicorp/errwrap v1.1.0 // indirect 70 | github.com/hashicorp/hcl v1.0.0 // indirect 71 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 72 | github.com/jinzhu/copier v0.3.2 // indirect 73 | github.com/klauspost/compress v1.14.2 // indirect 74 | github.com/klauspost/pgzip v1.2.5 // indirect 75 | github.com/kr/pretty v0.3.0 // indirect 76 | github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 // indirect 77 | github.com/magiconair/properties v1.8.6 // indirect 78 | github.com/mattn/go-colorable v0.1.12 // indirect 79 | github.com/mattn/go-isatty v0.0.14 // indirect 80 | github.com/mattn/go-runewidth v0.0.13 // indirect 81 | github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect 82 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect 83 | github.com/mholt/archiver/v3 v3.5.1 // indirect 84 | github.com/miekg/pkcs11 v1.1.1 // indirect 85 | github.com/mitchellh/go-homedir v1.1.0 // indirect 86 | github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect 87 | github.com/mitchellh/mapstructure v1.5.0 // indirect 88 | github.com/moby/sys/mountinfo v0.6.0 // indirect 89 | github.com/morikuni/aec v1.0.0 // indirect 90 | github.com/nwaples/rardecode v1.1.0 // indirect 91 | github.com/olekukonko/tablewriter v0.0.5 // indirect 92 | github.com/onsi/ginkgo v1.16.5 // indirect 93 | github.com/onsi/gomega v1.18.1 // indirect 94 | github.com/opencontainers/go-digest v1.0.0 // indirect 95 | github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198 // indirect 96 | github.com/pelletier/go-toml v1.9.4 // indirect 97 | github.com/pelletier/go-toml/v2 v2.0.0-beta.8 // indirect 98 | github.com/pierrec/lz4/v4 v4.1.2 // indirect 99 | github.com/pkg/errors v0.9.1 // indirect 100 | github.com/pmezard/go-difflib v1.0.0 // indirect 101 | github.com/prometheus/client_golang v1.12.1 // indirect 102 | github.com/prometheus/client_model v0.2.0 // indirect 103 | github.com/prometheus/common v0.32.1 // indirect 104 | github.com/prometheus/procfs v0.7.3 // indirect 105 | github.com/rivo/uniseg v0.2.0 // indirect 106 | github.com/rogpeppe/go-internal v1.8.0 // indirect 107 | github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e // indirect 108 | github.com/spdx/tools-golang v0.2.0 // indirect 109 | github.com/spf13/afero v1.8.2 // indirect 110 | github.com/spf13/cast v1.4.1 // indirect 111 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 112 | github.com/stretchr/objx v0.3.0 // indirect 113 | github.com/subosito/gotenv v1.2.0 // indirect 114 | github.com/ulikunitz/xz v0.5.10 // indirect 115 | github.com/vbatts/tar-split v0.11.2 // indirect 116 | github.com/vifraa/gopom v0.1.0 // indirect 117 | github.com/wagoodman/go-progress v0.0.0-20200731105512-1020f39e6240 // indirect 118 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect 119 | github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect 120 | golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f // indirect 121 | golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect 122 | golang.org/x/net v0.0.0-20220412020605-290c469a71a5 // indirect 123 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect 124 | golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect 125 | golang.org/x/text v0.3.7 // indirect 126 | golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f // indirect 127 | google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4 // indirect 128 | google.golang.org/grpc v1.46.0 // indirect 129 | google.golang.org/protobuf v1.28.0 // indirect 130 | gopkg.in/ini.v1 v1.66.4 // indirect 131 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 132 | ) 133 | -------------------------------------------------------------------------------- /internal/bus/bus.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package bus provides access to a singleton instance of an event bus (provided by the calling application). The event bus 3 | is intended to allow for the syft library to publish events which library consumers can subscribe to. These events 4 | can provide static information, but also have an object as a payload for which the consumer can poll for updates. 5 | This is akin to a logger, except instead of only allowing strings to be logged, rich objects that can be interacted with. 6 | 7 | Note that the singleton instance is only allowed to publish events and not subscribe to them --this is intentional. 8 | Internal library interactions should continue to use traditional in-execution-path approaches for data sharing 9 | (e.g. function returns and channels) and not depend on bus subscriptions for critical interactions (e.g. one part of the 10 | lib publishes an event and another part of the lib subscribes and reacts to that event). The bus is provided only as a 11 | means for consumers to observe events emitted from the library (such as to provide a rich UI) and not to allow 12 | consumers to augment or otherwise change execution. 13 | */ 14 | package bus 15 | 16 | import "github.com/wagoodman/go-partybus" 17 | 18 | var publisher partybus.Publisher 19 | var active bool 20 | 21 | // SetPublisher sets the singleton event bus publisher. This is optional; if no bus is provided, the library will 22 | // behave no differently than if a bus had been provided. 23 | func SetPublisher(p partybus.Publisher) { 24 | publisher = p 25 | if p != nil { 26 | active = true 27 | } 28 | } 29 | 30 | // Publish an event onto the bus. If there is no bus set by the calling application, this does nothing. 31 | func Publish(event partybus.Event) { 32 | if active { 33 | publisher.Publish(event) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /internal/config/application.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | 8 | "github.com/sirupsen/logrus" 9 | "github.com/spf13/viper" 10 | "gopkg.in/yaml.v2" 11 | ) 12 | 13 | type defaultValueLoader interface { 14 | loadDefaultValues(*viper.Viper) 15 | } 16 | 17 | type parser interface { 18 | parseConfigValues() error 19 | } 20 | 21 | // Application is the main syft application configuration. 22 | type Application struct { 23 | Package pkg `yaml:"package" json:"package" mapstructure:"package"` // package cataloging related options 24 | Exclusions []string `yaml:"exclude" json:"exclude" mapstructure:"exclude"` // --exclude, ignore paths within an image 25 | Platform string `yaml:"platform" json:"platform" mapstructure:"platform"` // --platform, override OS and architecture from image 26 | Output string `yaml:"output" json:"output" mapstructure:"output"` // --output, the file to write report output to 27 | Format string `yaml:"format" json:"format" mapstructure:"format"` // --format, the format to use for output 28 | Quiet bool `yaml:"quiet" json:"quiet" mapstructure:"quiet"` // -q, indicates to not show any status output to stderr (ETUI or logging UI) 29 | Log logging `yaml:"log" json:"log" mapstructure:"log"` // all logging-related options 30 | Debug bool `yaml:"debug" json:"debug" mapstructure:"debug"` // -D/--debug, enable debug logging 31 | } 32 | 33 | func newApplicationConfig(v *viper.Viper) *Application { 34 | config := &Application{} 35 | config.loadDefaultValues(v) 36 | return config 37 | } 38 | 39 | // LoadApplicationConfig populates the given viper object with a default application config values 40 | func LoadApplicationConfig(v *viper.Viper) (*Application, error) { 41 | // the user may not have a config, and this is OK, we can use the default config + default cobra cli values instead 42 | config := newApplicationConfig(v) 43 | 44 | // TODO: in the future when we have a user-modifiable configuration, reading such contents would be here 45 | 46 | if err := v.Unmarshal(config); err != nil { 47 | return nil, fmt.Errorf("unable to parse config: %w", err) 48 | } 49 | 50 | if err := config.parseConfigValues(); err != nil { 51 | return nil, fmt.Errorf("invalid application config: %w", err) 52 | } 53 | 54 | return config, nil 55 | } 56 | 57 | // init loads the default configuration values into the viper instance (before the config values are read and parsed). 58 | func (cfg Application) loadDefaultValues(v *viper.Viper) { 59 | // for each field in the configuration struct, see if the field implements the defaultValueLoader interface and invoke it if it does 60 | value := reflect.ValueOf(cfg) 61 | for i := 0; i < value.NumField(); i++ { 62 | // note: the defaultValueLoader method receiver is NOT a pointer receiver. 63 | if loadable, ok := value.Field(i).Interface().(defaultValueLoader); ok { 64 | // the field implements defaultValueLoader, call it 65 | loadable.loadDefaultValues(v) 66 | } 67 | } 68 | } 69 | 70 | func (cfg *Application) parseConfigValues() error { 71 | // parse application config options 72 | for _, optionFn := range []func() error{ 73 | cfg.parseLogLevelOption, 74 | } { 75 | if err := optionFn(); err != nil { 76 | return err 77 | } 78 | } 79 | 80 | // parse nested config options 81 | // for each field in the configuration struct, see if the field implements the parser interface 82 | // note: the app config is a pointer, so we need to grab the elements explicitly (to traverse the address) 83 | value := reflect.ValueOf(cfg).Elem() 84 | for i := 0; i < value.NumField(); i++ { 85 | // note: since the interface method of parser is a pointer receiver we need to get the value of the field as a pointer. 86 | if parsable, ok := value.Field(i).Addr().Interface().(parser); ok { 87 | // the field implements parser, call it 88 | if err := parsable.parseConfigValues(); err != nil { 89 | return err 90 | } 91 | } 92 | } 93 | return nil 94 | } 95 | 96 | func (cfg *Application) parseLogLevelOption() error { 97 | switch { 98 | case cfg.Quiet: 99 | cfg.Log.LevelOpt = logrus.PanicLevel 100 | case cfg.Log.Level != "": 101 | lvl, err := logrus.ParseLevel(strings.ToLower(cfg.Log.Level)) 102 | if err != nil { 103 | return fmt.Errorf("bad log level configured (%q): %w", cfg.Log.Level, err) 104 | } 105 | 106 | cfg.Log.LevelOpt = lvl 107 | case cfg.Debug: 108 | cfg.Log.LevelOpt = logrus.DebugLevel 109 | default: 110 | cfg.Log.LevelOpt = logrus.WarnLevel 111 | } 112 | 113 | return nil 114 | } 115 | 116 | func (cfg Application) String() string { 117 | // yaml is pretty human friendly (at least when compared to json) 118 | appCfgStr, err := yaml.Marshal(&cfg) 119 | 120 | if err != nil { 121 | return err.Error() 122 | } 123 | 124 | return string(appCfgStr) 125 | } 126 | -------------------------------------------------------------------------------- /internal/config/cataloger_options.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/spf13/viper" 8 | 9 | "github.com/anchore/syft/syft/source" 10 | ) 11 | 12 | type catalogerOptions struct { 13 | Enabled bool `yaml:"enabled" json:"enabled" mapstructure:"enabled"` 14 | Scope string `yaml:"scope" json:"scope" mapstructure:"scope"` 15 | ScopeOpt source.Scope `yaml:"-" json:"-"` 16 | } 17 | 18 | func (cfg catalogerOptions) loadDefaultValues(v *viper.Viper) { 19 | v.SetDefault("package.cataloger.enabled", true) 20 | } 21 | 22 | func (cfg *catalogerOptions) parseConfigValues() error { 23 | cfg.Scope = strings.ToLower(cfg.Scope) 24 | if cfg.Scope == "all" { 25 | cfg.Scope = "all-layers" 26 | } 27 | 28 | scopeOption := source.ParseScope(cfg.Scope) 29 | if scopeOption == source.UnknownScope { 30 | return fmt.Errorf("bad scope value %q", cfg.Scope) 31 | } 32 | 33 | cfg.ScopeOpt = scopeOption 34 | 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /internal/config/logging.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | "github.com/spf13/viper" 6 | ) 7 | 8 | // logging contains all logging-related configuration options available to the user via the application config. 9 | type logging struct { 10 | Structured bool `yaml:"structured" json:"structured" mapstructure:"structured"` // show all log entries as JSON formatted strings 11 | LevelOpt logrus.Level `yaml:"-" json:"-"` // the native log level object used by the logger 12 | Level string `yaml:"level" json:"level" mapstructure:"level"` // the log level string hint 13 | FileLocation string `yaml:"file" json:"file-location" mapstructure:"file"` // the file path to write logs to 14 | } 15 | 16 | func (cfg logging) loadDefaultValues(v *viper.Viper) { 17 | v.SetDefault("log.structured", false) 18 | v.SetDefault("log.file", "") 19 | } 20 | -------------------------------------------------------------------------------- /internal/config/pkg.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/spf13/viper" 5 | 6 | "github.com/anchore/syft/syft/pkg/cataloger" 7 | ) 8 | 9 | type pkg struct { 10 | Cataloger catalogerOptions `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"` 11 | SearchUnindexedArchives bool `yaml:"search-unindexed-archives" json:"search-unindexed-archives" mapstructure:"search-unindexed-archives"` 12 | SearchIndexedArchives bool `yaml:"search-indexed-archives" json:"search-indexed-archives" mapstructure:"search-indexed-archives"` 13 | } 14 | 15 | func (cfg pkg) loadDefaultValues(v *viper.Viper) { 16 | cfg.Cataloger.loadDefaultValues(v) 17 | c := cataloger.DefaultSearchConfig() 18 | v.SetDefault("package.search-unindexed-archives", c.IncludeUnindexedArchives) 19 | v.SetDefault("package.search-indexed-archives", c.IncludeIndexedArchives) 20 | } 21 | 22 | func (cfg *pkg) parseConfigValues() error { 23 | return cfg.Cataloger.parseConfigValues() 24 | } 25 | 26 | func (cfg pkg) ToConfig() cataloger.Config { 27 | return cataloger.Config{ 28 | Search: cataloger.SearchConfig{ 29 | IncludeIndexedArchives: cfg.SearchIndexedArchives, 30 | IncludeUnindexedArchives: cfg.SearchUnindexedArchives, 31 | Scope: cfg.Cataloger.ScopeOpt, 32 | }, 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /internal/constants.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | const ( 4 | ApplicationName = "sbom-cli-plugin" 5 | BinaryName = "docker-sbom" 6 | SyftName = "syft" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/input.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | // IsPipedInput returns true if there is no input device, which means the user **may** be providing input via a pipe. 9 | func IsPipedInput() (bool, error) { 10 | fi, err := os.Stdin.Stat() 11 | if err != nil { 12 | return false, fmt.Errorf("unable to determine if there is piped input: %w", err) 13 | } 14 | 15 | // note: we should NOT use the absence of a character device here as the hint that there may be input expected 16 | // on stdin, as running syft as a subprocess you would expect no character device to be present but input can 17 | // be from either stdin or indicated by the CLI. Checking if stdin is a pipe is the most direct way to determine 18 | // if there *may* be bytes that will show up on stdin that should be used for the analysis source. 19 | return fi.Mode()&os.ModeNamedPipe != 0, nil 20 | } 21 | 22 | // IsTerminal returns true if there is a terminal present. 23 | func IsTerminal() bool { 24 | stat, _ := os.Stdin.Stat() 25 | return (stat.Mode() & os.ModeCharDevice) != 0 26 | } 27 | -------------------------------------------------------------------------------- /internal/log/log.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package log contains the singleton object and helper functions for facilitating logging within the syft library. 3 | */ 4 | package log 5 | 6 | import "github.com/anchore/syft/syft/logger" 7 | 8 | // Log is the singleton used to facilitate logging internally within syft 9 | var Log logger.Logger = &nopLogger{} 10 | 11 | // Errorf takes a formatted template string and template arguments for the error logging level. 12 | func Errorf(format string, args ...interface{}) { 13 | Log.Errorf(format, args...) 14 | } 15 | 16 | // Error logs the given arguments at the error logging level. 17 | func Error(args ...interface{}) { 18 | Log.Error(args...) 19 | } 20 | 21 | // Warnf takes a formatted template string and template arguments for the warning logging level. 22 | func Warnf(format string, args ...interface{}) { 23 | Log.Warnf(format, args...) 24 | } 25 | 26 | // Warn logs the given arguments at the warning logging level. 27 | func Warn(args ...interface{}) { 28 | Log.Warn(args...) 29 | } 30 | 31 | // Infof takes a formatted template string and template arguments for the info logging level. 32 | func Infof(format string, args ...interface{}) { 33 | Log.Infof(format, args...) 34 | } 35 | 36 | // Info logs the given arguments at the info logging level. 37 | func Info(args ...interface{}) { 38 | Log.Info(args...) 39 | } 40 | 41 | // Debugf takes a formatted template string and template arguments for the debug logging level. 42 | func Debugf(format string, args ...interface{}) { 43 | Log.Debugf(format, args...) 44 | } 45 | 46 | // Debug logs the given arguments at the debug logging level. 47 | func Debug(args ...interface{}) { 48 | Log.Debug(args...) 49 | } 50 | -------------------------------------------------------------------------------- /internal/log/nop.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | type nopLogger struct{} 4 | 5 | func (l *nopLogger) Errorf(format string, args ...interface{}) {} 6 | func (l *nopLogger) Error(args ...interface{}) {} 7 | func (l *nopLogger) Warnf(format string, args ...interface{}) {} 8 | func (l *nopLogger) Warn(args ...interface{}) {} 9 | func (l *nopLogger) Infof(format string, args ...interface{}) {} 10 | func (l *nopLogger) Info(args ...interface{}) {} 11 | func (l *nopLogger) Debugf(format string, args ...interface{}) {} 12 | func (l *nopLogger) Debug(args ...interface{}) {} 13 | -------------------------------------------------------------------------------- /internal/logger/logrus.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package logger contains implementations for the syft.logger.Logger interface. 3 | */ 4 | package logger 5 | 6 | import ( 7 | "fmt" 8 | "io" 9 | "io/fs" 10 | "io/ioutil" 11 | "os" 12 | 13 | "github.com/sirupsen/logrus" 14 | prefixed "github.com/x-cray/logrus-prefixed-formatter" 15 | ) 16 | 17 | const defaultLogFilePermissions fs.FileMode = 0644 18 | 19 | // LogrusConfig contains all configurable values for the Logrus logger 20 | type LogrusConfig struct { 21 | EnableConsole bool 22 | EnableFile bool 23 | Structured bool 24 | Level logrus.Level 25 | FileLocation string 26 | } 27 | 28 | // LogrusLogger contains all runtime values for using Logrus with the configured output target and input configuration values. 29 | type LogrusLogger struct { 30 | Config LogrusConfig 31 | Logger *logrus.Logger 32 | Output io.Writer 33 | } 34 | 35 | // LogrusNestedLogger is a wrapper for Logrus to enable nested logging configuration (loggers that always attach key-value pairs to all log entries) 36 | type LogrusNestedLogger struct { 37 | Logger *logrus.Entry 38 | } 39 | 40 | // NewLogrusLogger creates a new LogrusLogger with the given configuration 41 | func NewLogrusLogger(cfg LogrusConfig) *LogrusLogger { 42 | appLogger := logrus.New() 43 | 44 | var output io.Writer 45 | switch { 46 | case cfg.EnableConsole && cfg.EnableFile: 47 | logFile, err := os.OpenFile(cfg.FileLocation, os.O_WRONLY|os.O_CREATE, defaultLogFilePermissions) 48 | if err != nil { 49 | panic(fmt.Errorf("unable to setup log file: %w", err)) 50 | } 51 | output = io.MultiWriter(os.Stderr, logFile) 52 | case cfg.EnableConsole: 53 | output = os.Stderr 54 | case cfg.EnableFile: 55 | logFile, err := os.OpenFile(cfg.FileLocation, os.O_WRONLY|os.O_CREATE, defaultLogFilePermissions) 56 | if err != nil { 57 | panic(fmt.Errorf("unable to setup log file: %w", err)) 58 | } 59 | output = logFile 60 | default: 61 | output = ioutil.Discard 62 | } 63 | 64 | appLogger.SetOutput(output) 65 | appLogger.SetLevel(cfg.Level) 66 | 67 | if cfg.Structured { 68 | appLogger.SetFormatter(&logrus.JSONFormatter{ 69 | TimestampFormat: "2006-01-02 15:04:05", 70 | DisableTimestamp: false, 71 | DisableHTMLEscape: false, 72 | PrettyPrint: false, 73 | }) 74 | } else { 75 | appLogger.SetFormatter(&prefixed.TextFormatter{ 76 | TimestampFormat: "2006-01-02 15:04:05", 77 | ForceColors: true, 78 | ForceFormatting: true, 79 | }) 80 | } 81 | 82 | return &LogrusLogger{ 83 | Config: cfg, 84 | Logger: appLogger, 85 | Output: output, 86 | } 87 | } 88 | 89 | // Debugf takes a formatted template string and template arguments for the debug logging level. 90 | func (l *LogrusLogger) Debugf(format string, args ...interface{}) { 91 | l.Logger.Debugf(format, args...) 92 | } 93 | 94 | // Infof takes a formatted template string and template arguments for the info logging level. 95 | func (l *LogrusLogger) Infof(format string, args ...interface{}) { 96 | l.Logger.Infof(format, args...) 97 | } 98 | 99 | // Warnf takes a formatted template string and template arguments for the warning logging level. 100 | func (l *LogrusLogger) Warnf(format string, args ...interface{}) { 101 | l.Logger.Warnf(format, args...) 102 | } 103 | 104 | // Errorf takes a formatted template string and template arguments for the error logging level. 105 | func (l *LogrusLogger) Errorf(format string, args ...interface{}) { 106 | l.Logger.Errorf(format, args...) 107 | } 108 | 109 | // Debug logs the given arguments at the debug logging level. 110 | func (l *LogrusLogger) Debug(args ...interface{}) { 111 | l.Logger.Debug(args...) 112 | } 113 | 114 | // Info logs the given arguments at the info logging level. 115 | func (l *LogrusLogger) Info(args ...interface{}) { 116 | l.Logger.Info(args...) 117 | } 118 | 119 | // Warn logs the given arguments at the warning logging level. 120 | func (l *LogrusLogger) Warn(args ...interface{}) { 121 | l.Logger.Warn(args...) 122 | } 123 | 124 | // Error logs the given arguments at the error logging level. 125 | func (l *LogrusLogger) Error(args ...interface{}) { 126 | l.Logger.Error(args...) 127 | } 128 | 129 | // Debugf takes a formatted template string and template arguments for the debug logging level. 130 | func (l *LogrusNestedLogger) Debugf(format string, args ...interface{}) { 131 | l.Logger.Debugf(format, args...) 132 | } 133 | 134 | // Infof takes a formatted template string and template arguments for the info logging level. 135 | func (l *LogrusNestedLogger) Infof(format string, args ...interface{}) { 136 | l.Logger.Infof(format, args...) 137 | } 138 | 139 | // Warnf takes a formatted template string and template arguments for the warning logging level. 140 | func (l *LogrusNestedLogger) Warnf(format string, args ...interface{}) { 141 | l.Logger.Warnf(format, args...) 142 | } 143 | 144 | // Errorf takes a formatted template string and template arguments for the error logging level. 145 | func (l *LogrusNestedLogger) Errorf(format string, args ...interface{}) { 146 | l.Logger.Errorf(format, args...) 147 | } 148 | 149 | // Debug logs the given arguments at the debug logging level. 150 | func (l *LogrusNestedLogger) Debug(args ...interface{}) { 151 | l.Logger.Debug(args...) 152 | } 153 | 154 | // Info logs the given arguments at the info logging level. 155 | func (l *LogrusNestedLogger) Info(args ...interface{}) { 156 | l.Logger.Info(args...) 157 | } 158 | 159 | // Warn logs the given arguments at the warning logging level. 160 | func (l *LogrusNestedLogger) Warn(args ...interface{}) { 161 | l.Logger.Warn(args...) 162 | } 163 | 164 | // Error logs the given arguments at the error logging level. 165 | func (l *LogrusNestedLogger) Error(args ...interface{}) { 166 | l.Logger.Error(args...) 167 | } 168 | -------------------------------------------------------------------------------- /internal/schema.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | const SchemaVersion = "0.1.0" 4 | -------------------------------------------------------------------------------- /internal/ui/common_event_handlers.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/wagoodman/go-partybus" 7 | 8 | syftEventParsers "github.com/anchore/syft/syft/event/parsers" 9 | ) 10 | 11 | // handleExit is a UI function for processing the Exit bus event, 12 | // and calling the given function to output the contents. 13 | func handleExit(event partybus.Event) error { 14 | // show the report to stdout 15 | fn, err := syftEventParsers.ParseExit(event) 16 | if err != nil { 17 | return fmt.Errorf("bad CatalogerFinished event: %w", err) 18 | } 19 | 20 | if err := fn(); err != nil { 21 | return fmt.Errorf("unable to show package catalog report: %v", err) 22 | } 23 | return nil 24 | } 25 | -------------------------------------------------------------------------------- /internal/ui/ephemeral_terminal_ui.go: -------------------------------------------------------------------------------- 1 | //go:build linux || darwin 2 | // +build linux darwin 3 | 4 | package ui 5 | 6 | import ( 7 | "bytes" 8 | "context" 9 | "fmt" 10 | "io" 11 | "os" 12 | "sync" 13 | 14 | "github.com/docker/sbom-cli-plugin/internal/log" 15 | "github.com/docker/sbom-cli-plugin/internal/logger" 16 | "github.com/docker/sbom-cli-plugin/internal/version" 17 | "github.com/gookit/color" 18 | "github.com/wagoodman/go-partybus" 19 | "github.com/wagoodman/jotframe/pkg/frame" 20 | 21 | syftEvent "github.com/anchore/syft/syft/event" 22 | "github.com/anchore/syft/ui" 23 | ) 24 | 25 | // ephemeralTerminalUI provides an "ephemeral" terminal user interface to display the application state dynamically. 26 | // The terminal is placed into raw mode and the cursor is manipulated to allow for a dynamic, multi-line 27 | // UI (provided by the jotframe lib), for this reason all other application mechanisms that write to the screen 28 | // must be suppressed before starting (such as logs); since bytes in the device and in application memory combine to make 29 | // a shared state, bytes coming from elsewhere to the screen will disrupt this state. 30 | // 31 | // This UI is primarily driven off of events from the event bus, creating single-line terminal widgets to represent a 32 | // published element on the event bus, typically polling the element for the latest state. This allows for the UI to 33 | // control update frequency to the screen, provide "liveness" indications that are interpolated between bus events, 34 | // and overall loosely couple the bus events from screen interactions. 35 | // 36 | // By convention, all elements published on the bus should be treated as read-only, and publishers on the bus should 37 | // attempt to enforce this when possible by wrapping complex objects with interfaces to prescribe interactions. Also by 38 | // convention, each new event that the UI should respond to should be added either in this package as a handler function, 39 | // or in the shared ui package as a function on the main handler object. All handler functions should be completed 40 | // processing an event before the ETUI exits (coordinated with a sync.WaitGroup) 41 | type ephemeralTerminalUI struct { 42 | unsubscribe func() error 43 | handler *ui.Handler 44 | waitGroup *sync.WaitGroup 45 | frame *frame.Frame 46 | logBuffer *bytes.Buffer 47 | uiOutput *os.File 48 | } 49 | 50 | // NewEphemeralTerminalUI writes all events to a TUI and writes the final report to the given writer. 51 | func NewEphemeralTerminalUI() UI { 52 | return &ephemeralTerminalUI{ 53 | handler: ui.NewHandler(), 54 | waitGroup: &sync.WaitGroup{}, 55 | uiOutput: os.Stderr, 56 | } 57 | } 58 | 59 | func (h *ephemeralTerminalUI) Setup(unsubscribe func() error) error { 60 | h.unsubscribe = unsubscribe 61 | hideCursor(h.uiOutput) 62 | 63 | // prep the logger to not clobber the screen from now on (logrus only) 64 | h.logBuffer = bytes.NewBufferString("") 65 | logWrapper, ok := log.Log.(*logger.LogrusLogger) 66 | if ok { 67 | logWrapper.Logger.SetOutput(h.logBuffer) 68 | } 69 | 70 | return h.openScreen() 71 | } 72 | 73 | func (h *ephemeralTerminalUI) Handle(event partybus.Event) error { 74 | ctx := context.Background() 75 | switch { 76 | case h.handler.RespondsTo(event): 77 | if err := h.handler.Handle(ctx, h.frame, event, h.waitGroup); err != nil { 78 | log.Errorf("unable to show %s event: %+v", event.Type, err) 79 | } 80 | 81 | case event.Type == syftEvent.Exit: 82 | // we need to close the screen now since signaling the sbom is ready means that we 83 | // are about to write bytes to stdout, so we should reset the terminal state first 84 | h.closeScreen(false) 85 | 86 | if err := handleExit(event); err != nil { 87 | log.Errorf("unable to show %s event: %+v", event.Type, err) 88 | } 89 | 90 | // this is the last expected event, stop listening to events 91 | return h.unsubscribe() 92 | } 93 | return nil 94 | } 95 | 96 | func (h *ephemeralTerminalUI) openScreen() error { 97 | config := frame.Config{ 98 | PositionPolicy: frame.PolicyFloatForward, 99 | // only report output to stderr, reserve report output for stdout 100 | Output: h.uiOutput, 101 | } 102 | 103 | fr, err := frame.New(config) 104 | if err != nil { 105 | return fmt.Errorf("failed to create the screen object: %w", err) 106 | } 107 | h.frame = fr 108 | 109 | header, err := fr.AppendHeader() 110 | if err != nil { 111 | return err 112 | } 113 | 114 | content := color.Bold.Sprint("Syft") + " " + color.HEX("#777777").Sprint(version.FromBuild().SyftVersion) 115 | _, err = header.Write([]byte(content)) 116 | return err 117 | } 118 | 119 | func (h *ephemeralTerminalUI) closeScreen(force bool) { 120 | // we may have other background processes still displaying progress, wait for them to 121 | // finish before discontinuing dynamic content and showing the final report 122 | if !h.frame.IsClosed() { 123 | if !force { 124 | h.waitGroup.Wait() 125 | } 126 | h.frame.Close() 127 | // TODO: there is a race condition within frame.Close() that sometimes leads to an extra blank line being output 128 | frame.Close() 129 | 130 | // only flush the log on close 131 | h.flushLog() 132 | } 133 | } 134 | 135 | func (h *ephemeralTerminalUI) flushLog() { 136 | // flush any errors to the screen before the report 137 | logWrapper, ok := log.Log.(*logger.LogrusLogger) 138 | if ok { 139 | fmt.Fprint(logWrapper.Output, h.logBuffer.String()) 140 | logWrapper.Logger.SetOutput(h.uiOutput) 141 | } else { 142 | fmt.Fprint(h.uiOutput, h.logBuffer.String()) 143 | } 144 | } 145 | 146 | func (h *ephemeralTerminalUI) Teardown(force bool) error { 147 | h.closeScreen(force) 148 | showCursor(h.uiOutput) 149 | return nil 150 | } 151 | 152 | func hideCursor(output io.Writer) { 153 | fmt.Fprint(output, "\x1b[?25l") 154 | } 155 | 156 | func showCursor(output io.Writer) { 157 | fmt.Fprint(output, "\x1b[?25h") 158 | } 159 | -------------------------------------------------------------------------------- /internal/ui/logger_ui.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "github.com/docker/sbom-cli-plugin/internal/log" 5 | "github.com/wagoodman/go-partybus" 6 | 7 | syftEvent "github.com/anchore/syft/syft/event" 8 | ) 9 | 10 | type loggerUI struct { 11 | unsubscribe func() error 12 | } 13 | 14 | // NewLoggerUI writes all events to the common application logger and writes the final report to the given writer. 15 | func NewLoggerUI() UI { 16 | return &loggerUI{} 17 | } 18 | 19 | func (l *loggerUI) Setup(unsubscribe func() error) error { 20 | l.unsubscribe = unsubscribe 21 | return nil 22 | } 23 | 24 | func (l loggerUI) Handle(event partybus.Event) error { 25 | // ignore all events except for the final event 26 | if event.Type != syftEvent.Exit { 27 | return nil 28 | } 29 | 30 | if err := handleExit(event); err != nil { 31 | log.Warnf("unable to show catalog image finished event: %+v", err) 32 | } 33 | 34 | // this is the last expected event, stop listening to events 35 | return l.unsubscribe() 36 | } 37 | 38 | func (l loggerUI) Teardown(_ bool) error { 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /internal/ui/select.go: -------------------------------------------------------------------------------- 1 | //go:build linux || darwin 2 | // +build linux darwin 3 | 4 | package ui 5 | 6 | import ( 7 | "os" 8 | "runtime" 9 | 10 | "golang.org/x/term" 11 | ) 12 | 13 | // Select is responsible for determining the specific UI function given select user option, the current platform 14 | // config values, and environment status (such as a TTY being present). The first UI in the returned slice of UIs 15 | // is intended to be used and the UIs that follow are meant to be attempted only in a fallback posture when there 16 | // are environmental problems (e.g. cannot write to the terminal). A writer is provided to capture the output of 17 | // the final SBOM report. 18 | func Select(verbose, quiet bool) (uis []UI) { 19 | isStdoutATty := term.IsTerminal(int(os.Stdout.Fd())) 20 | isStderrATty := term.IsTerminal(int(os.Stderr.Fd())) 21 | notATerminal := !isStderrATty && !isStdoutATty 22 | 23 | switch { 24 | case runtime.GOOS == "windows" || verbose || quiet || notATerminal || !isStderrATty: 25 | uis = append(uis, NewLoggerUI()) 26 | default: 27 | uis = append(uis, NewEphemeralTerminalUI()) 28 | } 29 | 30 | return uis 31 | } 32 | -------------------------------------------------------------------------------- /internal/ui/select_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package ui 5 | 6 | // Select is responsible for determining the specific UI function given select user option, the current platform 7 | // config values, and environment status (such as a TTY being present). The first UI in the returned slice of UIs 8 | // is intended to be used and the UIs that follow are meant to be attempted only in a fallback posture when there 9 | // are environmental problems (e.g. cannot write to the terminal). A writer is provided to capture the output of 10 | // the final SBOM report. 11 | func Select(verbose, quiet bool) (uis []UI) { 12 | return append(uis, NewLoggerUI()) 13 | } 14 | -------------------------------------------------------------------------------- /internal/ui/ui.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "github.com/wagoodman/go-partybus" 5 | ) 6 | 7 | type UI interface { 8 | Setup(unsubscribe func() error) error 9 | partybus.Handler 10 | Teardown(force bool) error 11 | } 12 | -------------------------------------------------------------------------------- /internal/version/build.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package version contains all build time metadata (version, build time, git commit, etc). 3 | */ 4 | package version 5 | 6 | import ( 7 | "fmt" 8 | "runtime" 9 | "runtime/debug" 10 | 11 | "github.com/docker/sbom-cli-plugin/internal/log" 12 | ) 13 | 14 | const valueNotProvided = "[not provided]" 15 | 16 | // all variables here are provided as build-time arguments, with clear default values 17 | var ( 18 | version = valueNotProvided 19 | gitCommit = valueNotProvided 20 | gitDescription = valueNotProvided 21 | ) 22 | 23 | // Version defines the application version details (generally from build information) 24 | type Version struct { 25 | Version string `json:"version"` // application semantic version 26 | SyftVersion string `json:"syftVersion"` // the version of syft being used by the docker-sbom-cli-plugin 27 | GitCommit string `json:"gitCommit"` // git SHA at build-time 28 | GitDescription string `json:"gitDescription"` // output of 'git describe --dirty --always --tags' 29 | GoVersion string `json:"goVersion"` // go runtime version at build-time 30 | Compiler string `json:"compiler"` // compiler used at build-time 31 | Platform string `json:"platform"` // GOOS and GOARCH at build-time 32 | } 33 | 34 | // FromBuild provides all version details 35 | func FromBuild() Version { 36 | return Version{ 37 | Version: version, 38 | SyftVersion: syftVersion(), 39 | GitCommit: gitCommit, 40 | GitDescription: gitDescription, 41 | GoVersion: runtime.Version(), 42 | Compiler: runtime.Compiler, 43 | Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), 44 | } 45 | } 46 | 47 | func syftVersion() string { 48 | buildInfo, ok := debug.ReadBuildInfo() 49 | if !ok { 50 | log.Warn("unable to find the buildinfo section of the binary (syft version is unknown)") 51 | return valueNotProvided 52 | } 53 | 54 | for _, d := range buildInfo.Deps { 55 | if d.Path == "github.com/anchore/syft" { 56 | return d.Version 57 | } 58 | } 59 | 60 | log.Warn("unable to find 'github.com/anchore/syft' from the buildinfo section of the binary") 61 | 62 | return valueNotProvided 63 | } 64 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/docker/sbom-cli-plugin/cmd" 4 | 5 | func main() { 6 | cmd.Execute() 7 | } 8 | -------------------------------------------------------------------------------- /test/cli/all_formats_expressible_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/anchore/syft/syft" 9 | ) 10 | 11 | func TestAllFormatsExpressible(t *testing.T) { 12 | commonAssertions := []traitAssertion{ 13 | func(tb testing.TB, stdout, _ string, _ int) { 14 | tb.Helper() 15 | if len(stdout) < 1000 { 16 | tb.Errorf("there may not be any report output (len=%d)", len(stdout)) 17 | } 18 | }, 19 | assertSuccessfulReturnCode, 20 | } 21 | 22 | imageStr := getFixtureImage(t, "image-pkg-coverage") 23 | 24 | for _, o := range syft.FormatIDs() { 25 | t.Run(fmt.Sprintf("format:%s", o), func(t *testing.T) { 26 | cmd, stdout, stderr := runSyft(t, nil, "sbom", imageStr, "--format", string(o)) 27 | for _, traitFn := range commonAssertions { 28 | traitFn(t, stdout, stderr, cmd.ProcessState.ExitCode()) 29 | } 30 | if t.Failed() { 31 | t.Log("STDOUT:\n", stdout) 32 | t.Log("STDERR:\n", stderr) 33 | t.Log("COMMAND:", strings.Join(cmd.Args, " ")) 34 | } 35 | }) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/cli/sbom_cmd_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "path/filepath" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/docker/sbom-cli-plugin/internal" 9 | ) 10 | 11 | func TestSBOMCmdFlags(t *testing.T) { 12 | hiddenPackagesImage := getFixtureImage(t, "image-hidden-packages") 13 | coverageImage := getFixtureImage(t, "image-pkg-coverage") 14 | tmp := t.TempDir() + "/" 15 | 16 | tests := []struct { 17 | name string 18 | args []string 19 | env map[string]string 20 | assertions []traitAssertion 21 | }{ 22 | { 23 | name: "no-args-shows-help", 24 | args: []string{"sbom"}, 25 | assertions: []traitAssertion{ 26 | assertInOutput("an image argument is required"), // specific error that should be shown 27 | assertInOutput("View the packaged-based Software Bill Of Materials (SBOM) for an image"), // excerpt from help description 28 | assertFailingReturnCode, 29 | }, 30 | }, 31 | { 32 | name: "use-version-option", 33 | args: []string{"sbom", "version"}, 34 | assertions: []traitAssertion{ 35 | assertInOutput("Application:"), 36 | assertInOutput("docker-sbom ("), 37 | assertInOutput("Provider:"), 38 | assertInOutput("GitDescription:"), 39 | assertInOutput("syft (v0.46.3)"), 40 | assertNotInOutput("not provided"), 41 | assertSuccessfulReturnCode, 42 | }, 43 | }, 44 | { 45 | name: "use-short-version-option", 46 | args: []string{"sbom", "--version"}, 47 | assertions: []traitAssertion{ 48 | assertInOutput("sbom-cli-plugin"), 49 | assertInOutput(", build"), 50 | assertNotInOutput("not provided"), 51 | assertSuccessfulReturnCode, 52 | }, 53 | }, 54 | { 55 | name: "json-format-flag", 56 | args: []string{"sbom", "--format", "json", coverageImage}, 57 | assertions: []traitAssertion{ 58 | assertJsonReport, 59 | assertJsonDescriptor(internal.SyftName, "v0.46.3"), 60 | assertNotInOutput("not provided"), 61 | assertSuccessfulReturnCode, 62 | }, 63 | }, 64 | { 65 | name: "table-format-flag", 66 | args: []string{"sbom", "--format", "table", coverageImage}, 67 | assertions: []traitAssertion{ 68 | assertTableReport, 69 | assertSuccessfulReturnCode, 70 | }, 71 | }, 72 | { 73 | name: "default-format-flag", 74 | args: []string{"sbom", coverageImage}, 75 | assertions: []traitAssertion{ 76 | assertTableReport, 77 | assertSuccessfulReturnCode, 78 | }, 79 | }, 80 | { 81 | name: "squashed-scope-flag", 82 | args: []string{"sbom", "--format", "json", "--layers", "squashed", hiddenPackagesImage}, 83 | assertions: []traitAssertion{ 84 | assertPackageCount(162), 85 | assertInOutput("squashed"), 86 | assertNotInOutput("vsftpd"), // hidden package 87 | assertSuccessfulReturnCode, 88 | }, 89 | }, 90 | { 91 | name: "all-layers-scope-flag", 92 | args: []string{"sbom", "--format", "json", "--layers", "all-layers", hiddenPackagesImage}, 93 | assertions: []traitAssertion{ 94 | assertPackageCount(163), 95 | assertInOutput("all-layers"), 96 | assertInOutput("vsftpd"), // hidden package 97 | assertSuccessfulReturnCode, 98 | }, 99 | }, 100 | { 101 | name: "platform-option-wired-up", 102 | args: []string{"sbom", "--platform", "arm64", "--format", "json", "busybox:1.31"}, 103 | assertions: []traitAssertion{ 104 | assertInOutput("sha256:dcd4bbdd7ea2360002c684968429a2105997c3ce5821e84bfc2703c5ec984971"), // linux/arm64 image digest 105 | assertSuccessfulReturnCode, 106 | }, 107 | }, 108 | { 109 | name: "json-output-flag", 110 | args: []string{"sbom", "--format", "json", "--output", filepath.Join(tmp, "output-1.json"), coverageImage}, 111 | assertions: []traitAssertion{ 112 | assertSuccessfulReturnCode, 113 | assertFileOutput(t, filepath.Join(tmp, "output-1.json"), 114 | assertJsonReport, 115 | ), 116 | }, 117 | }, 118 | { 119 | name: "json-short-output-flag", 120 | args: []string{"sbom", "--format", "json", "-o", filepath.Join(tmp, "output-2.json"), coverageImage}, 121 | assertions: []traitAssertion{ 122 | assertSuccessfulReturnCode, 123 | assertFileOutput(t, filepath.Join(tmp, "output-2.json"), 124 | assertJsonReport, 125 | ), 126 | }, 127 | }, 128 | } 129 | 130 | for _, tt := range tests { 131 | t.Run(tt.name, func(t *testing.T) { 132 | cmd, stdout, stderr := runSyft(t, tt.env, tt.args...) 133 | for _, traitFn := range tt.assertions { 134 | traitFn(t, stdout, stderr, cmd.ProcessState.ExitCode()) 135 | } 136 | if t.Failed() { 137 | t.Log("STDOUT:\n", stdout) 138 | t.Log("STDERR:\n", stderr) 139 | t.Log("COMMAND:", strings.Join(cmd.Args, " ")) 140 | } 141 | }) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /test/cli/test-fixtures/image-hidden-packages/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM centos:7.9.2009 2 | # all-layers scope should pickup on vsftpd 3 | RUN yum install -y vsftpd 4 | RUN yum remove -y vsftpd -------------------------------------------------------------------------------- /test/cli/test-fixtures/image-pkg-coverage/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | COPY pkgs/ . 3 | # we duplicate to show a package count difference between all-layers and squashed scopes 4 | COPY lib lib 5 | COPY etc/ . -------------------------------------------------------------------------------- /test/cli/test-fixtures/image-pkg-coverage/composer/composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "746ba78c06aef0cf954135ea909f9eb9", 8 | "packages": [ 9 | { 10 | "name": "adoy/fastcgi-client", 11 | "version": "1.0.2", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/adoy/PHP-FastCGI-Client.git", 15 | "reference": "6d9a552f0206a1db7feb442824540aa6c55e5b27" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/adoy/PHP-FastCGI-Client/zipball/6d9a552f0206a1db7feb442824540aa6c55e5b27", 20 | "reference": "6d9a552f0206a1db7feb442824540aa6c55e5b27", 21 | "shasum": "" 22 | }, 23 | "type": "library", 24 | "autoload": { 25 | "psr-0": { 26 | "Adoy\\FastCGI\\": "src" 27 | } 28 | }, 29 | "notification-url": "https://packagist.org/downloads/", 30 | "license": [ 31 | "MIT" 32 | ], 33 | "authors": [ 34 | { 35 | "name": "Pierrick Charron", 36 | "email": "pierrick@adoy.net" 37 | } 38 | ], 39 | "description": "Lightweight, single file FastCGI client for PHP.", 40 | "keywords": [ 41 | "fastcgi", 42 | "fcgi" 43 | ], 44 | "time": "2019-12-11T13:49:21+00:00" 45 | }, 46 | { 47 | "name": "alcaeus/mongo-php-adapter", 48 | "version": "1.1.11", 49 | "source": { 50 | "type": "git", 51 | "url": "https://github.com/alcaeus/mongo-php-adapter.git", 52 | "reference": "43b6add94c8b4cb9890d662cba4c0defde733dcf" 53 | }, 54 | "dist": { 55 | "type": "zip", 56 | "url": "https://api.github.com/repos/alcaeus/mongo-php-adapter/zipball/43b6add94c8b4cb9890d662cba4c0defde733dcf", 57 | "reference": "43b6add94c8b4cb9890d662cba4c0defde733dcf", 58 | "shasum": "" 59 | }, 60 | "require": { 61 | "ext-ctype": "*", 62 | "ext-hash": "*", 63 | "ext-mongodb": "^1.2.0", 64 | "mongodb/mongodb": "^1.0.1", 65 | "php": "^5.6 || ^7.0" 66 | }, 67 | "provide": { 68 | "ext-mongo": "1.6.14" 69 | }, 70 | "require-dev": { 71 | "phpunit/phpunit": "^5.7.27 || ^6.0 || ^7.0", 72 | "squizlabs/php_codesniffer": "^3.2" 73 | }, 74 | "type": "library", 75 | "extra": { 76 | "branch-alias": { 77 | "dev-master": "1.1.x-dev" 78 | } 79 | }, 80 | "autoload": { 81 | "psr-0": { 82 | "Mongo": "lib/Mongo" 83 | }, 84 | "psr-4": { 85 | "Alcaeus\\MongoDbAdapter\\": "lib/Alcaeus/MongoDbAdapter" 86 | }, 87 | "files": [ 88 | "lib/Mongo/functions.php" 89 | ] 90 | }, 91 | "notification-url": "https://packagist.org/downloads/", 92 | "license": [ 93 | "MIT" 94 | ], 95 | "authors": [ 96 | { 97 | "name": "alcaeus", 98 | "email": "alcaeus@alcaeus.org" 99 | }, 100 | { 101 | "name": "Olivier Lechevalier", 102 | "email": "olivier.lechevalier@gmail.com" 103 | } 104 | ], 105 | "description": "Adapter to provide ext-mongo interface on top of mongo-php-libary", 106 | "keywords": [ 107 | "database", 108 | "mongodb" 109 | ], 110 | "time": "2019-11-11T20:47:32+00:00" 111 | } 112 | ], 113 | "packages-dev": [ 114 | { 115 | "name": "behat/gherkin", 116 | "version": "v4.6.2", 117 | "source": { 118 | "type": "git", 119 | "url": "https://github.com/Behat/Gherkin.git", 120 | "reference": "51ac4500c4dc30cbaaabcd2f25694299df666a31" 121 | }, 122 | "dist": { 123 | "type": "zip", 124 | "url": "https://api.github.com/repos/Behat/Gherkin/zipball/51ac4500c4dc30cbaaabcd2f25694299df666a31", 125 | "reference": "51ac4500c4dc30cbaaabcd2f25694299df666a31", 126 | "shasum": "" 127 | }, 128 | "require": { 129 | "php": ">=5.3.1" 130 | }, 131 | "require-dev": { 132 | "phpunit/phpunit": "~4.5|~5", 133 | "symfony/phpunit-bridge": "~2.7|~3|~4", 134 | "symfony/yaml": "~2.3|~3|~4" 135 | }, 136 | "suggest": { 137 | "symfony/yaml": "If you want to parse features, represented in YAML files" 138 | }, 139 | "type": "library", 140 | "extra": { 141 | "branch-alias": { 142 | "dev-master": "4.4-dev" 143 | } 144 | }, 145 | "autoload": { 146 | "psr-0": { 147 | "Behat\\Gherkin": "src/" 148 | } 149 | }, 150 | "notification-url": "https://packagist.org/downloads/", 151 | "license": [ 152 | "MIT" 153 | ], 154 | "authors": [ 155 | { 156 | "name": "Konstantin Kudryashov", 157 | "email": "ever.zet@gmail.com", 158 | "homepage": "http://everzet.com" 159 | } 160 | ], 161 | "description": "Gherkin DSL parser for PHP 5.3", 162 | "homepage": "http://behat.org/", 163 | "keywords": [ 164 | "BDD", 165 | "Behat", 166 | "Cucumber", 167 | "DSL", 168 | "gherkin", 169 | "parser" 170 | ], 171 | "time": "2020-03-17T14:03:26+00:00" 172 | }, 173 | { 174 | "name": "codeception/codeception", 175 | "version": "4.1.6", 176 | "source": { 177 | "type": "git", 178 | "url": "https://github.com/Codeception/Codeception.git", 179 | "reference": "5515b6a6c6f1e1c909aaff2e5f3a15c177dfd1a9" 180 | }, 181 | "dist": { 182 | "type": "zip", 183 | "url": "https://api.github.com/repos/Codeception/Codeception/zipball/5515b6a6c6f1e1c909aaff2e5f3a15c177dfd1a9", 184 | "reference": "5515b6a6c6f1e1c909aaff2e5f3a15c177dfd1a9", 185 | "shasum": "" 186 | }, 187 | "require": { 188 | "behat/gherkin": "^4.4.0", 189 | "codeception/lib-asserts": "^1.0", 190 | "codeception/phpunit-wrapper": ">6.0.15 <6.1.0 | ^6.6.1 | ^7.7.1 | ^8.1.1 | ^9.0", 191 | "codeception/stub": "^2.0 | ^3.0", 192 | "ext-curl": "*", 193 | "ext-json": "*", 194 | "ext-mbstring": "*", 195 | "guzzlehttp/psr7": "~1.4", 196 | "php": ">=5.6.0 <8.0", 197 | "symfony/console": ">=2.7 <6.0", 198 | "symfony/css-selector": ">=2.7 <6.0", 199 | "symfony/event-dispatcher": ">=2.7 <6.0", 200 | "symfony/finder": ">=2.7 <6.0", 201 | "symfony/yaml": ">=2.7 <6.0" 202 | }, 203 | "require-dev": { 204 | "codeception/module-asserts": "*@dev", 205 | "codeception/module-cli": "*@dev", 206 | "codeception/module-db": "*@dev", 207 | "codeception/module-filesystem": "*@dev", 208 | "codeception/module-phpbrowser": "*@dev", 209 | "codeception/specify": "~0.3", 210 | "codeception/util-universalframework": "*@dev", 211 | "monolog/monolog": "~1.8", 212 | "squizlabs/php_codesniffer": "~2.0", 213 | "symfony/process": ">=2.7 <6.0", 214 | "vlucas/phpdotenv": "^2.0 | ^3.0 | ^4.0" 215 | }, 216 | "suggest": { 217 | "codeception/specify": "BDD-style code blocks", 218 | "codeception/verify": "BDD-style assertions", 219 | "hoa/console": "For interactive console functionality", 220 | "stecman/symfony-console-completion": "For BASH autocompletion", 221 | "symfony/phpunit-bridge": "For phpunit-bridge support" 222 | }, 223 | "bin": [ 224 | "codecept" 225 | ], 226 | "type": "library", 227 | "extra": { 228 | "branch-alias": [] 229 | }, 230 | "autoload": { 231 | "psr-4": { 232 | "Codeception\\": "src/Codeception", 233 | "Codeception\\Extension\\": "ext" 234 | } 235 | }, 236 | "notification-url": "https://packagist.org/downloads/", 237 | "license": [ 238 | "MIT" 239 | ], 240 | "authors": [ 241 | { 242 | "name": "Michael Bodnarchuk", 243 | "email": "davert@mail.ua", 244 | "homepage": "http://codegyre.com" 245 | } 246 | ], 247 | "description": "BDD-style testing framework", 248 | "homepage": "http://codeception.com/", 249 | "keywords": [ 250 | "BDD", 251 | "TDD", 252 | "acceptance testing", 253 | "functional testing", 254 | "unit testing" 255 | ], 256 | "time": "2020-06-07T16:31:51+00:00" 257 | } 258 | ], 259 | "aliases": [], 260 | "minimum-stability": "stable", 261 | "stability-flags": { 262 | "intelligence/bt-base": 20, 263 | "intelligence/bt-common": 20, 264 | "intelligence/bt-lib-tools": 20, 265 | "intelligence/ci-lib-cache": 20, 266 | "intelligence/ci-lib-client": 20, 267 | "intelligence/ci-lib-contributed": 20, 268 | "intelligence/ci-lib-eav": 20, 269 | "intelligence/ci-lib-report": 20, 270 | "intelligence/git-leaks-elastic-client": 20, 271 | "intelligence/phishing-elastic-client": 20, 272 | "libs/gib-sso-client": 20, 273 | "mozhin/phplib": 20 274 | }, 275 | "prefer-stable": false, 276 | "prefer-lowest": false, 277 | "platform": { 278 | "php": "^7.4.0", 279 | "ext-amqp": "^1.9", 280 | "ext-ctype": "*", 281 | "ext-curl": "^7.4", 282 | "ext-date": "^7.4", 283 | "ext-fileinfo": "*", 284 | "ext-geoip": "^1.1", 285 | "ext-gettext": "*", 286 | "ext-iconv": "*", 287 | "ext-imagick": "^3.4", 288 | "ext-imap": "^7.4", 289 | "ext-intl": "^7.4", 290 | "ext-json": "*", 291 | "ext-mbstring": "^7.4", 292 | "ext-mongodb": "^1.4", 293 | "ext-mysqli": "^7.4", 294 | "ext-pdo_mysql": "^7.4", 295 | "ext-redis": ">=3.1" 296 | }, 297 | "platform-dev": [] 298 | } 299 | -------------------------------------------------------------------------------- /test/cli/test-fixtures/image-pkg-coverage/etc/os-release: -------------------------------------------------------------------------------- 1 | NAME="Ubuntu" 2 | VERSION="20.04 LTS (Focal Fossa)" 3 | ID=ubuntu 4 | ID_LIKE=debian 5 | PRETTY_NAME="Ubuntu 20.04 LTS" 6 | VERSION_ID="20.04" 7 | HOME_URL="https://www.ubuntu.com/" 8 | SUPPORT_URL="https://help.ubuntu.com/" 9 | BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" 10 | PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" 11 | VERSION_CODENAME=focal 12 | UBUNTU_CODENAME=focal 13 | -------------------------------------------------------------------------------- /test/cli/test-fixtures/image-pkg-coverage/lib/apk/db/installed: -------------------------------------------------------------------------------- 1 | C:Q1p78yvTLG094tHE1+dToJGbmYzQE= 2 | P:libc-utils 3 | V:0.7.2-r0 4 | A:x86_64 5 | S:1175 6 | I:4096 7 | T:Meta package to pull in correct libc 8 | U:http://alpinelinux.org 9 | L:BSD 10 | o:libc-dev 11 | m:Natanael Copa 12 | t:1575749004 13 | c:97b1c2842faa3bfa30f5811ffbf16d5ff9f1a479 14 | D:musl-utils 15 | 16 | C:Q1bTtF5526tETKfL+lnigzIDvm+2o= 17 | P:musl-utils 18 | V:1.1.24-r2 19 | A:x86_64 20 | S:37944 21 | I:151552 22 | T:the musl c library (libc) implementation 23 | U:https://musl.libc.org/ 24 | L:MIT BSD GPL2+ 25 | o:musl 26 | m:Timo Teräs 27 | t:1584790550 28 | c:4024cc3b29ad4c65544ad068b8f59172b5494306 29 | D:scanelf so:libc.musl-x86_64.so.1 30 | p:cmd:getconf cmd:getent cmd:iconv cmd:ldconfig cmd:ldd 31 | r:libiconv 32 | F:sbin 33 | R:ldconfig 34 | a:0:0:755 35 | Z:Q1Kja2+POZKxEkUOZqwSjC6kmaED4= 36 | F:usr 37 | F:usr/bin 38 | R:iconv 39 | a:0:0:755 40 | Z:Q1CVmFbdY+Hv6/jAHl1gec2Kbx1EY= 41 | R:ldd 42 | a:0:0:755 43 | Z:Q1yFAhGggmL7ERgbIA7KQxyTzf3ks= 44 | R:getconf 45 | a:0:0:755 46 | Z:Q1dAdYK8M/INibRQF5B3Rw7cmNDDA= 47 | R:getent 48 | a:0:0:755 49 | Z:Q1eR2Dz/WylabgbWMTkd2+hGmEya4= 50 | -------------------------------------------------------------------------------- /test/cli/test-fixtures/image-pkg-coverage/pkgs/go/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/anchore/syft 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/bmatcuk/doublestar v1.3.1 7 | ) 8 | -------------------------------------------------------------------------------- /test/cli/test-fixtures/image-pkg-coverage/pkgs/java/example-java-app-maven-0.1.0.jar: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:93e3cea225b18e08da7891f79fc4bc7642903940023698d9b4dc93b94b340873 3 | size 628541 4 | -------------------------------------------------------------------------------- /test/cli/test-fixtures/image-pkg-coverage/pkgs/java/example-jenkins-plugin.hpi: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:0f40996e0af30cbb5ed6b0c697dee396492db23c47343eda6649b639d95f337f 3 | size 10417 4 | -------------------------------------------------------------------------------- /test/cli/test-fixtures/image-pkg-coverage/pkgs/java/generate-fixtures.md: -------------------------------------------------------------------------------- 1 | See the syft/cataloger/java/test-fixtures/java-builds dir to generate test fixtures and copy to here manually. -------------------------------------------------------------------------------- /test/cli/test-fixtures/image-pkg-coverage/pkgs/javascript/package-json/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "6.14.6", 3 | "name": "npm", 4 | "description": "a package manager for JavaScript", 5 | "keywords": [ 6 | "install", 7 | "modules", 8 | "package manager", 9 | "package.json" 10 | ], 11 | "preferGlobal": true, 12 | "config": { 13 | "publishtest": false 14 | }, 15 | "homepage": "https://docs.npmjs.com/", 16 | "author": "Isaac Z. Schlueter (http://blog.izs.me)", 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/npm/cli" 20 | }, 21 | "bugs": { 22 | "url": "https://npm.community/c/bugs" 23 | }, 24 | "directories": { 25 | "bin": "./bin", 26 | "doc": "./doc", 27 | "lib": "./lib", 28 | "man": "./man" 29 | }, 30 | "main": "./lib/npm.js", 31 | "bin": { 32 | "npm": "./bin/npm-cli.js", 33 | "npx": "./bin/npx-cli.js" 34 | }, 35 | "dependencies": { 36 | "JSONStream": "^1.3.5", 37 | "abbrev": "~1.1.1", 38 | "ansicolors": "~0.3.2", 39 | "ansistyles": "~0.1.3", 40 | "aproba": "^2.0.0", 41 | "archy": "~1.0.0", 42 | "bin-links": "^1.1.7", 43 | "bluebird": "^3.5.5", 44 | "byte-size": "^5.0.1", 45 | "cacache": "^12.0.3", 46 | "call-limit": "^1.1.1", 47 | "chownr": "^1.1.4", 48 | "ci-info": "^2.0.0", 49 | "cli-columns": "^3.1.2", 50 | "cli-table3": "^0.5.1", 51 | "cmd-shim": "^3.0.3", 52 | "columnify": "~1.5.4", 53 | "config-chain": "^1.1.12", 54 | "detect-indent": "~5.0.0", 55 | "detect-newline": "^2.1.0", 56 | "dezalgo": "~1.0.3", 57 | "editor": "~1.0.0", 58 | "figgy-pudding": "^3.5.1", 59 | "find-npm-prefix": "^1.0.2", 60 | "fs-vacuum": "~1.2.10", 61 | "fs-write-stream-atomic": "~1.0.10", 62 | "gentle-fs": "^2.3.0", 63 | "glob": "^7.1.6", 64 | "graceful-fs": "^4.2.4", 65 | "has-unicode": "~2.0.1", 66 | "hosted-git-info": "^2.8.8", 67 | "iferr": "^1.0.2", 68 | "infer-owner": "^1.0.4", 69 | "inflight": "~1.0.6", 70 | "inherits": "^2.0.4", 71 | "ini": "^1.3.5", 72 | "init-package-json": "^1.10.3", 73 | "is-cidr": "^3.0.0", 74 | "json-parse-better-errors": "^1.0.2", 75 | "lazy-property": "~1.0.0", 76 | "libcipm": "^4.0.7", 77 | "libnpm": "^3.0.1", 78 | "libnpmaccess": "^3.0.2", 79 | "libnpmhook": "^5.0.3", 80 | "libnpmorg": "^1.0.1", 81 | "libnpmsearch": "^2.0.2", 82 | "libnpmteam": "^1.0.2", 83 | "libnpx": "^10.2.2", 84 | "lock-verify": "^2.1.0", 85 | "lockfile": "^1.0.4", 86 | "lodash._baseuniq": "~4.6.0", 87 | "lodash.clonedeep": "~4.5.0", 88 | "lodash.union": "~4.6.0", 89 | "lodash.uniq": "~4.5.0", 90 | "lodash.without": "~4.4.0", 91 | "lru-cache": "^5.1.1", 92 | "meant": "~1.0.1", 93 | "mississippi": "^3.0.0", 94 | "mkdirp": "^0.5.5", 95 | "move-concurrently": "^1.0.1", 96 | "node-gyp": "^5.1.0", 97 | "nopt": "^4.0.3", 98 | "normalize-package-data": "^2.5.0", 99 | "npm-audit-report": "^1.3.2", 100 | "npm-cache-filename": "~1.0.2", 101 | "npm-install-checks": "^3.0.2", 102 | "npm-lifecycle": "^3.1.4", 103 | "npm-package-arg": "^6.1.1", 104 | "npm-packlist": "^1.4.8", 105 | "npm-pick-manifest": "^3.0.2", 106 | "npm-profile": "^4.0.4", 107 | "npm-registry-fetch": "^4.0.5", 108 | "npm-user-validate": "~1.0.0", 109 | "npmlog": "~4.1.2", 110 | "once": "~1.4.0", 111 | "opener": "^1.5.1", 112 | "osenv": "^0.1.5", 113 | "pacote": "^9.5.12", 114 | "path-is-inside": "~1.0.2", 115 | "promise-inflight": "~1.0.1", 116 | "qrcode-terminal": "^0.12.0", 117 | "query-string": "^6.8.2", 118 | "qw": "~1.0.1", 119 | "read": "~1.0.7", 120 | "read-cmd-shim": "^1.0.5", 121 | "read-installed": "~4.0.3", 122 | "read-package-json": "^2.1.1", 123 | "read-package-tree": "^5.3.1", 124 | "readable-stream": "^3.6.0", 125 | "readdir-scoped-modules": "^1.1.0", 126 | "request": "^2.88.0", 127 | "retry": "^0.12.0", 128 | "rimraf": "^2.7.1", 129 | "safe-buffer": "^5.1.2", 130 | "semver": "^5.7.1", 131 | "sha": "^3.0.0", 132 | "slide": "~1.1.6", 133 | "sorted-object": "~2.0.1", 134 | "sorted-union-stream": "~2.1.3", 135 | "ssri": "^6.0.1", 136 | "stringify-package": "^1.0.1", 137 | "tar": "^4.4.13", 138 | "text-table": "~0.2.0", 139 | "tiny-relative-date": "^1.3.0", 140 | "uid-number": "0.0.6", 141 | "umask": "~1.1.0", 142 | "unique-filename": "^1.1.1", 143 | "unpipe": "~1.0.0", 144 | "update-notifier": "^2.5.0", 145 | "uuid": "^3.3.3", 146 | "validate-npm-package-license": "^3.0.4", 147 | "validate-npm-package-name": "~3.0.0", 148 | "which": "^1.3.1", 149 | "worker-farm": "^1.7.0", 150 | "write-file-atomic": "^2.4.3" 151 | }, 152 | "bundleDependencies": [ 153 | "abbrev", 154 | "ansicolors", 155 | "ansistyles", 156 | "aproba", 157 | "archy", 158 | "bin-links", 159 | "bluebird", 160 | "byte-size", 161 | "cacache", 162 | "call-limit", 163 | "chownr", 164 | "ci-info", 165 | "cli-columns", 166 | "cli-table3", 167 | "cmd-shim", 168 | "columnify", 169 | "config-chain", 170 | "debuglog", 171 | "detect-indent", 172 | "detect-newline", 173 | "dezalgo", 174 | "editor", 175 | "figgy-pudding", 176 | "find-npm-prefix", 177 | "fs-vacuum", 178 | "fs-write-stream-atomic", 179 | "gentle-fs", 180 | "glob", 181 | "graceful-fs", 182 | "has-unicode", 183 | "hosted-git-info", 184 | "iferr", 185 | "imurmurhash", 186 | "infer-owner", 187 | "inflight", 188 | "inherits", 189 | "ini", 190 | "init-package-json", 191 | "is-cidr", 192 | "json-parse-better-errors", 193 | "JSONStream", 194 | "lazy-property", 195 | "libcipm", 196 | "libnpm", 197 | "libnpmaccess", 198 | "libnpmhook", 199 | "libnpmorg", 200 | "libnpmsearch", 201 | "libnpmteam", 202 | "libnpx", 203 | "lock-verify", 204 | "lockfile", 205 | "lodash._baseindexof", 206 | "lodash._baseuniq", 207 | "lodash._bindcallback", 208 | "lodash._cacheindexof", 209 | "lodash._createcache", 210 | "lodash._getnative", 211 | "lodash.clonedeep", 212 | "lodash.restparam", 213 | "lodash.union", 214 | "lodash.uniq", 215 | "lodash.without", 216 | "lru-cache", 217 | "meant", 218 | "mississippi", 219 | "mkdirp", 220 | "move-concurrently", 221 | "node-gyp", 222 | "nopt", 223 | "normalize-package-data", 224 | "npm-audit-report", 225 | "npm-cache-filename", 226 | "npm-install-checks", 227 | "npm-lifecycle", 228 | "npm-package-arg", 229 | "npm-packlist", 230 | "npm-pick-manifest", 231 | "npm-profile", 232 | "npm-registry-fetch", 233 | "npm-user-validate", 234 | "npmlog", 235 | "once", 236 | "opener", 237 | "osenv", 238 | "pacote", 239 | "path-is-inside", 240 | "promise-inflight", 241 | "qrcode-terminal", 242 | "query-string", 243 | "qw", 244 | "read-cmd-shim", 245 | "read-installed", 246 | "read-package-json", 247 | "read-package-tree", 248 | "read", 249 | "readable-stream", 250 | "readdir-scoped-modules", 251 | "request", 252 | "retry", 253 | "rimraf", 254 | "safe-buffer", 255 | "semver", 256 | "sha", 257 | "slide", 258 | "sorted-object", 259 | "sorted-union-stream", 260 | "ssri", 261 | "stringify-package", 262 | "tar", 263 | "text-table", 264 | "tiny-relative-date", 265 | "uid-number", 266 | "umask", 267 | "unique-filename", 268 | "unpipe", 269 | "update-notifier", 270 | "uuid", 271 | "validate-npm-package-license", 272 | "validate-npm-package-name", 273 | "which", 274 | "worker-farm", 275 | "write-file-atomic" 276 | ], 277 | "devDependencies": { 278 | "deep-equal": "^1.0.1", 279 | "get-stream": "^4.1.0", 280 | "licensee": "^7.0.3", 281 | "marked": "^0.6.3", 282 | "marked-man": "^0.6.0", 283 | "npm-registry-couchapp": "^2.7.4", 284 | "npm-registry-mock": "^1.3.1", 285 | "require-inject": "^1.4.4", 286 | "sprintf-js": "^1.1.2", 287 | "standard": "^11.0.1", 288 | "tacks": "^1.3.0", 289 | "tap": "^12.7.0", 290 | "tar-stream": "^2.1.0" 291 | }, 292 | "scripts": { 293 | "dumpconf": "env | grep npm | sort | uniq", 294 | "prepare": "node bin/npm-cli.js rebuild && node bin/npm-cli.js --no-audit --no-timing prune --prefix=. --no-global && rimraf test/*/*/node_modules && make -j4 mandocs", 295 | "preversion": "bash scripts/update-authors.sh && git add AUTHORS && git commit -m \"update AUTHORS\" || true", 296 | "licenses": "licensee --production --errors-only", 297 | "tap": "tap -J --timeout 300 --no-esm", 298 | "tap-cover": "tap -J --nyc-arg=--cache --coverage --timeout 600 --no-esm", 299 | "lint": "standard", 300 | "pretest": "npm run lint", 301 | "test": "npm run test-tap --", 302 | "test:nocleanup": "NO_TEST_CLEANUP=1 npm run test --", 303 | "sudotest": "sudo npm run tap -- \"test/tap/*.js\"", 304 | "sudotest:nocleanup": "sudo NO_TEST_CLEANUP=1 npm run tap -- \"test/tap/*.js\"", 305 | "posttest": "rimraf test/npm_cache*", 306 | "test-coverage": "npm run tap-cover -- \"test/tap/*.js\" \"test/network/*.js\"", 307 | "test-tap": "npm run tap -- \"test/tap/*.js\" \"test/network/*.js\"", 308 | "test-node": "tap --timeout 240 \"test/tap/*.js\" \"test/network/*.js\"" 309 | }, 310 | "license": "Artistic-2.0", 311 | "engines": { 312 | "node": "6 >=6.2.0 || 8 || >=9.3.0" 313 | } 314 | } -------------------------------------------------------------------------------- /test/cli/test-fixtures/image-pkg-coverage/pkgs/javascript/package-lock/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "requires": true, 3 | "lockfileVersion": 1, 4 | "dependencies": { 5 | "get-stdin": { 6 | "version": "8.0.0", 7 | "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", 8 | "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/cli/test-fixtures/image-pkg-coverage/pkgs/javascript/yarn/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4": 6 | version "7.10.4" 7 | resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" 8 | integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== 9 | dependencies: 10 | "@babel/highlight" "^7.10.4" 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/cli/test-fixtures/image-pkg-coverage/pkgs/lib/apk/db/installed: -------------------------------------------------------------------------------- 1 | C:Q1p78yvTLG094tHE1+dToJGbmYzQE= 2 | P:libc-utils 3 | V:0.7.2-r0 4 | A:x86_64 5 | S:1175 6 | I:4096 7 | T:Meta package to pull in correct libc 8 | U:http://alpinelinux.org 9 | L:BSD 10 | o:libc-dev 11 | m:Natanael Copa 12 | t:1575749004 13 | c:97b1c2842faa3bfa30f5811ffbf16d5ff9f1a479 14 | D:musl-utils 15 | 16 | C:Q1bTtF5526tETKfL+lnigzIDvm+2o= 17 | P:musl-utils 18 | V:1.1.24-r2 19 | A:x86_64 20 | S:37944 21 | I:151552 22 | T:the musl c library (libc) implementation 23 | U:https://musl.libc.org/ 24 | L:MIT BSD GPL2+ 25 | o:musl 26 | m:Timo Teräs 27 | t:1584790550 28 | c:4024cc3b29ad4c65544ad068b8f59172b5494306 29 | D:scanelf so:libc.musl-x86_64.so.1 30 | p:cmd:getconf cmd:getent cmd:iconv cmd:ldconfig cmd:ldd 31 | r:libiconv 32 | F:sbin 33 | R:ldconfig 34 | a:0:0:755 35 | Z:Q1Kja2+POZKxEkUOZqwSjC6kmaED4= 36 | F:usr 37 | F:usr/bin 38 | R:iconv 39 | a:0:0:755 40 | Z:Q1CVmFbdY+Hv6/jAHl1gec2Kbx1EY= 41 | R:ldd 42 | a:0:0:755 43 | Z:Q1yFAhGggmL7ERgbIA7KQxyTzf3ks= 44 | R:getconf 45 | a:0:0:755 46 | Z:Q1dAdYK8M/INibRQF5B3Rw7cmNDDA= 47 | R:getent 48 | a:0:0:755 49 | Z:Q1eR2Dz/WylabgbWMTkd2+hGmEya4= -------------------------------------------------------------------------------- /test/cli/test-fixtures/image-pkg-coverage/pkgs/php/vendor/composer/installed.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | { 4 | "name": "nikic/fast-route", 5 | "version": "v1.3.0", 6 | "version_normalized": "1.3.0.0", 7 | "source": { 8 | "type": "git", 9 | "url": "https://github.com/nikic/FastRoute.git", 10 | "reference": "181d480e08d9476e61381e04a71b34dc0432e812" 11 | }, 12 | "dist": { 13 | "type": "zip", 14 | "url": "https://api.github.com/repos/nikic/FastRoute/zipball/181d480e08d9476e61381e04a71b34dc0432e812", 15 | "reference": "181d480e08d9476e61381e04a71b34dc0432e812", 16 | "shasum": "" 17 | }, 18 | "require": { 19 | "php": ">=5.4.0" 20 | }, 21 | "require-dev": { 22 | "phpunit/phpunit": "^4.8.35|~5.7" 23 | }, 24 | "time": "2018-02-13T20:26:39+00:00", 25 | "type": "library", 26 | "installation-source": "dist", 27 | "autoload": { 28 | "psr-4": { 29 | "FastRoute\\": "src/" 30 | }, 31 | "files": [ 32 | "src/functions.php" 33 | ] 34 | }, 35 | "notification-url": "https://packagist.org/downloads/", 36 | "license": [ 37 | "BSD-3-Clause" 38 | ], 39 | "authors": [ 40 | { 41 | "name": "Nikita Popov", 42 | "email": "nikic@php.net" 43 | } 44 | ], 45 | "description": "Fast request router for PHP", 46 | "keywords": [ 47 | "router", 48 | "routing" 49 | ], 50 | "support": { 51 | "issues": "https://github.com/nikic/FastRoute/issues", 52 | "source": "https://github.com/nikic/FastRoute/tree/master" 53 | }, 54 | "install-path": "../nikic/fast-route" 55 | }, 56 | { 57 | "name": "psr/container", 58 | "version": "2.0.2", 59 | "version_normalized": "2.0.2.0", 60 | "source": { 61 | "type": "git", 62 | "url": "https://github.com/php-fig/container.git", 63 | "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" 64 | }, 65 | "dist": { 66 | "type": "zip", 67 | "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", 68 | "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", 69 | "shasum": "" 70 | }, 71 | "require": { 72 | "php": ">=7.4.0" 73 | }, 74 | "time": "2021-11-05T16:47:00+00:00", 75 | "type": "library", 76 | "extra": { 77 | "branch-alias": { 78 | "dev-master": "2.0.x-dev" 79 | } 80 | }, 81 | "installation-source": "dist", 82 | "autoload": { 83 | "psr-4": { 84 | "Psr\\Container\\": "src/" 85 | } 86 | }, 87 | "notification-url": "https://packagist.org/downloads/", 88 | "license": [ 89 | "MIT" 90 | ], 91 | "authors": [ 92 | { 93 | "name": "PHP-FIG", 94 | "homepage": "https://www.php-fig.org/" 95 | } 96 | ], 97 | "description": "Common Container Interface (PHP FIG PSR-11)", 98 | "homepage": "https://github.com/php-fig/container", 99 | "keywords": [ 100 | "PSR-11", 101 | "container", 102 | "container-interface", 103 | "container-interop", 104 | "psr" 105 | ], 106 | "support": { 107 | "issues": "https://github.com/php-fig/container/issues", 108 | "source": "https://github.com/php-fig/container/tree/2.0.2" 109 | }, 110 | "install-path": "../psr/container" 111 | }, 112 | { 113 | "name": "psr/http-factory", 114 | "version": "1.0.1", 115 | "version_normalized": "1.0.1.0", 116 | "source": { 117 | "type": "git", 118 | "url": "https://github.com/php-fig/http-factory.git", 119 | "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be" 120 | }, 121 | "dist": { 122 | "type": "zip", 123 | "url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be", 124 | "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be", 125 | "shasum": "" 126 | }, 127 | "require": { 128 | "php": ">=7.0.0", 129 | "psr/http-message": "^1.0" 130 | }, 131 | "time": "2019-04-30T12:38:16+00:00", 132 | "type": "library", 133 | "extra": { 134 | "branch-alias": { 135 | "dev-master": "1.0.x-dev" 136 | } 137 | }, 138 | "installation-source": "dist", 139 | "autoload": { 140 | "psr-4": { 141 | "Psr\\Http\\Message\\": "src/" 142 | } 143 | }, 144 | "notification-url": "https://packagist.org/downloads/", 145 | "license": [ 146 | "MIT" 147 | ], 148 | "authors": [ 149 | { 150 | "name": "PHP-FIG", 151 | "homepage": "http://www.php-fig.org/" 152 | } 153 | ], 154 | "description": "Common interfaces for PSR-7 HTTP message factories", 155 | "keywords": [ 156 | "factory", 157 | "http", 158 | "message", 159 | "psr", 160 | "psr-17", 161 | "psr-7", 162 | "request", 163 | "response" 164 | ], 165 | "support": { 166 | "source": "https://github.com/php-fig/http-factory/tree/master" 167 | }, 168 | "install-path": "../psr/http-factory" 169 | } 170 | ], 171 | "dev": true, 172 | "dev-package-names": [] 173 | } 174 | -------------------------------------------------------------------------------- /test/cli/test-fixtures/image-pkg-coverage/pkgs/python/dist-info/METADATA: -------------------------------------------------------------------------------- 1 | Metadata-Version: 2.1 2 | Name: Pygments 3 | Version: 2.6.1 4 | Summary: Pygments is a syntax highlighting package written in Python. 5 | Home-page: https://pygments.org/ 6 | Author: Georg Brandl 7 | Author-email: georg@python.org 8 | License: BSD License 9 | Keywords: syntax highlighting 10 | Platform: any 11 | Classifier: License :: OSI Approved :: BSD License 12 | Classifier: Intended Audience :: Developers 13 | Classifier: Intended Audience :: End Users/Desktop 14 | Classifier: Intended Audience :: System Administrators 15 | Classifier: Development Status :: 6 - Mature 16 | Classifier: Programming Language :: Python 17 | Classifier: Programming Language :: Python :: 3 18 | Classifier: Programming Language :: Python :: 3.5 19 | Classifier: Programming Language :: Python :: 3.6 20 | Classifier: Programming Language :: Python :: 3.7 21 | Classifier: Programming Language :: Python :: 3.8 22 | Classifier: Programming Language :: Python :: Implementation :: CPython 23 | Classifier: Programming Language :: Python :: Implementation :: PyPy 24 | Classifier: Operating System :: OS Independent 25 | Classifier: Topic :: Text Processing :: Filters 26 | Classifier: Topic :: Utilities 27 | Requires-Python: >=3.5 28 | 29 | 30 | Pygments 31 | ~~~~~~~~ 32 | 33 | Pygments is a syntax highlighting package written in Python. 34 | 35 | It is a generic syntax highlighter suitable for use in code hosting, forums, 36 | wikis or other applications that need to prettify source code. Highlights 37 | are: 38 | 39 | * a wide range of over 500 languages and other text formats is supported 40 | * special attention is paid to details, increasing quality by a fair amount 41 | * support for new languages and formats are added easily 42 | * a number of output formats, presently HTML, LaTeX, RTF, SVG, all image formats that PIL supports and ANSI sequences 43 | * it is usable as a command-line tool and as a library 44 | 45 | :copyright: Copyright 2006-2019 by the Pygments team, see AUTHORS. 46 | :license: BSD, see LICENSE for details. 47 | 48 | -------------------------------------------------------------------------------- /test/cli/test-fixtures/image-pkg-coverage/pkgs/python/dist-info/RECORD: -------------------------------------------------------------------------------- 1 | ../../../bin/pygmentize,sha256=dDhv_U2jiCpmFQwIRHpFRLAHUO4R1jIJPEvT_QYTFp8,220 2 | Pygments-2.6.1.dist-info/AUTHORS,sha256=PVpa2_Oku6BGuiUvutvuPnWGpzxqFy2I8-NIrqCvqUY,8449 3 | Pygments-2.6.1.dist-info/RECORD,, 4 | pygments/__pycache__/__init__.cpython-38.pyc,, 5 | pygments/util.py,sha256=586xXHiJGGZxqk5PMBu3vBhE68DLuAe5MBARWrSPGxA,10778 -------------------------------------------------------------------------------- /test/cli/test-fixtures/image-pkg-coverage/pkgs/python/dist-info/top_level.txt: -------------------------------------------------------------------------------- 1 | top-level-pkg -------------------------------------------------------------------------------- /test/cli/test-fixtures/image-pkg-coverage/pkgs/python/egg-info/PKG-INFO: -------------------------------------------------------------------------------- 1 | Metadata-Version: 2.1 2 | Name: requests 3 | Version: 2.22.0 4 | Summary: Python HTTP for Humans. 5 | Home-page: http://python-requests.org 6 | Author: Kenneth Reitz 7 | Author-email: me@kennethreitz.org 8 | License: Apache 2.0 9 | Description: Requests: HTTP for Humans™ 10 | ========================== 11 | 12 | [![image](https://img.shields.io/pypi/v/requests.svg)](https://pypi.org/project/requests/) 13 | [![image](https://img.shields.io/pypi/l/requests.svg)](https://pypi.org/project/requests/) 14 | [![image](https://img.shields.io/pypi/pyversions/requests.svg)](https://pypi.org/project/requests/) 15 | [![codecov.io](https://codecov.io/github/requests/requests/coverage.svg?branch=master)](https://codecov.io/github/requests/requests) 16 | [![image](https://img.shields.io/github/contributors/requests/requests.svg)](https://github.com/requests/requests/graphs/contributors) 17 | [![image](https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg)](https://saythanks.io/to/kennethreitz) 18 | 19 | Requests is the only *Non-GMO* HTTP library for Python, safe for human 20 | consumption. 21 | 22 | ![image](https://farm5.staticflickr.com/4317/35198386374_1939af3de6_k_d.jpg) 23 | 24 | Behold, the power of Requests: 25 | 26 | ``` {.sourceCode .python} 27 | >>> import requests 28 | >>> r = requests.get('https://api.github.com/user', auth=('user', 'pass')) 29 | >>> r.status_code 30 | 200 31 | >>> r.headers['content-type'] 32 | 'application/json; charset=utf8' 33 | >>> r.encoding 34 | 'utf-8' 35 | >>> r.text 36 | u'{"type":"User"...' 37 | >>> r.json() 38 | {u'disk_usage': 368627, u'private_gists': 484, ...} 39 | ``` 40 | 41 | See [the similar code, sans Requests](https://gist.github.com/973705). 42 | 43 | [![image](https://raw.githubusercontent.com/requests/requests/master/docs/_static/requests-logo-small.png)](http://docs.python-requests.org/) 44 | 45 | Requests allows you to send *organic, grass-fed* HTTP/1.1 requests, 46 | without the need for manual labor. There's no need to manually add query 47 | strings to your URLs, or to form-encode your POST data. Keep-alive and 48 | HTTP connection pooling are 100% automatic, thanks to 49 | [urllib3](https://github.com/shazow/urllib3). 50 | 51 | Besides, all the cool kids are doing it. Requests is one of the most 52 | downloaded Python packages of all time, pulling in over 11,000,000 53 | downloads every month. You don't want to be left out! 54 | 55 | Feature Support 56 | --------------- 57 | 58 | Requests is ready for today's web. 59 | 60 | - International Domains and URLs 61 | - Keep-Alive & Connection Pooling 62 | - Sessions with Cookie Persistence 63 | - Browser-style SSL Verification 64 | - Basic/Digest Authentication 65 | - Elegant Key/Value Cookies 66 | - Automatic Decompression 67 | - Automatic Content Decoding 68 | - Unicode Response Bodies 69 | - Multipart File Uploads 70 | - HTTP(S) Proxy Support 71 | - Connection Timeouts 72 | - Streaming Downloads 73 | - `.netrc` Support 74 | - Chunked Requests 75 | 76 | Requests officially supports Python 2.7 & 3.4–3.7, and runs great on 77 | PyPy. 78 | 79 | Installation 80 | ------------ 81 | 82 | To install Requests, simply use [pipenv](http://pipenv.org/) (or pip, of 83 | course): 84 | 85 | ``` {.sourceCode .bash} 86 | $ pipenv install requests 87 | ✨🍰✨ 88 | ``` 89 | 90 | Satisfaction guaranteed. 91 | 92 | Documentation 93 | ------------- 94 | 95 | Fantastic documentation is available at 96 | , for a limited time only. 97 | 98 | How to Contribute 99 | ----------------- 100 | 101 | 1. Become more familiar with the project by reading our [Contributor's Guide](http://docs.python-requests.org/en/latest/dev/contributing/) and our [development philosophy](http://docs.python-requests.org/en/latest/dev/philosophy/). 102 | 2. Check for open issues or open a fresh issue to start a discussion 103 | around a feature idea or a bug. There is a [Contributor 104 | Friendly](https://github.com/requests/requests/issues?direction=desc&labels=Contributor+Friendly&page=1&sort=updated&state=open) 105 | tag for issues that should be ideal for people who are not very 106 | familiar with the codebase yet. 107 | 3. Fork [the repository](https://github.com/requests/requests) on 108 | GitHub to start making your changes to the **master** branch (or 109 | branch off of it). 110 | 4. Write a test which shows that the bug was fixed or that the feature 111 | works as expected. 112 | 5. Send a pull request and bug the maintainer until it gets merged and 113 | published. :) Make sure to add yourself to 114 | [AUTHORS](https://github.com/requests/requests/blob/master/AUTHORS.rst). 115 | 116 | 117 | Platform: UNKNOWN 118 | Classifier: Development Status :: 5 - Production/Stable 119 | Classifier: Intended Audience :: Developers 120 | Classifier: Natural Language :: English 121 | Classifier: License :: OSI Approved :: Apache Software License 122 | Classifier: Programming Language :: Python 123 | Classifier: Programming Language :: Python :: 2 124 | Classifier: Programming Language :: Python :: 2.7 125 | Classifier: Programming Language :: Python :: 3 126 | Classifier: Programming Language :: Python :: 3.5 127 | Classifier: Programming Language :: Python :: 3.6 128 | Classifier: Programming Language :: Python :: 3.7 129 | Classifier: Programming Language :: Python :: Implementation :: CPython 130 | Classifier: Programming Language :: Python :: Implementation :: PyPy 131 | Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.* 132 | Description-Content-Type: text/markdown 133 | Provides-Extra: security 134 | Provides-Extra: socks -------------------------------------------------------------------------------- /test/cli/test-fixtures/image-pkg-coverage/pkgs/python/egg-info/top_level.txt: -------------------------------------------------------------------------------- 1 | top-level-pkg -------------------------------------------------------------------------------- /test/cli/test-fixtures/image-pkg-coverage/pkgs/python/requires/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | jsonschema==2.6.0 2 | passlib==1.7.2 -------------------------------------------------------------------------------- /test/cli/test-fixtures/image-pkg-coverage/pkgs/python/requires/requirements.txt: -------------------------------------------------------------------------------- 1 | flask==4.0.0 2 | # this is an ignored line 3 | 4 | -------------------------------------------------------------------------------- /test/cli/test-fixtures/image-pkg-coverage/pkgs/python/requires/test-requirements.txt: -------------------------------------------------------------------------------- 1 | python-dateutil==2.8.1 2 | python-swiftclient==3.8.1 3 | pytz==2019.3 -------------------------------------------------------------------------------- /test/cli/test-fixtures/image-pkg-coverage/pkgs/python/setup/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | # TODO: if py gets upgrade to >=1.6, 4 | # remove _width_of_current_line in terminal.py 5 | INSTALL_REQUIRES = [ 6 | "py>=1.5.0", 7 | "packaging", 8 | "attrs>=17.4.0", # should match oldattrs tox env. 9 | "more-itertools>=4.0.0", 10 | 'atomicwrites>=1.0;sys_platform=="win32"', 11 | 'pathlib2>=2.2.0;python_version<"3.6"', 12 | 'colorama;sys_platform=="win32"', 13 | "pluggy>=0.12,<1.0", 14 | 'importlib-metadata>=0.12;python_version<"3.8"', 15 | "wcwidth", 16 | ] 17 | 18 | 19 | def main(): 20 | setup( 21 | use_scm_version={"write_to": "src/_pytest/_version.py"}, 22 | setup_requires=["setuptools-scm", "setuptools>=40.0"], 23 | package_dir={"": "src"}, 24 | extras_require={ 25 | "testing": [ 26 | "argcomplete", 27 | "hypothesis>=3.56", 28 | "mock", 29 | "nose", 30 | "requests", 31 | "xmlschema", 32 | ], 33 | "checkqa-mypy": [ 34 | "mypy==v0.770", # keep this in sync with .pre-commit-config.yaml. 35 | ], 36 | }, 37 | install_requires=INSTALL_REQUIRES, 38 | ) 39 | 40 | 41 | if __name__ == "__main__": 42 | main() 43 | -------------------------------------------------------------------------------- /test/cli/test-fixtures/image-pkg-coverage/pkgs/python/someotherpkg-3.19.0-py3.8.egg-info/PKG-INFO: -------------------------------------------------------------------------------- 1 | Metadata-Version: 2.1 2 | Name: someotherpkg 3 | Version: 3.19.0 4 | Summary: Python HTTP for Humans. 5 | Home-page: http://python-requests.org 6 | Author: Kenneth Reitz 7 | Author-email: me@kennethreitz.org 8 | License: Apache 2.0 9 | -------------------------------------------------------------------------------- /test/cli/test-fixtures/image-pkg-coverage/pkgs/python/someotherpkg-3.19.0-py3.8.egg-info/top_level.txt: -------------------------------------------------------------------------------- 1 | top-level-pkg -------------------------------------------------------------------------------- /test/cli/test-fixtures/image-pkg-coverage/pkgs/python/somerequests-3.22.0.dist-info/METADATA: -------------------------------------------------------------------------------- 1 | Metadata-Version: 2.1 2 | Name: somerequests 3 | Version: 3.22.0 4 | Summary: stuff 5 | Home-page: stuff 6 | Author: Georg Brandl 7 | Author-email: georg@python.org 8 | License: BSD License 9 | Platform: any 10 | Classifier: License :: OSI Approved :: BSD License 11 | Classifier: Intended Audience :: Developers 12 | Classifier: Intended Audience :: End Users/Desktop 13 | Classifier: Intended Audience :: System Administrators 14 | Classifier: Development Status :: 6 - Mature 15 | Classifier: Programming Language :: Python 16 | Classifier: Programming Language :: Python :: 3 17 | Classifier: Programming Language :: Python :: 3.5 18 | Classifier: Programming Language :: Python :: 3.6 19 | Classifier: Programming Language :: Python :: 3.7 20 | Classifier: Programming Language :: Python :: 3.8 21 | Classifier: Programming Language :: Python :: Implementation :: CPython 22 | Classifier: Programming Language :: Python :: Implementation :: PyPy 23 | Classifier: Operating System :: OS Independent 24 | Classifier: Topic :: Text Processing :: Filters 25 | Classifier: Topic :: Utilities 26 | Requires-Python: >=3.5 -------------------------------------------------------------------------------- /test/cli/test-fixtures/image-pkg-coverage/pkgs/python/somerequests-3.22.0.dist-info/top_level.txt: -------------------------------------------------------------------------------- 1 | top-level-pkg -------------------------------------------------------------------------------- /test/cli/test-fixtures/image-pkg-coverage/pkgs/ruby/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actionmailer (4.1.1) 5 | actionpack (= 4.1.1) 6 | actionview (= 4.1.1) 7 | mail (~> 2.5.4) 8 | actionpack (4.1.1) 9 | actionview (= 4.1.1) 10 | activesupport (= 4.1.1) 11 | rack (~> 1.5.2) 12 | rack-test (~> 0.6.2) 13 | actionview (4.1.1) 14 | activesupport (= 4.1.1) 15 | builder (~> 3.1) 16 | erubis (~> 2.7.0) 17 | activemodel (4.1.1) 18 | activesupport (= 4.1.1) 19 | builder (~> 3.1) 20 | activerecord (4.1.1) 21 | activemodel (= 4.1.1) 22 | activesupport (= 4.1.1) 23 | arel (~> 5.0.0) 24 | activesupport (4.1.1) 25 | i18n (~> 0.6, >= 0.6.9) 26 | json (~> 1.7, >= 1.7.7) 27 | minitest (~> 5.1) 28 | thread_safe (~> 0.1) 29 | tzinfo (~> 1.1) 30 | arel (5.0.1.20140414130214) 31 | bootstrap-sass (3.1.1.1) 32 | sass (~> 3.2) 33 | builder (3.2.2) 34 | coffee-rails (4.0.1) 35 | coffee-script (>= 2.2.0) 36 | railties (>= 4.0.0, < 5.0) 37 | coffee-script (2.2.0) 38 | coffee-script-source 39 | execjs 40 | coffee-script-source (1.7.0) 41 | erubis (2.7.0) 42 | execjs (2.0.2) 43 | hike (1.2.3) 44 | i18n (0.6.9) 45 | jbuilder (2.0.7) 46 | activesupport (>= 3.0.0, < 5) 47 | multi_json (~> 1.2) 48 | jquery-rails (3.1.0) 49 | railties (>= 3.0, < 5.0) 50 | thor (>= 0.14, < 2.0) 51 | json (1.8.1) 52 | kgio (2.9.2) 53 | libv8 (3.16.14.3) 54 | mail (2.5.4) 55 | mime-types (~> 1.16) 56 | treetop (~> 1.4.8) 57 | mime-types (1.25.1) 58 | minitest (5.3.4) 59 | multi_json (1.10.1) 60 | mysql2 (0.3.16) 61 | polyglot (0.3.4) 62 | rack (1.5.2) 63 | rack-test (0.6.2) 64 | rack (>= 1.0) 65 | rails (4.1.1) 66 | actionmailer (= 4.1.1) 67 | actionpack (= 4.1.1) 68 | actionview (= 4.1.1) 69 | activemodel (= 4.1.1) 70 | activerecord (= 4.1.1) 71 | activesupport (= 4.1.1) 72 | bundler (>= 1.3.0, < 2.0) 73 | railties (= 4.1.1) 74 | sprockets-rails (~> 2.0) 75 | railties (4.1.1) 76 | actionpack (= 4.1.1) 77 | activesupport (= 4.1.1) 78 | rake (>= 0.8.7) 79 | thor (>= 0.18.1, < 2.0) 80 | raindrops (0.13.0) 81 | rake (10.3.2) 82 | rdoc (4.1.1) 83 | json (~> 1.4) 84 | ref (1.0.5) 85 | sass (3.2.19) 86 | sass-rails (4.0.3) 87 | railties (>= 4.0.0, < 5.0) 88 | sass (~> 3.2.0) 89 | sprockets (~> 2.8, <= 2.11.0) 90 | sprockets-rails (~> 2.0) 91 | sdoc (0.4.0) 92 | json (~> 1.8) 93 | rdoc (~> 4.0, < 5.0) 94 | spring (1.1.3) 95 | sprockets (2.11.0) 96 | hike (~> 1.2) 97 | multi_json (~> 1.0) 98 | rack (~> 1.0) 99 | tilt (~> 1.1, != 1.3.0) 100 | sprockets-rails (2.1.3) 101 | actionpack (>= 3.0) 102 | activesupport (>= 3.0) 103 | sprockets (~> 2.8) 104 | sqlite3 (1.3.9) 105 | therubyracer (0.12.1) 106 | libv8 (~> 3.16.14.0) 107 | ref 108 | thor (0.19.1) 109 | thread_safe (0.3.3) 110 | tilt (1.4.1) 111 | treetop (1.4.15) 112 | polyglot 113 | polyglot (>= 0.3.1) 114 | turbolinks (2.2.2) 115 | coffee-rails 116 | tzinfo (1.2.0) 117 | thread_safe (~> 0.1) 118 | uglifier (2.5.0) 119 | execjs (>= 0.3.0) 120 | json (>= 1.8.0) 121 | unicorn (4.8.3) 122 | kgio (~> 2.6) 123 | rack 124 | raindrops (~> 0.7) 125 | 126 | BAD-SECTION 127 | bad-entry (5.5.5) 128 | bad-kgio (~> 2.6) 129 | bad-rack 130 | bad-raindrops (~> 0.7) 131 | 132 | PLATFORMS 133 | ruby 134 | 135 | DEPENDENCIES 136 | bootstrap-sass 137 | coffee-rails (~> 4.0.0) 138 | jbuilder (~> 2.0) 139 | jquery-rails 140 | mysql2 (~> 0.3.16) 141 | rails (= 4.1.1) 142 | sass-rails (~> 4.0.3) 143 | sdoc (~> 0.4.0) 144 | spring 145 | sqlite3 146 | therubyracer 147 | turbolinks 148 | uglifier (>= 1.3.0) 149 | unicorn -------------------------------------------------------------------------------- /test/cli/test-fixtures/image-pkg-coverage/pkgs/ruby/specifications/bundler.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # -*- encoding: utf-8 -*- 3 | # stub: bundler 2.1.4 ruby lib 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "bundler".freeze 7 | s.version = "2.1.4" 8 | 9 | s.required_rubygems_version = Gem::Requirement.new(">= 2.5.2".freeze) if s.respond_to? :required_rubygems_version= 10 | s.require_paths = ["lib".freeze] 11 | s.authors = ["Andr\u00E9 Arko".freeze, "Samuel Giddins".freeze, "Colby Swandale".freeze, "Hiroshi Shibata".freeze, "David Rodr\u00EDguez".freeze, "Grey Baker".f 12 | s.bindir = "exe".freeze 13 | s.date = "2020-01-05" 14 | s.description = "Bundler manages an application's dependencies through its entire life, across many machines, systematically and repeatably".freeze 15 | s.email = ["team@bundler.io".freeze] 16 | s.executables = ["bundle".freeze, "bundler".freeze] 17 | s.files = ["exe/bundle".freeze, "exe/bundler".freeze] 18 | s.homepage = "https://bundler.io".freeze 19 | s.licenses = ["MIT".freeze] 20 | s.required_ruby_version = Gem::Requirement.new(">= 2.3.0".freeze) 21 | s.rubygems_version = "3.1.2".freeze 22 | s.summary = "The best way to manage your application's dependencies".freeze 23 | 24 | s.installed_by_version = "3.1.2" if s.respond_to? :installed_by_version 25 | end -------------------------------------------------------------------------------- /test/cli/test-fixtures/image-pkg-coverage/pkgs/ruby/specifications/default/unbundler.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # -*- encoding: utf-8 -*- 3 | # stub: unbundler 2.1.4 ruby lib 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "unbundler".freeze 7 | s.version = "3.1.4" 8 | 9 | s.required_rubygems_version = Gem::Requirement.new(">= 2.5.2".freeze) if s.respond_to? :required_rubygems_version= 10 | s.require_paths = ["lib".freeze] 11 | s.authors = ["Andr\u00E9 Arko".freeze, "Samuel Giddins".freeze, "Colby Swandale".freeze, "Hiroshi Shibata".freeze, "David Rodr\u00EDguez".freeze, "Grey Baker".f 12 | s.bindir = "exe".freeze 13 | s.date = "2020-01-05" 14 | s.description = "Bundler manages an application's dependencies through its entire life, across many machines, systematically and repeatably".freeze 15 | s.email = ["team@unbundler.io".freeze] 16 | s.executables = ["unbundle".freeze, "unbundler".freeze] 17 | s.files = ["exe/unbundle".freeze, "exe/unbundler".freeze] 18 | s.homepage = "https://unbundler.io".freeze 19 | s.licenses = ["MIT".freeze] 20 | s.required_ruby_version = Gem::Requirement.new(">= 2.3.0".freeze) 21 | s.rubygems_version = "3.1.2".freeze 22 | s.summary = "The best way to manage your application's dependencies".freeze 23 | 24 | s.installed_by_version = "3.1.2" if s.respond_to? :installed_by_version 25 | end -------------------------------------------------------------------------------- /test/cli/test-fixtures/image-pkg-coverage/pkgs/rust/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | 4 | [[package]] 5 | name = "memchr" 6 | version = "2.3.3" 7 | source = "registry+https://github.com/rust-lang/crates.io-index" 8 | checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" 9 | 10 | [[package]] 11 | name = "nom" 12 | version = "4.2.3" 13 | source = "registry+https://github.com/rust-lang/crates.io-index" 14 | checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6" 15 | dependencies = [ 16 | "memchr", 17 | "version_check", 18 | ] 19 | 20 | [[package]] 21 | name = "version_check" 22 | version = "0.1.5" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" 25 | 26 | -------------------------------------------------------------------------------- /test/cli/test-fixtures/image-pkg-coverage/pkgs/var/lib/dpkg/status: -------------------------------------------------------------------------------- 1 | Package: apt 2 | Status: install ok installed 3 | Priority: required 4 | Section: admin 5 | Installed-Size: 4064 6 | Maintainer: APT Development Team 7 | Architecture: amd64 8 | Version: 1.8.2 9 | Source: apt-dev 10 | Replaces: apt-transport-https (<< 1.5~alpha4~), apt-utils (<< 1.3~exp2~) 11 | Provides: apt-transport-https (= 1.8.2) 12 | Depends: adduser, gpgv | gpgv2 | gpgv1, debian-archive-keyring, libapt-pkg5.0 (>= 1.7.0~alpha3~), libc6 (>= 2.15), libgcc1 (>= 1:3.0), libgnutls30 (>= 3.6.6), libseccomp2 (>= 1.0.1), libstdc++6 (>= 5.2) 13 | Recommends: ca-certificates 14 | Suggests: apt-doc, aptitude | synaptic | wajig, dpkg-dev (>= 1.17.2), gnupg | gnupg2 | gnupg1, powermgmt-base 15 | Breaks: apt-transport-https (<< 1.5~alpha4~), apt-utils (<< 1.3~exp2~), aptitude (<< 0.8.10) 16 | Conffiles: 17 | /etc/apt/apt.conf.d/01autoremove 76120d358bc9037bb6358e737b3050b5 18 | /etc/cron.daily/apt-compat 49e9b2cfa17849700d4db735d04244f3 19 | /etc/kernel/postinst.d/apt-auto-removal 4ad976a68f045517cf4696cec7b8aa3a 20 | /etc/logrotate.d/apt 179f2ed4f85cbaca12fa3d69c2a4a1c3 21 | Description: commandline package manager 22 | This package provides commandline tools for searching and 23 | managing as well as querying information about packages 24 | as a low-level access to all features of the libapt-pkg library. 25 | . 26 | These include: 27 | * apt-get for retrieval of packages and information about them 28 | from authenticated sources and for installation, upgrade and 29 | removal of packages together with their dependencies 30 | * apt-cache for querying available information about installed 31 | as well as installable packages 32 | * apt-cdrom to use removable media as a source for packages 33 | * apt-config as an interface to the configuration settings 34 | * apt-key as an interface to manage authentication keys 35 | 36 | -------------------------------------------------------------------------------- /test/cli/test-fixtures/image-pkg-coverage/pkgs/var/lib/dpkg/status.d/dash: -------------------------------------------------------------------------------- 1 | Package: dash 2 | Version: 0.5.8-2.4 3 | Architecture: amd64 4 | Essential: yes 5 | Maintainer: Gerrit Pape 6 | Installed-Size: 204 7 | Pre-Depends: libc6 (>= 2.14) 8 | Depends: debianutils (>= 2.15), dpkg (>= 1.15.0) 9 | Section: shells 10 | Priority: required 11 | Homepage: http://gondor.apana.org.au/~herbert/dash/ 12 | Description: POSIX-compliant shell 13 | The Debian Almquist Shell (dash) is a POSIX-compliant shell derived 14 | from ash. 15 | . 16 | Since it executes scripts faster than bash, and has fewer library 17 | dependencies (making it more robust against software or hardware 18 | failures), it is used as the default system shell on Debian systems. 19 | -------------------------------------------------------------------------------- /test/cli/test-fixtures/image-pkg-coverage/pkgs/var/lib/dpkg/status.d/netbase: -------------------------------------------------------------------------------- 1 | Package: netbase 2 | Version: 5.4 3 | Architecture: all 4 | Maintainer: Marco d'Itri 5 | Installed-Size: 44 6 | Section: admin 7 | Priority: important 8 | Multi-Arch: foreign 9 | Description: Basic TCP/IP networking system 10 | This package provides the necessary infrastructure for basic TCP/IP based 11 | networking. 12 | -------------------------------------------------------------------------------- /test/cli/test-fixtures/image-pkg-coverage/pkgs/var/lib/rpm/Packages: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/docker/sbom-cli-plugin/b17d47dc0b20061e7924e835716caef3c6cc6a46/test/cli/test-fixtures/image-pkg-coverage/pkgs/var/lib/rpm/Packages -------------------------------------------------------------------------------- /test/cli/test-fixtures/image-pkg-coverage/pkgs/var/lib/rpm/generate-fixture.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eux 3 | 4 | docker create --name generate-rpmdb-fixture centos:latest sh -c 'tail -f /dev/null' 5 | 6 | function cleanup { 7 | docker kill generate-rpmdb-fixture 8 | docker rm generate-rpmdb-fixture 9 | } 10 | trap cleanup EXIT 11 | 12 | docker start generate-rpmdb-fixture 13 | docker exec -i --tty=false generate-rpmdb-fixture bash <<-EOF 14 | mkdir -p /scratch 15 | cd /scratch 16 | rpm --initdb --dbpath /scratch 17 | curl -sSLO https://github.com/wagoodman/dive/releases/download/v0.9.2/dive_0.9.2_linux_amd64.rpm 18 | rpm --dbpath /scratch -ivh dive_0.9.2_linux_amd64.rpm 19 | rm dive_0.9.2_linux_amd64.rpm 20 | rpm --dbpath /scratch -qa 21 | EOF 22 | 23 | docker cp generate-rpmdb-fixture:/scratch/Packages . 24 | -------------------------------------------------------------------------------- /test/cli/trait_assertions_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | "os/exec" 7 | "path/filepath" 8 | "regexp" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/acarl005/stripansi" 13 | "github.com/stretchr/testify/assert" 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | type traitAssertion func(tb testing.TB, stdout, stderr string, rc int) 18 | 19 | func assertFileOutput(tb testing.TB, path string, assertions ...traitAssertion) traitAssertion { 20 | tb.Helper() 21 | 22 | return func(tb testing.TB, _, stderr string, rc int) { 23 | content, err := os.ReadFile(path) 24 | require.NoError(tb, err) 25 | contentStr := string(content) 26 | 27 | for _, assertion := range assertions { 28 | // treat the file content as stdout 29 | assertion(tb, contentStr, stderr, rc) 30 | } 31 | } 32 | } 33 | 34 | func assertJsonReport(tb testing.TB, stdout, _ string, _ int) { 35 | tb.Helper() 36 | var data interface{} 37 | 38 | if err := json.Unmarshal([]byte(stdout), &data); err != nil { 39 | tb.Errorf("expected to find a JSON report, but was unmarshalable: %+v", err) 40 | } 41 | } 42 | 43 | func assertTableReport(tb testing.TB, stdout, _ string, _ int) { 44 | tb.Helper() 45 | if !strings.Contains(stdout, "NAME") || !strings.Contains(stdout, "VERSION") || !strings.Contains(stdout, "TYPE") { 46 | tb.Errorf("expected to find a table report, but did not") 47 | } 48 | } 49 | 50 | //func assertScope(scope source.Scope) traitAssertion { 51 | // return func(tb testing.TB, stdout, stderr string, rc int) { 52 | // tb.Helper() 53 | // // we can only verify source with the json report 54 | // assertJsonReport(tb, stdout, stderr, rc) 55 | // 56 | // if !strings.Contains(stdout, fmt.Sprintf(`"scope": "%s"`, scope.String())) { 57 | // tb.Errorf("JSON report did not indicate the %q scope", scope) 58 | // } 59 | // } 60 | //} 61 | 62 | func assertLoggingLevel(level string) traitAssertion { 63 | // match examples: 64 | // "[0000] INFO" 65 | // "[0012] DEBUG" 66 | logPattern := regexp.MustCompile(`(?m)^\[\d\d\d\d\]\s+` + strings.ToUpper(level)) 67 | return func(tb testing.TB, _, stderr string, _ int) { 68 | tb.Helper() 69 | if !logPattern.MatchString(stripansi.Strip(stderr)) { 70 | tb.Errorf("output did not indicate the %q logging level", level) 71 | } 72 | } 73 | } 74 | 75 | func assertNotInOutput(data string) traitAssertion { 76 | return func(tb testing.TB, stdout, stderr string, _ int) { 77 | tb.Helper() 78 | if strings.Contains(stripansi.Strip(stderr), data) { 79 | tb.Errorf("data=%q was found in stderr, but should not have been there", data) 80 | } 81 | if strings.Contains(stripansi.Strip(stdout), data) { 82 | tb.Errorf("data=%q was found in stdout, but should not have been there", data) 83 | } 84 | } 85 | } 86 | 87 | func assertInOutput(data string) traitAssertion { 88 | return func(tb testing.TB, stdout, stderr string, _ int) { 89 | tb.Helper() 90 | if !strings.Contains(stripansi.Strip(stderr), data) && !strings.Contains(stripansi.Strip(stdout), data) { 91 | tb.Errorf("data=%q was NOT found in any output, but should have been there", data) 92 | } 93 | } 94 | } 95 | 96 | func assertStdoutLengthGreaterThan(length uint) traitAssertion { 97 | return func(tb testing.TB, stdout, _ string, _ int) { 98 | tb.Helper() 99 | if uint(len(stdout)) < length { 100 | tb.Errorf("not enough output (expected at least %d, got %d)", length, len(stdout)) 101 | } 102 | } 103 | } 104 | 105 | func assertPackageCount(length uint) traitAssertion { 106 | return func(tb testing.TB, stdout, _ string, _ int) { 107 | tb.Helper() 108 | type partial struct { 109 | Artifacts []interface{} `json:"artifacts"` 110 | } 111 | var data partial 112 | 113 | if err := json.Unmarshal([]byte(stdout), &data); err != nil { 114 | tb.Errorf("expected to find a JSON report, but was unmarshalable: %+v", err) 115 | } 116 | 117 | if uint(len(data.Artifacts)) != length { 118 | tb.Errorf("expected package count of %d, but found %d", length, len(data.Artifacts)) 119 | 120 | } 121 | } 122 | } 123 | 124 | func assertJsonDescriptor(name, version string) traitAssertion { 125 | return func(tb testing.TB, stdout, _ string, _ int) { 126 | tb.Helper() 127 | type partial struct { 128 | Descriptor struct { 129 | Name string `json:"name"` 130 | Version string `json:"version"` 131 | } `json:"descriptor"` 132 | } 133 | var data partial 134 | 135 | if err := json.Unmarshal([]byte(stdout), &data); err != nil { 136 | tb.Errorf("expected to find a JSON report, but was unmarshalable: %+v", err) 137 | } 138 | 139 | assert.Equal(tb, name, data.Descriptor.Name, "unexpected tool name") 140 | assert.Equal(tb, version, data.Descriptor.Version, "unexpected tool version") 141 | } 142 | } 143 | 144 | func assertFailingReturnCode(tb testing.TB, _, _ string, rc int) { 145 | tb.Helper() 146 | if rc == 0 { 147 | tb.Errorf("expected a failure but got rc=%d", rc) 148 | } 149 | } 150 | 151 | func assertSuccessfulReturnCode(tb testing.TB, _, _ string, rc int) { 152 | tb.Helper() 153 | if rc != 0 { 154 | tb.Errorf("expected no failure but got rc=%d", rc) 155 | } 156 | } 157 | 158 | func assertVerifyAttestation(coverageImage string) traitAssertion { 159 | return func(tb testing.TB, stdout, _ string, _ int) { 160 | tb.Helper() 161 | cosignPath := filepath.Join(repoRoot(tb), ".tmp/cosign") 162 | err := os.WriteFile("attestation.json", []byte(stdout), 0664) 163 | if err != nil { 164 | tb.Errorf("could not write attestation to disk") 165 | } 166 | defer os.Remove("attestation.json") 167 | attachCmd := exec.Command( 168 | cosignPath, 169 | "attach", 170 | "attestation", 171 | "--attestation", 172 | "attestation.json", 173 | coverageImage, // TODO which remote image to use? 174 | ) 175 | 176 | stdout, stderr := runCommand(attachCmd, nil) 177 | if attachCmd.ProcessState.ExitCode() != 0 { 178 | tb.Log("STDOUT", stdout) 179 | tb.Log("STDERR", stderr) 180 | tb.Fatalf("could not attach image") 181 | } 182 | 183 | verifyCmd := exec.Command( 184 | cosignPath, 185 | "verify-attestation", 186 | coverageImage, // TODO which remote image to use? 187 | ) 188 | 189 | stdout, stderr = runCommand(verifyCmd, nil) 190 | if attachCmd.ProcessState.ExitCode() != 0 { 191 | tb.Log("STDOUT", stdout) 192 | tb.Log("STDERR", stderr) 193 | tb.Fatalf("could not verify attestation") 194 | } 195 | } 196 | } 197 | 198 | func assertFileExists(file string) traitAssertion { 199 | return func(tb testing.TB, _, _ string, _ int) { 200 | tb.Helper() 201 | if _, err := os.Stat(file); err != nil { 202 | tb.Errorf("expected file to exist %s", file) 203 | } 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /test/cli/utils_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "math" 7 | "os" 8 | "os/exec" 9 | "path" 10 | "path/filepath" 11 | "runtime" 12 | "strings" 13 | "testing" 14 | "time" 15 | 16 | "github.com/docker/sbom-cli-plugin/internal" 17 | "github.com/stretchr/testify/require" 18 | 19 | "github.com/anchore/stereoscope/pkg/imagetest" 20 | ) 21 | 22 | func setupPKI(t *testing.T, pw string) func() { 23 | err := os.Setenv("COSIGN_PASSWORD", pw) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | cosignPath := filepath.Join(repoRoot(t), ".tmp/cosign") 29 | cmd := exec.Command(cosignPath, "generate-key-pair") 30 | stdout, stderr := runCommand(cmd, nil) 31 | if cmd.ProcessState.ExitCode() != 0 { 32 | t.Log("STDOUT", stdout) 33 | t.Log("STDERR", stderr) 34 | t.Fatalf("could not generate keypair") 35 | } 36 | 37 | return func() { 38 | err := os.Unsetenv("COSIGN_PASSWORD") 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | 43 | err = os.Remove("cosign.key") 44 | if err != nil { 45 | t.Fatalf("could not cleanup cosign.key") 46 | } 47 | 48 | err = os.Remove("cosign.pub") 49 | if err != nil { 50 | t.Fatalf("could not cleanup cosign.key") 51 | } 52 | } 53 | } 54 | 55 | func getFixtureImage(t testing.TB, fixtureImageName string) string { 56 | t.Logf("obtaining fixture image for %s", fixtureImageName) 57 | img := imagetest.GetFixtureImage(t, "docker", fixtureImageName) 58 | require.NotNil(t, img) 59 | require.NotEmpty(t, img.Metadata.Tags) 60 | return img.Metadata.Tags[0].String() 61 | } 62 | 63 | func pullDockerImage(t testing.TB, image string) { 64 | cmd := exec.Command("docker", "pull", image) 65 | stdout, stderr := runCommand(cmd, nil) 66 | if cmd.ProcessState.ExitCode() != 0 { 67 | t.Log("STDOUT", stdout) 68 | t.Log("STDERR", stderr) 69 | t.Fatalf("could not pull docker image") 70 | } 71 | } 72 | 73 | func runSyftInDocker(t testing.TB, env map[string]string, image string, args ...string) (*exec.Cmd, string, string) { 74 | allArgs := append( 75 | []string{ 76 | "run", 77 | "-t", 78 | "-e", 79 | "SYFT_CHECK_FOR_APP_UPDATE=false", 80 | "-v", 81 | fmt.Sprintf("%s:/syft", getSyftBinaryLocationByOS(t, "linux")), 82 | image, 83 | "/syft", 84 | }, 85 | args..., 86 | ) 87 | cmd := exec.Command("docker", allArgs...) 88 | stdout, stderr := runCommand(cmd, env) 89 | return cmd, stdout, stderr 90 | } 91 | 92 | func runSyft(t testing.TB, env map[string]string, args ...string) (*exec.Cmd, string, string) { 93 | cmd := getSyftCommand(t, args...) 94 | if env == nil { 95 | env = make(map[string]string) 96 | } 97 | 98 | // we should not have tests reaching out for app update checks 99 | env["SYFT_CHECK_FOR_APP_UPDATE"] = "false" 100 | 101 | stdout, stderr := runCommand(cmd, env) 102 | return cmd, stdout, stderr 103 | } 104 | 105 | func runCosign(t testing.TB, env map[string]string, args ...string) (*exec.Cmd, string, string) { 106 | cmd := getCosignCommand(t, args...) 107 | if env == nil { 108 | env = make(map[string]string) 109 | } 110 | 111 | stdout, stderr := runCommand(cmd, env) 112 | return cmd, stdout, stderr 113 | } 114 | 115 | func getCosignCommand(t testing.TB, args ...string) *exec.Cmd { 116 | return exec.Command(filepath.Join(repoRoot(t), ".tmp/cosign"), args...) 117 | } 118 | 119 | func runCommand(cmd *exec.Cmd, env map[string]string) (string, string) { 120 | if env != nil { 121 | cmd.Env = append(os.Environ(), envMapToSlice(env)...) 122 | } 123 | var stdout, stderr bytes.Buffer 124 | cmd.Stdout = &stdout 125 | cmd.Stderr = &stderr 126 | 127 | // ignore errors since this may be what the test expects 128 | cmd.Run() 129 | 130 | return stdout.String(), stderr.String() 131 | } 132 | 133 | func envMapToSlice(env map[string]string) (envList []string) { 134 | for key, val := range env { 135 | if key == "" { 136 | continue 137 | } 138 | envList = append(envList, fmt.Sprintf("%s=%s", key, val)) 139 | } 140 | return 141 | } 142 | 143 | func getSyftCommand(t testing.TB, args ...string) *exec.Cmd { 144 | return exec.Command(getSyftBinaryLocation(t), args...) 145 | } 146 | 147 | func getSyftBinaryLocation(t testing.TB) string { 148 | if os.Getenv("SYFT_BINARY_LOCATION") != "" { 149 | // SYFT_BINARY_LOCATION is the absolute path to the snapshot binary 150 | return os.Getenv("SYFT_BINARY_LOCATION") 151 | } 152 | return getSyftBinaryLocationByOS(t, runtime.GOOS) 153 | } 154 | 155 | func getSyftBinaryLocationByOS(t testing.TB, goOS string) string { 156 | // note: there is a subtle - vs _ difference between these versions 157 | switch goOS { 158 | case "darwin", "linux": 159 | return path.Join(repoRoot(t), fmt.Sprintf("snapshot/%s_%s_%s/%s", internal.ApplicationName, goOS, runtime.GOARCH, internal.BinaryName)) 160 | default: 161 | t.Fatalf("unsupported OS: %s", runtime.GOOS) 162 | } 163 | return "" 164 | } 165 | 166 | func repoRoot(t testing.TB) string { 167 | t.Helper() 168 | root, err := exec.Command("git", "rev-parse", "--show-toplevel").Output() 169 | if err != nil { 170 | t.Fatalf("unable to find repo root dir: %+v", err) 171 | } 172 | absRepoRoot, err := filepath.Abs(strings.TrimSpace(string(root))) 173 | if err != nil { 174 | t.Fatal("unable to get abs path to repo root:", err) 175 | } 176 | return absRepoRoot 177 | } 178 | 179 | func testRetryIntervals(done <-chan struct{}) <-chan time.Duration { 180 | return exponentialBackoffDurations(250*time.Millisecond, 4*time.Second, 2, done) 181 | } 182 | 183 | func exponentialBackoffDurations(minDuration, maxDuration time.Duration, step float64, done <-chan struct{}) <-chan time.Duration { 184 | sleepDurations := make(chan time.Duration) 185 | go func() { 186 | defer close(sleepDurations) 187 | retryLoop: 188 | for attempt := 0; ; attempt++ { 189 | duration := exponentialBackoffDuration(minDuration, maxDuration, step, attempt) 190 | 191 | select { 192 | case sleepDurations <- duration: 193 | break 194 | case <-done: 195 | break retryLoop 196 | } 197 | 198 | if duration == maxDuration { 199 | break 200 | } 201 | } 202 | }() 203 | return sleepDurations 204 | } 205 | 206 | func exponentialBackoffDuration(minDuration, maxDuration time.Duration, step float64, attempt int) time.Duration { 207 | duration := time.Duration(float64(minDuration) * math.Pow(step, float64(attempt))) 208 | if duration < minDuration { 209 | return minDuration 210 | } else if duration > maxDuration { 211 | return maxDuration 212 | } 213 | return duration 214 | } 215 | -------------------------------------------------------------------------------- /test/install/.dockerignore: -------------------------------------------------------------------------------- 1 | ** -------------------------------------------------------------------------------- /test/install/.gitignore: -------------------------------------------------------------------------------- 1 | cache/ -------------------------------------------------------------------------------- /test/install/0_search_for_asset_test.sh: -------------------------------------------------------------------------------- 1 | . test_harness.sh 2 | 3 | # search for an asset in a snapshot checksums file 4 | test_search_for_asset() { 5 | fixture=./test-fixtures/sbom-cli-plugin_0.0.0-SNAPSHOT-ac27dcf_checksums.txt 6 | 7 | # search_for_asset [checksums-file-path] [name] [os] [arch] [format] 8 | 9 | # positive case 10 | actual=$(search_for_asset "${fixture}" "sbom-cli-plugin" "linux" "amd64" "tar.gz") 11 | assertEquals "sbom-cli-plugin_0.0.0-SNAPSHOT-ac27dcf_linux_amd64.tar.gz" "${actual}" "unable to find snapshot asset" 12 | 13 | # negative case 14 | actual=$(search_for_asset "${fixture}" "sbom-cli-plugin" "linux" "amd64" "zip") 15 | assertEquals "" "${actual}" "found a snapshot asset but did not expect to (format)" 16 | } 17 | 18 | run_test_case test_search_for_asset -------------------------------------------------------------------------------- /test/install/1_download_snapshot_asset_test.sh: -------------------------------------------------------------------------------- 1 | . test_harness.sh 2 | 3 | DOWNLOAD_SNAPSHOT_POSITIVE_CASES=0 4 | 5 | # helper for asserting test_positive_snapshot_download_asset positive cases 6 | test_positive_snapshot_download_asset() { 7 | os="$1" 8 | arch="$2" 9 | format="$3" 10 | 11 | # for troubleshooting 12 | # log_set_priority 10 13 | 14 | name=${PROJECT_NAME} 15 | github_download=$(snapshot_download_url) 16 | version=$(snapshot_version) 17 | 18 | tmpdir=$(mktemp -d) 19 | 20 | actual_filepath=$(download_asset "${github_download}" "${tmpdir}" "${name}" "${os}" "${arch}" "${version}" "${format}" ) 21 | 22 | assertFileExists "${actual_filepath}" "download_asset os=${os} arch=${arch} format=${format}" 23 | 24 | assertFilesEqual \ 25 | "$(snapshot_dir)/${name}_${version}_${os}_${arch}.${format}" \ 26 | "${actual_filepath}" \ 27 | "unable to download os=${os} arch=${arch} format=${format}" 28 | 29 | ((DOWNLOAD_SNAPSHOT_POSITIVE_CASES++)) 30 | 31 | rm -rf -- "$tmpdir" 32 | } 33 | 34 | 35 | test_download_snapshot_asset_exercised_all_assets() { 36 | expected=$(snapshot_assets_count) 37 | 38 | assertEquals "${expected}" "${DOWNLOAD_SNAPSHOT_POSITIVE_CASES}" "did not download all possible assets (missing an os/arch/format variant?)" 39 | } 40 | 41 | # helper for asserting download_asset negative cases 42 | test_negative_snapshot_download_asset() { 43 | os="$1" 44 | arch="$2" 45 | format="$3" 46 | 47 | # for troubleshooting 48 | # log_set_priority 10 49 | 50 | name=${PROJECT_NAME} 51 | github_download=$(snapshot_download_url) 52 | version=$(snapshot_version) 53 | 54 | tmpdir=$(mktemp -d) 55 | 56 | actual_filepath=$(download_asset "${github_download}" "${tmpdir}" "${name}" "${os}" "${arch}" "${version}" "${format}") 57 | 58 | assertEquals "" "${actual_filepath}" "unable to download os=${os} arch=${arch} format=${format}" 59 | 60 | rm -rf -- "$tmpdir" 61 | } 62 | 63 | 64 | worker_pid=$(setup_snapshot_server) 65 | trap 'teardown_snapshot_server ${worker_pid}' EXIT 66 | 67 | # exercise all possible assets 68 | run_test_case test_positive_snapshot_download_asset "linux" "amd64" "tar.gz" 69 | run_test_case test_positive_snapshot_download_asset "linux" "arm64" "tar.gz" 70 | run_test_case test_positive_snapshot_download_asset "darwin" "amd64" "tar.gz" 71 | run_test_case test_positive_snapshot_download_asset "darwin" "arm64" "tar.gz" 72 | run_test_case test_positive_snapshot_download_asset "windows" "amd64" "zip" 73 | run_test_case test_positive_snapshot_download_asset "windows" "arm64" "zip" 74 | # note: the mac signing process produces a dmg which is not part of the snapshot process (thus is not exercised here) 75 | 76 | # let's make certain we covered all assets that were expected 77 | run_test_case test_download_snapshot_asset_exercised_all_assets 78 | 79 | # make certain we handle missing assets alright 80 | run_test_case test_negative_snapshot_download_asset "bogus" "amd64" "zip" 81 | 82 | trap - EXIT 83 | teardown_snapshot_server "${worker_pid}" -------------------------------------------------------------------------------- /test/install/2_download_release_asset_test.sh: -------------------------------------------------------------------------------- 1 | . test_harness.sh 2 | 3 | test_download_release_asset() { 4 | release="$1" 5 | os="$2" 6 | arch="$3" 7 | format="$4" 8 | expected_mime_type="$5" 9 | 10 | # for troubleshooting 11 | # log_set_priority 10 12 | 13 | name=${PROJECT_NAME} 14 | version=$(tag_to_version ${release}) 15 | github_download="https://github.com/${OWNER}/${REPO}/releases/download/${release}" 16 | 17 | tmpdir=$(mktemp -d) 18 | 19 | actual_filepath=$(download_asset "${github_download}" "${tmpdir}" "${name}" "${os}" "${arch}" "${version}" "${format}" ) 20 | 21 | assertFileExists "${actual_filepath}" "download_asset os=${os} arch=${arch} format=${format}" 22 | 23 | actual_mime_type=$(file -b --mime-type ${actual_filepath}) 24 | 25 | assertEquals "${expected_mime_type}" "${actual_mime_type}" "unexpected mimetype for os=${os} arch=${arch} format=${format}" 26 | 27 | rm -rf -- "$tmpdir" 28 | } 29 | 30 | # always test against the latest release 31 | release=$(get_release_tag "${OWNER}" "${REPO}" "latest" ) 32 | 33 | # exercise all possible assets against a real github release (based on asset listing from https://github.com/docker/sbom-cli-plugin/releases/tag/v0.1.0) 34 | run_test_case test_download_release_asset "${release}" "darwin" "amd64" "tar.gz" "application/gzip" 35 | run_test_case test_download_release_asset "${release}" "darwin" "arm64" "tar.gz" "application/gzip" 36 | run_test_case test_download_release_asset "${release}" "linux" "amd64" "tar.gz" "application/gzip" 37 | run_test_case test_download_release_asset "${release}" "linux" "arm64" "tar.gz" "application/gzip" 38 | run_test_case test_download_release_asset "${release}" "windows" "amd64" "zip" "application/zip" 39 | run_test_case test_download_release_asset "${release}" "windows" "arm64" "zip" "application/zip" 40 | -------------------------------------------------------------------------------- /test/install/3_install_asset_test.sh: -------------------------------------------------------------------------------- 1 | . test_harness.sh 2 | 3 | INSTALL_ARCHIVE_POSITIVE_CASES=0 4 | 5 | # helper for asserting install_asset positive cases 6 | test_positive_snapshot_install_asset() { 7 | os="$1" 8 | arch="$2" 9 | format="$3" 10 | 11 | # for troubleshooting 12 | # log_set_priority 10 13 | 14 | name=${PROJECT_NAME} 15 | binary=$(get_binary_name "${os}" "${arch}" "${BINARY}") 16 | github_download=$(snapshot_download_url) 17 | version=$(snapshot_version) 18 | 19 | download_dir=$(mktemp -d) 20 | install_dir=$(mktemp -d) 21 | 22 | download_and_install_asset "${github_download}" "${download_dir}" "${install_dir}" "${name}" "${os}" "${arch}" "${version}" "${format}" "${binary}" 23 | 24 | assertEquals "0" "$?" "download/install did not succeed" 25 | 26 | expected_path="${install_dir}/${binary}" 27 | assertFileExists "${expected_path}" "install_asset os=${os} arch=${arch} format=${format}" 28 | 29 | build_dir_name="${name}" 30 | 31 | assertFilesEqual \ 32 | "$(snapshot_dir)/${build_dir_name}_${os}_${arch}/${binary}" \ 33 | "${expected_path}" \ 34 | "unable to verify installation of os=${os} arch=${arch} format=${format}" 35 | 36 | ((INSTALL_ARCHIVE_POSITIVE_CASES++)) 37 | 38 | rm -rf -- "$download_dir" 39 | rm -rf -- "$install_dir" 40 | } 41 | 42 | # helper for asserting install_asset negative cases 43 | test_negative_snapshot_install_asset() { 44 | os="$1" 45 | arch="$2" 46 | format="$3" 47 | 48 | # for troubleshooting 49 | # log_set_priority 10 50 | 51 | name=${PROJECT_NAME} 52 | binary=$(get_binary_name "${os}" "${arch}" "${BINARY}") 53 | github_download=$(snapshot_download_url) 54 | version=$(snapshot_version) 55 | 56 | download_dir=$(mktemp -d) 57 | install_dir=$(mktemp -d) 58 | 59 | download_and_install_asset "${github_download}" "${download_dir}" "${install_dir}" "${name}" "${os}" "${arch}" "${version}" "${format}" "${binary}" 60 | 61 | assertNotEquals "0" "$?" "download/install should have failed but did not" 62 | 63 | rm -rf -- "$download_dir" 64 | rm -rf -- "$install_dir" 65 | } 66 | 67 | 68 | test_install_asset_exercised_all_archive_assets() { 69 | expected=$(snapshot_assets_archive_count) 70 | 71 | assertEquals "${expected}" "${INSTALL_ARCHIVE_POSITIVE_CASES}" "did not download all possible archive assets (missing an os/arch/format variant?)" 72 | } 73 | 74 | 75 | worker_pid=$(setup_snapshot_server) 76 | trap 'teardown_snapshot_server ${worker_pid}' EXIT 77 | 78 | # exercise all possible archive assets (not rpm/deb/dmg) against a snapshot build 79 | run_test_case test_positive_snapshot_install_asset "linux" "amd64" "tar.gz" 80 | run_test_case test_positive_snapshot_install_asset "linux" "arm64" "tar.gz" 81 | run_test_case test_positive_snapshot_install_asset "darwin" "arm64" "tar.gz" 82 | run_test_case test_positive_snapshot_install_asset "darwin" "amd64" "tar.gz" 83 | run_test_case test_positive_snapshot_install_asset "windows" "arm64" "zip" 84 | run_test_case test_positive_snapshot_install_asset "windows" "amd64" "zip" 85 | 86 | # let's make certain we covered all assets that were expected 87 | run_test_case test_install_asset_exercised_all_archive_assets 88 | 89 | # make certain we handle missing assets alright 90 | run_test_case test_negative_snapshot_install_asset "bogus" "amd64" "zip" 91 | 92 | trap - EXIT 93 | teardown_snapshot_server "${worker_pid}" 94 | -------------------------------------------------------------------------------- /test/install/Makefile: -------------------------------------------------------------------------------- 1 | NAME=syft 2 | 3 | IMAGE_NAME=$(NAME)-install.sh-env 4 | UBUNTU_IMAGE=$(IMAGE_NAME):ubuntu-20.04 5 | ALPINE_IMAGE=$(IMAGE_NAME):alpine-3.6 6 | BUSYBOX_IMAGE=busybox:1.35 7 | 8 | ENVS=./environments 9 | DOCKER_RUN=docker run --rm -t -w /project/test/install -v $(shell pwd)/../../:/project 10 | UNIT=make unit-local 11 | 12 | # acceptance testing is running the current install.sh against the latest release. Note: this could be a problem down 13 | # the line if there are breaking changes made that don't align with the latest release (but will be OK with the next 14 | # release) 15 | ACCEPTANCE_CMD=sh -c '../../install.sh && docker sbom version' 16 | 17 | # CI cache busting values; change these if you want CI to not use previous stored cache 18 | INSTALL_TEST_CACHE_BUSTER=894d8ca 19 | 20 | define title 21 | @printf '\n≡≡≡[ $(1) ]≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡\n' 22 | endef 23 | 24 | .PHONY: test 25 | test: unit acceptance 26 | 27 | .PHONY: ci-test-mac 28 | ci-test-mac: unit-local acceptance-local 29 | 30 | # note: do not add acceptance-local to this list 31 | .PHONY: acceptance 32 | # TODO: add acceptance tests back after first release 33 | #acceptance: acceptance-ubuntu-20.04 acceptance-alpine-3.6 acceptance-busybox-1.35 34 | acceptance: 35 | @echo "[acceptance tests disabled]" 36 | 37 | .PHONY: unit 38 | unit: unit-ubuntu-20.04 39 | 40 | .PHONY: unit-local 41 | unit-local: 42 | $(call title,unit tests) 43 | @for f in $(shell ls *_test.sh); do echo "Running unit test suite '$${f}'"; bash $${f} || exit 1; done 44 | 45 | .PHONY: acceptance-local 46 | acceptance-local: acceptance-current-release-local 47 | 48 | .PHONY: acceptance-current-release-local 49 | acceptance-current-release-local: 50 | $(ACCEPTANCE_CMD) 51 | 52 | 53 | .PHONY: save 54 | save: ubuntu-20.04 alpine-3.6 busybox-1.35 55 | @mkdir cache || true 56 | docker image save -o cache/ubuntu-env.tar $(UBUNTU_IMAGE) 57 | docker image save -o cache/alpine-env.tar $(ALPINE_IMAGE) 58 | docker image save -o cache/busybox-env.tar $(BUSYBOX_IMAGE) 59 | 60 | .PHONY: load 61 | load: 62 | docker image load -i cache/ubuntu-env.tar 63 | docker image load -i cache/alpine-env.tar 64 | docker image load -i cache/busybox-env.tar 65 | 66 | ## UBUNTU ####################################################### 67 | 68 | .PHONY: acceptance-ubuntu-20.04 69 | acceptance-ubuntu-20.04: ubuntu-20.04 70 | $(call title,ubuntu:20.04 - acceptance) 71 | $(DOCKER_RUN) $(UBUNTU_IMAGE) \ 72 | $(ACCEPTANCE_CMD) 73 | 74 | .PHONY: unit-ubuntu-20.04 75 | unit-ubuntu-20.04: ubuntu-20.04 76 | $(call title,ubuntu:20.04 - unit) 77 | $(DOCKER_RUN) $(UBUNTU_IMAGE) \ 78 | $(UNIT) 79 | 80 | .PHONY: ubuntu-20.04 81 | ubuntu-20.04: 82 | $(call title,ubuntu:20.04 - build environment) 83 | docker build -t $(UBUNTU_IMAGE) -f $(ENVS)/Dockerfile-ubuntu-20.04 . 84 | 85 | ## ALPINE ####################################################### 86 | 87 | # note: unit tests cannot be run with sh (alpine dosn't have bash by default) 88 | 89 | .PHONY: acceptance-alpine-3.6 90 | acceptance-alpine-3.6: alpine-3.6 91 | $(call title,alpine:3.6 - acceptance) 92 | $(DOCKER_RUN) $(ALPINE_IMAGE) \ 93 | $(ACCEPTANCE_CMD) 94 | 95 | .PHONY: alpine-3.6 96 | alpine-3.6: 97 | $(call title,alpine:3.6 - build environment) 98 | docker build -t $(ALPINE_IMAGE) -f $(ENVS)/Dockerfile-alpine-3.6 . 99 | 100 | ## BUSYBOX ####################################################### 101 | 102 | # note: unit tests cannot be run with sh (busybox dosn't have bash by default) 103 | 104 | # note: busybox by default will not have cacerts, so you will get TLS warnings (we want to test under these conditions) 105 | 106 | .PHONY: acceptance-busybox-1.35 107 | acceptance-busybox-1.35: busybox-1.35 108 | $(call title,busybox-1.35 - acceptance) 109 | $(DOCKER_RUN) $(BUSYBOX_IMAGE) \ 110 | $(ACCEPTANCE_CMD) 111 | @echo "\n*** test note: you should see syft spit out a 'x509: certificate signed by unknown authority' error --this is expected ***" 112 | 113 | .PHONY: busybox-1.35 114 | busybox-1.35: 115 | $(call title,busybox-1.35 - build environment) 116 | docker pull $(BUSYBOX_IMAGE) 117 | 118 | ## For CI ######################################################## 119 | 120 | .PHONY: cache.fingerprint 121 | cache.fingerprint: 122 | $(call title,Install test fixture fingerprint) 123 | @find ./environments/* -type f -exec md5sum {} + | awk '{print $1}' | sort | tee /dev/stderr | md5sum | tee cache.fingerprint && echo "$(INSTALL_TEST_CACHE_BUSTER)" >> cache.fingerprint 124 | -------------------------------------------------------------------------------- /test/install/environments/Dockerfile-alpine-3.6: -------------------------------------------------------------------------------- 1 | FROM alpine:3.6 2 | RUN apk update && apk add python3 wget unzip make ca-certificates -------------------------------------------------------------------------------- /test/install/environments/Dockerfile-ubuntu-20.04: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | RUN apt update -y && apt install make python3 curl unzip -y -------------------------------------------------------------------------------- /test/install/github_test.sh: -------------------------------------------------------------------------------- 1 | . test_harness.sh 2 | 3 | # check that we can extract single json values 4 | test_extract_json_value() { 5 | fixture=./test-fixtures/github-api-docker-sbom-cli-plugin-v0.1.0-release.json 6 | content=$(cat ${fixture}) 7 | 8 | actual=$(extract_json_value "${content}" "tag_name") 9 | assertEquals "v0.1.0" "${actual}" "unable to find tag_name" 10 | 11 | actual=$(extract_json_value "${content}" "id") 12 | assertEquals "57501596" "${actual}" "unable to find tag_name" 13 | } 14 | 15 | run_test_case test_extract_json_value 16 | 17 | 18 | # check that we can extract github release tag from github api json 19 | test_github_release_tag() { 20 | fixture=./test-fixtures/github-api-docker-sbom-cli-plugin-v0.1.0-release.json 21 | content=$(cat ${fixture}) 22 | 23 | actual=$(github_release_tag "${content}") 24 | assertEquals "v0.1.0" "${actual}" "unable to find release tag" 25 | } 26 | 27 | run_test_case test_github_release_tag 28 | 29 | 30 | # download a checksums file from a locally served-up snapshot directory and compare against the file in the snapshot dir 31 | test_download_github_release_checksums_snapshot() { 32 | tmpdir=$(mktemp -d) 33 | 34 | github_download=$(snapshot_download_url) 35 | name=${PROJECT_NAME} 36 | version=$(snapshot_version) 37 | 38 | actual_filepath=$(download_github_release_checksums "${github_download}" "${name}" "${version}" "${tmpdir}") 39 | assertFilesEqual \ 40 | "$(snapshot_checksums_path)" \ 41 | "${actual_filepath}" \ 42 | "unable to find release tag" 43 | 44 | rm -rf -- "$tmpdir" 45 | } 46 | 47 | run_test_case_with_snapshot_release test_download_github_release_checksums_snapshot 48 | -------------------------------------------------------------------------------- /test/install/test-fixtures/github-api-docker-sbom-cli-plugin-v0.1.0-release.json: -------------------------------------------------------------------------------- 1 | {"id":57501596,"tag_name":"v0.1.0","update_url":"/docker/sbom-cli-plugin/releases/tag/v0.1.0","update_authenticity_token":"7XbNZgRHpbHegdv-xRlbe84Y983YgyXa3YKWwv_e0ocqTHagsHq5dxCTQUQnuX3vbsgdWQU3A3__hkVNhKGHSg","delete_url":"/docker/sbom-cli-plugin/releases/tag/v0.1.0","delete_authenticity_token":"6tLaRtXKUc-zz4tHIwCbbD7CksxIHK5imZE1gnA39oVCe6fYux5a8cPD9J52kGUzM1Hs9JPBjceG7yyszBk_2A","edit_url":"/docker/sbom-cli-plugin/releases/edit/v0.1.0"} 2 | -------------------------------------------------------------------------------- /test/install/test-fixtures/sbom-cli-plugin_0.0.0-SNAPSHOT-ac27dcf_checksums.txt: -------------------------------------------------------------------------------- 1 | 0b4629c37b1b96db6925ec1aca80cfa3fc1530e806e31e6cafb0830ab4d50b4b docker-sbom-cli-plugin_0.0.0-SNAPSHOT-ac27dcf_linux_amd64.tar.gz 2 | 3dc8ef313fe5e6b026688eb248d1ec6f52ebec2cc2bb193590b9d6aacb78970b docker-sbom-cli-plugin_0.0.0-SNAPSHOT-ac27dcf_linux_arm64.tar.gz 3 | 45bac6ead378d1e39aecc2dea5c1f9d3a7419b165bb15162f4ead185dbf0c673 docker-sbom-cli-plugin_0.0.0-SNAPSHOT-ac27dcf_darwin_amd64.tar.gz 4 | 7c8722542d6246c948d833a876271ab236bc5120f8475e3075d3153856ad8024 docker-sbom-cli-plugin_0.0.0-SNAPSHOT-ac27dcf_darwin_arm64.tar.gz 5 | 9d959b164af7af4ebb4a491145e8524817ba1d546f8bda7b635eb29304c0ad10 docker-sbom-cli-plugin_0.0.0-SNAPSHOT-ac27dcf_windows_arm64.zip 6 | f516eb22558acaa1ffc2534d23c2038e43ddd0f16d4211d8c21cd872dc9750a2 docker-sbom-cli-plugin_0.0.0-SNAPSHOT-ac27dcf_windows_amd64.zip 7 | -------------------------------------------------------------------------------- /test/install/test_harness.sh: -------------------------------------------------------------------------------- 1 | # disable using the install.sh entrypoint such that we can unit test 2 | # script functions without invoking main() 3 | TEST_INSTALL_SH=true 4 | 5 | . ../../install.sh 6 | set -u 7 | 8 | echoerr() { 9 | echo "$@" 1>&2 10 | } 11 | 12 | printferr() { 13 | printf "%s" "$*" >&2 14 | } 15 | 16 | 17 | assertTrue() { 18 | if eval "$1"; then 19 | echo "assertTrue failed: $2" 20 | exit 2 21 | fi 22 | } 23 | 24 | assertFalse() { 25 | if eval "$1"; then 26 | echo "assertFalse failed: $2" 27 | exit 2 28 | fi 29 | } 30 | 31 | assertEquals() { 32 | want=$1 33 | got=$2 34 | msg=$3 35 | if [ "$want" != "$got" ]; then 36 | echo "assertEquals failed: want='$want' got='$got' $msg" 37 | exit 2 38 | fi 39 | } 40 | 41 | assertFilesDoesNotExist() { 42 | path="$1" 43 | msg=$2 44 | if [ -f "${path}" ]; then 45 | echo "assertFilesDoesNotExist failed: path exists '$path': $msg" 46 | exit 2 47 | fi 48 | } 49 | 50 | assertFileExists() { 51 | path="$1" 52 | msg=$2 53 | if [ ! -f "${path}" ]; then 54 | echo "assertFileExists failed: path does not exist '$path': $msg" 55 | exit 2 56 | fi 57 | } 58 | 59 | assertFilesEqual() { 60 | want=$1 61 | got=$2 62 | msg=$3 63 | 64 | diff "$1" "$2" 65 | if [ $? -ne 0 ]; then 66 | echo "assertFilesEqual failed: $msg" 67 | exit 2 68 | fi 69 | } 70 | 71 | assertNotEquals() { 72 | want=$1 73 | got=$2 74 | msg=$3 75 | if [ "$want" = "$got" ]; then 76 | echo "assertNotEquals failed: want='$want' got='$got' $msg" 77 | exit 2 78 | fi 79 | } 80 | 81 | log_test_case() { 82 | echo " running $@" 83 | } 84 | 85 | run_test_case_with_snapshot_release() { 86 | log_test_case ${@:1} 87 | 88 | worker_pid=$(setup_snapshot_server) 89 | trap "teardown_snapshot_server $worker_pid" EXIT 90 | 91 | # run test function with all arguments 92 | ${@:1} 93 | 94 | trap - EXIT 95 | teardown_snapshot_server "${worker_pid}" 96 | } 97 | 98 | serve_port=8000 99 | 100 | setup_snapshot_server() { 101 | # if you want to see proof in the logs, feel free to adjust the redirection 102 | python3 -m http.server --directory "$(snapshot_dir)" $serve_port &> /dev/null & 103 | worker_pid=$! 104 | 105 | echoerr "serving up $(snapshot_dir) on port $serve_port" 106 | 107 | echoerr "$(ls -1 $(snapshot_dir) | sed 's/^/ ▕―― /')" 108 | 109 | check_snapshots_server_ready 110 | 111 | echoerr "snapshot server ready! (worker=${worker_pid})" 112 | 113 | echo "$worker_pid" 114 | } 115 | 116 | check_snapshots_server_ready() { 117 | i=0 118 | until $(curl -m 3 --output /dev/null --silent --head --fail localhost:$serve_port/); do 119 | sleep 1 120 | ((i=i+1)) 121 | if [ "$i" -gt "30" ]; then 122 | echoerr "could not connect to local snapshot server! bailing..." 123 | exit 1 124 | fi 125 | printferr '.' 126 | done 127 | } 128 | 129 | teardown_snapshot_server() { 130 | worker_pid="$1" 131 | echoerr "stopping worker=${worker_pid}" 132 | kill "$worker_pid" 133 | } 134 | 135 | snapshot_version() { 136 | partial=$(ls ../../snapshot/*_checksums.txt | grep -o "_.*_checksums.txt") 137 | partial="${partial%_checksums.txt}" 138 | echo "${partial#_}" 139 | } 140 | 141 | snapshot_download_url() { 142 | echo "localhost:${serve_port}" 143 | } 144 | 145 | snapshot_dir() { 146 | echo "../../snapshot" 147 | } 148 | 149 | snapshot_checksums_path() { 150 | echo "$(ls $(snapshot_dir)/*_checksums.txt)" 151 | } 152 | 153 | snapshot_assets_count() { 154 | # example output before wc -l: 155 | 156 | # ../../snapshot/sbom-cli-plugin_0.1.0-SNAPSHOT-e5e847a_linux_arm64.deb 157 | # ../../snapshot/sbom-cli-plugin_0.1.0-SNAPSHOT-e5e847a_linux_arm64.tar.gz 158 | # ../../snapshot/sbom-cli-plugin_0.1.0-SNAPSHOT-e5e847a_darwin_arm64.tar.gz 159 | # ../../snapshot/sbom-cli-plugin_0.1.0-SNAPSHOT-e5e847a_darwin_amd64.tar.gz 160 | # ../../snapshot/sbom-cli-plugin_0.1.0-SNAPSHOT-e5e847a_windows_amd64.zip 161 | # ../../snapshot/sbom-cli-plugin_0.1.0-SNAPSHOT-e5e847a_windows_arm64.zip 162 | 163 | echo "$(find ../../snapshot -maxdepth 1 -type f | grep 'sbom-cli-plugin_' | grep -v checksums | wc -l | tr -d '[:space:]')" 164 | } 165 | 166 | 167 | snapshot_assets_archive_count() { 168 | # example output before wc -l: 169 | 170 | # ../../snapshot/sbom-cli-plugin_0.1.0-SNAPSHOT-e5e847a_linux_arm64.tar.gz 171 | # ../../snapshot/sbom-cli-plugin_0.1.0-SNAPSHOT-e5e847a_darwin_arm64.tar.gz 172 | # ../../snapshot/sbom-cli-plugin_0.1.0-SNAPSHOT-e5e847a_darwin_amd64.zip 173 | # ../../snapshot/sbom-cli-plugin_0.1.0-SNAPSHOT-e5e847a_windows_amd64.zip 174 | # ../../snapshot/sbom-cli-plugin_0.1.0-SNAPSHOT-e5e847a_windows_arm4.zip 175 | # ../../snapshot/sbom-cli-plugin_0.1.0-SNAPSHOT-e5e847a_linux_amd64.tar.gz 176 | # ../../snapshot/sbom-cli-plugin_0.1.0-SNAPSHOT-e5e847a_darwin_amd64.tar.gz 177 | 178 | echo "$(find ../../snapshot -maxdepth 1 -type f | grep 'sbom-cli-plugin_' | grep 'tar\|zip' | wc -l | tr -d '[:space:]')" 179 | } 180 | 181 | 182 | run_test_case() { 183 | log_test_case ${@:1} 184 | ${@:1} 185 | } 186 | --------------------------------------------------------------------------------