Compare commits

...
24 Commits
Author SHA1 Message Date
Christopher Angelo Phillips 3286a4d4cc update docker manifest tagging (#600)
Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
2021-10-28 15:41:39 -04:00
Christopher Angelo Phillips 0ff7013382 update manifest to use Tag over Version
Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
2021-10-28 15:05:34 -04:00
Christopher Angelo Phillips 71708e5bf8 update goreleaser with correct tag information (#598)
Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
2021-10-28 13:37:22 -04:00
Jonas Galvão Xavier dd024298eb stop hidding command from help doc (#596)
Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>
2021-10-27 13:49:04 -07:00
Christopher Angelo Phillips 1c63943055 Add arm64 image support and Darwin M1 support to .goreleaser.yaml (#591)
* update support arm64

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>

* small update syntax

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>

* restore release command

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>

* add docker manifests

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
2021-10-26 09:42:35 -04:00
Alex Goodman ff5fa2dc1d pull in space suffix fix (#587)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-10-25 10:14:17 -04:00
Alex Goodman fb588ff500 replace table presenter with format object (#586)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-10-24 08:09:27 -04:00
Alex Goodman d5b425e1b5 force exiting the etui upon worker error (#583)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-10-22 13:44:50 -04:00
Dan Luhring 7b319e32bb Remove webinar announcement (#582)
Signed-off-by: Dan Luhring <dan.luhring@anchore.com>
2021-10-22 07:48:36 -04:00
Alex Goodman 89242e6ed3 bump chronicle to v0.2.0-beta (#581)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-10-21 18:34:17 +00:00
Alex Goodman 4bf08e6b6d swap out the changelog generator for chronicle tool (#580)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-10-21 13:37:13 -04:00
Alex Goodman 87ce9fff06 swap go binary parsing warning logging entry (#579)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-10-21 17:03:07 +00:00
Christopher Angelo Phillips 10fa8dc7c9 Add windows support (#548)
* update  build tags, ui support, and stereoscope, and release for windows support

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
2021-10-21 12:49:36 -04:00
Alex Goodman 560b05c2c9 Introduce new format pattern + port json processing (#550)
* add new format pattern

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* add syftjson format

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* add internal formats helper

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* add SBOM encode/decode to lib API

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* remove json presenter + update presenter tests to use common utils

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* remove presenter format enum type + add formats shim in presenter helper

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* add MustCPE helper for tests

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* update usage of format enum

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* add test fixtures for encode/decode tests

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* fix integration test

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* migrate format detection to use reader

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* address review comments

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-10-20 21:36:34 +00:00
Alex Goodman 5e315c0f17 Disable ETUI for piped input (#571)
* fixed piped input

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* allow pipedinput helper to raise an error

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* factor out verbosity check to function

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-10-20 12:40:52 -04:00
Alex Goodman 6f3fa494d7 Show help text when no args are given (#573)
* show help text when no args are given

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* repurpose the input args validation function

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* ensure app does not check for update in cli tests

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-10-20 09:51:05 -04:00
Alex GoodmanandChristopher Angelo Phillips a82463574b Rollback goreleaser version (#570)
* rollback goreleaser version

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* update go sum

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>

Co-authored-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
2021-10-19 12:01:51 -04:00
Alex Goodman cbdb72ea22 remove unnecessary codeql checkout from second parent commit (#567)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-10-18 13:31:19 -04:00
Christopher Angelo Phillips 7a005fec06 update to point at new script for new release (#551)
* update to point at local script release

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
2021-10-15 13:26:34 -04:00
Dan Luhring c0f55866ec Announce upcoming webinar (#553)
Signed-off-by: Dan Luhring <dan.luhring@anchore.com>
2021-10-15 12:13:28 -04:00
Christopher Angelo Phillips 9ea6330ebc update split in py package_cataloger to cap at 2 (#549)
Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
2021-10-14 12:32:06 -04:00
Christopher Angelo Phillips 7d13fbf0d4 540 add additional go package metadata (#546)
* update with distinct golang metadata types

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
2021-10-13 11:00:28 -04:00
Dan Luhring 10368f6fe8 Add Syft logo (#545)
Signed-off-by: Dan Luhring <dan.luhring@anchore.com>
2021-10-11 21:06:27 +00:00
Josh Bressers f2840029d1 Allow Syft to build with podman (#537)
* Don't check the Built-By flag

Signed-off-by: Josh Bressers <josh@bress.net>

* Remove alpine pinning to resolve conflict with main

Signed-off-by: Josh Bressers <josh@bress.net>
2021-10-11 12:39:34 -04:00
95 changed files with 3902 additions and 600 deletions
+398
View File
@@ -0,0 +1,398 @@
#!/bin/sh
set -e
# Code generated by godownloader on 2019-12-25T12:47:14Z. DO NOT EDIT.
#
usage() {
this=$1
cat <<EOF
$this: download go binaries for goreleaser/goreleaser
Usage: $this [-b] bindir [-d] [tag]
-b sets bindir or installation directory, Defaults to ./bin
-d turns on debug logging
[tag] is a tag from
https://github.com/goreleaser/goreleaser/releases
If tag is missing, then the latest will be used.
Generated by godownloader
https://github.com/goreleaser/godownloader
EOF
exit 2
}
parse_args() {
#BINDIR is ./bin unless set be ENV
# over-ridden by flag below
BINDIR=${BINDIR:-./bin}
while getopts "b:dh?x" arg; do
case "$arg" in
b) BINDIR="$OPTARG" ;;
d) log_set_priority 10 ;;
h | \?) usage "$0" ;;
x) set -x ;;
esac
done
shift $((OPTIND - 1))
TAG=$1
}
# this function wraps all the destructive operations
# if a curl|bash cuts off the end of the script due to
# network, either nothing will happen or will syntax error
# out preventing half-done work
execute() {
tmpdir=$(mktemp -d)
log_debug "downloading files into ${tmpdir}"
http_download "${tmpdir}/${TARBALL}" "${TARBALL_URL}"
http_download "${tmpdir}/${CHECKSUM}" "${CHECKSUM_URL}"
hash_sha256_verify "${tmpdir}/${TARBALL}" "${tmpdir}/${CHECKSUM}"
srcdir="${tmpdir}"
(cd "${tmpdir}" && untar "${TARBALL}")
test ! -d "${BINDIR}" && install -d "${BINDIR}"
for binexe in $BINARIES; do
if [ "$OS" = "windows" ]; then
binexe="${binexe}.exe"
fi
install "${srcdir}/${binexe}" "${BINDIR}/"
log_info "installed ${BINDIR}/${binexe}"
done
rm -rf "${tmpdir}"
}
get_binaries() {
case "$PLATFORM" in
darwin/386) BINARIES="goreleaser" ;;
darwin/amd64) BINARIES="goreleaser" ;;
darwin/arm64) BINARIES="goreleaser" ;;
darwin/armv6) BINARIES="goreleaser" ;;
linux/386) BINARIES="goreleaser" ;;
linux/amd64) BINARIES="goreleaser" ;;
linux/arm64) BINARIES="goreleaser" ;;
linux/armv6) BINARIES="goreleaser" ;;
windows/386) BINARIES="goreleaser" ;;
windows/amd64) BINARIES="goreleaser" ;;
windows/arm64) BINARIES="goreleaser" ;;
windows/armv6) BINARIES="goreleaser" ;;
*)
log_crit "platform $PLATFORM is not supported. Make sure this script is up-to-date and file request at https://github.com/${PREFIX}/issues/new"
exit 1
;;
esac
}
tag_to_version() {
if [ -z "${TAG}" ]; then
log_info "checking GitHub for latest tag"
else
log_info "checking GitHub for tag '${TAG}'"
fi
REALTAG=$(github_release "$OWNER/$REPO" "${TAG}") && true
if test -z "$REALTAG"; then
log_crit "unable to find '${TAG}' - use 'latest' or see https://github.com/${PREFIX}/releases for details"
exit 1
fi
# if version starts with 'v', remove it
TAG="$REALTAG"
VERSION=${TAG#v}
}
adjust_format() {
# change format (tar.gz or zip) based on OS
case ${OS} in
windows) FORMAT=zip ;;
esac
true
}
adjust_os() {
# adjust archive name based on OS
case ${OS} in
386) OS=i386 ;;
amd64) OS=x86_64 ;;
darwin) OS=Darwin ;;
linux) OS=Linux ;;
windows) OS=Windows ;;
esac
true
}
adjust_arch() {
# adjust archive name based on ARCH
case ${ARCH} in
386) ARCH=i386 ;;
amd64) ARCH=x86_64 ;;
darwin) ARCH=Darwin ;;
linux) ARCH=Linux ;;
windows) ARCH=Windows ;;
esac
true
}
cat /dev/null <<EOF
------------------------------------------------------------------------
https://github.com/client9/shlib - portable posix shell functions
Public domain - http://unlicense.org
https://github.com/client9/shlib/blob/master/LICENSE.md
but credit (and pull requests) appreciated.
------------------------------------------------------------------------
EOF
is_command() {
command -v "$1" >/dev/null
}
echoerr() {
echo "$@" 1>&2
}
log_prefix() {
echo "$0"
}
_logp=6
log_set_priority() {
_logp="$1"
}
log_priority() {
if test -z "$1"; then
echo "$_logp"
return
fi
[ "$1" -le "$_logp" ]
}
log_tag() {
case $1 in
0) echo "emerg" ;;
1) echo "alert" ;;
2) echo "crit" ;;
3) echo "err" ;;
4) echo "warning" ;;
5) echo "notice" ;;
6) echo "info" ;;
7) echo "debug" ;;
*) echo "$1" ;;
esac
}
log_debug() {
log_priority 7 || return 0
echoerr "$(log_prefix)" "$(log_tag 7)" "$@"
}
log_info() {
log_priority 6 || return 0
echoerr "$(log_prefix)" "$(log_tag 6)" "$@"
}
log_err() {
log_priority 3 || return 0
echoerr "$(log_prefix)" "$(log_tag 3)" "$@"
}
log_crit() {
log_priority 2 || return 0
echoerr "$(log_prefix)" "$(log_tag 2)" "$@"
}
uname_os() {
os=$(uname -s | tr '[:upper:]' '[:lower:]')
case "$os" in
cygwin_nt*) os="windows" ;;
mingw*) os="windows" ;;
msys_nt*) os="windows" ;;
esac
echo "$os"
}
uname_arch() {
arch=$(uname -m)
case $arch in
x86_64) arch="amd64" ;;
x86) arch="386" ;;
i686) arch="386" ;;
i386) arch="386" ;;
aarch64) arch="arm64" ;;
armv5*) arch="armv5" ;;
armv6*) arch="armv6" ;;
armv7*) arch="armv7" ;;
esac
echo ${arch}
}
uname_os_check() {
os=$(uname_os)
case "$os" in
darwin) return 0 ;;
dragonfly) return 0 ;;
freebsd) return 0 ;;
linux) return 0 ;;
android) return 0 ;;
nacl) return 0 ;;
netbsd) return 0 ;;
openbsd) return 0 ;;
plan9) return 0 ;;
solaris) return 0 ;;
windows) return 0 ;;
esac
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"
return 1
}
uname_arch_check() {
arch=$(uname_arch)
case "$arch" in
386) return 0 ;;
amd64) return 0 ;;
arm64) return 0 ;;
armv5) return 0 ;;
armv6) return 0 ;;
armv7) return 0 ;;
ppc64) return 0 ;;
ppc64le) return 0 ;;
mips) return 0 ;;
mipsle) return 0 ;;
mips64) return 0 ;;
mips64le) return 0 ;;
s390x) return 0 ;;
amd64p32) return 0 ;;
esac
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"
return 1
}
untar() {
tarball=$1
case "${tarball}" in
*.tar.gz | *.tgz) tar --no-same-owner -xzf "${tarball}" ;;
*.tar) tar --no-same-owner -xf "${tarball}" ;;
*.zip) unzip "${tarball}" ;;
*)
log_err "untar unknown archive format for ${tarball}"
return 1
;;
esac
}
http_download_curl() {
local_file=$1
source_url=$2
header=$3
if [ -z "$header" ]; then
code=$(curl -w '%{http_code}' -sL -o "$local_file" "$source_url")
else
code=$(curl -w '%{http_code}' -sL -H "$header" -o "$local_file" "$source_url")
fi
if [ "$code" != "200" ]; then
log_debug "http_download_curl received HTTP status $code"
return 1
fi
return 0
}
http_download_wget() {
local_file=$1
source_url=$2
header=$3
if [ -z "$header" ]; then
wget -q -O "$local_file" "$source_url"
else
wget -q --header "$header" -O "$local_file" "$source_url"
fi
}
http_download() {
log_debug "http_download $2"
if is_command curl; then
http_download_curl "$@"
return
elif is_command wget; then
http_download_wget "$@"
return
fi
log_crit "http_download unable to find wget or curl"
return 1
}
http_copy() {
tmp=$(mktemp)
http_download "${tmp}" "$1" "$2" || return 1
body=$(cat "$tmp")
rm -f "${tmp}"
echo "$body"
}
github_release() {
owner_repo=$1
version=$2
test -z "$version" && version="latest"
giturl="https://github.com/${owner_repo}/releases/${version}"
json=$(http_copy "$giturl" "Accept:application/json")
test -z "$json" && return 1
version=$(echo "$json" | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//')
test -z "$version" && return 1
echo "$version"
}
hash_sha256() {
TARGET=${1:-/dev/stdin}
if is_command gsha256sum; then
hash=$(gsha256sum "$TARGET") || return 1
echo "$hash" | cut -d ' ' -f 1
elif is_command sha256sum; then
hash=$(sha256sum "$TARGET") || return 1
echo "$hash" | cut -d ' ' -f 1
elif is_command shasum; then
hash=$(shasum -a 256 "$TARGET" 2>/dev/null) || return 1
echo "$hash" | cut -d ' ' -f 1
elif is_command openssl; then
hash=$(openssl -dst openssl dgst -sha256 "$TARGET") || return 1
echo "$hash" | cut -d ' ' -f a
else
log_crit "hash_sha256 unable to find command to compute sha-256 hash"
return 1
fi
}
hash_sha256_verify() {
TARGET=$1
checksums=$2
if [ -z "$checksums" ]; then
log_err "hash_sha256_verify checksum file not specified in arg2"
return 1
fi
BASENAME=${TARGET##*/}
want=$(grep "${BASENAME}" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1)
if [ -z "$want" ]; then
log_err "hash_sha256_verify unable to find checksum for '${TARGET}' in '${checksums}'"
return 1
fi
got=$(hash_sha256 "$TARGET")
if [ "$want" != "$got" ]; then
log_err "hash_sha256_verify checksum for '$TARGET' did not verify ${want} vs $got"
return 1
fi
}
cat /dev/null <<EOF
------------------------------------------------------------------------
End of functions from https://github.com/client9/shlib
------------------------------------------------------------------------
EOF
PROJECT_NAME="goreleaser"
OWNER=goreleaser
REPO="goreleaser"
BINARY=goreleaser
FORMAT=tar.gz
OS=$(uname_os)
ARCH=$(uname_arch)
PREFIX="$OWNER/$REPO"
# use in logging routines
log_prefix() {
echo "$PREFIX"
}
PLATFORM="${OS}/${ARCH}"
GITHUB_DOWNLOAD=https://github.com/${OWNER}/${REPO}/releases/download
uname_os_check "$OS"
uname_arch_check "$ARCH"
parse_args "$@"
get_binaries
tag_to_version
adjust_format
adjust_os
adjust_arch
log_info "found version: ${VERSION} for ${TAG}/${OS}/${ARCH}"
NAME=${PROJECT_NAME}_${OS}_${ARCH}
TARBALL=${NAME}.${FORMAT}
TARBALL_URL=${GITHUB_DOWNLOAD}/${TAG}/${TARBALL}
CHECKSUM=checksums.txt
CHECKSUM_URL=${GITHUB_DOWNLOAD}/${TAG}/${CHECKSUM}
execute
log_err "this script is deprecated, please do not use it anymore. check https://github.com/goreleaser/godownloader/issues/207"
-9
View File
@@ -31,15 +31,6 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
-4
View File
@@ -96,10 +96,6 @@ jobs:
steps:
- uses: docker-practice/actions-setup-docker@v1
# note, it is important to always be auth'd into docker.io to prevent rate limiting issues
- name: Login to Docker Hub
run: echo ${{ secrets.TOOLBOX_DOCKER_PASS }} | docker login docker.io -u ${{ secrets.TOOLBOX_DOCKER_USER }} --password-stdin
- uses: actions/setup-go@v2
with:
go-version: ${{ env.GO_VERSION }}
+7
View File
@@ -232,6 +232,9 @@ jobs:
- uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Restore tool cache
id: tool-cache
uses: actions/cache@v2.1.3
@@ -305,6 +308,10 @@ jobs:
needs: [Build-Snapshot-Artifacts]
runs-on: ubuntu-20.04
steps:
- uses: actions/setup-go@v2
with:
go-version: ${{ env.GO_VERSION }}
- uses: actions/checkout@v2
- name: Restore go cache
+64 -10
View File
@@ -12,9 +12,27 @@ builds:
env:
- CGO_ENABLED=0
goos:
# windows not supported yet (due to jotframe)
# - windows
- linux
goarch:
- amd64
- arm64
# Set the modified timestamp on the output binary to the git timestamp (to ensure a reproducible build)
mod_timestamp: '{{ .CommitTimestamp }}'
ldflags: |
-w
-s
-extldflags '-static'
-X github.com/anchore/syft/internal/version.version={{.Version}}
-X github.com/anchore/syft/internal/version.gitCommit={{.Commit}}
-X github.com/anchore/syft/internal/version.buildDate={{.Date}}
-X github.com/anchore/syft/internal/version.gitTreeState={{.Env.BUILD_GIT_TREE_STATE}}
- binary: syft
id: syft-win
env:
- CGO_ENABLED=0
goos:
- windows
goarch:
- amd64
# Set the modified timestamp on the output binary to the git timestamp (to ensure a reproducible build)
@@ -27,7 +45,8 @@ builds:
-X github.com/anchore/syft/internal/version.gitCommit={{.Commit}}
-X github.com/anchore/syft/internal/version.buildDate={{.Date}}
-X github.com/anchore/syft/internal/version.gitTreeState={{.Env.BUILD_GIT_TREE_STATE}}
# For more info on this macOS build, see: https://github.com/mitchellh/gon#usage-with-goreleaser
# For more info on this macOS build, see: https://github.com/mitchellh/gon#usage-with-goreleaser
- binary: syft
id: syft-macos
env:
@@ -36,6 +55,7 @@ builds:
- darwin
goarch:
- amd64
- arm64
# Set the modified timestamp on the output binary to the git timestamp (to ensure a reproducible build)
mod_timestamp: '{{ .CommitTimestamp }}'
ldflags: |
@@ -51,10 +71,11 @@ archives:
- format: tar.gz
builds:
- syft # i.e. Linux only
- format: zip # This is a hack! We don't actually intend to use _this_ ZIP file, we just need goreleaser to consider the ZIP file produced by gon (which will have the same file name) to be an artifact so we can use it downstream in publishing (e.g. to a homebrew tap)
- format: zip # This is a hack for syft-macos! We don't actually intend to use _this_ ZIP file, we just need goreleaser to consider the ZIP file produced by gon (which will have the same file name) to be an artifact so we can use it downstream in publishing (e.g. to a homebrew tap)
id: syft-zip
builds:
- syft-macos
- syft-win
signs:
- artifacts: checksum
@@ -92,15 +113,48 @@ brews:
description: *description
dockers:
- dockerfile: Dockerfile
image_templates:
- image_templates:
- "anchore/syft:latest"
- "anchore/syft:{{ .Tag }}"
- "anchore/syft:v{{ .Major }}"
- "anchore/syft:v{{ .Major }}.{{ .Minor }}"
- "anchore/syft:{{ .Tag }}-amd64"
- "anchore/syft:v{{ .Major }}-amd64"
- "anchore/syft:v{{ .Major }}.{{ .Minor }}-amd64"
dockerfile: Dockerfile
use: buildx
build_flag_templates:
- "--platform=linux/amd64"
- "--build-arg=BUILD_DATE={{.Date}}"
- "--build-arg=BUILD_VERSION={{.Version}}"
- "--build-arg=VCS_REF={{.FullCommit}}"
- "--build-arg=VCS_URL={{.GitURL}}"
use_buildx: true
- image_templates:
- "anchore/syft:{{ .Tag }}-arm64v8"
- "anchore/syft:v{{ .Major }}-arm64v8"
- "anchore/syft:v{{ .Major }}.{{ .Minor }}-arm64v8"
goarch: arm64
dockerfile: Dockerfile
use: buildx
build_flag_templates:
- "--platform=linux/arm64/v8"
- "--build-arg=BUILD_DATE={{.Date}}"
- "--build-arg=BUILD_VERSION={{.Version}}"
- "--build-arg=VCS_REF={{.FullCommit}}"
- "--build-arg=VCS_URL={{.GitURL}}"
docker_manifests:
- name_template: anchore/syft:{{ .Tag }}
image_templates:
- anchore/syft:{{ .Tag }}-amd64
- anchore/syft:v{{ .Major }}-amd64
- anchore/syft:v{{ .Major }}.{{ .Minor }}-amd64
- anchore/syft:{{ .Tag }}-arm64v8
- anchore/syft:v{{ .Major }}-arm64v8
- anchore/syft:v{{ .Major }}.{{ .Minor }}-arm64v8
- name_template: anchore/syft:latest
image_templates:
- anchore/syft:{{ .Tag }}-amd64
- anchore/syft:v{{ .Major }}-amd64
- anchore/syft:v{{ .Major }}.{{ .Minor }}-amd64
- anchore/syft:{{ .Tag }}-arm64v8
- anchore/syft:v{{ .Major }}-arm64v8
- anchore/syft:v{{ .Major }}.{{ .Minor }}-arm64v8
+9 -21
View File
@@ -103,7 +103,8 @@ bootstrap-tools: $(TEMPDIR)
GO111MODULE=off GOBIN=$(shell realpath $(TEMPDIR)) go get -u golang.org/x/perf/cmd/benchstat
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(TEMPDIR)/ v1.42.1
curl -sSfL https://raw.githubusercontent.com/wagoodman/go-bouncer/master/bouncer.sh | sh -s -- -b $(TEMPDIR)/ v0.2.0
curl -sfL https://install.goreleaser.com/github.com/goreleaser/goreleaser.sh | sh -s -- -b $(TEMPDIR)/ v0.177.0
curl -sSfL https://raw.githubusercontent.com/anchore/chronicle/main/install.sh | sh -s -- -b $(TEMPDIR)/ v0.2.0-beta
.github/scripts/goreleaser-install.sh -b $(TEMPDIR)/ v0.177.0
.PHONY: bootstrap-go
bootstrap-go:
@@ -203,7 +204,9 @@ $(SNAPSHOTDIR): ## Build snapshot release binaries and packages
cat .goreleaser.yaml >> $(TEMPDIR)/goreleaser.yaml
# build release snapshots
# DOCKER_CLI_EXPERIMENTAL needed to support multi architecture builds for goreleaser
BUILD_GIT_TREE_STATE=$(GITTREESTATE) \
DOCKER_CLI_EXPERIMENTAL=enabled \
$(TEMPDIR)/goreleaser release --skip-publish --skip-sign --rm-dist --snapshot --config $(TEMPDIR)/goreleaser.yaml
# note: we cannot clean the snapshot directory since the pipeline builds the snapshot separately
@@ -249,33 +252,16 @@ cli: $(SNAPSHOTDIR) ## Run CLI tests
SYFT_BINARY_LOCATION='$(SNAPSHOT_CMD)' \
go test -count=1 -v ./test/cli
.PHONY: changlog-release
.PHONY: changelog-release
changelog-release:
@echo "Last tag: $(SECOND_TO_LAST_TAG)"
@docker run --rm \
-v "$(shell pwd)":/usr/local/src/your-app \
ferrarimarco/github-changelog-generator \
--user anchore \
--project $(BIN) \
-t ${GITHUB_TOKEN} \
--exclude-labels 'duplicate,question,invalid,wontfix,size:small,size:medium,size:large,size:x-large' \
--no-pr-wo-labels \
--no-issues-wo-labels \
--since-tag $(SECOND_TO_LAST_TAG)
$(TEMPDIR)/chronicle --since-tag $(SECOND_TO_LAST_TAG) --until-tag $(LAST_TAG) -vv > CHANGELOG.md
@printf '\n$(BOLD)$(CYAN)Release $(VERSION) Changelog$(RESET)\n\n'
@cat CHANGELOG.md
.PHONY: changelog-unreleased
changelog-unreleased: ## show the current changelog that will be produced on the next release (note: requires GITHUB_TOKEN set)
@docker run -it --rm \
-v "$(shell pwd)":/usr/local/src/your-app \
ferrarimarco/github-changelog-generator \
--user anchore \
--project $(BIN) \
-t ${GITHUB_TOKEN} \
--exclude-labels 'duplicate,question,invalid,wontfix,size:small,size:medium,size:large,size:x-large' \
--since-tag $(LAST_TAG)
$(TEMPDIR)/chronicle --since-tag $(LAST_TAG) -vv > CHANGELOG.md
@printf '\n$(BOLD)$(CYAN)Unreleased Changes (closed PRs and issues will not be in the final changelog)$(RESET)\n'
@@ -301,9 +287,11 @@ release: clean-dist changelog-release ## Build and publish final binaries and pa
cat .goreleaser.yaml >> $(TEMPDIR)/goreleaser.yaml
# release (note the version transformation from v0.7.0 --> 0.7.0)
# DOCKER_CLI_EXPERIMENTAL needed to support multi architecture builds for goreleaser
bash -c "\
BUILD_GIT_TREE_STATE=$(GITTREESTATE) \
VERSION=$(VERSION:v%=%) \
DOCKER_CLI_EXPERIMENTAL=enabled \
$(TEMPDIR)/goreleaser \
--rm-dist \
--config $(TEMPDIR)/goreleaser.yaml \
+5 -11
View File
@@ -1,4 +1,6 @@
# syft
<p align="center">
<img src="https://user-images.githubusercontent.com/5199289/136844524-1527b09f-c5cb-4aa9-be54-5aa92a6086c1.png" width="271">
</p>
[![Validations](https://github.com/anchore/syft/actions/workflows/validations.yaml/badge.svg)](https://github.com/anchore/syft/actions/workflows/validations.yaml)
[![Go Report Card](https://goreportcard.com/badge/github.com/anchore/syft)](https://goreportcard.com/report/github.com/anchore/syft)
@@ -7,19 +9,11 @@
[![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/anchore/syft/blob/main/LICENSE)
[![Slack Invite](https://img.shields.io/badge/Slack-Join-blue?logo=slack)](https://anchore.com/slack)
A CLI tool and go library for generating a Software Bill of Materials (SBOM) from container images and filesystems. Exceptional for vulnerability detection when used with a scanner tool like [Grype](https://github.com/anchore/grype).
## We'll be at KubeCon 2021!
Attending KubeCon 2021 in person? Join us for a meetup on **Tuesday, October 12th**.
We’ll have free swag, giveaways, snacks, and sips. Space will be limited, so make sure to [save your seat](https://get.anchore.com/2021-kubecon-na-opensource-happy-hour/)!
---
A CLI tool and Go library for generating a Software Bill of Materials (SBOM) from container images and filesystems. Exceptional for vulnerability detection when used with a scanner tool like [Grype](https://github.com/anchore/grype).
![syft-demo](https://user-images.githubusercontent.com/590471/90277200-2a253000-de33-11ea-893f-32c219eea11a.gif)
**Features**
## Features
- Catalog container images and filesystems to discover packages and libraries.
- Supports packages and libraries from various ecosystems (APK, DEB, RPM, Ruby Bundles, Python Wheel/Egg/requirements.txt, JavaScript NPM/Yarn, Java JAR/EAR/WAR, Jenkins plugins JPI/HPI, Go modules)
- Linux distribution identification (supports Alpine, BusyBox, CentOS/RedHat, Debian/Ubuntu flavored distributions)
+2 -3
View File
@@ -14,9 +14,8 @@ import (
// completionCmd represents the completion command
var completionCmd = &cobra.Command{
Hidden: true,
Use: "completion [bash|zsh|fish]",
Short: "Generate a shell completion for Syft (listing local docker images)",
Use: "completion [bash|zsh|fish]",
Short: "Generate a shell completion for Syft (listing local docker images)",
Long: `To load completions (docker image list):
Bash:
+3 -1
View File
@@ -44,6 +44,9 @@ func eventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription *
if err := subscription.Unsubscribe(); err != nil {
retErr = multierror.Append(retErr, err)
}
// the worker has exited, we may have been mid-handling events for the UI which should now be
// ignored, in which case forcing a teardown of the UI irregardless of the state is required.
forceTeardown = true
}
case e, isOpen := <-events:
if !isOpen {
@@ -53,7 +56,6 @@ func eventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription *
if err := ux.Handle(e); err != nil {
if errors.Is(err, partybus.ErrUnsubscribe) {
log.Warnf("unable to unsubscribe from the event bus")
events = nil
} else {
retErr = multierror.Append(retErr, err)
+32 -16
View File
@@ -6,6 +6,8 @@ import (
"io/ioutil"
"os"
"github.com/anchore/syft/syft/format"
"github.com/anchore/stereoscope"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/anchore"
@@ -48,8 +50,7 @@ const (
)
var (
packagesPresenterOpt packages.PresenterOption
packagesArgs = cobra.MaximumNArgs(1)
packagesPresenterOpt format.Option
packagesCmd = &cobra.Command{
Use: "packages [SOURCE]",
Short: "Generate a package SBOM",
@@ -58,21 +59,13 @@ var (
"appName": internal.ApplicationName,
"command": "packages",
}),
Args: packagesArgs,
Args: validateInputArgs,
SilenceUsage: true,
SilenceErrors: true,
PreRunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
err := cmd.Help()
if err != nil {
return err
}
return fmt.Errorf("an image/directory argument is required")
}
// set the presenter
presenterOption := packages.ParsePresenterOption(appConfig.Output)
if presenterOption == packages.UnknownPresenterOption {
presenterOption := format.ParseOption(appConfig.Output)
if presenterOption == format.UnknownFormatOption {
return fmt.Errorf("bad --output value '%s'", appConfig.Output)
}
packagesPresenterOpt = presenterOption
@@ -109,8 +102,8 @@ func setPackageFlags(flags *pflag.FlagSet) {
fmt.Sprintf("selection of layers to catalog, options=%v", source.AllScopes))
flags.StringP(
"output", "o", string(packages.TablePresenterOption),
fmt.Sprintf("report output formatter, options=%v", packages.AllPresenters),
"output", "o", string(format.TableOption),
fmt.Sprintf("report output formatter, options=%v", format.AllPresenters),
)
flags.StringP(
@@ -194,6 +187,18 @@ func bindPackagesConfigOptions(flags *pflag.FlagSet) error {
return nil
}
func validateInputArgs(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
// in the case that no arguments are given we want to show the help text and return with a non-0 return code.
if err := cmd.Help(); err != nil {
return fmt.Errorf("unable to display help: %w", err)
}
return fmt.Errorf("an image/directory argument is required")
}
return cobra.MaximumNArgs(1)(cmd, args)
}
func packagesExec(_ *cobra.Command, args []string) error {
// could be an image or a directory, with or without a scheme
userInput := args[0]
@@ -214,10 +219,21 @@ func packagesExec(_ *cobra.Command, args []string) error {
setupSignals(),
eventSubscription,
stereoscope.Cleanup,
ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet, reporter)...,
ui.Select(isVerbose(), appConfig.Quiet, reporter)...,
)
}
func isVerbose() (result bool) {
isPipedInput, err := internal.IsPipedInput()
if err != nil {
// since we can't tell if there was piped input we assume that there could be to disable the ETUI
log.Warnf("unable to determine if there is piped input: %+v", err)
return true
}
// verbosity should consider if there is piped input (in which case we should not show the ETUI)
return appConfig.CliOptions.Verbosity > 0 || isPipedInput
}
func packagesExecWorker(userInput string) <-chan error {
errs := make(chan error)
go func() {
+2 -10
View File
@@ -35,19 +35,11 @@ var powerUserCmd = &cobra.Command{
"appName": internal.ApplicationName,
"command": "power-user",
}),
Args: cobra.MaximumNArgs(1),
Args: validateInputArgs,
Hidden: true,
SilenceUsage: true,
SilenceErrors: true,
PreRunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
err := cmd.Help()
if err != nil {
return err
}
return fmt.Errorf("an image/directory argument is required")
}
if appConfig.Dev.ProfileCPU && appConfig.Dev.ProfileMem {
return fmt.Errorf("cannot profile CPU and memory simultaneously")
}
@@ -91,7 +83,7 @@ func powerUserExec(_ *cobra.Command, args []string) error {
setupSignals(),
eventSubscription,
stereoscope.Cleanup,
ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet, reporter)...,
ui.Select(isVerbose(), appConfig.Quiet, reporter)...,
)
}
+1
View File
@@ -33,6 +33,7 @@ func init() {
flag, "q", false,
"suppress all logging output",
)
if err := viper.BindPFlag(flag, rootCmd.PersistentFlags().Lookup(flag)); err != nil {
fmt.Printf("unable to bind flag '%s': %+v", flag, err)
os.Exit(1)
+1 -1
View File
@@ -11,7 +11,7 @@ require (
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
github.com/anchore/packageurl-go v0.0.0-20210922164639-b3fa992ebd29
github.com/anchore/stereoscope v0.0.0-20211005213828-538011008578
github.com/anchore/stereoscope v0.0.0-20211024152658-003132a67c10
github.com/antihax/optional v1.0.0
github.com/bmatcuk/doublestar/v2 v2.0.4
github.com/docker/docker v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible
+4 -2
View File
@@ -116,8 +116,8 @@ github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZV
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E=
github.com/anchore/packageurl-go v0.0.0-20210922164639-b3fa992ebd29 h1:K9LfnxwhqvihqU0+MF325FNy7fsKV9EGaUxdfR4gnWk=
github.com/anchore/packageurl-go v0.0.0-20210922164639-b3fa992ebd29/go.mod h1:Oc1UkGaJwY6ND6vtAqPSlYrptKRJngHwkwB6W7l1uP0=
github.com/anchore/stereoscope v0.0.0-20211005213828-538011008578 h1:gSpftl0RWfdTwlPmsOLgEawHIw16xwwG1mFD/wrVDRE=
github.com/anchore/stereoscope v0.0.0-20211005213828-538011008578/go.mod h1:kL7jfbXblrDcBhu5ja/s+VTYL3Mzof4eQNMJiSqcwXQ=
github.com/anchore/stereoscope v0.0.0-20211024152658-003132a67c10 h1:BmK/CgNlu+X9foWK2ZAIehxzYws760AZSGVNamQZpiw=
github.com/anchore/stereoscope v0.0.0-20211024152658-003132a67c10/go.mod h1:Rqltg0KqVKoDy4kCuJMLPFDfUg7pEmSqUEA9pOO1L7c=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
@@ -147,6 +147,8 @@ github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAw
github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/bmatcuk/doublestar/v2 v2.0.4 h1:6I6oUiT/sU27eE2OFcWqBhL1SwjyvQuOssxT4a1yidI=
github.com/bmatcuk/doublestar/v2 v2.0.4/go.mod h1:QMmcs3H2AUQICWhfzLXz+IYln8lRQmTZRptLie8RgRw=
github.com/bmatcuk/doublestar/v4 v4.0.2 h1:X0krlUVAVmtr2cRoTqR8aDMrDqnB36ht8wpWTiQ3jsA=
github.com/bmatcuk/doublestar/v4 v4.0.2/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bombsimon/wsl/v2 v2.0.0/go.mod h1:mf25kr/SqFEPhhcxW1+7pxzGlW+hIl/hYTKY95VwV8U=
github.com/bombsimon/wsl/v2 v2.2.0/go.mod h1:Azh8c3XGEJl9LyX0/sFC+CKMc7Ssgua0g+6abzXN4Pg=
github.com/bombsimon/wsl/v3 v3.0.0/go.mod h1:st10JtZYLE4D5sC7b8xV4zTKZwAQjCH/Hy2Pm1FNZIc=
+3 -3
View File
@@ -8,7 +8,7 @@ import (
"fmt"
"net/http"
"github.com/anchore/syft/internal/presenter/packages"
"github.com/anchore/syft/internal/formats/syftjson"
"github.com/wagoodman/go-progress"
@@ -26,8 +26,8 @@ type packageSBOMImportAPI interface {
func packageSbomModel(s source.Metadata, catalog *pkg.Catalog, d *distro.Distro, scope source.Scope) (*external.ImagePackageManifest, error) {
var buf bytes.Buffer
pres := packages.NewJSONPresenter(catalog, s, d, scope)
err := pres.Present(&buf)
err := syftjson.Format().Presenter(catalog, &s, d, scope).Present(&buf)
if err != nil {
return nil, fmt.Errorf("unable to serialize results: %w", err)
}
+8 -11
View File
@@ -9,18 +9,15 @@ import (
"strings"
"testing"
"github.com/anchore/syft/internal/presenter/packages"
"github.com/wagoodman/go-progress"
"github.com/anchore/syft/syft/distro"
"github.com/docker/docker/pkg/ioutils"
"github.com/anchore/client-go/pkg/external"
"github.com/anchore/syft/internal/formats/syftjson"
syftjsonModel "github.com/anchore/syft/internal/formats/syftjson/model"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
"github.com/docker/docker/pkg/ioutils"
"github.com/go-test/deep"
"github.com/wagoodman/go-progress"
)
func must(c pkg.CPE, e error) pkg.CPE {
@@ -88,19 +85,19 @@ func TestPackageSbomToModel(t *testing.T) {
}
var buf bytes.Buffer
pres := packages.NewJSONPresenter(c, m, &d, source.AllLayersScope)
pres := syftjson.Format().Presenter(c, &m, &d, source.AllLayersScope)
if err := pres.Present(&buf); err != nil {
t.Fatalf("unable to get expected json: %+v", err)
}
// unmarshal expected result
var expectedDoc packages.JSONDocument
var expectedDoc syftjsonModel.Document
if err := json.Unmarshal(buf.Bytes(), &expectedDoc); err != nil {
t.Fatalf("unable to parse json doc: %+v", err)
}
// unmarshal actual result
var actualDoc packages.JSONDocument
var actualDoc syftjsonModel.Document
if err := json.Unmarshal(modelJSON, &actualDoc); err != nil {
t.Fatalf("unable to parse json doc: %+v", err)
}
+25 -22
View File
@@ -124,35 +124,38 @@ func (cfg *Application) parseUploadOptions() error {
}
func (cfg *Application) parseLogLevelOption() error {
if cfg.Quiet {
switch {
case cfg.Quiet:
// TODO: this is bad: quiet option trumps all other logging options (such as to a file on disk)
// we should be able to quiet the console logging and leave file logging alone...
// ... this will be an enhancement for later
cfg.Log.LevelOpt = logrus.PanicLevel
} else {
if cfg.Log.Level != "" {
if cfg.CliOptions.Verbosity > 0 {
return fmt.Errorf("cannot explicitly set log level (cfg file or env var) and use -v flag together")
}
case cfg.Log.Level != "":
if cfg.CliOptions.Verbosity > 0 {
return fmt.Errorf("cannot explicitly set log level (cfg file or env var) and use -v flag together")
}
lvl, err := logrus.ParseLevel(strings.ToLower(cfg.Log.Level))
if err != nil {
return fmt.Errorf("bad log level configured (%q): %w", cfg.Log.Level, err)
}
// set the log level explicitly
cfg.Log.LevelOpt = lvl
} else {
// set the log level implicitly
switch v := cfg.CliOptions.Verbosity; {
case v == 1:
cfg.Log.LevelOpt = logrus.InfoLevel
case v >= 2:
cfg.Log.LevelOpt = logrus.DebugLevel
default:
cfg.Log.LevelOpt = logrus.ErrorLevel
}
lvl, err := logrus.ParseLevel(strings.ToLower(cfg.Log.Level))
if err != nil {
return fmt.Errorf("bad log level configured (%q): %w", cfg.Log.Level, err)
}
cfg.Log.LevelOpt = lvl
if cfg.Log.LevelOpt >= logrus.InfoLevel {
cfg.CliOptions.Verbosity = 1
}
default:
switch v := cfg.CliOptions.Verbosity; {
case v == 1:
cfg.Log.LevelOpt = logrus.InfoLevel
case v >= 2:
cfg.Log.LevelOpt = logrus.DebugLevel
default:
cfg.Log.LevelOpt = logrus.ErrorLevel
}
}
return nil
}
@@ -1,15 +1,16 @@
package packages
package testutils
import (
"bytes"
"testing"
"github.com/anchore/syft/syft/presenter"
"github.com/anchore/go-testutils"
"github.com/anchore/stereoscope/pkg/filetree"
"github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/presenter"
"github.com/anchore/syft/syft/source"
"github.com/sergi/go-diff/diffmatchpatch"
"github.com/stretchr/testify/assert"
@@ -17,7 +18,7 @@ import (
type redactor func(s []byte) []byte
func assertPresenterAgainstGoldenImageSnapshot(t *testing.T, pres presenter.Presenter, testImage string, updateSnapshot bool, redactors ...redactor) {
func AssertPresenterAgainstGoldenImageSnapshot(t *testing.T, pres presenter.Presenter, testImage string, updateSnapshot bool, redactors ...redactor) {
var buffer bytes.Buffer
// grab the latest image contents and persist
@@ -50,7 +51,7 @@ func assertPresenterAgainstGoldenImageSnapshot(t *testing.T, pres presenter.Pres
}
}
func assertPresenterAgainstGoldenSnapshot(t *testing.T, pres presenter.Presenter, updateSnapshot bool, redactors ...redactor) {
func AssertPresenterAgainstGoldenSnapshot(t *testing.T, pres presenter.Presenter, updateSnapshot bool, redactors ...redactor) {
var buffer bytes.Buffer
err := pres.Present(&buffer)
@@ -77,7 +78,7 @@ func assertPresenterAgainstGoldenSnapshot(t *testing.T, pres presenter.Presenter
}
}
func presenterImageInput(t testing.TB, testImage string) (*pkg.Catalog, source.Metadata, *distro.Distro) {
func ImageInput(t testing.TB, testImage string) (*pkg.Catalog, source.Metadata, *distro.Distro) {
t.Helper()
catalog := pkg.NewCatalog()
img := imagetest.GetGoldenFixtureImage(t, testImage)
@@ -104,7 +105,7 @@ func presenterImageInput(t testing.TB, testImage string) (*pkg.Catalog, source.M
},
PURL: "a-purl-1",
CPEs: []pkg.CPE{
must(pkg.NewCPE("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*")),
pkg.MustCPE("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*"),
},
})
catalog.Add(pkg.Package{
@@ -123,7 +124,7 @@ func presenterImageInput(t testing.TB, testImage string) (*pkg.Catalog, source.M
},
PURL: "a-purl-2",
CPEs: []pkg.CPE{
must(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")),
pkg.MustCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"),
},
})
@@ -139,7 +140,7 @@ func presenterImageInput(t testing.TB, testImage string) (*pkg.Catalog, source.M
return catalog, src.Metadata, &dist
}
func presenterDirectoryInput(t testing.TB) (*pkg.Catalog, source.Metadata, *distro.Distro) {
func DirectoryInput(t testing.TB) (*pkg.Catalog, source.Metadata, *distro.Distro) {
catalog := pkg.NewCatalog()
// populate catalog with test data
@@ -160,13 +161,13 @@ func presenterDirectoryInput(t testing.TB) (*pkg.Catalog, source.Metadata, *dist
Version: "1.0.1",
Files: []pkg.PythonFileRecord{
{
Path: "/some/path/pkg1/depedencies/foo",
Path: "/some/path/pkg1/dependencies/foo",
},
},
},
PURL: "a-purl-2",
CPEs: []pkg.CPE{
must(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")),
pkg.MustCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"),
},
})
catalog.Add(pkg.Package{
@@ -185,7 +186,7 @@ func presenterDirectoryInput(t testing.TB) (*pkg.Catalog, source.Metadata, *dist
},
PURL: "a-purl-2",
CPEs: []pkg.CPE{
must(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")),
pkg.MustCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"),
},
})
+37
View File
@@ -0,0 +1,37 @@
package formats
import (
"bytes"
"github.com/anchore/syft/internal/formats/table"
"github.com/anchore/syft/internal/formats/syftjson"
"github.com/anchore/syft/syft/format"
)
// TODO: eventually this is the source of truth for all formatters
func All() []format.Format {
return []format.Format{
syftjson.Format(),
table.Format(),
}
}
func Identify(by []byte) (*format.Format, error) {
for _, f := range All() {
if err := f.Validate(bytes.NewReader(by)); err != nil {
continue
}
return &f, nil
}
return nil, nil
}
func ByOption(option format.Option) *format.Format {
for _, f := range All() {
if f.Option == option {
return &f
}
}
return nil
}
+34
View File
@@ -0,0 +1,34 @@
package formats
import (
"io"
"os"
"testing"
"github.com/anchore/syft/syft/format"
"github.com/stretchr/testify/assert"
)
func TestIdentify(t *testing.T) {
tests := []struct {
fixture string
expected format.Option
}{
{
fixture: "test-fixtures/alpine-syft.json",
expected: format.JSONOption,
},
}
for _, test := range tests {
t.Run(test.fixture, func(t *testing.T) {
f, err := os.Open(test.fixture)
assert.NoError(t, err)
by, err := io.ReadAll(f)
assert.NoError(t, err)
frmt, err := Identify(by)
assert.NoError(t, err)
assert.NotNil(t, frmt)
assert.Equal(t, test.expected, frmt.Option)
})
}
}
+24
View File
@@ -0,0 +1,24 @@
package syftjson
import (
"encoding/json"
"fmt"
"io"
"github.com/anchore/syft/internal/formats/syftjson/model"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
func decoder(reader io.Reader) (*pkg.Catalog, *source.Metadata, *distro.Distro, source.Scope, error) {
dec := json.NewDecoder(reader)
var doc model.Document
err := dec.Decode(&doc)
if err != nil {
return nil, nil, nil, source.UnknownScope, fmt.Errorf("unable to decode syft-json: %w", err)
}
return toSyftModel(doc)
}
+52
View File
@@ -0,0 +1,52 @@
package syftjson
import (
"bytes"
"strings"
"testing"
"github.com/anchore/syft/syft/source"
"github.com/anchore/syft/internal/formats/common/testutils"
"github.com/go-test/deep"
"github.com/stretchr/testify/assert"
)
func TestEncodeDecodeCycle(t *testing.T) {
testImage := "image-simple"
originalCatalog, originalMetadata, _ := testutils.ImageInput(t, testImage)
var buf bytes.Buffer
assert.NoError(t, encoder(&buf, originalCatalog, &originalMetadata, nil, source.SquashedScope))
actualCatalog, actualMetadata, _, _, err := decoder(bytes.NewReader(buf.Bytes()))
assert.NoError(t, err)
for _, d := range deep.Equal(originalMetadata, *actualMetadata) {
t.Errorf("metadata difference: %+v", d)
}
actualPackages := actualCatalog.Sorted()
for idx, p := range originalCatalog.Sorted() {
if !assert.Equal(t, p.Name, actualPackages[idx].Name) {
t.Errorf("different package at idx=%d: %s vs %s", idx, p.Name, actualPackages[idx].Name)
continue
}
// ids will never be equal
p.ID = ""
actualPackages[idx].ID = ""
for _, d := range deep.Equal(*p, *actualPackages[idx]) {
if strings.Contains(d, ".VirtualPath: ") {
// location.Virtual path is not exposed in the json output
continue
}
if strings.HasSuffix(d, "<nil slice> != []") {
// semantically the same
continue
}
t.Errorf("package difference (%s): %+v", p.Name, d)
}
}
}
+23
View File
@@ -0,0 +1,23 @@
package syftjson
import (
"encoding/json"
"io"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
func encoder(output io.Writer, catalog *pkg.Catalog, srcMetadata *source.Metadata, d *distro.Distro, scope source.Scope) error {
// TODO: application config not available yet
doc := ToFormatModel(catalog, srcMetadata, d, scope, nil)
enc := json.NewEncoder(output)
// prevent > and < from being escaped in the payload
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
return enc.Encode(&doc)
}
+31
View File
@@ -0,0 +1,31 @@
package syftjson
import (
"flag"
"testing"
"github.com/anchore/syft/syft/source"
"github.com/anchore/syft/internal/formats/common/testutils"
"github.com/anchore/syft/syft/format"
)
var updateJson = flag.Bool("update-json", false, "update the *.golden files for json presenters")
func TestDirectoryPresenter(t *testing.T) {
catalog, metadata, distro := testutils.DirectoryInput(t)
testutils.AssertPresenterAgainstGoldenSnapshot(t,
format.NewPresenter(encoder, catalog, &metadata, distro, source.SquashedScope),
*updateJson,
)
}
func TestImagePresenter(t *testing.T) {
testImage := "image-simple"
catalog, metadata, distro := testutils.ImageInput(t, testImage)
testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
format.NewPresenter(encoder, catalog, &metadata, distro, source.SquashedScope),
testImage,
*updateJson,
)
}
+12
View File
@@ -0,0 +1,12 @@
package syftjson
import "github.com/anchore/syft/syft/format"
func Format() format.Format {
return format.NewFormat(
format.JSONOption,
encoder,
decoder,
validator,
)
}
@@ -0,0 +1,8 @@
package model
// Distro provides information about a detected Linux Distro.
type Distro struct {
Name string `json:"name"` // Name of the Linux distribution
Version string `json:"version"` // Version of the Linux distribution (major or major.minor version)
IDLike string `json:"idLike"` // the ID_LIKE field found within the /etc/os-release file
}
@@ -0,0 +1,23 @@
package model
// Document represents the syft cataloging findings as a JSON document
type Document struct {
Artifacts []Package `json:"artifacts"` // Artifacts is the list of packages discovered and placed into the catalog
ArtifactRelationships []Relationship `json:"artifactRelationships"`
Source Source `json:"source"` // Source represents the original object that was cataloged
Distro Distro `json:"distro"` // Distro represents the Linux distribution that was detected from the source
Descriptor Descriptor `json:"descriptor"` // Descriptor is a block containing self-describing information about syft
Schema Schema `json:"schema"` // Schema is a block reserved for defining the version for the shape of this JSON document and where to find the schema document to validate the shape
}
// Descriptor describes what created the document as well as surrounding metadata
type Descriptor struct {
Name string `json:"name"`
Version string `json:"version"`
Configuration interface{} `json:"configuration,omitempty"`
}
type Schema struct {
Version string `json:"version"`
URL string `json:"url"`
}
+123
View File
@@ -0,0 +1,123 @@
package model
import (
"encoding/json"
"fmt"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
// Package represents a pkg.Package object specialized for JSON marshaling and unmarshaling.
type Package struct {
PackageBasicData
PackageCustomData
}
// PackageBasicData contains non-ambiguous values (type-wise) from pkg.Package.
type PackageBasicData struct {
ID string `json:"id"`
Name string `json:"name"`
Version string `json:"version"`
Type pkg.Type `json:"type"`
FoundBy string `json:"foundBy"`
Locations []source.Location `json:"locations"`
Licenses []string `json:"licenses"`
Language pkg.Language `json:"language"`
CPEs []string `json:"cpes"`
PURL string `json:"purl"`
}
// PackageCustomData contains ambiguous values (type-wise) from pkg.Package.
type PackageCustomData struct {
MetadataType pkg.MetadataType `json:"metadataType"`
Metadata interface{} `json:"metadata"`
}
// packageMetadataUnpacker is all values needed from Package to disambiguate ambiguous fields during json unmarshaling.
type packageMetadataUnpacker struct {
MetadataType pkg.MetadataType `json:"metadataType"`
Metadata json.RawMessage `json:"metadata"`
}
func (p *packageMetadataUnpacker) String() string {
return fmt.Sprintf("metadataType: %s, metadata: %s", p.MetadataType, string(p.Metadata))
}
// UnmarshalJSON is a custom unmarshaller for handling basic values and values with ambiguous types.
// nolint:funlen
func (p *Package) UnmarshalJSON(b []byte) error {
var basic PackageBasicData
if err := json.Unmarshal(b, &basic); err != nil {
return err
}
p.PackageBasicData = basic
var unpacker packageMetadataUnpacker
if err := json.Unmarshal(b, &unpacker); err != nil {
log.Warnf("failed to unmarshall into packageMetadataUnpacker: %v", err)
return err
}
p.MetadataType = unpacker.MetadataType
switch p.MetadataType {
case pkg.ApkMetadataType:
var payload pkg.ApkMetadata
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
return err
}
p.Metadata = payload
case pkg.RpmdbMetadataType:
var payload pkg.RpmdbMetadata
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
return err
}
p.Metadata = payload
case pkg.DpkgMetadataType:
var payload pkg.DpkgMetadata
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
return err
}
p.Metadata = payload
case pkg.JavaMetadataType:
var payload pkg.JavaMetadata
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
return err
}
p.Metadata = payload
case pkg.RustCargoPackageMetadataType:
var payload pkg.CargoPackageMetadata
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
return err
}
p.Metadata = payload
case pkg.GemMetadataType:
var payload pkg.GemMetadata
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
return err
}
p.Metadata = payload
case pkg.KbPackageMetadataType:
var payload pkg.KbPackageMetadata
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
return err
}
p.Metadata = payload
case pkg.PythonPackageMetadataType:
var payload pkg.PythonPackageMetadata
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
return err
}
p.Metadata = payload
case pkg.NpmPackageJSONMetadataType:
var payload pkg.NpmPackageJSONMetadata
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
return err
}
p.Metadata = payload
}
return nil
}
@@ -0,0 +1,8 @@
package model
type Relationship struct {
Parent string `json:"parent"`
Child string `json:"child"`
Type string `json:"type"`
Metadata interface{} `json:"metadata"`
}
+57
View File
@@ -0,0 +1,57 @@
package model
import (
"encoding/json"
"fmt"
"strconv"
"github.com/anchore/syft/syft/source"
)
// Source object represents the thing that was cataloged
type Source struct {
Type string `json:"type"`
Target interface{} `json:"target"`
}
// sourceUnpacker is used to unmarshal Source objects
type sourceUnpacker struct {
Type string `json:"type"`
Target json.RawMessage `json:"target"`
}
type ImageSource struct {
source.ImageMetadata
Scope source.Scope `json:"scope"`
}
// UnmarshalJSON populates a source object from JSON bytes.
func (s *Source) UnmarshalJSON(b []byte) error {
var unpacker sourceUnpacker
if err := json.Unmarshal(b, &unpacker); err != nil {
return err
}
s.Type = unpacker.Type
switch s.Type {
case "directory":
if target, err := strconv.Unquote(string(unpacker.Target)); err == nil {
s.Target = target
} else {
s.Target = string(unpacker.Target[:])
}
case "image":
var payload ImageSource
if err := json.Unmarshal(unpacker.Target, &payload); err != nil {
return err
}
s.Target = payload
default:
return fmt.Errorf("unsupported package metadata type: %+v", s.Type)
}
return nil
}
@@ -0,0 +1,4 @@
# Note: changes to this file will result in updating several test values. Consider making a new image fixture instead of editing this one.
FROM scratch
ADD file-1.txt /somefile-1.txt
ADD file-2.txt /somefile-2.txt
@@ -0,0 +1 @@
this file has contents
@@ -0,0 +1 @@
file-2 contents!
@@ -0,0 +1,86 @@
{
"artifacts": [
{
"id": "package-1-id",
"name": "package-1",
"version": "1.0.1",
"type": "python",
"foundBy": "the-cataloger-1",
"locations": [
{
"path": "/some/path/pkg1"
}
],
"licenses": [
"MIT"
],
"language": "python",
"cpes": [
"cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"
],
"purl": "a-purl-2",
"metadataType": "PythonPackageMetadata",
"metadata": {
"name": "package-1",
"version": "1.0.1",
"license": "",
"author": "",
"authorEmail": "",
"platform": "",
"files": [
{
"path": "/some/path/pkg1/dependencies/foo"
}
],
"sitePackagesRootPath": ""
}
},
{
"id": "package-2-id",
"name": "package-2",
"version": "2.0.1",
"type": "deb",
"foundBy": "the-cataloger-2",
"locations": [
{
"path": "/some/path/pkg1"
}
],
"licenses": [],
"language": "",
"cpes": [
"cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"
],
"purl": "a-purl-2",
"metadataType": "DpkgMetadata",
"metadata": {
"package": "package-2",
"source": "",
"version": "2.0.1",
"sourceVersion": "",
"architecture": "",
"maintainer": "",
"installedSize": 0,
"files": null
}
}
],
"artifactRelationships": [],
"source": {
"type": "directory",
"target": "/some/path"
},
"distro": {
"name": "debian",
"version": "1.2.3",
"idLike": "like!"
},
"descriptor": {
"name": "syft",
"version": "[not provided]"
},
"schema": {
"version": "1.1.0",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.1.0.json"
}
}
@@ -0,0 +1,108 @@
{
"artifacts": [
{
"id": "package-1-id",
"name": "package-1",
"version": "1.0.1",
"type": "python",
"foundBy": "the-cataloger-1",
"locations": [
{
"path": "/somefile-1.txt",
"layerID": "sha256:fb6beecb75b39f4bb813dbf177e501edd5ddb3e69bb45cedeb78c676ee1b7a59"
}
],
"licenses": [
"MIT"
],
"language": "python",
"cpes": [
"cpe:2.3:*:some:package:1:*:*:*:*:*:*:*"
],
"purl": "a-purl-1",
"metadataType": "PythonPackageMetadata",
"metadata": {
"name": "package-1",
"version": "1.0.1",
"license": "",
"author": "",
"authorEmail": "",
"platform": "",
"sitePackagesRootPath": ""
}
},
{
"id": "package-2-id",
"name": "package-2",
"version": "2.0.1",
"type": "deb",
"foundBy": "the-cataloger-2",
"locations": [
{
"path": "/somefile-2.txt",
"layerID": "sha256:319b588ce64253a87b533c8ed01cf0025e0eac98e7b516e12532957e1244fdec"
}
],
"licenses": [],
"language": "",
"cpes": [
"cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"
],
"purl": "a-purl-2",
"metadataType": "DpkgMetadata",
"metadata": {
"package": "package-2",
"source": "",
"version": "2.0.1",
"sourceVersion": "",
"architecture": "",
"maintainer": "",
"installedSize": 0,
"files": null
}
}
],
"artifactRelationships": [],
"source": {
"type": "image",
"target": {
"userInput": "user-image-input",
"imageID": "sha256:2480160b55bec40c44d3b145c7b2c1c47160db8575c3dcae086d76b9370ae7ca",
"manifestDigest": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368",
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"tags": [
"stereoscope-fixture-image-simple:85066c51088bdd274f7a89e99e00490f666c49e72ffc955707cd6e18f0e22c5b"
],
"imageSize": 38,
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:fb6beecb75b39f4bb813dbf177e501edd5ddb3e69bb45cedeb78c676ee1b7a59",
"size": 22
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:319b588ce64253a87b533c8ed01cf0025e0eac98e7b516e12532957e1244fdec",
"size": 16
}
],
"manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjo2NjcsImRpZ2VzdCI6InNoYTI1NjoyNDgwMTYwYjU1YmVjNDBjNDRkM2IxNDVjN2IyYzFjNDcxNjBkYjg1NzVjM2RjYWUwODZkNzZiOTM3MGFlN2NhIn0sImxheWVycyI6W3sibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjIwNDgsImRpZ2VzdCI6InNoYTI1NjpmYjZiZWVjYjc1YjM5ZjRiYjgxM2RiZjE3N2U1MDFlZGQ1ZGRiM2U2OWJiNDVjZWRlYjc4YzY3NmVlMWI3YTU5In0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6MjA0OCwiZGlnZXN0Ijoic2hhMjU2OjMxOWI1ODhjZTY0MjUzYTg3YjUzM2M4ZWQwMWNmMDAyNWUwZWFjOThlN2I1MTZlMTI1MzI5NTdlMTI0NGZkZWMifV19",
"config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiV29ya2luZ0RpciI6Ii8iLCJPbkJ1aWxkIjpudWxsfSwiY3JlYXRlZCI6IjIwMjEtMTAtMDRUMTE6NDA6MDAuNjM4Mzk0NVoiLCJoaXN0b3J5IjpbeyJjcmVhdGVkIjoiMjAyMS0xMC0wNFQxMTo0MDowMC41OTA3MzE2WiIsImNyZWF0ZWRfYnkiOiJBREQgZmlsZS0xLnR4dCAvc29tZWZpbGUtMS50eHQgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn0seyJjcmVhdGVkIjoiMjAyMS0xMC0wNFQxMTo0MDowMC42MzgzOTQ1WiIsImNyZWF0ZWRfYnkiOiJBREQgZmlsZS0yLnR4dCAvc29tZWZpbGUtMi50eHQgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn1dLCJvcyI6ImxpbnV4Iiwicm9vdGZzIjp7InR5cGUiOiJsYXllcnMiLCJkaWZmX2lkcyI6WyJzaGEyNTY6ZmI2YmVlY2I3NWIzOWY0YmI4MTNkYmYxNzdlNTAxZWRkNWRkYjNlNjliYjQ1Y2VkZWI3OGM2NzZlZTFiN2E1OSIsInNoYTI1NjozMTliNTg4Y2U2NDI1M2E4N2I1MzNjOGVkMDFjZjAwMjVlMGVhYzk4ZTdiNTE2ZTEyNTMyOTU3ZTEyNDRmZGVjIl19fQ==",
"repoDigests": [],
"scope": "Squashed"
}
},
"distro": {
"name": "debian",
"version": "1.2.3",
"idLike": "like!"
},
"descriptor": {
"name": "syft",
"version": "[not provided]"
},
"schema": {
"version": "1.1.0",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.1.0.json"
}
}
@@ -0,0 +1,133 @@
package syftjson
import (
"fmt"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/formats/syftjson/model"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
// TODO: this is export4ed for the use of the power-user command (temp)
func ToFormatModel(catalog *pkg.Catalog, srcMetadata *source.Metadata, d *distro.Distro, scope source.Scope, applicationConfig interface{}) model.Document {
src, err := toSourceModel(srcMetadata, scope)
if err != nil {
log.Warnf("unable to create syft-json source object: %+v", err)
}
return model.Document{
Artifacts: toPackageModels(catalog),
ArtifactRelationships: toRelationshipModel(pkg.NewRelationships(catalog)),
Source: src,
Distro: toDistroModel(d),
Descriptor: model.Descriptor{
Name: internal.ApplicationName,
Version: version.FromBuild().Version,
Configuration: applicationConfig,
},
Schema: model.Schema{
Version: internal.JSONSchemaVersion,
URL: fmt.Sprintf("https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-%s.json", internal.JSONSchemaVersion),
},
}
}
func toPackageModels(catalog *pkg.Catalog) []model.Package {
artifacts := make([]model.Package, 0)
if catalog == nil {
return artifacts
}
for _, p := range catalog.Sorted() {
artifacts = append(artifacts, toPackageModel(p))
}
return artifacts
}
// toPackageModel crates a new Package from the given pkg.Package.
func toPackageModel(p *pkg.Package) model.Package {
var cpes = make([]string, len(p.CPEs))
for i, c := range p.CPEs {
cpes[i] = c.BindToFmtString()
}
// ensure collections are never nil for presentation reasons
var locations = make([]source.Location, 0)
if p.Locations != nil {
locations = p.Locations
}
var licenses = make([]string, 0)
if p.Licenses != nil {
licenses = p.Licenses
}
return model.Package{
PackageBasicData: model.PackageBasicData{
ID: string(p.ID),
Name: p.Name,
Version: p.Version,
Type: p.Type,
FoundBy: p.FoundBy,
Locations: locations,
Licenses: licenses,
Language: p.Language,
CPEs: cpes,
PURL: p.PURL,
},
PackageCustomData: model.PackageCustomData{
MetadataType: p.MetadataType,
Metadata: p.Metadata,
},
}
}
func toRelationshipModel(relationships []pkg.Relationship) []model.Relationship {
result := make([]model.Relationship, len(relationships))
for i, r := range relationships {
result[i] = model.Relationship{
Parent: string(r.Parent),
Child: string(r.Child),
Type: string(r.Type),
Metadata: r.Metadata,
}
}
return result
}
// toSourceModel creates a new source object to be represented into JSON.
func toSourceModel(src *source.Metadata, scope source.Scope) (model.Source, error) {
switch src.Scheme {
case source.ImageScheme:
return model.Source{
Type: "image",
Target: model.ImageSource{
ImageMetadata: src.ImageMetadata,
Scope: scope,
},
}, nil
case source.DirectoryScheme:
return model.Source{
Type: "directory",
Target: src.Path,
}, nil
default:
return model.Source{}, fmt.Errorf("unsupported source: %q", src.Scheme)
}
}
// toDistroModel creates a struct with the Linux distribution to be represented in JSON.
func toDistroModel(d *distro.Distro) model.Distro {
if d == nil {
return model.Distro{}
}
return model.Distro{
Name: d.Name(),
Version: d.FullVersion(),
IDLike: d.IDLike,
}
}
@@ -0,0 +1,73 @@
package syftjson
import (
"github.com/anchore/syft/internal/formats/syftjson/model"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
func toSyftModel(doc model.Document) (*pkg.Catalog, *source.Metadata, *distro.Distro, source.Scope, error) {
dist, err := distro.NewDistro(distro.Type(doc.Distro.Name), doc.Distro.Version, doc.Distro.IDLike)
if err != nil {
return nil, nil, nil, source.UnknownScope, err
}
srcMetadata, scope := toSyftSourceData(doc.Source)
return toSyftCatalog(doc.Artifacts), srcMetadata, &dist, scope, nil
}
func toSyftSourceData(s model.Source) (*source.Metadata, source.Scope) {
switch s.Type {
case "directory":
return &source.Metadata{
Scheme: source.DirectoryScheme,
Path: s.Target.(string),
}, source.UnknownScope
case "image":
parsedSource := s.Target.(model.ImageSource)
return &source.Metadata{
Scheme: source.ImageScheme,
ImageMetadata: parsedSource.ImageMetadata,
}, parsedSource.Scope
}
return nil, source.UnknownScope
}
func toSyftCatalog(pkgs []model.Package) *pkg.Catalog {
catalog := pkg.NewCatalog()
for _, p := range pkgs {
catalog.Add(toSyftPackage(p))
}
return catalog
}
func toSyftPackage(p model.Package) pkg.Package {
var cpes []pkg.CPE
for _, c := range p.CPEs {
value, err := pkg.NewCPE(c)
if err != nil {
log.Warnf("excluding invalid CPE %q: %v", c, err)
continue
}
cpes = append(cpes, value)
}
return pkg.Package{
ID: pkg.ID(p.ID),
Name: p.Name,
Version: p.Version,
FoundBy: p.FoundBy,
Locations: p.Locations,
Licenses: p.Licenses,
Language: p.Language,
Type: p.Type,
CPEs: cpes,
PURL: p.PURL,
MetadataType: p.MetadataType,
Metadata: p.Metadata,
}
}
+31
View File
@@ -0,0 +1,31 @@
package syftjson
import (
"encoding/json"
"fmt"
"io"
"strings"
"github.com/anchore/syft/internal/formats/syftjson/model"
)
func validator(reader io.Reader) error {
type Document struct {
Schema model.Schema `json:"schema"`
}
dec := json.NewDecoder(reader)
var doc Document
err := dec.Decode(&doc)
if err != nil {
return fmt.Errorf("unable to decode: %w", err)
}
// note: we accept all schema versions
// TODO: add per-schema version parsing
if strings.Contains(doc.Schema.URL, "anchore/syft") {
return nil
}
return fmt.Errorf("could not extract syft schema")
}
@@ -1,4 +1,4 @@
package packages
package table
import (
"fmt"
@@ -8,24 +8,16 @@ import (
"github.com/olekukonko/tablewriter"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
type TablePresenter struct {
catalog *pkg.Catalog
}
func NewTablePresenter(catalog *pkg.Catalog) *TablePresenter {
return &TablePresenter{
catalog: catalog,
}
}
func (pres *TablePresenter) Present(output io.Writer) error {
rows := make([][]string, 0)
func encoder(output io.Writer, catalog *pkg.Catalog, _ *source.Metadata, _ *distro.Distro, _ source.Scope) error {
var rows [][]string
columns := []string{"Name", "Version", "Type"}
for _, p := range pres.catalog.Sorted() {
for _, p := range catalog.Sorted() {
row := []string{
p.Name,
p.Version,
@@ -35,8 +27,8 @@ func (pres *TablePresenter) Present(output io.Writer) error {
}
if len(rows) == 0 {
fmt.Fprintln(output, "No packages discovered")
return nil
_, err := fmt.Fprintln(output, "No packages discovered")
return err
}
// sort by name, version, then type
@@ -1,21 +1,22 @@
package packages
package table
import (
"flag"
"testing"
"github.com/anchore/syft/internal/formats/common/testutils"
"github.com/anchore/syft/syft/format"
"github.com/anchore/syft/syft/source"
"github.com/go-test/deep"
)
var updateTablePresenterGoldenFiles = flag.Bool("update-table", false, "update the *.golden files for table presenters")
var updateTableGoldenFiles = flag.Bool("update-table", false, "update the *.golden files for table format")
func TestTablePresenter(t *testing.T) {
testImage := "image-simple"
catalog, _, _ := presenterImageInput(t, testImage)
assertPresenterAgainstGoldenImageSnapshot(t,
NewTablePresenter(catalog),
testImage,
*updateTablePresenterGoldenFiles,
catalog, metadata, distro := testutils.DirectoryInput(t)
testutils.AssertPresenterAgainstGoldenSnapshot(t,
format.NewPresenter(encoder, catalog, &metadata, distro, source.SquashedScope),
*updateTableGoldenFiles,
)
}
+12
View File
@@ -0,0 +1,12 @@
package table
import "github.com/anchore/syft/syft/format"
func Format() format.Format {
return format.NewFormat(
format.TableOption,
encoder,
nil,
nil,
)
}
File diff suppressed because it is too large Load Diff
+16
View File
@@ -0,0 +1,16 @@
package internal
import (
"fmt"
"os"
)
// IsPipedInput returns true if there is no input device, which means the user **may** be providing input via a pipe.
func IsPipedInput() (bool, error) {
fi, err := os.Stdin.Stat()
if err != nil {
return false, fmt.Errorf("unable to determine if there is piped input: %w", err)
}
return fi.Mode()&os.ModeCharDevice == 0, nil
}
@@ -4,13 +4,15 @@ import (
"flag"
"regexp"
"testing"
"github.com/anchore/syft/internal/formats/common/testutils"
)
var updateCycloneDx = flag.Bool("update-cyclonedx", false, "update the *.golden files for cyclone-dx presenters")
func TestCycloneDxDirectoryPresenter(t *testing.T) {
catalog, metadata, _ := presenterDirectoryInput(t)
assertPresenterAgainstGoldenSnapshot(t,
catalog, metadata, _ := testutils.DirectoryInput(t)
testutils.AssertPresenterAgainstGoldenSnapshot(t,
NewCycloneDxPresenter(catalog, metadata),
*updateCycloneDx,
cycloneDxRedactor,
@@ -19,8 +21,8 @@ func TestCycloneDxDirectoryPresenter(t *testing.T) {
func TestCycloneDxImagePresenter(t *testing.T) {
testImage := "image-simple"
catalog, metadata, _ := presenterImageInput(t, testImage)
assertPresenterAgainstGoldenImageSnapshot(t,
catalog, metadata, _ := testutils.ImageInput(t, testImage)
testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
NewCycloneDxPresenter(catalog, metadata),
testImage,
*updateCycloneDx,
@@ -1,23 +0,0 @@
package packages
import "github.com/anchore/syft/syft/distro"
// JSONDistribution provides information about a detected Linux JSONDistribution.
type JSONDistribution struct {
Name string `json:"name"` // Name of the Linux distribution
Version string `json:"version"` // Version of the Linux distribution (major or major.minor version)
IDLike string `json:"idLike"` // the ID_LIKE field found within the /etc/os-release file
}
// NewJSONDistribution creates a struct with the Linux distribution to be represented in JSON.
func NewJSONDistribution(d *distro.Distro) JSONDistribution {
if d == nil {
return JSONDistribution{}
}
return JSONDistribution{
Name: d.Name(),
Version: d.FullVersion(),
IDLike: d.IDLike,
}
}
@@ -1,62 +0,0 @@
package packages
import (
"fmt"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
// JSONDocument represents the syft cataloging findings as a JSON document
type JSONDocument struct {
Artifacts []JSONPackage `json:"artifacts"` // Artifacts is the list of packages discovered and placed into the catalog
ArtifactRelationships []JSONRelationship `json:"artifactRelationships"`
Source JSONSource `json:"source"` // Source represents the original object that was cataloged
Distro JSONDistribution `json:"distro"` // Distro represents the Linux distribution that was detected from the source
Descriptor JSONDescriptor `json:"descriptor"` // Descriptor is a block containing self-describing information about syft
Schema JSONSchema `json:"schema"` // Schema is a block reserved for defining the version for the shape of this JSON document and where to find the schema document to validate the shape
}
// NewJSONDocument creates and populates a new JSON document struct from the given cataloging results.
func NewJSONDocument(catalog *pkg.Catalog, srcMetadata source.Metadata, d *distro.Distro, scope source.Scope, configuration interface{}) (JSONDocument, error) {
src, err := NewJSONSource(srcMetadata, scope)
if err != nil {
return JSONDocument{}, err
}
artifacts, err := NewJSONPackages(catalog)
if err != nil {
return JSONDocument{}, err
}
return JSONDocument{
Artifacts: artifacts,
ArtifactRelationships: newJSONRelationships(pkg.NewRelationships(catalog)),
Source: src,
Distro: NewJSONDistribution(d),
Descriptor: JSONDescriptor{
Name: internal.ApplicationName,
Version: version.FromBuild().Version,
Configuration: configuration,
},
Schema: JSONSchema{
Version: internal.JSONSchemaVersion,
URL: fmt.Sprintf("https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-%s.json", internal.JSONSchemaVersion),
},
}, nil
}
// JSONDescriptor describes what created the document as well as surrounding metadata
type JSONDescriptor struct {
Name string `json:"name"`
Version string `json:"version"`
Configuration interface{} `json:"configuration,omitempty"`
}
type JSONSchema struct {
Version string `json:"version"`
URL string `json:"url"`
}
@@ -1,71 +0,0 @@
package packages
import (
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
// JSONPackage represents a pkg.Package object specialized for JSON marshaling and unmarshaling.
type JSONPackage struct {
ID string `json:"id"`
Name string `json:"name"`
Version string `json:"version"`
Type string `json:"type"`
FoundBy string `json:"foundBy"`
Locations []source.Location `json:"locations"`
Licenses []string `json:"licenses"`
Language string `json:"language"`
CPEs []string `json:"cpes"`
PURL string `json:"purl"`
MetadataType string `json:"metadataType"`
Metadata interface{} `json:"metadata"`
}
func NewJSONPackages(catalog *pkg.Catalog) ([]JSONPackage, error) {
artifacts := make([]JSONPackage, 0)
if catalog == nil {
return artifacts, nil
}
for _, p := range catalog.Sorted() {
art, err := NewJSONPackage(p)
if err != nil {
return nil, err
}
artifacts = append(artifacts, art)
}
return artifacts, nil
}
// NewJSONPackage crates a new JSONPackage from the given pkg.Package.
func NewJSONPackage(p *pkg.Package) (JSONPackage, error) {
var cpes = make([]string, len(p.CPEs))
for i, c := range p.CPEs {
cpes[i] = c.BindToFmtString()
}
// ensure collections are never nil for presentation reasons
var locations = make([]source.Location, 0)
if p.Locations != nil {
locations = p.Locations
}
var licenses = make([]string, 0)
if p.Licenses != nil {
licenses = p.Licenses
}
return JSONPackage{
ID: string(p.ID),
Name: p.Name,
Version: p.Version,
Type: string(p.Type),
FoundBy: p.FoundBy,
Locations: locations,
Licenses: licenses,
Language: string(p.Language),
CPEs: cpes,
PURL: p.PURL,
MetadataType: string(p.MetadataType),
Metadata: p.Metadata,
}, nil
}
@@ -1,43 +0,0 @@
package packages
import (
"encoding/json"
"io"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
// JSONPresenter is a JSON presentation object for the syft results
type JSONPresenter struct {
catalog *pkg.Catalog
srcMetadata source.Metadata
distro *distro.Distro
scope source.Scope
}
// NewJSONPresenter creates a new JSON presenter object for the given cataloging results.
func NewJSONPresenter(catalog *pkg.Catalog, s source.Metadata, d *distro.Distro, scope source.Scope) *JSONPresenter {
return &JSONPresenter{
catalog: catalog,
srcMetadata: s,
distro: d,
scope: scope,
}
}
// Present the catalog results to the given writer.
func (pres *JSONPresenter) Present(output io.Writer) error {
// we do not pass in configuration for backwards compatibility
doc, err := NewJSONDocument(pres.catalog, pres.srcMetadata, pres.distro, pres.scope, nil)
if err != nil {
return err
}
enc := json.NewEncoder(output)
// prevent > and < from being escaped in the payload
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
return enc.Encode(&doc)
}
@@ -1,37 +0,0 @@
package packages
import (
"flag"
"testing"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
var updateJSONGoldenFiles = flag.Bool("update-json", false, "update the *.golden files for json presenters")
func must(c pkg.CPE, e error) pkg.CPE {
if e != nil {
panic(e)
}
return c
}
func TestJSONDirectoryPresenter(t *testing.T) {
catalog, metadata, dist := presenterDirectoryInput(t)
assertPresenterAgainstGoldenSnapshot(t,
NewJSONPresenter(catalog, metadata, dist, source.SquashedScope),
*updateJSONGoldenFiles,
)
}
func TestJSONImagePresenter(t *testing.T) {
testImage := "image-simple"
catalog, metadata, dist := presenterImageInput(t, testImage)
assertPresenterAgainstGoldenImageSnapshot(t,
NewJSONPresenter(catalog, metadata, dist, source.SquashedScope),
testImage,
*updateJSONGoldenFiles,
)
}
@@ -1,23 +0,0 @@
package packages
import "github.com/anchore/syft/syft/pkg"
type JSONRelationship struct {
Parent string `json:"parent"`
Child string `json:"child"`
Type string `json:"type"`
Metadata interface{} `json:"metadata"`
}
func newJSONRelationships(relationships []pkg.Relationship) []JSONRelationship {
result := make([]JSONRelationship, len(relationships))
for i, r := range relationships {
result[i] = JSONRelationship{
Parent: string(r.Parent),
Child: string(r.Child),
Type: string(r.Type),
Metadata: r.Metadata,
}
}
return result
}
@@ -1,39 +0,0 @@
package packages
import (
"fmt"
"github.com/anchore/syft/syft/source"
)
// JSONSource object represents the thing that was cataloged
type JSONSource struct {
Type string `json:"type"`
Target interface{} `json:"target"`
}
type JSONImageSource struct {
source.ImageMetadata
Scope source.Scope `json:"scope"`
}
// NewJSONSource creates a new source object to be represented into JSON.
func NewJSONSource(src source.Metadata, scope source.Scope) (JSONSource, error) {
switch src.Scheme {
case source.ImageScheme:
return JSONSource{
Type: "image",
Target: JSONImageSource{
Scope: scope,
ImageMetadata: src.ImageMetadata,
},
}, nil
case source.DirectoryScheme:
return JSONSource{
Type: "directory",
Target: src.Path,
}, nil
default:
return JSONSource{}, fmt.Errorf("unsupported source: %q", src.Scheme)
}
}
@@ -12,7 +12,7 @@ import (
)
func Test_getSPDXExternalRefs(t *testing.T) {
testCPE := must(pkg.NewCPE("cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*"))
testCPE := pkg.MustCPE("cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*")
tests := []struct {
name string
input pkg.Package
@@ -4,13 +4,15 @@ import (
"flag"
"regexp"
"testing"
"github.com/anchore/syft/internal/formats/common/testutils"
)
var updateSpdxJson = flag.Bool("update-spdx-json", false, "update the *.golden files for spdx-json presenters")
func TestSPDXJSONDirectoryPresenter(t *testing.T) {
catalog, metadata, _ := presenterDirectoryInput(t)
assertPresenterAgainstGoldenSnapshot(t,
catalog, metadata, _ := testutils.DirectoryInput(t)
testutils.AssertPresenterAgainstGoldenSnapshot(t,
NewSPDXJSONPresenter(catalog, metadata),
*updateSpdxJson,
spdxJsonRedactor,
@@ -19,8 +21,8 @@ func TestSPDXJSONDirectoryPresenter(t *testing.T) {
func TestSPDXJSONImagePresenter(t *testing.T) {
testImage := "image-simple"
catalog, metadata, _ := presenterImageInput(t, testImage)
assertPresenterAgainstGoldenImageSnapshot(t,
catalog, metadata, _ := testutils.ImageInput(t, testImage)
testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
NewSPDXJSONPresenter(catalog, metadata),
testImage,
*updateSpdxJson,
@@ -4,13 +4,15 @@ import (
"flag"
"regexp"
"testing"
"github.com/anchore/syft/internal/formats/common/testutils"
)
var updateSpdxTagValue = flag.Bool("update-spdx-tv", false, "update the *.golden files for spdx-tv presenters")
func TestSPDXTagValueDirectoryPresenter(t *testing.T) {
catalog, metadata, _ := presenterDirectoryInput(t)
assertPresenterAgainstGoldenSnapshot(t,
catalog, metadata, _ := testutils.DirectoryInput(t)
testutils.AssertPresenterAgainstGoldenSnapshot(t,
NewSPDXTagValuePresenter(catalog, metadata),
*updateSpdxTagValue,
spdxTagValueRedactor,
@@ -19,8 +21,8 @@ func TestSPDXTagValueDirectoryPresenter(t *testing.T) {
func TestSPDXTagValueImagePresenter(t *testing.T) {
testImage := "image-simple"
catalog, metadata, _ := presenterImageInput(t, testImage)
assertPresenterAgainstGoldenImageSnapshot(t,
catalog, metadata, _ := testutils.ImageInput(t, testImage)
testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
NewSPDXTagValuePresenter(catalog, metadata),
testImage,
*updateSpdxTagValue,
@@ -3,7 +3,7 @@
"name": "/some/path",
"spdxVersion": "SPDX-2.2",
"creationInfo": {
"created": "2021-09-16T20:44:35.198887Z",
"created": "2021-10-12T18:40:22.948394Z",
"creators": [
"Organization: Anchore, Inc",
"Tool: syft-[not provided]"
@@ -11,7 +11,7 @@
"licenseListVersion": "3.14"
},
"dataLicense": "CC0-1.0",
"documentNamespace": "https://anchore.com/syft/image/",
"documentNamespace": "https:/anchore.com/syft/dir/some/path-98ae71fb-f276-4c5c-acf7-25770bf7bca2",
"packages": [
{
"SPDXID": "SPDXRef-Package-python-package-1-1.0.1",
@@ -32,7 +32,7 @@
],
"filesAnalyzed": false,
"hasFiles": [
"SPDXRef-File-package-1-04cd22424378dcd6c77fce08beb52493b5494a60ea5e1f9bdf9b16dc0cacffe9"
"SPDXRef-File-package-1-efae7fecc76ca25da40f79d7ef5b8933510434914835832c7976f3e866aa756a"
],
"licenseDeclared": "MIT",
"sourceInfo": "acquired package info from installed python package manifest file: /some/path/pkg1",
@@ -63,17 +63,17 @@
],
"files": [
{
"SPDXID": "SPDXRef-File-package-1-04cd22424378dcd6c77fce08beb52493b5494a60ea5e1f9bdf9b16dc0cacffe9",
"SPDXID": "SPDXRef-File-package-1-efae7fecc76ca25da40f79d7ef5b8933510434914835832c7976f3e866aa756a",
"name": "foo",
"licenseConcluded": "",
"fileName": "/some/path/pkg1/depedencies/foo"
"fileName": "/some/path/pkg1/dependencies/foo"
}
],
"relationships": [
{
"spdxElementId": "SPDXRef-Package-python-package-1-1.0.1",
"relationshipType": "CONTAINS",
"relatedSpdxElement": "SPDXRef-File-package-1-04cd22424378dcd6c77fce08beb52493b5494a60ea5e1f9bdf9b16dc0cacffe9"
"relatedSpdxElement": "SPDXRef-File-package-1-efae7fecc76ca25da40f79d7ef5b8933510434914835832c7976f3e866aa756a"
}
]
}
@@ -3,7 +3,7 @@
"name": "user-image-input",
"spdxVersion": "SPDX-2.2",
"creationInfo": {
"created": "2021-09-16T20:44:35.203911Z",
"created": "2021-10-12T18:40:22.953633Z",
"creators": [
"Organization: Anchore, Inc",
"Tool: syft-[not provided]"
@@ -11,7 +11,7 @@
"licenseListVersion": "3.14"
},
"dataLicense": "CC0-1.0",
"documentNamespace": "https://anchore.com/syft/image/user-image-input",
"documentNamespace": "https:/anchore.com/syft/image/user-image-input-149edbad-3c01-4ee0-b3a0-75232312bf51",
"packages": [
{
"SPDXID": "SPDXRef-Package-python-package-1-1.0.1",
@@ -1,11 +1,11 @@
[Image]
Layer: 0
Digest: sha256:ffb5e9eaa453a002110719d12c294960117ca2903953d1faa40f01dc3f77045c
Digest: sha256:fb6beecb75b39f4bb813dbf177e501edd5ddb3e69bb45cedeb78c676ee1b7a59
Size: 22
MediaType: application/vnd.docker.image.rootfs.diff.tar.gzip
Layer: 1
Digest: sha256:8463854829fc53d47b9dcdf7ee79fe7eb4ca7933c910f67f8521412f7a2f5c21
Digest: sha256:319b588ce64253a87b533c8ed01cf0025e0eac98e7b516e12532957e1244fdec
Size: 16
MediaType: application/vnd.docker.image.rootfs.diff.tar.gzip
@@ -3,13 +3,15 @@ package packages
import (
"flag"
"testing"
"github.com/anchore/syft/internal/formats/common/testutils"
)
var updateTextPresenterGoldenFiles = flag.Bool("update-text", false, "update the *.golden files for text presenters")
func TestTextDirectoryPresenter(t *testing.T) {
catalog, metadata, _ := presenterDirectoryInput(t)
assertPresenterAgainstGoldenSnapshot(t,
catalog, metadata, _ := testutils.DirectoryInput(t)
testutils.AssertPresenterAgainstGoldenSnapshot(t,
NewTextPresenter(catalog, metadata),
*updateTextPresenterGoldenFiles,
)
@@ -17,8 +19,8 @@ func TestTextDirectoryPresenter(t *testing.T) {
func TestTextImagePresenter(t *testing.T) {
testImage := "image-simple"
catalog, metadata, _ := presenterImageInput(t, testImage)
assertPresenterAgainstGoldenImageSnapshot(t,
catalog, metadata, _ := testutils.ImageInput(t, testImage)
testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
NewTextPresenter(catalog, metadata),
testImage,
*updateTextPresenterGoldenFiles,
@@ -1,7 +1,8 @@
package poweruser
import (
"github.com/anchore/syft/internal/presenter/packages"
"github.com/anchore/syft/internal/formats/syftjson"
"github.com/anchore/syft/internal/formats/syftjson/model"
)
type JSONDocument struct {
@@ -13,16 +14,11 @@ type JSONDocument struct {
FileContents []JSONFileContents `json:"fileContents,omitempty"` // note: must have omitempty
FileMetadata []JSONFileMetadata `json:"fileMetadata,omitempty"` // note: must have omitempty
Secrets []JSONSecrets `json:"secrets,omitempty"` // note: must have omitempty
packages.JSONDocument
model.Document
}
// NewJSONDocument creates and populates a new JSON document struct from the given cataloging results.
func NewJSONDocument(config JSONDocumentConfig) (JSONDocument, error) {
pkgsDoc, err := packages.NewJSONDocument(config.PackageCatalog, config.SourceMetadata, config.Distro, config.ApplicationConfig.Package.Cataloger.ScopeOpt, config.ApplicationConfig)
if err != nil {
return JSONDocument{}, err
}
fileMetadata, err := NewJSONFileMetadata(config.FileMetadata, config.FileDigests)
if err != nil {
return JSONDocument{}, err
@@ -33,6 +29,6 @@ func NewJSONDocument(config JSONDocumentConfig) (JSONDocument, error) {
FileContents: NewJSONFileContents(config.FileContents),
FileMetadata: fileMetadata,
Secrets: NewJSONSecrets(config.Secrets),
JSONDocument: pkgsDoc,
Document: syftjson.ToFormatModel(config.PackageCatalog, &config.SourceMetadata, config.Distro, config.ApplicationConfig.Package.Cataloger.ScopeOpt, config.ApplicationConfig),
}, nil
}
+24
View File
@@ -0,0 +1,24 @@
package ui
import (
"fmt"
"io"
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
"github.com/wagoodman/go-partybus"
)
// handleCatalogerPresenterReady is a UI function for processing the CatalogerFinished bus event, displaying the catalog
// via the given presenter to stdout.
func handleCatalogerPresenterReady(event partybus.Event, reportOutput io.Writer) error {
// show the report to stdout
pres, err := syftEventParsers.ParsePresenterReady(event)
if err != nil {
return fmt.Errorf("bad CatalogerFinished event: %w", err)
}
if err := pres.Present(reportOutput); err != nil {
return fmt.Errorf("unable to show package catalog report: %w", err)
}
return nil
}
+3
View File
@@ -1,3 +1,6 @@
//go:build linux || darwin
// +build linux darwin
package ui
import (
@@ -1,3 +1,6 @@
//go:build linux || darwin
// +build linux darwin
package ui
import (
@@ -14,21 +17,6 @@ import (
"github.com/wagoodman/go-partybus"
)
// handleCatalogerPresenterReady is a UI function for processing the CatalogerFinished bus event, displaying the catalog
// via the given presenter to stdout.
func handleCatalogerPresenterReady(event partybus.Event, reportOutput io.Writer) error {
// show the report to stdout
pres, err := syftEventParsers.ParsePresenterReady(event)
if err != nil {
return fmt.Errorf("bad CatalogerFinished event: %w", err)
}
if err := pres.Present(reportOutput); err != nil {
return fmt.Errorf("unable to show package catalog report: %w", err)
}
return nil
}
// handleAppUpdateAvailable is a UI handler function to display a new application version to the top of the screen.
func handleAppUpdateAvailable(_ context.Context, fr *frame.Frame, event partybus.Event, _ *sync.WaitGroup) error {
newVersion, err := syftEventParsers.ParseAppUpdateAvailable(event)
+3
View File
@@ -1,3 +1,6 @@
//go:build linux || darwin
// +build linux darwin
package ui
import (
+17
View File
@@ -0,0 +1,17 @@
//go:build windows
// +build windows
package ui
import (
"io"
)
// Select is responsible for determining the specific UI function given select user option, the current platform
// config values, and environment status (such as a TTY being present). The first UI in the returned slice of UIs
// is intended to be used and the UIs that follow are meant to be attempted only in a fallback posture when there
// are environmental problems (e.g. cannot write to the terminal). A writer is provided to capture the output of
// the final SBOM report.
func Select(verbose, quiet bool, reportWriter io.Writer) (uis []UI) {
return append(uis, NewLoggerUI(reportWriter))
}
+48
View File
@@ -0,0 +1,48 @@
package syft
import (
"bytes"
"fmt"
"io"
"github.com/anchore/syft/internal/formats"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/format"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
// Encode takes all SBOM elements and a format option and encodes an SBOM document.
// TODO: encapsulate input data into common sbom document object
func Encode(catalog *pkg.Catalog, metadata *source.Metadata, dist *distro.Distro, scope source.Scope, option format.Option) ([]byte, error) {
f := formats.ByOption(option)
if f == nil {
return nil, fmt.Errorf("unsupported format: %+v", option)
}
buff := bytes.Buffer{}
if err := f.Encode(&buff, catalog, dist, metadata, scope); err != nil {
return nil, fmt.Errorf("unable to encode sbom: %w", err)
}
return buff.Bytes(), nil
}
// Decode takes a reader for an SBOM and generates all internal SBOM elements.
// TODO: encapsulate return data into common sbom document object
func Decode(reader io.Reader) (*pkg.Catalog, *source.Metadata, *distro.Distro, source.Scope, format.Option, error) {
by, err := io.ReadAll(reader)
if err != nil {
return nil, nil, nil, source.UnknownScope, format.UnknownFormatOption, fmt.Errorf("unable to read sbom: %w", err)
}
f, err := formats.Identify(by)
if err != nil {
return nil, nil, nil, source.UnknownScope, format.UnknownFormatOption, fmt.Errorf("unable to detect format: %w", err)
}
if f == nil {
return nil, nil, nil, source.UnknownScope, format.UnknownFormatOption, fmt.Errorf("unable to identify format")
}
c, m, d, s, err := f.Decode(bytes.NewReader(by))
return c, m, d, s, f.Option, err
}
+53
View File
@@ -0,0 +1,53 @@
package syft
import (
"bytes"
"testing"
"github.com/go-test/deep"
"github.com/anchore/syft/syft/format"
"github.com/anchore/syft/syft/source"
"github.com/stretchr/testify/assert"
)
// TestEncodeDecodeEncodeCycleComparison is testing for differences in how SBOM documents get encoded on multiple cycles.
// By encding and decoding the sbom we can compare the differences between the set of resulting objects. However,
// this requires specific comparisons being done, and select redactions/omissions being made. Additionally, there are
// already unit tests on each format encoder-decoder for properly functioning comparisons in depth, so there is no need
// to do an object-to-object comparison. For this reason this test focuses on a bytes-to-bytes comparison after an
// encode-decode-encode loop which will detect lossy behavior in both directions.
func TestEncodeDecodeEncodeCycleComparison(t *testing.T) {
testImage := "image-simple"
tests := []struct {
format format.Option
}{
{
format: format.JSONOption,
},
}
for _, test := range tests {
t.Run(testImage, func(t *testing.T) {
src, err := source.NewFromDirectory("./test-fixtures/pkgs")
if err != nil {
t.Fatalf("cant get dir")
}
originalCatalog, d, err := CatalogPackages(&src, source.SquashedScope)
by1, err := Encode(originalCatalog, &src.Metadata, d, source.SquashedScope, test.format)
assert.NoError(t, err)
newCatalog, newMetadata, newDistro, newScope, newFormat, err := Decode(bytes.NewReader(by1))
assert.NoError(t, err)
assert.Equal(t, test.format, newFormat)
by2, err := Encode(newCatalog, newMetadata, newDistro, newScope, test.format)
assert.NoError(t, err)
for _, diff := range deep.Equal(by1, by2) {
t.Errorf(diff)
}
assert.True(t, bytes.Equal(by1, by2))
})
}
}
+13
View File
@@ -0,0 +1,13 @@
package format
import (
"io"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
// Decoder is a function that can convert an SBOM document of a specific format from a reader into Syft native objects.
type Decoder func(reader io.Reader) (*pkg.Catalog, *source.Metadata, *distro.Distro, source.Scope, error)
+12
View File
@@ -0,0 +1,12 @@
package format
import (
"io"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
// Encoder is a function that can transform Syft native objects into an SBOM document of a specific format written to the given writer.
type Encoder func(io.Writer, *pkg.Catalog, *source.Metadata, *distro.Distro, source.Scope) error
+62
View File
@@ -0,0 +1,62 @@
package format
import (
"errors"
"io"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
var (
ErrEncodingNotSupported = errors.New("encoding not supported")
ErrDecodingNotSupported = errors.New("decoding not supported")
ErrValidationNotSupported = errors.New("validation not supported")
)
type Format struct {
Option Option
encoder Encoder
decoder Decoder
validator Validator
}
func NewFormat(option Option, encoder Encoder, decoder Decoder, validator Validator) Format {
return Format{
Option: option,
encoder: encoder,
decoder: decoder,
validator: validator,
}
}
func (f Format) Encode(output io.Writer, catalog *pkg.Catalog, d *distro.Distro, metadata *source.Metadata, scope source.Scope) error {
if f.encoder == nil {
return ErrEncodingNotSupported
}
return f.encoder(output, catalog, metadata, d, scope)
}
func (f Format) Decode(reader io.Reader) (*pkg.Catalog, *source.Metadata, *distro.Distro, source.Scope, error) {
if f.decoder == nil {
return nil, nil, nil, source.UnknownScope, ErrDecodingNotSupported
}
return f.decoder(reader)
}
func (f Format) Validate(reader io.Reader) error {
if f.validator == nil {
return ErrValidationNotSupported
}
return f.validator(reader)
}
func (f Format) Presenter(catalog *pkg.Catalog, metadata *source.Metadata, d *distro.Distro, scope source.Scope) *Presenter {
if f.encoder == nil {
return nil
}
return NewPresenter(f.encoder, catalog, metadata, d, scope)
}
+43
View File
@@ -0,0 +1,43 @@
package format
import "strings"
const (
UnknownFormatOption Option = "UnknownFormatOption"
JSONOption Option = "json"
TextOption Option = "text"
TableOption Option = "table"
CycloneDxOption Option = "cyclonedx"
SPDXTagValueOption Option = "spdx-tag-value"
SPDXJSONOption Option = "spdx-json"
)
var AllPresenters = []Option{
JSONOption,
TextOption,
TableOption,
CycloneDxOption,
SPDXTagValueOption,
SPDXJSONOption,
}
type Option string
func ParseOption(userStr string) Option {
switch strings.ToLower(userStr) {
case string(JSONOption):
return JSONOption
case string(TextOption):
return TextOption
case string(TableOption):
return TableOption
case string(CycloneDxOption), "cyclone", "cyclone-dx":
return CycloneDxOption
case string(SPDXTagValueOption), "spdx", "spdx-tagvalue", "spdxtagvalue", "spdx-tv", "spdxtv":
return SPDXTagValueOption
case string(SPDXJSONOption), "spdxjson":
return SPDXJSONOption
default:
return UnknownFormatOption
}
}
+32
View File
@@ -0,0 +1,32 @@
package format
import (
"io"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
type Presenter struct {
catalog *pkg.Catalog
srcMetadata *source.Metadata
distro *distro.Distro
scope source.Scope
encoder Encoder
}
func NewPresenter(encoder Encoder, catalog *pkg.Catalog, srcMetadata *source.Metadata, d *distro.Distro, scope source.Scope) *Presenter {
return &Presenter{
catalog: catalog,
srcMetadata: srcMetadata,
distro: d,
encoder: encoder,
scope: scope,
}
}
func (pres *Presenter) Present(output io.Writer) error {
return pres.encoder(output, pres.catalog, pres.srcMetadata, pres.distro, pres.scope)
}
+12
View File
@@ -0,0 +1,12 @@
package format
import "io"
// Validator reads the SBOM from the given reader and assesses whether the document conforms to the specific SBOM format.
// The validator should positively confirm if the SBOM is not only the format but also has the minimal set of values
// that the format requires. For example, all syftjson formatted documents have a schema section which should have
// "anchore/syft" within the version --if this isn't found then the validator should raise an error. These active
// assertions protect against "simple" format decoding validations that may lead to false positives (e.g. I decoded
// json successfully therefore this must be the target format, however, all values are their default zero-value and
// really represent a different format that also uses json)
type Validator func(reader io.Reader) error
+1 -1
View File
@@ -50,7 +50,7 @@ func (c *Cataloger) Catalog(resolver source.FileResolver) ([]pkg.Package, error)
goPkgs, err := parseGoBin(location.RealPath, r)
if err != nil {
log.Infof("could not parse go bin for: %w", err)
log.Warnf("could not parse possible go binary: %+v", err)
}
r.Close()
+13 -11
View File
@@ -21,26 +21,27 @@ func parseGoBin(path string, reader io.ReadCloser) ([]pkg.Package, error) {
return nil, err
}
_, mod := findVers(x)
goVersion, mod := findVers(x)
pkgs := buildGoPkgInfo(path, mod)
pkgs := buildGoPkgInfo(path, mod, goVersion)
return pkgs, nil
}
func buildGoPkgInfo(path, mod string) []pkg.Package {
func buildGoPkgInfo(path, mod, goVersion string) []pkg.Package {
pkgsSlice := make([]pkg.Package, 0)
scanner := bufio.NewScanner(strings.NewReader(mod))
// filter mod dependencies: [dep, name, version, sha]
for scanner.Scan() {
fields := strings.Fields(scanner.Text())
// must have dep, name, version
if len(fields) < 3 {
// must have dep, name, version, sha
if len(fields) < 4 {
continue
}
switch fields[0] {
case packageIdentifier:
if fields[0] == packageIdentifier || fields[0] == replaceIdentifier {
pkgsSlice = append(pkgsSlice, pkg.Package{
Name: fields[1],
Version: fields[2],
@@ -51,11 +52,12 @@ func buildGoPkgInfo(path, mod string) []pkg.Package {
RealPath: path,
},
},
MetadataType: pkg.GolangBinMetadataType,
Metadata: pkg.GolangBinMetadata{
GoCompiledVersion: goVersion,
H1Digest: fields[3],
},
})
case replaceIdentifier:
pkg := &pkgsSlice[len(pkgsSlice)-1]
pkg.Name = fields[1]
pkg.Version = fields[2]
}
}
+28 -1
View File
@@ -8,6 +8,8 @@ import (
"github.com/stretchr/testify/assert"
)
const goCompiledVersion = "1.17"
func TestBuildGoPkgInfo(t *testing.T) {
tests := []struct {
name string
@@ -33,6 +35,11 @@ func TestBuildGoPkgInfo(t *testing.T) {
Locations: []source.Location{
{},
},
MetadataType: pkg.GolangBinMetadataType,
Metadata: pkg.GolangBinMetadata{
GoCompiledVersion: goCompiledVersion,
H1Digest: "h1:VSVdnH7cQ7V+B33qSJHTCRlNgra1607Q8PzEmnvb2Ic=",
},
},
{
Name: "github.com/anchore/client-go",
@@ -42,6 +49,11 @@ func TestBuildGoPkgInfo(t *testing.T) {
Locations: []source.Location{
{},
},
MetadataType: pkg.GolangBinMetadataType,
Metadata: pkg.GolangBinMetadata{
GoCompiledVersion: goCompiledVersion,
H1Digest: "h1:DYssiUV1pBmKqzKsm4mqXx8artqC0Q8HgZsVI3lMsAg=",
},
},
},
},
@@ -62,6 +74,11 @@ func TestBuildGoPkgInfo(t *testing.T) {
Locations: []source.Location{
{},
},
MetadataType: pkg.GolangBinMetadataType,
Metadata: pkg.GolangBinMetadata{
GoCompiledVersion: goCompiledVersion,
H1Digest: "h1:KlOXYy8wQWTUJYFgkUI40Lzr06ofg5IRXUK5C7qZt1k=",
},
},
{
Name: "golang.org/x/sys",
@@ -71,6 +88,11 @@ func TestBuildGoPkgInfo(t *testing.T) {
Locations: []source.Location{
{},
},
MetadataType: pkg.GolangBinMetadataType,
Metadata: pkg.GolangBinMetadata{
GoCompiledVersion: goCompiledVersion,
H1Digest: "h1:PjhxBct4MZii8FFR8+oeS7QOvxKOTZXgk63EU2XpfJE=",
},
},
{
Name: "golang.org/x/term",
@@ -80,6 +102,11 @@ func TestBuildGoPkgInfo(t *testing.T) {
Locations: []source.Location{
{},
},
MetadataType: pkg.GolangBinMetadataType,
Metadata: pkg.GolangBinMetadata{
GoCompiledVersion: goCompiledVersion,
H1Digest: "h1:Ihq/mm/suC88gF8WFcVwk+OV6Tq+wyA1O0E5UEvDglI=",
},
},
},
},
@@ -88,7 +115,7 @@ func TestBuildGoPkgInfo(t *testing.T) {
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
pkgs := buildGoPkgInfo("", tt.mod)
pkgs := buildGoPkgInfo("", tt.mod, goCompiledVersion)
assert.Equal(t, tt.expected, pkgs)
})
}
@@ -90,6 +90,7 @@ func TestParseJar(t *testing.T) {
fixture: "test-fixtures/java-builds/packages/example-jenkins-plugin.hpi",
ignoreExtras: []string{
"Plugin-Version", // has dynamic date
"Built-By", // podman returns the real UID
"Build-Jdk", // can't guarantee the JDK used at build time
},
expected: map[string]pkg.Package{
@@ -112,7 +113,7 @@ func TestParseJar(t *testing.T) {
"Plugin-License-Url": "https://opensource.org/licenses/MIT",
"Plugin-License-Name": "MIT License",
"Created-By": "Apache Maven",
"Built-By": "?",
//"Built-By": "?",
//"Build-Jdk": "14.0.1",
"Jenkins-Version": "2.164.3",
"Minimum-Java-Version": "1.8",
@@ -162,6 +163,7 @@ func TestParseJar(t *testing.T) {
fixture: "test-fixtures/java-builds/packages/example-java-app-maven-0.1.0.jar",
ignoreExtras: []string{
"Build-Jdk", // can't guarantee the JDK used at build time
"Built-By", // podman returns the real UID
},
expected: map[string]pkg.Package{
"example-java-app-maven": {
@@ -178,7 +180,7 @@ func TestParseJar(t *testing.T) {
// extra fields...
"Archiver-Version": "Plexus Archiver",
"Created-By": "Apache Maven 3.6.3",
"Built-By": "?",
//"Built-By": "?",
//"Build-Jdk": "14.0.1",
"Main-Class": "hello.HelloWorld",
},
@@ -78,6 +78,8 @@ func TestPythonPackageWheelCataloger(t *testing.T) {
{Path: "Pygments-2.6.1.dist-info/RECORD"},
{Path: "pygments/__pycache__/__init__.cpython-38.pyc"},
{Path: "pygments/util.py", Digest: &pkg.PythonFileDigest{"sha256", "586xXHiJGGZxqk5PMBu3vBhE68DLuAe5MBARWrSPGxA"}, Size: "10778"},
{Path: "pygments/x_util.py", Digest: &pkg.PythonFileDigest{"sha256", "qpzzsOW31KT955agi-7NS--90I0iNiJCyLJQnRCHgKI="}, Size: "10778"},
},
TopLevelPackages: []string{"pygments", "something_else"},
},
@@ -6,6 +6,7 @@ import (
"io"
"strings"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/pkg"
)
@@ -38,10 +39,10 @@ func parseWheelOrEggRecord(reader io.Reader) ([]pkg.PythonFileRecord, error) {
if item == "" {
continue
}
fields := strings.Split(item, "=")
fields := strings.SplitN(item, "=", 2)
if len(fields) != 2 {
return nil, fmt.Errorf("unexpected python record digest: %q", item)
log.Warnf("unexpected python record digest: %q", item)
continue
}
record.Digest = &pkg.PythonFileDigest{
@@ -32,6 +32,7 @@ func TestParseWheelEggRecord(t *testing.T) {
{Path: "Pygments-2.6.1.dist-info/RECORD"},
{Path: "pygments/__pycache__/__init__.cpython-38.pyc"},
{Path: "pygments/util.py", Digest: &pkg.PythonFileDigest{"sha256", "586xXHiJGGZxqk5PMBu3vBhE68DLuAe5MBARWrSPGxA"}, Size: "10778"},
{Path: "pygments/x_util.py", Digest: &pkg.PythonFileDigest{"sha256", "qpzzsOW31KT955agi-7NS--90I0iNiJCyLJQnRCHgKI="}, Size: "10778"},
},
},
}
@@ -2,4 +2,5 @@
Pygments-2.6.1.dist-info/AUTHORS,sha256=PVpa2_Oku6BGuiUvutvuPnWGpzxqFy2I8-NIrqCvqUY,8449
Pygments-2.6.1.dist-info/RECORD,,
pygments/__pycache__/__init__.cpython-38.pyc,,
pygments/util.py,sha256=586xXHiJGGZxqk5PMBu3vBhE68DLuAe5MBARWrSPGxA,10778
pygments/util.py,sha256=586xXHiJGGZxqk5PMBu3vBhE68DLuAe5MBARWrSPGxA,10778
pygments/x_util.py,sha256=qpzzsOW31KT955agi-7NS--90I0iNiJCyLJQnRCHgKI=,10778
+8
View File
@@ -35,6 +35,14 @@ func NewCPE(cpeStr string) (CPE, error) {
return *value, nil
}
func MustCPE(cpeStr string) CPE {
c, err := NewCPE(cpeStr)
if err != nil {
panic(err)
}
return c
}
func normalizeCpeField(field string) string {
// keep dashes and forward slashes unescaped
return strings.ReplaceAll(wfn.StripSlashes(field), `\/`, "/")
+7
View File
@@ -0,0 +1,7 @@
package pkg
// GolangBinMetadata represents all captured data for a Golang Binary
type GolangBinMetadata struct {
GoCompiledVersion string
H1Digest string
}
+2
View File
@@ -15,6 +15,7 @@ const (
PythonPackageMetadataType MetadataType = "PythonPackageMetadata"
RustCargoPackageMetadataType MetadataType = "RustCargoPackageMetadata"
KbPackageMetadataType MetadataType = "KbPackageMetadata"
GolangBinMetadataType MetadataType = "GolangBinMetadata"
)
var AllMetadataTypes = []MetadataType{
@@ -27,4 +28,5 @@ var AllMetadataTypes = []MetadataType{
PythonPackageMetadataType,
RustCargoPackageMetadataType,
KbPackageMetadataType,
GolangBinMetadataType,
}
+13 -10
View File
@@ -5,26 +5,29 @@ a specific Presenter implementation given user configuration.
package packages
import (
"github.com/anchore/syft/internal/formats"
"github.com/anchore/syft/internal/presenter/packages"
"github.com/anchore/syft/syft/format"
"github.com/anchore/syft/syft/presenter"
)
// Presenter returns a presenter for images or directories
func Presenter(option PresenterOption, config PresenterConfig) presenter.Presenter {
func Presenter(option format.Option, config PresenterConfig) presenter.Presenter {
switch option {
case JSONPresenterOption:
return packages.NewJSONPresenter(config.Catalog, config.SourceMetadata, config.Distro, config.Scope)
case TextPresenterOption:
case format.TextOption:
return packages.NewTextPresenter(config.Catalog, config.SourceMetadata)
case TablePresenterOption:
return packages.NewTablePresenter(config.Catalog)
case CycloneDxPresenterOption:
case format.CycloneDxOption:
return packages.NewCycloneDxPresenter(config.Catalog, config.SourceMetadata)
case SPDXTagValuePresenterOption:
case format.SPDXTagValueOption:
return packages.NewSPDXTagValuePresenter(config.Catalog, config.SourceMetadata)
case SPDXJSONPresenterOption:
case format.SPDXJSONOption:
return packages.NewSPDXJSONPresenter(config.Catalog, config.SourceMetadata)
default:
return nil
// TODO: the final state is that all other cases would be replaced by formats.ByOption (wed remove this function entirely)
f := formats.ByOption(option)
if f == nil {
return nil
}
return f.Presenter(config.Catalog, &config.SourceMetadata, config.Distro, config.Scope)
}
}
@@ -1,43 +0,0 @@
package packages
import "strings"
const (
UnknownPresenterOption PresenterOption = "UnknownPresenterOption"
JSONPresenterOption PresenterOption = "json"
TextPresenterOption PresenterOption = "text"
TablePresenterOption PresenterOption = "table"
CycloneDxPresenterOption PresenterOption = "cyclonedx"
SPDXTagValuePresenterOption PresenterOption = "spdx-tag-value"
SPDXJSONPresenterOption PresenterOption = "spdx-json"
)
var AllPresenters = []PresenterOption{
JSONPresenterOption,
TextPresenterOption,
TablePresenterOption,
CycloneDxPresenterOption,
SPDXTagValuePresenterOption,
SPDXJSONPresenterOption,
}
type PresenterOption string
func ParsePresenterOption(userStr string) PresenterOption {
switch strings.ToLower(userStr) {
case string(JSONPresenterOption):
return JSONPresenterOption
case string(TextPresenterOption):
return TextPresenterOption
case string(TablePresenterOption):
return TablePresenterOption
case string(CycloneDxPresenterOption), "cyclone", "cyclone-dx":
return CycloneDxPresenterOption
case string(SPDXTagValuePresenterOption), "spdx", "spdx-tagvalue", "spdxtagvalue", "spdx-tv":
return SPDXTagValuePresenterOption
case string(SPDXJSONPresenterOption), "spdxjson":
return SPDXJSONPresenterOption
default:
return UnknownPresenterOption
}
}
+21
View File
@@ -0,0 +1,21 @@
//go:build linux || darwin
// +build linux darwin
package source
import (
"os"
"syscall"
)
// GetXid is the UID GID system info for unix
func GetXid(info os.FileInfo) (uid, gid int) {
uid = -1
gid = -1
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
uid = int(stat.Uid)
gid = int(stat.Gid)
}
return uid, gid
}
+13
View File
@@ -0,0 +1,13 @@
//go:build windows
// +build windows
package source
import (
"os"
)
// GetXid is a placeholder for windows file information
func GetXid(info os.FileInfo) (uid, gid int) {
return -1, -1
}
+1 -7
View File
@@ -2,7 +2,6 @@ package source
import (
"os"
"syscall"
"github.com/anchore/syft/internal/log"
@@ -39,12 +38,7 @@ func fileMetadataByLocation(img *image.Image, location Location) (FileMetadata,
}
func fileMetadataFromPath(path string, info os.FileInfo) FileMetadata {
uid := -1
gid := -1
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
uid = int(stat.Uid)
gid = int(stat.Gid)
}
uid, gid := GetXid(info)
f, err := os.Open(path)
if err != nil {
+52
View File
@@ -0,0 +1,52 @@
{
"name": "npm-lock",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"collapse-white-space": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.0.0.tgz",
"integrity": "sha512-eh9krktAIMDL0KHuN7WTBJ/0PMv8KUvfQRBkIlGmW61idRM2DJjgd1qXEPr4wyk2PimZZeNww3RVYo6CMvDGlg=="
},
"end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
"dev": true,
"requires": {
"once": "^1.4.0"
}
},
"insert-css": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/insert-css/-/insert-css-2.0.0.tgz",
"integrity": "sha1-610Ql7dUL0x56jBg067gfQU4gPQ="
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
"requires": {
"wrappy": "1"
}
},
"pump": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
"dev": true,
"requires": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true
}
}
}
+8
View File
@@ -14,6 +14,7 @@ import (
type traitAssertion func(tb testing.TB, stdout, stderr string, rc int)
func assertJsonReport(tb testing.TB, stdout, _ string, _ int) {
tb.Helper()
var data interface{}
if err := json.Unmarshal([]byte(stdout), &data); err != nil {
@@ -22,6 +23,7 @@ func assertJsonReport(tb testing.TB, stdout, _ string, _ int) {
}
func assertTableReport(tb testing.TB, stdout, _ string, _ int) {
tb.Helper()
if !strings.Contains(stdout, "NAME") || !strings.Contains(stdout, "VERSION") || !strings.Contains(stdout, "TYPE") {
tb.Errorf("expected to find a table report, but did not")
}
@@ -29,6 +31,7 @@ func assertTableReport(tb testing.TB, stdout, _ string, _ int) {
func assertScope(scope source.Scope) traitAssertion {
return func(tb testing.TB, stdout, stderr string, rc int) {
tb.Helper()
// we can only verify source with the json report
assertJsonReport(tb, stdout, stderr, rc)
@@ -44,6 +47,7 @@ func assertLoggingLevel(level string) traitAssertion {
// "[0012] DEBUG"
logPattern := regexp.MustCompile(`(?m)^\[\d\d\d\d\]\s+` + strings.ToUpper(level))
return func(tb testing.TB, _, stderr string, _ int) {
tb.Helper()
if !logPattern.MatchString(stripansi.Strip(stderr)) {
tb.Errorf("output did not indicate the %q logging level", level)
}
@@ -52,6 +56,7 @@ func assertLoggingLevel(level string) traitAssertion {
func assertNotInOutput(data string) traitAssertion {
return func(tb testing.TB, stdout, stderr string, _ int) {
tb.Helper()
if strings.Contains(stripansi.Strip(stderr), data) {
tb.Errorf("data=%q was found in stderr, but should not have been there", data)
}
@@ -63,6 +68,7 @@ func assertNotInOutput(data string) traitAssertion {
func assertInOutput(data string) traitAssertion {
return func(tb testing.TB, stdout, stderr string, _ int) {
tb.Helper()
if !strings.Contains(stripansi.Strip(stderr), data) && !strings.Contains(stripansi.Strip(stdout), data) {
tb.Errorf("data=%q was NOT found in any output, but should have been there", data)
}
@@ -70,12 +76,14 @@ func assertInOutput(data string) traitAssertion {
}
func assertFailingReturnCode(tb testing.TB, _, _ string, rc int) {
tb.Helper()
if rc == 0 {
tb.Errorf("expected a failure but got rc=%d", rc)
}
}
func assertSuccessfulReturnCode(tb testing.TB, _, _ string, rc int) {
tb.Helper()
if rc != 0 {
tb.Errorf("expected no failure but got rc=%d", rc)
}
+17 -10
View File
@@ -50,23 +50,20 @@ func runSyftInDocker(t testing.TB, env map[string]string, image string, args ...
func runSyft(t testing.TB, env map[string]string, args ...string) (*exec.Cmd, string, string) {
cmd := getSyftCommand(t, args...)
if env != nil {
env["SYFT_CHECK_FOR_APP_UPDATE"] = "false"
if env == nil {
env = make(map[string]string)
}
// we should not have tests reaching out for app update checks
env["SYFT_CHECK_FOR_APP_UPDATE"] = "false"
stdout, stderr := runCommand(cmd, env)
return cmd, stdout, stderr
}
func runCommand(cmd *exec.Cmd, env map[string]string) (string, string) {
if env != nil {
var envList []string
for key, val := range env {
if key == "" {
continue
}
envList = append(envList, fmt.Sprintf("%s=%s", key, val))
}
cmd.Env = envList
cmd.Env = append(os.Environ(), envMapToSlice(env)...)
}
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
@@ -78,6 +75,16 @@ func runCommand(cmd *exec.Cmd, env map[string]string) (string, string) {
return stdout.String(), stderr.String()
}
func envMapToSlice(env map[string]string) (envList []string) {
for key, val := range env {
if key == "" {
continue
}
envList = append(envList, fmt.Sprintf("%s=%s", key, val))
}
return
}
func getSyftCommand(t testing.TB, args ...string) *exec.Cmd {
return exec.Command(getSyftBinaryLocation(t), args...)
}
@@ -3,10 +3,11 @@ package integration
import (
"bytes"
"encoding/json"
exportedPackages "github.com/anchore/syft/syft/presenter/packages"
"testing"
internalPackages "github.com/anchore/syft/internal/presenter/packages"
syftjsonModel "github.com/anchore/syft/internal/formats/syftjson/model"
"github.com/anchore/syft/syft/format"
exportedPackages "github.com/anchore/syft/syft/presenter/packages"
)
func TestPackageOwnershipRelationships(t *testing.T) {
@@ -24,7 +25,7 @@ func TestPackageOwnershipRelationships(t *testing.T) {
t.Run(test.fixture, func(t *testing.T) {
catalog, d, src := catalogFixtureImage(t, test.fixture)
p := exportedPackages.Presenter(exportedPackages.JSONPresenterOption, exportedPackages.PresenterConfig{
p := exportedPackages.Presenter(format.JSONOption, exportedPackages.PresenterConfig{
SourceMetadata: src.Metadata,
Catalog: catalog,
Distro: d,
@@ -39,7 +40,7 @@ func TestPackageOwnershipRelationships(t *testing.T) {
t.Fatalf("unable to present: %+v", err)
}
var doc internalPackages.JSONDocument
var doc syftjsonModel.Document
decoder := json.NewDecoder(output)
if err := decoder.Decode(&doc); err != nil {
t.Fatalf("unable to decode json doc: %+v", err)