mirror of
https://github.com/anchore/syft.git
synced 2026-10-12 22:27:22 +02:00
Compare commits
52
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ecf11f0e3d | ||
|
|
628c2e44a5 | ||
|
|
aba0de6e65 | ||
|
|
cb0278f3bc | ||
|
|
b519340b86 | ||
|
|
2356539ebe | ||
|
|
1537e73083 | ||
|
|
52adfcbd44 | ||
|
|
a3f0d659da | ||
|
|
8a0fa5d3ad | ||
|
|
253faf5652 | ||
|
|
69d2b1ba3c | ||
|
|
b08a11e46d | ||
|
|
fef951c29b | ||
|
|
a906b9a03a | ||
|
|
9bbc9ff633 | ||
|
|
bb0f35bac4 | ||
|
|
6d0ee326d8 | ||
|
|
23a67dd729 | ||
|
|
3e20edee55 | ||
|
|
8ec3f1d102 | ||
|
|
ecab44bba1 | ||
|
|
484284706c | ||
|
|
3ac95ac4f6 | ||
|
|
a2882ee810 | ||
|
|
358b3a2cf8 | ||
|
|
9aca23f766 | ||
|
|
4a2d1d7225 | ||
|
|
3286a4d4cc | ||
|
|
0ff7013382 | ||
|
|
71708e5bf8 | ||
|
|
dd024298eb | ||
|
|
1c63943055 | ||
|
|
ff5fa2dc1d | ||
|
|
fb588ff500 | ||
|
|
d5b425e1b5 | ||
|
|
7b319e32bb | ||
|
|
89242e6ed3 | ||
|
|
4bf08e6b6d | ||
|
|
87ce9fff06 | ||
|
|
10fa8dc7c9 | ||
|
|
560b05c2c9 | ||
|
|
5e315c0f17 | ||
|
|
6f3fa494d7 | ||
|
|
a82463574b | ||
|
|
cbdb72ea22 | ||
|
|
7a005fec06 | ||
|
|
c0f55866ec | ||
|
|
9ea6330ebc | ||
|
|
7d13fbf0d4 | ||
|
|
10368f6fe8 | ||
|
|
f2840029d1 |
Executable
+398
@@ -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"
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -15,10 +15,10 @@ RESET := $(shell tput -T linux sgr0)
|
||||
TITLE := $(BOLD)$(PURPLE)
|
||||
SUCCESS := $(BOLD)$(GREEN)
|
||||
# the quality gate lower threshold for unit test total % coverage (by function statements)
|
||||
COVERAGE_THRESHOLD := 65
|
||||
COVERAGE_THRESHOLD := 62
|
||||
# CI cache busting values; change these if you want CI to not use previous stored cache
|
||||
INTEGRATION_CACHE_BUSTER="88738d2f"
|
||||
CLI_CACHE_BUSTER="789bacdf"
|
||||
CLI_CACHE_BUSTER="9a2c03cf"
|
||||
BOOTSTRAP_CACHE="c7afb99ad"
|
||||
|
||||
## Build variables
|
||||
@@ -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 \
|
||||
|
||||
@@ -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" alt="Cute pink owl syft logo">
|
||||
</p>
|
||||
|
||||
[](https://github.com/anchore/syft/actions/workflows/validations.yaml)
|
||||
[](https://goreportcard.com/report/github.com/anchore/syft)
|
||||
@@ -7,19 +9,11 @@
|
||||
[](https://github.com/anchore/syft/blob/main/LICENSE)
|
||||
[](https://anchore.com/slack)
|
||||
|
||||
A CLI tool and go library for generating a Software Bill of Materials (SBOM) from container images and filesystems. Exceptional for vulnerability detection when used with a scanner tool like [Grype](https://github.com/anchore/grype).
|
||||
|
||||
## We'll be at KubeCon 2021!
|
||||
|
||||
Attending KubeCon 2021 in person? Join us for a meetup on **Tuesday, October 12th**.
|
||||
|
||||
We’ll have free swag, giveaways, snacks, and sips. Space will be limited, so make sure to [save your seat](https://get.anchore.com/2021-kubecon-na-opensource-happy-hour/)!
|
||||
|
||||
---
|
||||
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).
|
||||
|
||||

|
||||
|
||||
**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)
|
||||
@@ -89,6 +83,7 @@ docker-archive:path/to/yourimage.tar use a tarball from disk for archives crea
|
||||
oci-archive:path/to/yourimage.tar use a tarball from disk for OCI archives (from Skopeo or otherwise)
|
||||
oci-dir:path/to/yourimage read directly from a path on disk for OCI layout directories (from Skopeo or otherwise)
|
||||
dir:path/to/yourproject read directly from a path on disk (any directory)
|
||||
file:path/to/yourproject/file read directly from a path on disk (any single file)
|
||||
registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required)
|
||||
```
|
||||
|
||||
@@ -100,7 +95,7 @@ The output format for Syft is configurable as well:
|
||||
syft packages <image> -o <format>
|
||||
```
|
||||
|
||||
Where the `format`s available are:
|
||||
Where the `formats` available are:
|
||||
- `json`: Use this to get as much information out of Syft as possible!
|
||||
- `text`: A row-oriented, human-and-machine-friendly output.
|
||||
- `cyclonedx`: A XML report conforming to the [CycloneDX 1.2 specification](https://cyclonedx.org/specification/overview/).
|
||||
@@ -108,6 +103,88 @@ Where the `format`s available are:
|
||||
- `spdx-json`: A JSON report conforming to the [SPDX 2.2 JSON Schema](https://github.com/spdx/spdx-spec/blob/v2.2/schemas/spdx-schema.json).
|
||||
- `table`: A columnar summary (default).
|
||||
|
||||
## Private Registry Authentication
|
||||
|
||||
### Local Docker Credentials
|
||||
When a container runtime is not present, Syft can still utilize credentials configured in common credential sources (such as `~/.docker/config.json`).
|
||||
It will pull images from private registries using these credentials. The config file is where your credentials are stored when authenticating with private registries via some command like `docker login`.
|
||||
For more information see the `go-containerregistry` [documentation](https://github.com/google/go-containerregistry/tree/main/pkg/authn).
|
||||
|
||||
|
||||
An example `config.json` looks something like this:
|
||||
```
|
||||
// config.json
|
||||
{
|
||||
"auths": {
|
||||
"registry.example.com": {
|
||||
"username": "AzureDiamond",
|
||||
"password": "hunter2"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can run the following command as an example. It details the mount/environment configuration a container needs to access a private registry:
|
||||
|
||||
`docker run -v ./config.json:/config/config.json -e "DOCKER_CONFIG=/config" anchore/syft:latest <private_image>`
|
||||
|
||||
|
||||
### Docker Credentials in Kubernetes
|
||||
The below section shows a simple workflow on how to mount this config file as a secret into a container on kubernetes.
|
||||
1. Create a secret. The value of `config.json` is important. It refers to the specification detailed [here](https://github.com/google/go-containerregistry/tree/main/pkg/authn#the-config-file).
|
||||
Below this section is the `secret.yaml` file that the pod configuration will consume as a volume.
|
||||
The key `config.json` is important. It will end up being the name of the file when mounted into the pod.
|
||||
```
|
||||
# secret.yaml
|
||||
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: registry-config
|
||||
namespace: syft
|
||||
data:
|
||||
config.json: <base64 encoded config.json>
|
||||
```
|
||||
|
||||
`kubectl apply -f secret.yaml`
|
||||
|
||||
|
||||
2. Create your pod running syft. The env `DOCKER_CONFIG` is important because it advertises where to look for the credential file.
|
||||
In the below example, setting `DOCKER_CONFIG=/config` informs syft that credentials can be found at `/config/config.json`.
|
||||
This is why we used `config.json` as the key for our secret. When mounted into containers the secrets' key is used as the filename.
|
||||
The `volumeMounts` section mounts our secret to `/config`. The `volumes` section names our volume and leverages the secret we created in step one.
|
||||
```
|
||||
# pod.yaml
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
spec:
|
||||
containers:
|
||||
- image: anchore/syft:latest
|
||||
name: syft-private-registry-demo
|
||||
env:
|
||||
- name: DOCKER_CONFIG
|
||||
value: /config
|
||||
volumeMounts:
|
||||
- mountPath: /config
|
||||
name: registry-config
|
||||
readOnly: true
|
||||
args:
|
||||
- <private_image>
|
||||
volumes:
|
||||
- name: registry-config
|
||||
secret:
|
||||
secretName: registry-config
|
||||
```
|
||||
|
||||
`kubectl apply -f pod.yaml`
|
||||
|
||||
|
||||
3. The user can now run `kubectl logs syft-private-registry-demo`. The logs should show the syft analysis for the `<private_image>` provided in the pod configuration.
|
||||
|
||||
Using the above information, users should be able to configure private registry access without having to do so in the `grype` or `syft` configuration files.
|
||||
They will also not be dependent on a docker daemon, (or some other runtime software) for registry configuration and access.
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration search paths:
|
||||
|
||||
+2
-3
@@ -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
@@ -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)
|
||||
|
||||
+54
-28
@@ -10,13 +10,15 @@ import (
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/anchore"
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/formats"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/ui"
|
||||
"github.com/anchore/syft/syft"
|
||||
"github.com/anchore/syft/syft/distro"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/syft/format"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/presenter/packages"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/pkg/profile"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -43,13 +45,13 @@ const (
|
||||
{{.appName}} {{.command}} oci-archive:path/to/yourimage.tar use a tarball from disk for OCI archives (from Skopeo or otherwise)
|
||||
{{.appName}} {{.command}} oci-dir:path/to/yourimage read directly from a path on disk for OCI layout directories (from Skopeo or otherwise)
|
||||
{{.appName}} {{.command}} dir:path/to/yourproject read directly from a path on disk (any directory)
|
||||
{{.appName}} {{.command}} file:path/to/yourproject/file read directly from a path on disk (any single file)
|
||||
{{.appName}} {{.command}} registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required)
|
||||
`
|
||||
)
|
||||
|
||||
var (
|
||||
packagesPresenterOpt packages.PresenterOption
|
||||
packagesArgs = cobra.MaximumNArgs(1)
|
||||
packagesPresenterOpt format.Option
|
||||
packagesCmd = &cobra.Command{
|
||||
Use: "packages [SOURCE]",
|
||||
Short: "Generate a package SBOM",
|
||||
@@ -58,21 +60,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 +103,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.AllOptions),
|
||||
)
|
||||
|
||||
flags.StringP(
|
||||
@@ -194,6 +188,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,15 +220,32 @@ 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() {
|
||||
defer close(errs)
|
||||
|
||||
f := formats.ByOption(packagesPresenterOpt)
|
||||
if f == nil {
|
||||
errs <- fmt.Errorf("unknown format: %s", packagesPresenterOpt)
|
||||
return
|
||||
}
|
||||
|
||||
checkForApplicationUpdate()
|
||||
|
||||
src, cleanup, err := source.New(userInput, appConfig.Registry.ToOptions())
|
||||
@@ -232,33 +255,37 @@ func packagesExecWorker(userInput string) <-chan error {
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
catalog, d, err := syft.CatalogPackages(src, appConfig.Package.Cataloger.ScopeOpt)
|
||||
catalog, relationships, d, err := syft.CatalogPackages(src, appConfig.Package.Cataloger.ScopeOpt)
|
||||
if err != nil {
|
||||
errs <- fmt.Errorf("failed to catalog input: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
if appConfig.Anchore.Host != "" {
|
||||
if err := runPackageSbomUpload(src, src.Metadata, catalog, d, appConfig.Package.Cataloger.ScopeOpt); err != nil {
|
||||
if err := runPackageSbomUpload(src, src.Metadata, catalog, d); err != nil {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
bus.Publish(partybus.Event{
|
||||
Type: event.PresenterReady,
|
||||
Value: packages.Presenter(packagesPresenterOpt, packages.PresenterConfig{
|
||||
SourceMetadata: src.Metadata,
|
||||
Catalog: catalog,
|
||||
sbomResult := sbom.SBOM{
|
||||
Artifacts: sbom.Artifacts{
|
||||
PackageCatalog: catalog,
|
||||
Distro: d,
|
||||
Scope: appConfig.Package.Cataloger.ScopeOpt,
|
||||
}),
|
||||
},
|
||||
Relationships: relationships,
|
||||
Source: src.Metadata,
|
||||
}
|
||||
|
||||
bus.Publish(partybus.Event{
|
||||
Type: event.PresenterReady,
|
||||
Value: f.Presenter(sbomResult),
|
||||
})
|
||||
}()
|
||||
return errs
|
||||
}
|
||||
|
||||
func runPackageSbomUpload(src *source.Source, s source.Metadata, catalog *pkg.Catalog, d *distro.Distro, scope source.Scope) error {
|
||||
func runPackageSbomUpload(src *source.Source, s source.Metadata, catalog *pkg.Catalog, d *distro.Distro) error {
|
||||
log.Infof("uploading results to %s", appConfig.Anchore.Host)
|
||||
|
||||
if src.Metadata.Scheme != source.ImageScheme {
|
||||
@@ -298,7 +325,6 @@ func runPackageSbomUpload(src *source.Source, s source.Metadata, catalog *pkg.Ca
|
||||
Distro: d,
|
||||
Dockerfile: dockerfileContents,
|
||||
OverwriteExistingUpload: appConfig.Anchore.OverwriteExistingImage,
|
||||
Scope: scope,
|
||||
Timeout: appConfig.Anchore.ImportTimeout,
|
||||
}
|
||||
|
||||
|
||||
+52
-25
@@ -4,6 +4,10 @@ import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
|
||||
"github.com/anchore/stereoscope"
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
@@ -35,19 +39,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,10 +87,9 @@ 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)...,
|
||||
)
|
||||
}
|
||||
|
||||
func powerUserExecWorker(userInput string) <-chan error {
|
||||
errs := make(chan error)
|
||||
go func() {
|
||||
@@ -115,29 +110,61 @@ func powerUserExecWorker(userInput string) <-chan error {
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
analysisResults := poweruser.JSONDocumentConfig{
|
||||
SourceMetadata: src.Metadata,
|
||||
ApplicationConfig: *appConfig,
|
||||
s := sbom.SBOM{
|
||||
Source: src.Metadata,
|
||||
}
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
var results []<-chan artifact.Relationship
|
||||
for _, task := range tasks {
|
||||
wg.Add(1)
|
||||
go func(task powerUserTask) {
|
||||
defer wg.Done()
|
||||
if err = task(&analysisResults, src); err != nil {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
}(task)
|
||||
c := make(chan artifact.Relationship)
|
||||
results = append(results, c)
|
||||
|
||||
go runTask(task, &s.Artifacts, src, c, errs)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
for relationship := range mergeResults(results...) {
|
||||
s.Relationships = append(s.Relationships, relationship)
|
||||
}
|
||||
|
||||
bus.Publish(partybus.Event{
|
||||
Type: event.PresenterReady,
|
||||
Value: poweruser.NewJSONPresenter(analysisResults),
|
||||
Value: poweruser.NewJSONPresenter(s, *appConfig),
|
||||
})
|
||||
}()
|
||||
return errs
|
||||
}
|
||||
|
||||
func runTask(t powerUserTask, a *sbom.Artifacts, src *source.Source, c chan<- artifact.Relationship, errs chan<- error) {
|
||||
defer close(c)
|
||||
|
||||
relationships, err := t(a, src)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
|
||||
for _, relationship := range relationships {
|
||||
c <- relationship
|
||||
}
|
||||
}
|
||||
|
||||
func mergeResults(cs ...<-chan artifact.Relationship) <-chan artifact.Relationship {
|
||||
var wg sync.WaitGroup
|
||||
var results = make(chan artifact.Relationship)
|
||||
|
||||
wg.Add(len(cs))
|
||||
for _, c := range cs {
|
||||
go func(c <-chan artifact.Relationship) {
|
||||
for n := range c {
|
||||
results <- n
|
||||
}
|
||||
wg.Done()
|
||||
}(c)
|
||||
}
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(results)
|
||||
}()
|
||||
return results
|
||||
}
|
||||
|
||||
+29
-26
@@ -4,13 +4,16 @@ import (
|
||||
"crypto"
|
||||
"fmt"
|
||||
|
||||
"github.com/anchore/syft/internal/presenter/poweruser"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
|
||||
"github.com/anchore/syft/syft"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
type powerUserTask func(*poweruser.JSONDocumentConfig, *source.Source) error
|
||||
type powerUserTask func(*sbom.Artifacts, *source.Source) ([]artifact.Relationship, error)
|
||||
|
||||
func powerUserTasks() ([]powerUserTask, error) {
|
||||
var tasks []powerUserTask
|
||||
@@ -42,16 +45,16 @@ func catalogPackagesTask() (powerUserTask, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
task := func(results *poweruser.JSONDocumentConfig, src *source.Source) error {
|
||||
packageCatalog, theDistro, err := syft.CatalogPackages(src, appConfig.Package.Cataloger.ScopeOpt)
|
||||
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
|
||||
packageCatalog, relationships, theDistro, err := syft.CatalogPackages(src, appConfig.Package.Cataloger.ScopeOpt)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
results.PackageCatalog = packageCatalog
|
||||
results.Distro = theDistro
|
||||
|
||||
return nil
|
||||
return relationships, nil
|
||||
}
|
||||
|
||||
return task, nil
|
||||
@@ -64,18 +67,18 @@ func catalogFileMetadataTask() (powerUserTask, error) {
|
||||
|
||||
metadataCataloger := file.NewMetadataCataloger()
|
||||
|
||||
task := func(results *poweruser.JSONDocumentConfig, src *source.Source) error {
|
||||
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
|
||||
resolver, err := src.FileResolver(appConfig.FileMetadata.Cataloger.ScopeOpt)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, err := metadataCataloger.Catalog(resolver)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
results.FileMetadata = result
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return task, nil
|
||||
@@ -110,18 +113,18 @@ func catalogFileDigestsTask() (powerUserTask, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
task := func(results *poweruser.JSONDocumentConfig, src *source.Source) error {
|
||||
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
|
||||
resolver, err := src.FileResolver(appConfig.FileMetadata.Cataloger.ScopeOpt)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, err := digestsCataloger.Catalog(resolver)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
results.FileDigests = result
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return task, nil
|
||||
@@ -142,18 +145,18 @@ func catalogSecretsTask() (powerUserTask, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
task := func(results *poweruser.JSONDocumentConfig, src *source.Source) error {
|
||||
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
|
||||
resolver, err := src.FileResolver(appConfig.Secrets.Cataloger.ScopeOpt)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, err := secretsCataloger.Catalog(resolver)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
results.Secrets = result
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return task, nil
|
||||
@@ -170,18 +173,18 @@ func catalogFileClassificationsTask() (powerUserTask, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
task := func(results *poweruser.JSONDocumentConfig, src *source.Source) error {
|
||||
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
|
||||
resolver, err := src.FileResolver(appConfig.FileClassification.Cataloger.ScopeOpt)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, err := classifierCataloger.Catalog(resolver)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
results.FileClassifications = result
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return task, nil
|
||||
@@ -197,18 +200,18 @@ func catalogContentsTask() (powerUserTask, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
task := func(results *poweruser.JSONDocumentConfig, src *source.Source) error {
|
||||
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
|
||||
resolver, err := src.FileResolver(appConfig.FileContents.Cataloger.ScopeOpt)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, err := contentsCataloger.Catalog(resolver)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
results.FileContents = result
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return task, nil
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -7,11 +7,12 @@ require (
|
||||
github.com/adrg/xdg v0.2.1
|
||||
github.com/alecthomas/jsonschema v0.0.0-20210301060011-54c507b6f074
|
||||
github.com/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf
|
||||
github.com/anchore/go-presenter v0.0.0-20211102174526-0dbf20f6c7fa
|
||||
github.com/anchore/go-rpmdb v0.0.0-20210914181456-a9c52348da63
|
||||
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
|
||||
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
|
||||
github.com/anchore/packageurl-go v0.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
|
||||
@@ -23,7 +24,9 @@ require (
|
||||
github.com/gookit/color v1.2.7
|
||||
github.com/hashicorp/go-multierror v1.1.0
|
||||
github.com/hashicorp/go-version v1.2.0
|
||||
github.com/jinzhu/copier v0.3.2
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2
|
||||
github.com/mitchellh/mapstructure v1.3.1
|
||||
github.com/olekukonko/tablewriter v0.0.4
|
||||
github.com/pelletier/go-toml v1.8.1
|
||||
|
||||
@@ -108,6 +108,8 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf h1:DYssiUV1pBmKqzKsm4mqXx8artqC0Q8HgZsVI3lMsAg=
|
||||
github.com/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf/go.mod h1:FaODhIA06mxO1E6R32JE0TL1JWZZkmjRIAd4ULvHUKk=
|
||||
github.com/anchore/go-presenter v0.0.0-20211102174526-0dbf20f6c7fa h1:mDLUAkgXsV5Z8D0EEj8eS6FBekolV/A+Xxbs9054bPw=
|
||||
github.com/anchore/go-presenter v0.0.0-20211102174526-0dbf20f6c7fa/go.mod h1:29jwxTSAS6pBcrmuwf1U3r1Tqp1o1XpuiOJ0NT9NoGg=
|
||||
github.com/anchore/go-rpmdb v0.0.0-20210914181456-a9c52348da63 h1:C9W/LAydEz/qdUhx1MdjO9l8NEcFKYknkxDVyo9LAoM=
|
||||
github.com/anchore/go-rpmdb v0.0.0-20210914181456-a9c52348da63/go.mod h1:6qH8c6U/3CBVvDDDBZnPSTbTINq3cIdADUYTaVf75EM=
|
||||
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 h1:VzprUTpc0vW0nnNKJfJieyH/TZ9UYAnTZs5/gHTdAe8=
|
||||
@@ -116,8 +118,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 +149,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=
|
||||
@@ -450,6 +454,8 @@ github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NH
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jarcoal/httpmock v1.0.5/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
|
||||
github.com/jingyugao/rowserrcheck v0.0.0-20191204022205-72ab7603b68a/go.mod h1:xRskid8CManxVta/ALEhJha/pweKBaVG6fWgc0yH25s=
|
||||
github.com/jinzhu/copier v0.3.2 h1:QdBOCbaouLDYaIPFfi1bKv5F5tPpeTwXe4sD0jqtz5w=
|
||||
github.com/jinzhu/copier v0.3.2/go.mod h1:24xnZezI2Yqac9J61UC6/dG/k76ttpq0DdJI3QmUvro=
|
||||
github.com/jirfag/go-printf-func-name v0.0.0-20191110105641-45db9963cdd3/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0=
|
||||
github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
@@ -540,6 +546,8 @@ github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk
|
||||
github.com/mitchellh/go-ps v0.0.0-20190716172923-621e5597135b/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
||||
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
|
||||
@@ -26,7 +26,6 @@ type ImportConfig struct {
|
||||
Distro *distro.Distro
|
||||
Dockerfile []byte
|
||||
OverwriteExistingUpload bool
|
||||
Scope source.Scope
|
||||
Timeout uint
|
||||
}
|
||||
|
||||
@@ -74,7 +73,7 @@ func (c *Client) Import(ctx context.Context, cfg ImportConfig) error {
|
||||
prog.N++
|
||||
sessionID := startOperation.Uuid
|
||||
|
||||
packageDigest, err := importPackageSBOM(authedCtx, c.client.ImportsApi, sessionID, cfg.SourceMetadata, cfg.Catalog, cfg.Distro, cfg.Scope, stage)
|
||||
packageDigest, err := importPackageSBOM(authedCtx, c.client.ImportsApi, sessionID, cfg.SourceMetadata, cfg.Catalog, cfg.Distro, stage)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to import Package SBOM: %w", err)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/anchore/syft/internal/presenter/packages"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
|
||||
"github.com/anchore/syft/internal/formats/syftjson"
|
||||
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
@@ -24,10 +26,19 @@ type packageSBOMImportAPI interface {
|
||||
ImportImagePackages(context.Context, string, external.ImagePackageManifest) (external.ImageImportContentResponse, *http.Response, error)
|
||||
}
|
||||
|
||||
func packageSbomModel(s source.Metadata, catalog *pkg.Catalog, d *distro.Distro, scope source.Scope) (*external.ImagePackageManifest, error) {
|
||||
func packageSbomModel(srcMetadata source.Metadata, catalog *pkg.Catalog, d *distro.Distro) (*external.ImagePackageManifest, error) {
|
||||
var buf bytes.Buffer
|
||||
pres := packages.NewJSONPresenter(catalog, s, d, scope)
|
||||
err := pres.Present(&buf)
|
||||
|
||||
// TODO: once the top-level API is refactored and SBOMs are the unit of work, then this function will be passed an SBOM and there would be no more need to create an SBOM object here.
|
||||
s := sbom.SBOM{
|
||||
Artifacts: sbom.Artifacts{
|
||||
PackageCatalog: catalog,
|
||||
Distro: d,
|
||||
},
|
||||
Source: srcMetadata,
|
||||
}
|
||||
|
||||
err := syftjson.Format().Presenter(s).Present(&buf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to serialize results: %w", err)
|
||||
}
|
||||
@@ -41,11 +52,11 @@ func packageSbomModel(s source.Metadata, catalog *pkg.Catalog, d *distro.Distro,
|
||||
return &model, nil
|
||||
}
|
||||
|
||||
func importPackageSBOM(ctx context.Context, api packageSBOMImportAPI, sessionID string, s source.Metadata, catalog *pkg.Catalog, d *distro.Distro, scope source.Scope, stage *progress.Stage) (string, error) {
|
||||
func importPackageSBOM(ctx context.Context, api packageSBOMImportAPI, sessionID string, s source.Metadata, catalog *pkg.Catalog, d *distro.Distro, stage *progress.Stage) (string, error) {
|
||||
log.Debug("importing package SBOM")
|
||||
stage.Current = "package SBOM"
|
||||
|
||||
model, err := packageSbomModel(s, catalog, d, scope)
|
||||
model, err := packageSbomModel(s, catalog, d)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to create PackageSBOM model: %w", err)
|
||||
}
|
||||
|
||||
@@ -9,18 +9,17 @@ 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/syft/syft/sbom"
|
||||
|
||||
"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 {
|
||||
@@ -75,7 +74,7 @@ func TestPackageSbomToModel(t *testing.T) {
|
||||
|
||||
c := pkg.NewCatalog(p)
|
||||
|
||||
model, err := packageSbomModel(m, c, &d, source.AllLayersScope)
|
||||
model, err := packageSbomModel(m, c, &d)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to generate model from source material: %+v", err)
|
||||
}
|
||||
@@ -87,20 +86,28 @@ func TestPackageSbomToModel(t *testing.T) {
|
||||
t.Fatalf("unable to marshal model: %+v", err)
|
||||
}
|
||||
|
||||
s := sbom.SBOM{
|
||||
Artifacts: sbom.Artifacts{
|
||||
PackageCatalog: c,
|
||||
Distro: &d,
|
||||
},
|
||||
Source: m,
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
pres := packages.NewJSONPresenter(c, m, &d, source.AllLayersScope)
|
||||
pres := syftjson.Format().Presenter(s)
|
||||
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)
|
||||
}
|
||||
@@ -190,7 +197,7 @@ func TestPackageSbomImport(t *testing.T) {
|
||||
|
||||
d, _ := distro.NewDistro(distro.CentOS, "8.0", "")
|
||||
|
||||
theModel, err := packageSbomModel(m, catalog, &d, source.AllLayersScope)
|
||||
theModel, err := packageSbomModel(m, catalog, &d)
|
||||
if err != nil {
|
||||
t.Fatalf("could not get sbom model: %+v", err)
|
||||
}
|
||||
@@ -229,7 +236,7 @@ func TestPackageSbomImport(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
|
||||
digest, err := importPackageSBOM(context.TODO(), test.api, sessionID, m, catalog, &d, source.AllLayersScope, &progress.Stage{})
|
||||
digest, err := importPackageSBOM(context.TODO(), test.api, sessionID, m, catalog, &d, &progress.Stage{})
|
||||
|
||||
// validate error handling
|
||||
if err != nil && !test.expectsError {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package spdxhelpers
|
||||
|
||||
import "github.com/anchore/syft/syft/pkg"
|
||||
|
||||
func Description(p pkg.Package) string {
|
||||
if hasMetadata(p) {
|
||||
switch metadata := p.Metadata.(type) {
|
||||
case pkg.ApkMetadata:
|
||||
return metadata.Description
|
||||
case pkg.NpmPackageJSONMetadata:
|
||||
return metadata.Description
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func hasMetadata(p pkg.Package) bool {
|
||||
return p.Metadata != nil
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package spdxhelpers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Description(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input pkg.Package
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
// note: since this is an optional field, no value is preferred over NONE or NOASSERTION
|
||||
name: "no metadata",
|
||||
input: pkg.Package{},
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "from apk",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.ApkMetadata{
|
||||
Description: "a description!",
|
||||
},
|
||||
},
|
||||
expected: "a description!",
|
||||
},
|
||||
{
|
||||
name: "from npm",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.NpmPackageJSONMetadata{
|
||||
Description: "a description!",
|
||||
},
|
||||
},
|
||||
expected: "a description!",
|
||||
},
|
||||
{
|
||||
// note: since this is an optional field, no value is preferred over NONE or NOASSERTION
|
||||
name: "empty",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.NpmPackageJSONMetadata{
|
||||
Homepage: "",
|
||||
},
|
||||
},
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, Description(test.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package spdxhelpers
|
||||
|
||||
import (
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func DocumentName(srcMetadata source.Metadata) string {
|
||||
switch srcMetadata.Scheme {
|
||||
case source.ImageScheme:
|
||||
return cleanSPDXName(srcMetadata.ImageMetadata.UserInput)
|
||||
case source.DirectoryScheme:
|
||||
return cleanSPDXName(srcMetadata.Path)
|
||||
}
|
||||
|
||||
// TODO: is this alright?
|
||||
return uuid.Must(uuid.NewRandom()).String()
|
||||
}
|
||||
|
||||
func cleanSPDXName(name string) string {
|
||||
// remove # according to specification
|
||||
name = strings.ReplaceAll(name, "#", "-")
|
||||
|
||||
// remove : for url construction
|
||||
name = strings.ReplaceAll(name, ":", "-")
|
||||
|
||||
// clean relative pathing
|
||||
return path.Clean(name)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package spdxhelpers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const SyftDocumentNamespace = "https://anchore.com/syft"
|
||||
|
||||
func DocumentNamespace(name string, srcMetadata source.Metadata) string {
|
||||
input := "unknown-source-type"
|
||||
switch srcMetadata.Scheme {
|
||||
case source.ImageScheme:
|
||||
input = "image"
|
||||
case source.DirectoryScheme:
|
||||
input = "dir"
|
||||
}
|
||||
|
||||
uniqueID := uuid.Must(uuid.NewRandom())
|
||||
identifier := path.Join(input, uniqueID.String())
|
||||
if name != "." {
|
||||
identifier = path.Join(input, fmt.Sprintf("%s-%s", name, uniqueID.String()))
|
||||
}
|
||||
|
||||
return path.Join(SyftDocumentNamespace, identifier)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package spdxhelpers
|
||||
|
||||
import "github.com/anchore/syft/syft/pkg"
|
||||
|
||||
func DownloadLocation(p pkg.Package) string {
|
||||
// 3.7: Package Download Location
|
||||
// Cardinality: mandatory, one
|
||||
// NONE if there is no download location whatsoever.
|
||||
// NOASSERTION if:
|
||||
// (i) the SPDX file creator has attempted to but cannot reach a reasonable objective determination;
|
||||
// (ii) the SPDX file creator has made no attempt to determine this field; or
|
||||
// (iii) the SPDX file creator has intentionally provided no information (no meaning should be implied by doing so).
|
||||
|
||||
if hasMetadata(p) {
|
||||
switch metadata := p.Metadata.(type) {
|
||||
case pkg.ApkMetadata:
|
||||
return NoneIfEmpty(metadata.URL)
|
||||
case pkg.NpmPackageJSONMetadata:
|
||||
return NoneIfEmpty(metadata.URL)
|
||||
}
|
||||
}
|
||||
return "NOASSERTION"
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package spdxhelpers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_DownloadLocation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input pkg.Package
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "no metadata",
|
||||
input: pkg.Package{},
|
||||
expected: "NOASSERTION",
|
||||
},
|
||||
{
|
||||
name: "from apk",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.ApkMetadata{
|
||||
URL: "http://a-place.gov",
|
||||
},
|
||||
},
|
||||
expected: "http://a-place.gov",
|
||||
},
|
||||
{
|
||||
name: "from npm",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.NpmPackageJSONMetadata{
|
||||
URL: "http://a-place.gov",
|
||||
},
|
||||
},
|
||||
expected: "http://a-place.gov",
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.NpmPackageJSONMetadata{
|
||||
URL: "",
|
||||
},
|
||||
},
|
||||
expected: "NONE",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, DownloadLocation(test.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package spdxhelpers
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/internal/formats/spdx22json/model"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func ExternalRefs(p pkg.Package) (externalRefs []model.ExternalRef) {
|
||||
externalRefs = make([]model.ExternalRef, 0)
|
||||
|
||||
for _, c := range p.CPEs {
|
||||
externalRefs = append(externalRefs, model.ExternalRef{
|
||||
ReferenceCategory: model.SecurityReferenceCategory,
|
||||
ReferenceLocator: c.BindToFmtString(),
|
||||
ReferenceType: model.Cpe23ExternalRefType,
|
||||
})
|
||||
}
|
||||
|
||||
if p.PURL != "" {
|
||||
externalRefs = append(externalRefs, model.ExternalRef{
|
||||
ReferenceCategory: model.PackageManagerReferenceCategory,
|
||||
ReferenceLocator: p.PURL,
|
||||
ReferenceType: model.PurlExternalRefType,
|
||||
})
|
||||
}
|
||||
return externalRefs
|
||||
}
|
||||
|
||||
func ExtractPURL(refs []model.ExternalRef) string {
|
||||
for _, r := range refs {
|
||||
if r.ReferenceType == model.PurlExternalRefType {
|
||||
return r.ReferenceLocator
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func ExtractCPEs(refs []model.ExternalRef) (cpes []pkg.CPE) {
|
||||
for _, r := range refs {
|
||||
if r.ReferenceType == model.Cpe23ExternalRefType {
|
||||
cpe, err := pkg.NewCPE(r.ReferenceLocator)
|
||||
if err != nil {
|
||||
log.Warnf("unable to extract SPDX CPE=%q: %+v", r.ReferenceLocator, err)
|
||||
continue
|
||||
}
|
||||
cpes = append(cpes, cpe)
|
||||
}
|
||||
}
|
||||
return cpes
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package spdxhelpers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/syft/internal/formats/spdx22json/model"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_ExternalRefs(t *testing.T) {
|
||||
testCPE := pkg.MustCPE("cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*")
|
||||
tests := []struct {
|
||||
name string
|
||||
input pkg.Package
|
||||
expected []model.ExternalRef
|
||||
}{
|
||||
{
|
||||
name: "cpe + purl",
|
||||
input: pkg.Package{
|
||||
CPEs: []pkg.CPE{
|
||||
testCPE,
|
||||
},
|
||||
PURL: "a-purl",
|
||||
},
|
||||
expected: []model.ExternalRef{
|
||||
{
|
||||
ReferenceCategory: model.SecurityReferenceCategory,
|
||||
ReferenceLocator: testCPE.BindToFmtString(),
|
||||
ReferenceType: model.Cpe23ExternalRefType,
|
||||
},
|
||||
{
|
||||
ReferenceCategory: model.PackageManagerReferenceCategory,
|
||||
ReferenceLocator: "a-purl",
|
||||
ReferenceType: model.PurlExternalRefType,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.ElementsMatch(t, test.expected, ExternalRefs(test.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package spdxhelpers
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/anchore/syft/internal/formats/spdx22json/model"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func Files(packageSpdxID string, p pkg.Package) (files []model.File, fileIDs []string, relationships []model.Relationship) {
|
||||
files = make([]model.File, 0)
|
||||
fileIDs = make([]string, 0)
|
||||
relationships = make([]model.Relationship, 0)
|
||||
|
||||
if !hasMetadata(p) {
|
||||
return files, fileIDs, relationships
|
||||
}
|
||||
|
||||
pkgFileOwner, ok := p.Metadata.(pkg.FileOwner)
|
||||
if !ok {
|
||||
return files, fileIDs, relationships
|
||||
}
|
||||
|
||||
for _, ownedFilePath := range pkgFileOwner.OwnedFiles() {
|
||||
baseFileName := filepath.Base(ownedFilePath)
|
||||
pathHash := sha256.Sum256([]byte(ownedFilePath))
|
||||
fileSpdxID := model.ElementID(fmt.Sprintf("File-%s-%x", p.Name, pathHash)).String()
|
||||
|
||||
fileIDs = append(fileIDs, fileSpdxID)
|
||||
|
||||
files = append(files, model.File{
|
||||
FileName: ownedFilePath,
|
||||
Item: model.Item{
|
||||
Element: model.Element{
|
||||
SPDXID: fileSpdxID,
|
||||
Name: baseFileName,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
relationships = append(relationships, model.Relationship{
|
||||
SpdxElementID: packageSpdxID,
|
||||
RelationshipType: model.ContainsRelationship,
|
||||
RelatedSpdxElement: fileSpdxID,
|
||||
})
|
||||
}
|
||||
|
||||
return files, fileIDs, relationships
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package spdxhelpers
|
||||
|
||||
import "github.com/anchore/syft/syft/pkg"
|
||||
|
||||
func Homepage(p pkg.Package) string {
|
||||
if hasMetadata(p) {
|
||||
switch metadata := p.Metadata.(type) {
|
||||
case pkg.GemMetadata:
|
||||
return metadata.Homepage
|
||||
case pkg.NpmPackageJSONMetadata:
|
||||
return metadata.Homepage
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package spdxhelpers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Homepage(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input pkg.Package
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
// note: since this is an optional field, no value is preferred over NONE or NOASSERTION
|
||||
name: "no metadata",
|
||||
input: pkg.Package{},
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "from gem",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.GemMetadata{
|
||||
Homepage: "http://a-place.gov",
|
||||
},
|
||||
},
|
||||
expected: "http://a-place.gov",
|
||||
},
|
||||
{
|
||||
name: "from npm",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.NpmPackageJSONMetadata{
|
||||
Homepage: "http://a-place.gov",
|
||||
},
|
||||
},
|
||||
expected: "http://a-place.gov",
|
||||
},
|
||||
{
|
||||
// note: since this is an optional field, no value is preferred over NONE or NOASSERTION
|
||||
name: "empty",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.NpmPackageJSONMetadata{
|
||||
Homepage: "",
|
||||
},
|
||||
},
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, Homepage(test.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package spdxhelpers
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/internal/spdxlicense"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func License(p pkg.Package) string {
|
||||
// source: https://spdx.github.io/spdx-spec/3-package-information/#313-concluded-license
|
||||
// The options to populate this field are limited to:
|
||||
// A valid SPDX License Expression as defined in Appendix IV;
|
||||
// NONE, if the SPDX file creator concludes there is no license available for this package; or
|
||||
// NOASSERTION if:
|
||||
// (i) the SPDX file creator has attempted to but cannot reach a reasonable objective determination;
|
||||
// (ii) the SPDX file creator has made no attempt to determine this field; or
|
||||
// (iii) the SPDX file creator has intentionally provided no information (no meaning should be implied by doing so).
|
||||
|
||||
if len(p.Licenses) == 0 {
|
||||
return "NONE"
|
||||
}
|
||||
|
||||
// take all licenses and assume an AND expression; for information about license expressions see https://spdx.github.io/spdx-spec/appendix-IV-SPDX-license-expressions/
|
||||
var parsedLicenses []string
|
||||
for _, l := range p.Licenses {
|
||||
if value, exists := spdxlicense.ID(l); exists {
|
||||
parsedLicenses = append(parsedLicenses, value)
|
||||
}
|
||||
}
|
||||
|
||||
if len(parsedLicenses) == 0 {
|
||||
return "NOASSERTION"
|
||||
}
|
||||
|
||||
return strings.Join(parsedLicenses, " AND ")
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package spdxhelpers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_License(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input pkg.Package
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "no licenses",
|
||||
input: pkg.Package{},
|
||||
expected: "NONE",
|
||||
},
|
||||
{
|
||||
name: "no SPDX licenses",
|
||||
input: pkg.Package{
|
||||
Licenses: []string{
|
||||
"made-up",
|
||||
},
|
||||
},
|
||||
expected: "NOASSERTION",
|
||||
},
|
||||
{
|
||||
name: "with SPDX license",
|
||||
input: pkg.Package{
|
||||
Licenses: []string{
|
||||
"MIT",
|
||||
},
|
||||
},
|
||||
expected: "MIT",
|
||||
},
|
||||
{
|
||||
name: "with SPDX license expression",
|
||||
input: pkg.Package{
|
||||
Licenses: []string{
|
||||
"MIT",
|
||||
"GPL-3.0",
|
||||
},
|
||||
},
|
||||
expected: "MIT AND GPL-3.0",
|
||||
},
|
||||
{
|
||||
name: "cap insensitive",
|
||||
input: pkg.Package{
|
||||
Licenses: []string{
|
||||
"gpl-3.0",
|
||||
},
|
||||
},
|
||||
expected: "GPL-3.0",
|
||||
},
|
||||
{
|
||||
name: "debian to spdx conversion",
|
||||
input: pkg.Package{
|
||||
Licenses: []string{
|
||||
"GPL-2",
|
||||
},
|
||||
},
|
||||
expected: "GPL-2.0",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, License(test.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package spdxhelpers
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func NoneIfEmpty(value string) string {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return "NONE"
|
||||
}
|
||||
return value
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package spdxhelpers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_noneIfEmpty(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "non-zero value",
|
||||
value: "something",
|
||||
expected: "something",
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
value: "",
|
||||
expected: "NONE",
|
||||
},
|
||||
{
|
||||
name: "space",
|
||||
value: " ",
|
||||
expected: "NONE",
|
||||
},
|
||||
{
|
||||
name: "tab",
|
||||
value: "\t",
|
||||
expected: "NONE",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, NoneIfEmpty(test.value))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package spdxhelpers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Originator(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input pkg.Package
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
// note: since this is an optional field, no value is preferred over NONE or NOASSERTION
|
||||
name: "no metadata",
|
||||
input: pkg.Package{},
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "from gem",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.GemMetadata{
|
||||
Authors: []string{
|
||||
"auth1",
|
||||
"auth2",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: "auth1",
|
||||
},
|
||||
{
|
||||
name: "from npm",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.NpmPackageJSONMetadata{
|
||||
Author: "auth",
|
||||
},
|
||||
},
|
||||
expected: "auth",
|
||||
},
|
||||
{
|
||||
name: "from apk",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.ApkMetadata{
|
||||
Maintainer: "auth",
|
||||
},
|
||||
},
|
||||
expected: "auth",
|
||||
},
|
||||
{
|
||||
name: "from python - just name",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.PythonPackageMetadata{
|
||||
Author: "auth",
|
||||
},
|
||||
},
|
||||
expected: "auth",
|
||||
},
|
||||
{
|
||||
name: "from python - just email",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.PythonPackageMetadata{
|
||||
AuthorEmail: "auth@auth.gov",
|
||||
},
|
||||
},
|
||||
expected: "auth@auth.gov",
|
||||
},
|
||||
{
|
||||
name: "from python - both name and email",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.PythonPackageMetadata{
|
||||
Author: "auth",
|
||||
AuthorEmail: "auth@auth.gov",
|
||||
},
|
||||
},
|
||||
expected: "auth <auth@auth.gov>",
|
||||
},
|
||||
{
|
||||
name: "from rpm",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.RpmdbMetadata{
|
||||
Vendor: "auth",
|
||||
},
|
||||
},
|
||||
expected: "auth",
|
||||
},
|
||||
{
|
||||
name: "from dpkg",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.DpkgMetadata{
|
||||
Maintainer: "auth",
|
||||
},
|
||||
},
|
||||
expected: "auth",
|
||||
},
|
||||
{
|
||||
// note: since this is an optional field, no value is preferred over NONE or NOASSERTION
|
||||
name: "empty",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.NpmPackageJSONMetadata{
|
||||
Author: "",
|
||||
},
|
||||
},
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, Originator(test.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package spdxhelpers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func Originator(p pkg.Package) string {
|
||||
if hasMetadata(p) {
|
||||
switch metadata := p.Metadata.(type) {
|
||||
case pkg.ApkMetadata:
|
||||
return metadata.Maintainer
|
||||
case pkg.NpmPackageJSONMetadata:
|
||||
return metadata.Author
|
||||
case pkg.PythonPackageMetadata:
|
||||
author := metadata.Author
|
||||
if author == "" {
|
||||
return metadata.AuthorEmail
|
||||
}
|
||||
if metadata.AuthorEmail != "" {
|
||||
author += fmt.Sprintf(" <%s>", metadata.AuthorEmail)
|
||||
}
|
||||
return author
|
||||
case pkg.GemMetadata:
|
||||
if len(metadata.Authors) > 0 {
|
||||
return metadata.Authors[0]
|
||||
}
|
||||
return ""
|
||||
case pkg.RpmdbMetadata:
|
||||
return metadata.Vendor
|
||||
case pkg.DpkgMetadata:
|
||||
return metadata.Maintainer
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package spdxhelpers
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func SourceInfo(p pkg.Package) string {
|
||||
answer := ""
|
||||
switch p.Type {
|
||||
case pkg.RpmPkg:
|
||||
answer = "acquired package info from RPM DB"
|
||||
case pkg.ApkPkg:
|
||||
answer = "acquired package info from APK DB"
|
||||
case pkg.DebPkg:
|
||||
answer = "acquired package info from DPKG DB"
|
||||
case pkg.NpmPkg:
|
||||
answer = "acquired package info from installed node module manifest file"
|
||||
case pkg.PythonPkg:
|
||||
answer = "acquired package info from installed python package manifest file"
|
||||
case pkg.JavaPkg, pkg.JenkinsPluginPkg:
|
||||
answer = "acquired package info from installed java archive"
|
||||
case pkg.GemPkg:
|
||||
answer = "acquired package info from installed gem metadata file"
|
||||
case pkg.GoModulePkg:
|
||||
answer = "acquired package info from go module information"
|
||||
case pkg.RustPkg:
|
||||
answer = "acquired package info from rust cargo manifest"
|
||||
default:
|
||||
answer = "acquired package info from the following paths"
|
||||
}
|
||||
var paths []string
|
||||
for _, l := range p.Locations {
|
||||
paths = append(paths, l.RealPath)
|
||||
}
|
||||
|
||||
return answer + ": " + strings.Join(paths, ", ")
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package spdxhelpers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_SourceInfo(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input pkg.Package
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "locations are captured",
|
||||
input: pkg.Package{
|
||||
// note: no type given
|
||||
Locations: []source.Location{
|
||||
{
|
||||
RealPath: "/a-place",
|
||||
VirtualPath: "/b-place",
|
||||
},
|
||||
{
|
||||
RealPath: "/c-place",
|
||||
VirtualPath: "/d-place",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []string{
|
||||
"from the following paths",
|
||||
"/a-place",
|
||||
"/c-place",
|
||||
},
|
||||
},
|
||||
{
|
||||
// note: no specific support for this
|
||||
input: pkg.Package{
|
||||
Type: pkg.KbPkg,
|
||||
},
|
||||
expected: []string{
|
||||
"from the following paths",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: pkg.Package{
|
||||
Type: pkg.RpmPkg,
|
||||
},
|
||||
expected: []string{
|
||||
"from RPM DB",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: pkg.Package{
|
||||
Type: pkg.ApkPkg,
|
||||
},
|
||||
expected: []string{
|
||||
"from APK DB",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: pkg.Package{
|
||||
Type: pkg.DebPkg,
|
||||
},
|
||||
expected: []string{
|
||||
"from DPKG DB",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: pkg.Package{
|
||||
Type: pkg.NpmPkg,
|
||||
},
|
||||
expected: []string{
|
||||
"from installed node module manifest file",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: pkg.Package{
|
||||
Type: pkg.PythonPkg,
|
||||
},
|
||||
expected: []string{
|
||||
"from installed python package manifest file",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: pkg.Package{
|
||||
Type: pkg.JavaPkg,
|
||||
},
|
||||
expected: []string{
|
||||
"from installed java archive",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: pkg.Package{
|
||||
Type: pkg.JenkinsPluginPkg,
|
||||
},
|
||||
expected: []string{
|
||||
"from installed java archive",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: pkg.Package{
|
||||
Type: pkg.GemPkg,
|
||||
},
|
||||
expected: []string{
|
||||
"from installed gem metadata file",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: pkg.Package{
|
||||
Type: pkg.GoModulePkg,
|
||||
},
|
||||
expected: []string{
|
||||
"from go module information",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: pkg.Package{
|
||||
Type: pkg.RustPkg,
|
||||
},
|
||||
expected: []string{
|
||||
"from rust cargo manifest",
|
||||
},
|
||||
},
|
||||
}
|
||||
var pkgTypes []pkg.Type
|
||||
for _, test := range tests {
|
||||
t.Run(test.name+" "+string(test.input.Type), func(t *testing.T) {
|
||||
if test.input.Type != "" {
|
||||
pkgTypes = append(pkgTypes, test.input.Type)
|
||||
}
|
||||
actual := SourceInfo(test.input)
|
||||
for _, expected := range test.expected {
|
||||
assert.Contains(t, actual, expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
assert.ElementsMatch(t, pkg.AllPkgs, pkgTypes, "missing one or more package types to test against (maybe a package type was added?)")
|
||||
}
|
||||
+72
-29
@@ -1,15 +1,17 @@
|
||||
package packages
|
||||
package testutils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/go-presenter"
|
||||
"github.com/anchore/go-testutils"
|
||||
"github.com/anchore/stereoscope/pkg/filetree"
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
"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/sbom"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/sergi/go-diff/diffmatchpatch"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -17,7 +19,19 @@ import (
|
||||
|
||||
type redactor func(s []byte) []byte
|
||||
|
||||
func assertPresenterAgainstGoldenImageSnapshot(t *testing.T, pres presenter.Presenter, testImage string, updateSnapshot bool, redactors ...redactor) {
|
||||
type imageCfg struct {
|
||||
fromSnapshot bool
|
||||
}
|
||||
|
||||
type ImageOption func(cfg *imageCfg)
|
||||
|
||||
func FromSnapshot() ImageOption {
|
||||
return func(cfg *imageCfg) {
|
||||
cfg.fromSnapshot = true
|
||||
}
|
||||
}
|
||||
|
||||
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 +64,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,17 +91,48 @@ 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, options ...ImageOption) sbom.SBOM {
|
||||
t.Helper()
|
||||
catalog := pkg.NewCatalog()
|
||||
img := imagetest.GetGoldenFixtureImage(t, testImage)
|
||||
var cfg imageCfg
|
||||
var img *image.Image
|
||||
for _, opt := range options {
|
||||
opt(&cfg)
|
||||
}
|
||||
|
||||
switch cfg.fromSnapshot {
|
||||
case true:
|
||||
img = imagetest.GetGoldenFixtureImage(t, testImage)
|
||||
default:
|
||||
img = imagetest.GetFixtureImage(t, "docker-archive", testImage)
|
||||
}
|
||||
|
||||
populateImageCatalog(catalog, img)
|
||||
|
||||
// this is a hard coded value that is not given by the fixture helper and must be provided manually
|
||||
img.Metadata.ManifestDigest = "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368"
|
||||
|
||||
src, err := source.NewFromImage(img, "user-image-input")
|
||||
assert.NoError(t, err)
|
||||
|
||||
dist, err := distro.NewDistro(distro.Debian, "1.2.3", "like!")
|
||||
assert.NoError(t, err)
|
||||
|
||||
return sbom.SBOM{
|
||||
Artifacts: sbom.Artifacts{
|
||||
PackageCatalog: catalog,
|
||||
Distro: &dist,
|
||||
},
|
||||
Source: src.Metadata,
|
||||
}
|
||||
}
|
||||
|
||||
func populateImageCatalog(catalog *pkg.Catalog, img *image.Image) {
|
||||
_, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks)
|
||||
_, ref2, _ := img.SquashedTree().File("/somefile-2.txt", filetree.FollowBasenameLinks)
|
||||
|
||||
// populate catalog with test data
|
||||
catalog.Add(pkg.Package{
|
||||
ID: "package-1-id",
|
||||
Name: "package-1",
|
||||
Version: "1.0.1",
|
||||
Locations: []source.Location{
|
||||
@@ -104,11 +149,10 @@ 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{
|
||||
ID: "package-2-id",
|
||||
Name: "package-2",
|
||||
Version: "2.0.1",
|
||||
Locations: []source.Location{
|
||||
@@ -123,28 +167,34 @@ 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:*:*:*:*:*:*:*"),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// this is a hard coded value that is not given by the fixture helper and must be provided manually
|
||||
img.Metadata.ManifestDigest = "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368"
|
||||
|
||||
src, err := source.NewFromImage(img, "user-image-input")
|
||||
assert.NoError(t, err)
|
||||
func DirectoryInput(t testing.TB) sbom.SBOM {
|
||||
catalog := newDirectoryCatalog()
|
||||
|
||||
dist, err := distro.NewDistro(distro.Debian, "1.2.3", "like!")
|
||||
assert.NoError(t, err)
|
||||
|
||||
return catalog, src.Metadata, &dist
|
||||
src, err := source.NewFromDirectory("/some/path")
|
||||
assert.NoError(t, err)
|
||||
|
||||
return sbom.SBOM{
|
||||
Artifacts: sbom.Artifacts{
|
||||
PackageCatalog: catalog,
|
||||
Distro: &dist,
|
||||
},
|
||||
Source: src.Metadata,
|
||||
}
|
||||
}
|
||||
|
||||
func presenterDirectoryInput(t testing.TB) (*pkg.Catalog, source.Metadata, *distro.Distro) {
|
||||
func newDirectoryCatalog() *pkg.Catalog {
|
||||
catalog := pkg.NewCatalog()
|
||||
|
||||
// populate catalog with test data
|
||||
catalog.Add(pkg.Package{
|
||||
ID: "package-1-id",
|
||||
Name: "package-1",
|
||||
Version: "1.0.1",
|
||||
Type: pkg.PythonPkg,
|
||||
@@ -160,17 +210,16 @@ 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{
|
||||
ID: "package-2-id",
|
||||
Name: "package-2",
|
||||
Version: "2.0.1",
|
||||
Type: pkg.DebPkg,
|
||||
@@ -185,15 +234,9 @@ 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:*:*:*:*:*:*:*"),
|
||||
},
|
||||
})
|
||||
|
||||
dist, err := distro.NewDistro(distro.Debian, "1.2.3", "like!")
|
||||
assert.NoError(t, err)
|
||||
|
||||
src, err := source.NewFromDirectory("/some/path")
|
||||
assert.NoError(t, err)
|
||||
|
||||
return catalog, src.Metadata, &dist
|
||||
return catalog
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package cyclonedx12xml
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"io"
|
||||
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
func encoder(output io.Writer, s sbom.SBOM) error {
|
||||
enc := xml.NewEncoder(output)
|
||||
enc.Indent("", " ")
|
||||
|
||||
_, err := output.Write([]byte(xml.Header))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = enc.Encode(toFormatModel(s))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = output.Write([]byte("\n"))
|
||||
return err
|
||||
}
|
||||
+7
-7
@@ -1,17 +1,18 @@
|
||||
package packages
|
||||
package cyclonedx12xml
|
||||
|
||||
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,
|
||||
NewCycloneDxPresenter(catalog, metadata),
|
||||
testutils.AssertPresenterAgainstGoldenSnapshot(t,
|
||||
Format().Presenter(testutils.DirectoryInput(t)),
|
||||
*updateCycloneDx,
|
||||
cycloneDxRedactor,
|
||||
)
|
||||
@@ -19,9 +20,8 @@ func TestCycloneDxDirectoryPresenter(t *testing.T) {
|
||||
|
||||
func TestCycloneDxImagePresenter(t *testing.T) {
|
||||
testImage := "image-simple"
|
||||
catalog, metadata, _ := presenterImageInput(t, testImage)
|
||||
assertPresenterAgainstGoldenImageSnapshot(t,
|
||||
NewCycloneDxPresenter(catalog, metadata),
|
||||
testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
|
||||
Format().Presenter(testutils.ImageInput(t, testImage)),
|
||||
testImage,
|
||||
*updateCycloneDx,
|
||||
cycloneDxRedactor,
|
||||
@@ -0,0 +1,12 @@
|
||||
package cyclonedx12xml
|
||||
|
||||
import "github.com/anchore/syft/syft/format"
|
||||
|
||||
func Format() format.Format {
|
||||
return format.NewFormat(
|
||||
format.CycloneDxOption,
|
||||
encoder,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
// Source: https://cyclonedx.org/ext/bom-descriptor/
|
||||
|
||||
// BomDescriptor represents all metadata surrounding the BOM report (such as when the BOM was made, with which tool, and the item being cataloged).
|
||||
type BomDescriptor struct {
|
||||
XMLName xml.Name `xml:"metadata"`
|
||||
Timestamp string `xml:"timestamp,omitempty"` // The date and time (timestamp) when the document was created
|
||||
Tools []BomDescriptorTool `xml:"tools>tool"` // The tool used to create the BOM.
|
||||
Component *BomDescriptorComponent `xml:"component"` // The component that the BOM describes.
|
||||
}
|
||||
|
||||
// BomDescriptorTool represents the tool that created the BOM report.
|
||||
type BomDescriptorTool struct {
|
||||
XMLName xml.Name `xml:"tool"`
|
||||
Vendor string `xml:"vendor,omitempty"` // The vendor of the tool used to create the BOM.
|
||||
Name string `xml:"name,omitempty"` // The name of the tool used to create the BOM.
|
||||
Version string `xml:"version,omitempty"` // The version of the tool used to create the BOM.
|
||||
// TODO: hashes, author, manufacture, supplier
|
||||
// TODO: add user-defined fields for the remaining build/version parameters
|
||||
}
|
||||
|
||||
// BomDescriptorComponent represents the software/package being cataloged.
|
||||
type BomDescriptorComponent struct {
|
||||
XMLName xml.Name `xml:"component"`
|
||||
Component
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package model
|
||||
|
||||
import "encoding/xml"
|
||||
|
||||
// Component represents a single element in the CycloneDX BOM
|
||||
type Component struct {
|
||||
XMLName xml.Name `xml:"component"`
|
||||
Type string `xml:"type,attr"` // Required; Describes if the component is a library, framework, application, container, operating system, firmware, hardware device, or file
|
||||
Supplier string `xml:"supplier,omitempty"` // The organization that supplied the component. The supplier may often be the manufacture, but may also be a distributor or repackager.
|
||||
Author string `xml:"author,omitempty"` // The person(s) or organization(s) that authored the component
|
||||
Publisher string `xml:"publisher,omitempty"` // The person(s) or organization(s) that published the component
|
||||
Group string `xml:"group,omitempty"` // The high-level classification that a project self-describes as. This will often be a shortened, single name of the company or project that produced the component, or the source package or domain name.
|
||||
Name string `xml:"name"` // Required; The name of the component as defined by the project
|
||||
Version string `xml:"version"` // Required; The version of the component as defined by the project
|
||||
Description string `xml:"description,omitempty"` // A description of the component
|
||||
Licenses *[]License `xml:"licenses>license"` // A node describing zero or more license names, SPDX license IDs or expressions
|
||||
PackageURL string `xml:"purl,omitempty"` // Specifies the package-url (PackageURL). The purl, if specified, must be valid and conform to the specification defined at: https://github.com/package-url/purl-spec
|
||||
// TODO: source, hashes, copyright, cpe, purl, swid, modified, pedigree, externalReferences
|
||||
// TODO: add user-defined parameters for syft-specific values (image layer index, cataloger, location path, etc.)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
// Source: https://github.com/CycloneDX/specification
|
||||
|
||||
// Document represents a CycloneDX BOM Document.
|
||||
type Document struct {
|
||||
XMLName xml.Name `xml:"bom"`
|
||||
XMLNs string `xml:"xmlns,attr"`
|
||||
Version int `xml:"version,attr"`
|
||||
SerialNumber string `xml:"serialNumber,attr"`
|
||||
BomDescriptor *BomDescriptor `xml:"metadata"` // The BOM descriptor extension
|
||||
Components []Component `xml:"components>component"` // The BOM contents
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package model
|
||||
|
||||
import "encoding/xml"
|
||||
|
||||
// License represents a single software license for a Component
|
||||
type License struct {
|
||||
XMLName xml.Name `xml:"license"`
|
||||
ID string `xml:"id,omitempty"` // A valid SPDX license ID
|
||||
Name string `xml:"name,omitempty"` // If SPDX does not define the license used, this field may be used to provide the license name
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package cyclonedx12xml
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"time"
|
||||
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/formats/cyclonedx12xml/model"
|
||||
"github.com/anchore/syft/internal/version"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// toFormatModel creates and populates a new in-memory representation of a CycloneDX 1.2 document
|
||||
func toFormatModel(s sbom.SBOM) model.Document {
|
||||
versionInfo := version.FromBuild()
|
||||
|
||||
doc := model.Document{
|
||||
XMLNs: "http://cyclonedx.org/schema/bom/1.2",
|
||||
Version: 1,
|
||||
SerialNumber: uuid.New().URN(),
|
||||
BomDescriptor: toBomDescriptor(internal.ApplicationName, versionInfo.Version, s.Source),
|
||||
}
|
||||
|
||||
// attach components
|
||||
for _, p := range s.Artifacts.PackageCatalog.Sorted() {
|
||||
doc.Components = append(doc.Components, toComponent(p))
|
||||
}
|
||||
|
||||
return doc
|
||||
}
|
||||
|
||||
func toComponent(p pkg.Package) model.Component {
|
||||
return model.Component{
|
||||
Type: "library", // TODO: this is not accurate
|
||||
Name: p.Name,
|
||||
Version: p.Version,
|
||||
PackageURL: p.PURL,
|
||||
Licenses: toLicenses(p.Licenses),
|
||||
}
|
||||
}
|
||||
|
||||
// NewBomDescriptor returns a new BomDescriptor tailored for the current time and "syft" tool details.
|
||||
func toBomDescriptor(name, version string, srcMetadata source.Metadata) *model.BomDescriptor {
|
||||
return &model.BomDescriptor{
|
||||
XMLName: xml.Name{},
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
Tools: []model.BomDescriptorTool{
|
||||
{
|
||||
Vendor: "anchore",
|
||||
Name: name,
|
||||
Version: version,
|
||||
},
|
||||
},
|
||||
Component: toBomDescriptorComponent(srcMetadata),
|
||||
}
|
||||
}
|
||||
|
||||
func toBomDescriptorComponent(srcMetadata source.Metadata) *model.BomDescriptorComponent {
|
||||
switch srcMetadata.Scheme {
|
||||
case source.ImageScheme:
|
||||
return &model.BomDescriptorComponent{
|
||||
Component: model.Component{
|
||||
Type: "container",
|
||||
Name: srcMetadata.ImageMetadata.UserInput,
|
||||
Version: srcMetadata.ImageMetadata.ManifestDigest,
|
||||
},
|
||||
}
|
||||
case source.DirectoryScheme:
|
||||
return &model.BomDescriptorComponent{
|
||||
Component: model.Component{
|
||||
Type: "file",
|
||||
Name: srcMetadata.Path,
|
||||
},
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func toLicenses(licenses []string) *[]model.License {
|
||||
if len(licenses) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var result []model.License
|
||||
for _, licenseName := range licenses {
|
||||
result = append(result, model.License{
|
||||
Name: licenseName,
|
||||
})
|
||||
}
|
||||
return &result
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package formats
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/anchore/syft/internal/formats/cyclonedx12xml"
|
||||
"github.com/anchore/syft/internal/formats/spdx22json"
|
||||
"github.com/anchore/syft/internal/formats/spdx22tagvalue"
|
||||
"github.com/anchore/syft/internal/formats/syftjson"
|
||||
"github.com/anchore/syft/internal/formats/table"
|
||||
"github.com/anchore/syft/internal/formats/text"
|
||||
"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(),
|
||||
cyclonedx12xml.Format(),
|
||||
spdx22json.Format(),
|
||||
spdx22tagvalue.Format(),
|
||||
text.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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package spdx22json
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/anchore/syft/internal/formats/spdx22json/model"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
func decoder(reader io.Reader) (*sbom.SBOM, error) {
|
||||
dec := json.NewDecoder(reader)
|
||||
|
||||
var doc model.Document
|
||||
err := dec.Decode(&doc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to decode syft-json: %w", err)
|
||||
}
|
||||
|
||||
return toSyftModel(doc)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package spdx22json
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"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"
|
||||
originalSBOM := testutils.ImageInput(t, testImage)
|
||||
|
||||
var buf bytes.Buffer
|
||||
assert.NoError(t, encoder(&buf, originalSBOM))
|
||||
|
||||
actualSBOM, err := decoder(bytes.NewReader(buf.Bytes()))
|
||||
assert.NoError(t, err)
|
||||
|
||||
for _, d := range deep.Equal(originalSBOM.Source, actualSBOM.Source) {
|
||||
t.Errorf("metadata difference: %+v", d)
|
||||
}
|
||||
|
||||
actualPackages := actualSBOM.Artifacts.PackageCatalog.Sorted()
|
||||
for idx, p := range originalSBOM.Artifacts.PackageCatalog.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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package spdx22json
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
func encoder(output io.Writer, s sbom.SBOM) error {
|
||||
doc := toFormatModel(s)
|
||||
|
||||
enc := json.NewEncoder(output)
|
||||
// prevent > and < from being escaped in the payload
|
||||
enc.SetEscapeHTML(false)
|
||||
enc.SetIndent("", " ")
|
||||
|
||||
return enc.Encode(&doc)
|
||||
}
|
||||
+7
-7
@@ -1,17 +1,18 @@
|
||||
package packages
|
||||
package spdx22json
|
||||
|
||||
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,
|
||||
NewSPDXJSONPresenter(catalog, metadata),
|
||||
testutils.AssertPresenterAgainstGoldenSnapshot(t,
|
||||
Format().Presenter(testutils.DirectoryInput(t)),
|
||||
*updateSpdxJson,
|
||||
spdxJsonRedactor,
|
||||
)
|
||||
@@ -19,9 +20,8 @@ func TestSPDXJSONDirectoryPresenter(t *testing.T) {
|
||||
|
||||
func TestSPDXJSONImagePresenter(t *testing.T) {
|
||||
testImage := "image-simple"
|
||||
catalog, metadata, _ := presenterImageInput(t, testImage)
|
||||
assertPresenterAgainstGoldenImageSnapshot(t,
|
||||
NewSPDXJSONPresenter(catalog, metadata),
|
||||
testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
|
||||
Format().Presenter(testutils.ImageInput(t, testImage, testutils.FromSnapshot())),
|
||||
testImage,
|
||||
*updateSpdxJson,
|
||||
spdxJsonRedactor,
|
||||
@@ -0,0 +1,13 @@
|
||||
package spdx22json
|
||||
|
||||
import "github.com/anchore/syft/syft/format"
|
||||
|
||||
// note: this format is LOSSY relative to the syftjson formation, which means that decoding and validation is not supported at this time
|
||||
func Format() format.Format {
|
||||
return format.NewFormat(
|
||||
format.SPDXJSONOption,
|
||||
encoder,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package spdx22
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package spdx22
|
||||
package model
|
||||
|
||||
type Checksum struct {
|
||||
// Identifies the algorithm used to produce the subject Checksum. One of: "SHA256", "SHA1", "SHA384", "MD2", "MD4", "SHA512", "MD6", "MD5", "SHA224"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package spdx22
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package spdx22
|
||||
package model
|
||||
|
||||
// derived from:
|
||||
// - https://spdx.github.io/spdx-spec/appendix-III-RDF-data-model-implementation-and-identifier-syntax/
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package spdx22
|
||||
package model
|
||||
|
||||
type Element struct {
|
||||
SPDXID string `json:"SPDXID"`
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package spdx22
|
||||
package model
|
||||
|
||||
// ElementID represents the identifier string portion of an SPDX element
|
||||
// identifier. DocElementID should be used for any attributes which can
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package spdx22
|
||||
package model
|
||||
|
||||
type ExternalDocumentRef struct {
|
||||
// externalDocumentId is a string containing letters, numbers, ., - and/or + which uniquely identifies an external document within this document.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package spdx22
|
||||
package model
|
||||
|
||||
type ReferenceCategory string
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package spdx22
|
||||
package model
|
||||
|
||||
type FileType string
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package spdx22
|
||||
package model
|
||||
|
||||
type HasExtractedLicensingInfo struct {
|
||||
// Verbatim license or licensing notice text that was discovered.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package spdx22
|
||||
package model
|
||||
|
||||
type Item struct {
|
||||
Element
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package spdx22
|
||||
package model
|
||||
|
||||
type Package struct {
|
||||
Item
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package spdx22
|
||||
package model
|
||||
|
||||
// Why are there two package identifier fields Package Checksum and Package Verification?
|
||||
// Although the values of the two fields Package Checksum and Package Verification are similar, they each serve a
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package spdx22
|
||||
package model
|
||||
|
||||
type Relationship struct {
|
||||
// Id to which the SPDX element is related
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package spdx22
|
||||
package model
|
||||
|
||||
type StartPointer struct {
|
||||
Offset int `json:"offset,omitempty"`
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
package spdx22
|
||||
package model
|
||||
|
||||
const Version = "SPDX-2.2"
|
||||
@@ -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!
|
||||
+6
-6
@@ -3,7 +3,7 @@
|
||||
"name": "/some/path",
|
||||
"spdxVersion": "SPDX-2.2",
|
||||
"creationInfo": {
|
||||
"created": "2021-09-16T20:44:35.198887Z",
|
||||
"created": "2021-10-29T16:26:08.995826Z",
|
||||
"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-5362d380-914a-458f-b059-d8d27899574c",
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
+2
-2
@@ -3,7 +3,7 @@
|
||||
"name": "user-image-input",
|
||||
"spdxVersion": "SPDX-2.2",
|
||||
"creationInfo": {
|
||||
"created": "2021-09-16T20:44:35.203911Z",
|
||||
"created": "2021-10-29T16:26:09.001799Z",
|
||||
"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-3ad8571c-513f-4fce-944e-5125353c3186",
|
||||
"packages": [
|
||||
{
|
||||
"SPDXID": "SPDXRef-Package-python-package-1-1.0.1",
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,83 @@
|
||||
package spdx22json
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/formats/common/spdxhelpers"
|
||||
"github.com/anchore/syft/internal/formats/spdx22json/model"
|
||||
"github.com/anchore/syft/internal/spdxlicense"
|
||||
"github.com/anchore/syft/internal/version"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
// toFormatModel creates and populates a new JSON document struct that follows the SPDX 2.2 spec from the given cataloging results.
|
||||
func toFormatModel(s sbom.SBOM) model.Document {
|
||||
name := spdxhelpers.DocumentName(s.Source)
|
||||
packages, files, relationships := extractFromCatalog(s.Artifacts.PackageCatalog)
|
||||
|
||||
return model.Document{
|
||||
Element: model.Element{
|
||||
SPDXID: model.ElementID("DOCUMENT").String(),
|
||||
Name: name,
|
||||
},
|
||||
SPDXVersion: model.Version,
|
||||
CreationInfo: model.CreationInfo{
|
||||
Created: time.Now().UTC(),
|
||||
Creators: []string{
|
||||
// note: key-value format derived from the JSON example document examples: https://github.com/spdx/spdx-spec/blob/v2.2/examples/SPDXJSONExample-v2.2.spdx.json
|
||||
"Organization: Anchore, Inc",
|
||||
"Tool: " + internal.ApplicationName + "-" + version.FromBuild().Version,
|
||||
},
|
||||
LicenseListVersion: spdxlicense.Version,
|
||||
},
|
||||
DataLicense: "CC0-1.0",
|
||||
DocumentNamespace: spdxhelpers.DocumentNamespace(name, s.Source),
|
||||
Packages: packages,
|
||||
Files: files,
|
||||
Relationships: relationships,
|
||||
}
|
||||
}
|
||||
|
||||
func extractFromCatalog(catalog *pkg.Catalog) ([]model.Package, []model.File, []model.Relationship) {
|
||||
packages := make([]model.Package, 0)
|
||||
relationships := make([]model.Relationship, 0)
|
||||
files := make([]model.File, 0)
|
||||
|
||||
for _, p := range catalog.Sorted() {
|
||||
license := spdxhelpers.License(p)
|
||||
packageSpdxID := model.ElementID(fmt.Sprintf("Package-%+v-%s-%s", p.Type, p.Name, p.Version)).String()
|
||||
|
||||
packageFiles, fileIDs, packageFileRelationships := spdxhelpers.Files(packageSpdxID, p)
|
||||
files = append(files, packageFiles...)
|
||||
|
||||
relationships = append(relationships, packageFileRelationships...)
|
||||
|
||||
// note: the license concluded and declared should be the same since we are collecting license information
|
||||
// from the project data itself (the installed package files).
|
||||
packages = append(packages, model.Package{
|
||||
Description: spdxhelpers.Description(p),
|
||||
DownloadLocation: spdxhelpers.DownloadLocation(p),
|
||||
ExternalRefs: spdxhelpers.ExternalRefs(p),
|
||||
FilesAnalyzed: false,
|
||||
HasFiles: fileIDs,
|
||||
Homepage: spdxhelpers.Homepage(p),
|
||||
LicenseDeclared: license, // The Declared License is what the authors of a project believe govern the package
|
||||
Originator: spdxhelpers.Originator(p),
|
||||
SourceInfo: spdxhelpers.SourceInfo(p),
|
||||
VersionInfo: p.Version,
|
||||
Item: model.Item{
|
||||
LicenseConcluded: license, // The Concluded License field is the license the SPDX file creator believes governs the package
|
||||
Element: model.Element{
|
||||
SPDXID: packageSpdxID,
|
||||
Name: p.Name,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return packages, files, relationships
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package spdx22json
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/internal/formats/spdx22json/model"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
func toSyftModel(doc model.Document) (*sbom.SBOM, error) {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package spdx22tagvalue
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
"github.com/spdx/tools-golang/tvsaver"
|
||||
)
|
||||
|
||||
func encoder(output io.Writer, s sbom.SBOM) error {
|
||||
model := toFormatModel(s)
|
||||
return tvsaver.Save2_2(&model, output)
|
||||
}
|
||||
+8
-7
@@ -1,17 +1,19 @@
|
||||
package packages
|
||||
package spdx22tagvalue
|
||||
|
||||
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,
|
||||
NewSPDXTagValuePresenter(catalog, metadata),
|
||||
|
||||
testutils.AssertPresenterAgainstGoldenSnapshot(t,
|
||||
Format().Presenter(testutils.DirectoryInput(t)),
|
||||
*updateSpdxTagValue,
|
||||
spdxTagValueRedactor,
|
||||
)
|
||||
@@ -19,9 +21,8 @@ func TestSPDXTagValueDirectoryPresenter(t *testing.T) {
|
||||
|
||||
func TestSPDXTagValueImagePresenter(t *testing.T) {
|
||||
testImage := "image-simple"
|
||||
catalog, metadata, _ := presenterImageInput(t, testImage)
|
||||
assertPresenterAgainstGoldenImageSnapshot(t,
|
||||
NewSPDXTagValuePresenter(catalog, metadata),
|
||||
testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
|
||||
Format().Presenter(testutils.ImageInput(t, testImage, testutils.FromSnapshot())),
|
||||
testImage,
|
||||
*updateSpdxTagValue,
|
||||
spdxTagValueRedactor,
|
||||
@@ -0,0 +1,13 @@
|
||||
package spdx22tagvalue
|
||||
|
||||
import "github.com/anchore/syft/syft/format"
|
||||
|
||||
// note: this format is LOSSY relative to the syftjson formation, which means that decoding and validation is not supported at this time
|
||||
func Format() format.Format {
|
||||
return format.NewFormat(
|
||||
format.SPDXTagValueOption,
|
||||
encoder,
|
||||
nil,
|
||||
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!
|
||||
BIN
Binary file not shown.
+16
-33
@@ -1,38 +1,23 @@
|
||||
package packages
|
||||
package spdx22tagvalue
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/anchore/syft/internal/spdxlicense"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/formats/common/spdxhelpers"
|
||||
"github.com/anchore/syft/internal/spdxlicense"
|
||||
"github.com/anchore/syft/internal/version"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/spdx/tools-golang/spdx"
|
||||
"github.com/spdx/tools-golang/tvsaver"
|
||||
)
|
||||
|
||||
// SPDXTagValuePresenter is a SPDX presentation object for the syft results (see https://github.com/spdx/spdx-spec)
|
||||
type SPDXTagValuePresenter struct {
|
||||
catalog *pkg.Catalog
|
||||
srcMetadata source.Metadata
|
||||
}
|
||||
|
||||
// NewJSONPresenter creates a new JSON presenter object for the given cataloging results.
|
||||
func NewSPDXTagValuePresenter(catalog *pkg.Catalog, srcMetadata source.Metadata) *SPDXTagValuePresenter {
|
||||
return &SPDXTagValuePresenter{
|
||||
catalog: catalog,
|
||||
srcMetadata: srcMetadata,
|
||||
}
|
||||
}
|
||||
|
||||
// Present the catalog results to the given writer.
|
||||
// nolint: funlen
|
||||
func (pres *SPDXTagValuePresenter) Present(output io.Writer) error {
|
||||
doc := spdx.Document2_2{
|
||||
// toFormatModel creates and populates a new JSON document struct that follows the SPDX 2.2 spec from the given cataloging results.
|
||||
// nolint:funlen
|
||||
func toFormatModel(s sbom.SBOM) spdx.Document2_2 {
|
||||
return spdx.Document2_2{
|
||||
CreationInfo: &spdx.CreationInfo2_2{
|
||||
// 2.1: SPDX Version; should be in the format "SPDX-2.2"
|
||||
// Cardinality: mandatory, one
|
||||
@@ -48,7 +33,7 @@ func (pres *SPDXTagValuePresenter) Present(output io.Writer) error {
|
||||
|
||||
// 2.4: Document Name
|
||||
// Cardinality: mandatory, one
|
||||
DocumentName: pres.srcMetadata.ImageMetadata.UserInput,
|
||||
DocumentName: s.Source.ImageMetadata.UserInput,
|
||||
|
||||
// 2.5: Document Namespace
|
||||
// Cardinality: mandatory, one
|
||||
@@ -67,7 +52,7 @@ func (pres *SPDXTagValuePresenter) Present(output io.Writer) error {
|
||||
// In many cases, the URI will point to a web accessible document, but this should not be assumed
|
||||
// to be the case.
|
||||
|
||||
DocumentNamespace: fmt.Sprintf("https://anchore.com/syft/image/%s", pres.srcMetadata.ImageMetadata.UserInput),
|
||||
DocumentNamespace: fmt.Sprintf("https://anchore.com/syft/image/%s", s.Source.ImageMetadata.UserInput),
|
||||
|
||||
// 2.6: External Document References
|
||||
// Cardinality: optional, one or many
|
||||
@@ -96,25 +81,23 @@ func (pres *SPDXTagValuePresenter) Present(output io.Writer) error {
|
||||
// Cardinality: optional, one
|
||||
DocumentComment: "",
|
||||
},
|
||||
Packages: pres.packages(),
|
||||
Packages: toFormatPackages(s.Artifacts.PackageCatalog),
|
||||
}
|
||||
|
||||
return tvsaver.Save2_2(&doc, output)
|
||||
}
|
||||
|
||||
// packages populates all Package Information from the package Catalog (see https://spdx.github.io/spdx-spec/3-package-information/)
|
||||
// nolint: funlen
|
||||
func (pres *SPDXTagValuePresenter) packages() map[spdx.ElementID]*spdx.Package2_2 {
|
||||
func toFormatPackages(catalog *pkg.Catalog) map[spdx.ElementID]*spdx.Package2_2 {
|
||||
results := make(map[spdx.ElementID]*spdx.Package2_2)
|
||||
|
||||
for p := range pres.catalog.Enumerate() {
|
||||
for p := range catalog.Enumerate() {
|
||||
// name should be guaranteed to be unique, but semantically useful and stable
|
||||
id := fmt.Sprintf("Package-%+v-%s", p.Type, p.Name)
|
||||
|
||||
// If the Concluded License is not the same as the Declared License, a written explanation should be provided
|
||||
// in the Comments on License field (section 3.16). With respect to NOASSERTION, a written explanation in
|
||||
// the Comments on License field (section 3.16) is preferred.
|
||||
license := getSPDXLicense(p)
|
||||
license := spdxhelpers.License(p)
|
||||
|
||||
results[spdx.ElementID(id)] = &spdx.Package2_2{
|
||||
|
||||
@@ -273,8 +256,8 @@ func (pres *SPDXTagValuePresenter) packages() map[spdx.ElementID]*spdx.Package2_
|
||||
return results
|
||||
}
|
||||
|
||||
func formatSPDXExternalRefs(p *pkg.Package) (refs []*spdx.PackageExternalReference2_2) {
|
||||
for _, ref := range getSPDXExternalRefs(p) {
|
||||
func formatSPDXExternalRefs(p pkg.Package) (refs []*spdx.PackageExternalReference2_2) {
|
||||
for _, ref := range spdxhelpers.ExternalRefs(p) {
|
||||
refs = append(refs, &spdx.PackageExternalReference2_2{
|
||||
Category: string(ref.ReferenceCategory),
|
||||
RefType: string(ref.ReferenceType),
|
||||
@@ -0,0 +1,23 @@
|
||||
package syftjson
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
|
||||
"github.com/anchore/syft/internal/formats/syftjson/model"
|
||||
)
|
||||
|
||||
func decoder(reader io.Reader) (*sbom.SBOM, error) {
|
||||
dec := json.NewDecoder(reader)
|
||||
|
||||
var doc model.Document
|
||||
err := dec.Decode(&doc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to decode syft-json: %w", err)
|
||||
}
|
||||
|
||||
return toSyftModel(doc)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package syftjson
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"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"
|
||||
originalSBOM := testutils.ImageInput(t, testImage)
|
||||
|
||||
var buf bytes.Buffer
|
||||
assert.NoError(t, encoder(&buf, originalSBOM))
|
||||
|
||||
actualSBOM, err := decoder(bytes.NewReader(buf.Bytes()))
|
||||
assert.NoError(t, err)
|
||||
|
||||
for _, d := range deep.Equal(originalSBOM.Source, actualSBOM.Source) {
|
||||
t.Errorf("metadata difference: %+v", d)
|
||||
}
|
||||
|
||||
actualPackages := actualSBOM.Artifacts.PackageCatalog.Sorted()
|
||||
for idx, p := range originalSBOM.Artifacts.PackageCatalog.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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package syftjson
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
func encoder(output io.Writer, s sbom.SBOM) error {
|
||||
// TODO: application config not available yet
|
||||
doc := ToFormatModel(s, nil)
|
||||
|
||||
enc := json.NewEncoder(output)
|
||||
// prevent > and < from being escaped in the payload
|
||||
enc.SetEscapeHTML(false)
|
||||
enc.SetIndent("", " ")
|
||||
|
||||
return enc.Encode(&doc)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package syftjson
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/syft/internal/formats/common/testutils"
|
||||
)
|
||||
|
||||
var updateJson = flag.Bool("update-json", false, "update the *.golden files for json presenters")
|
||||
|
||||
func TestDirectoryPresenter(t *testing.T) {
|
||||
testutils.AssertPresenterAgainstGoldenSnapshot(t,
|
||||
Format().Presenter(testutils.DirectoryInput(t)),
|
||||
*updateJson,
|
||||
)
|
||||
}
|
||||
|
||||
func TestImagePresenter(t *testing.T) {
|
||||
testImage := "image-simple"
|
||||
testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
|
||||
Format().Presenter(testutils.ImageInput(t, testImage, testutils.FromSnapshot())),
|
||||
testImage,
|
||||
*updateJson,
|
||||
)
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user