mirror of
https://github.com/anchore/syft.git
synced 2026-10-12 22:27:22 +02:00
Compare commits
296
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2aa4caf55 | ||
|
|
fe616acd98 | ||
|
|
6af132e088 | ||
|
|
00206233e6 | ||
|
|
bd9007fc0e | ||
|
|
e3b34813d7 | ||
|
|
f0b2d81599 | ||
|
|
0849539729 | ||
|
|
4f0099583a | ||
|
|
d76c868481 | ||
|
|
e38cde35ed | ||
|
|
9090c3a772 | ||
|
|
ef627d82ef | ||
|
|
45ea4177e8 | ||
|
|
e28b7b53b0 | ||
|
|
fc39710d06 | ||
|
|
e732f419f8 | ||
|
|
25835ef589 | ||
|
|
319c6ee2eb | ||
|
|
9c27fa7b0b | ||
|
|
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 | ||
|
|
b25f5b664b | ||
|
|
3462e18af3 | ||
|
|
05ac3f1eff | ||
|
|
a000a2926b | ||
|
|
9189ed68df | ||
|
|
ba1cd8d753 | ||
|
|
dd2c178085 | ||
|
|
1b23a94015 | ||
|
|
f47a6a88b1 | ||
|
|
4b7217f052 | ||
|
|
5e4b668e5a | ||
|
|
6480f06173 | ||
|
|
75aed5f3ec | ||
|
|
270fbb7665 | ||
|
|
0395c47445 | ||
|
|
e5caba043d | ||
|
|
d1ac476986 | ||
|
|
dd23d49986 | ||
|
|
6d4d083acc | ||
|
|
316d4341c8 | ||
|
|
3e8afc5274 | ||
|
|
93d00dc340 | ||
|
|
9fe1da8ee6 | ||
|
|
95df7dd035 | ||
|
|
ddfc8e20c0 | ||
|
|
3f305aa6cd | ||
|
|
2f99a35f51 | ||
|
|
06dcd3261d | ||
|
|
abbba3fc19 | ||
|
|
0799fd9d46 | ||
|
|
5de1a0a236 | ||
|
|
035ef03b0c | ||
|
|
9acabb8307 | ||
|
|
0f0fe59bf2 | ||
|
|
ff828fbac2 | ||
|
|
cba5b5723b | ||
|
|
b0e9b92a25 | ||
|
|
d25d3b1fb3 | ||
|
|
93b4bf9379 | ||
|
|
58f2be95fd | ||
|
|
6f038e9208 | ||
|
|
32a54b94be | ||
|
|
98d4749f86 | ||
|
|
3a5168917e | ||
|
|
58b21c03c3 | ||
|
|
2093d2a1dc | ||
|
|
dd56450f92 | ||
|
|
6f2b52971d | ||
|
|
fee74d57b8 | ||
|
|
fee35dd175 | ||
|
|
ecf4e5546a | ||
|
|
2de56c0749 | ||
|
|
fb0857ff93 | ||
|
|
962e82297c | ||
|
|
c833472b6e | ||
|
|
c825ae2404 | ||
|
|
706322f826 | ||
|
|
66e71c39e2 | ||
|
|
6dd698d89e | ||
|
|
50928ebd05 | ||
|
|
6e70d8af98 | ||
|
|
67b7d63875 | ||
|
|
5a2e2eb679 | ||
|
|
801e662633 | ||
|
|
6a398f9771 | ||
|
|
bf1bf057ab | ||
|
|
36b44b1d8e | ||
|
|
8f85c8affc | ||
|
|
776164a078 | ||
|
|
5ea1d78464 | ||
|
|
afbd8f8ea0 | ||
|
|
2f81a2548c | ||
|
|
2754c889eb | ||
|
|
8be0d988e7 | ||
|
|
80e23bdb50 | ||
|
|
17bbf840cf | ||
|
|
2ca2f03501 | ||
|
|
1201ad6df0 | ||
|
|
baa859d2ed | ||
|
|
a6c798f438 | ||
|
|
360eb74cc7 | ||
|
|
92c20c885e | ||
|
|
284b0c20b0 | ||
|
|
4cce32f5c9 | ||
|
|
26007db650 | ||
|
|
5af2687022 | ||
|
|
032cecbe7f | ||
|
|
5e1e2628cf | ||
|
|
1632fdd85e | ||
|
|
18af21d2a5 | ||
|
|
1b62b10b2b | ||
|
|
170681943c | ||
|
|
46043510ae | ||
|
|
c34060d188 | ||
|
|
24a2f9344b | ||
|
|
d9de63c837 | ||
|
|
0c29090b42 | ||
|
|
6d2f139397 | ||
|
|
9c7906364d | ||
|
|
7ae4504182 | ||
|
|
060e60b6dd | ||
|
|
33e6be0b74 | ||
|
|
fa7fd718cb | ||
|
|
65e4e17590 | ||
|
|
091fd1f0b0 | ||
|
|
b301b56db1 | ||
|
|
676bdf9816 | ||
|
|
c06ee1ecc2 | ||
|
|
136e439dc2 | ||
|
|
ecb16b6a34 | ||
|
|
6edbae08e5 | ||
|
|
0c7706f254 | ||
|
|
11866ab69e | ||
|
|
0c100c4037 | ||
|
|
cca9d33b36 | ||
|
|
8b315367e8 | ||
|
|
b771d7bfd3 | ||
|
|
0c9eca4a30 | ||
|
|
484730435b | ||
|
|
d5dfaaba53 | ||
|
|
9fb79bfa2e | ||
|
|
2a5207ec88 | ||
|
|
2ff3f58730 | ||
|
|
8c178c497e | ||
|
|
4abffc8fbc | ||
|
|
c363b2b532 | ||
|
|
c02ab88d5f | ||
|
|
cd542bab5b | ||
|
|
b5d4b2f7b2 | ||
|
|
51b13f8221 | ||
|
|
44915b435f | ||
|
|
d451a5ad30 | ||
|
|
c1551a03c5 | ||
|
|
9ad786d608 | ||
|
|
28c407572c | ||
|
|
f9d49885ae | ||
|
|
f438ee974c | ||
|
|
309cbf5264 | ||
|
|
46fffe5c6f | ||
|
|
080057b217 | ||
|
|
870a676a5d | ||
|
|
46bfb68113 | ||
|
|
5743e32e02 | ||
|
|
66ebe49a04 | ||
|
|
0511972dfa | ||
|
|
c56690fc52 | ||
|
|
ba3407a767 | ||
|
|
269832ce8d | ||
|
|
7a10cbae0c | ||
|
|
7ec0e3785f | ||
|
|
9ec09add67 | ||
|
|
557ad8be49 | ||
|
|
c332ba0867 | ||
|
|
485caa1ea3 | ||
|
|
cd925e5664 | ||
|
|
db35186c7d | ||
|
|
8551168702 | ||
|
|
772613647c | ||
|
|
8658abc8c4 | ||
|
|
ab83b7f840 | ||
|
|
0f26681ac5 | ||
|
|
9abdb174d5 | ||
|
|
44cf7ccb44 | ||
|
|
2a9b1b2680 | ||
|
|
cedf8cd6d7 | ||
|
|
cfec812804 | ||
|
|
7b25237cea | ||
|
|
ac4b653721 | ||
|
|
8464d9766a | ||
|
|
13a066ea18 | ||
|
|
51bf9f9e05 | ||
|
|
e256e86888 | ||
|
|
1b7c755536 | ||
|
|
8854d83934 | ||
|
|
3ef30f99be | ||
|
|
12f419111e | ||
|
|
68c4bf8bbe | ||
|
|
929b78efbf | ||
|
|
d420368ba9 | ||
|
|
40199096e9 | ||
|
|
0e9c1c1d86 | ||
|
|
77e4c89a5a | ||
|
|
e3b1522394 | ||
|
|
46636ccb3c | ||
|
|
36e4af1953 | ||
|
|
abca2c5f0b | ||
|
|
f180d1c537 | ||
|
|
b1b57f6ba6 | ||
|
|
4bde850f04 | ||
|
|
1d87f07da1 | ||
|
|
cf516add95 | ||
|
|
68d698e9f2 | ||
|
|
efcd8a8b9a | ||
|
|
ae32942a18 | ||
|
|
18dd2950ac | ||
|
|
6a960ec1f3 | ||
|
|
97f0f83544 | ||
|
|
a6cba5d9db | ||
|
|
f22d7d23c1 | ||
|
|
ff4ed40d50 | ||
|
|
4666ca8469 | ||
|
|
cb5e7d0e08 | ||
|
|
48a1d4711a | ||
|
|
35cc00b74a | ||
|
|
26a4dd36a8 | ||
|
|
e9e9fe6d9c | ||
|
|
2f8a568d4f | ||
|
|
c828e47765 | ||
|
|
83778677c1 | ||
|
|
a5ae8e9668 | ||
|
|
9f57e17887 | ||
|
|
ba81bfe529 | ||
|
|
a83d79f330 | ||
|
|
87e1a0f501 | ||
|
|
e9105c180a | ||
|
|
c7b26c55ac | ||
|
|
0c2a51554a | ||
|
|
5e62bca72f | ||
|
|
6195002ae5 | ||
|
|
8c4370b7cc | ||
|
|
6c80f9910f | ||
|
|
b963be219b | ||
|
|
40e9c4b146 | ||
|
|
1aa8a10f6b | ||
|
|
6d5ff0fd8e | ||
|
|
c5e3b631ac | ||
|
|
05b9d35ee6 | ||
|
|
555376e250 | ||
|
|
fee878028a | ||
|
|
5370daf027 | ||
|
|
8d838b18a8 | ||
|
|
bf2d5ed87e | ||
|
|
a350e5889b | ||
|
|
e4393e7a3d | ||
|
|
26d1975870 | ||
|
|
babb09b3a4 | ||
|
|
b207bc8ee2 | ||
|
|
5b5fa7ec90 | ||
|
|
6d730d24dd |
+14
-1
@@ -6,4 +6,17 @@ permit:
|
||||
- ISC
|
||||
ignore-packages:
|
||||
# packageurl-go is released under the MIT license located in the root of the repo at /mit.LICENSE
|
||||
- github.com/package-url/packageurl-go
|
||||
- github.com/anchore/packageurl-go
|
||||
|
||||
# from: https://github.com/spdx/tools-golang/blob/main/LICENSE.code
|
||||
# The tools-golang source code is provided and may be used, at your option,
|
||||
# under either:
|
||||
# * Apache License, version 2.0 (Apache-2.0), OR
|
||||
# * GNU General Public License, version 2.0 or later (GPL-2.0-or-later).
|
||||
# (we choose Apache-2.0)
|
||||
- github.com/spdx/tools-golang
|
||||
|
||||
# from: https://github.com/xi2/xz/blob/master/LICENSE
|
||||
# All these files have been put into the public domain.
|
||||
# You can do whatever you want with these files.
|
||||
- github.com/xi2/xz
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
contact_links:
|
||||
|
||||
- name: Help & Support Slack Channel ⛑️
|
||||
# link to toolbox-help channel
|
||||
url: https://anchorecommunity.slack.com/archives/C019BUXV7R6
|
||||
about: Ask a questions and get answers
|
||||
|
||||
- name: Developer Slack Channel 💬
|
||||
# link to toolbox-dev channel
|
||||
url: https://anchorecommunity.slack.com/archives/C0190NF9TSM
|
||||
about: Participate in design discussions and feature development
|
||||
- name: Join the Slack community 💬
|
||||
# link to our community Slack registration page
|
||||
url: https://anchore.com/slack
|
||||
about: 'Come chat with us! Ask for help, join our software development efforts, or just give us feedback!'
|
||||
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eu
|
||||
|
||||
ORIGINAL_STATE_DIR=$(mktemp -d "TEMP-original-state-XXXXXXXXX")
|
||||
TIDY_STATE_DIR=$(mktemp -d "TEMP-tidy-state-XXXXXXXXX")
|
||||
|
||||
trap "cp -v ${ORIGINAL_STATE_DIR}/* ./ && rm -fR ${ORIGINAL_STATE_DIR} ${TIDY_STATE_DIR}" EXIT
|
||||
|
||||
echo "Capturing original state of files..."
|
||||
cp -v go.mod go.sum "${ORIGINAL_STATE_DIR}"
|
||||
|
||||
echo "Capturing state of go.mod and go.sum after running go mod tidy..."
|
||||
go mod tidy
|
||||
cp -v go.mod go.sum "${TIDY_STATE_DIR}"
|
||||
echo ""
|
||||
|
||||
set +e
|
||||
|
||||
# Detect difference between the git HEAD state and the go mod tidy state
|
||||
DIFF_MOD=$(diff -u "${ORIGINAL_STATE_DIR}/go.mod" "${TIDY_STATE_DIR}/go.mod")
|
||||
DIFF_SUM=$(diff -u "${ORIGINAL_STATE_DIR}/go.sum" "${TIDY_STATE_DIR}/go.sum")
|
||||
|
||||
if [[ -n "${DIFF_MOD}" || -n "${DIFF_SUM}" ]]; then
|
||||
echo "go.mod diff:"
|
||||
echo "${DIFF_MOD}"
|
||||
echo "go.sum diff:"
|
||||
echo "${DIFF_SUM}"
|
||||
echo ""
|
||||
printf "FAILED! go.mod and/or go.sum are NOT tidy; please run 'go mod tidy'.\n\n"
|
||||
exit 1
|
||||
fi
|
||||
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"
|
||||
@@ -1,161 +0,0 @@
|
||||
name: 'Acceptance'
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
# ... only act on pushes to main
|
||||
branches:
|
||||
- main
|
||||
# ... do not act on release tags
|
||||
tags-ignore:
|
||||
- v*
|
||||
|
||||
env:
|
||||
GO_VERSION: "1.14.x"
|
||||
|
||||
jobs:
|
||||
Build-Snapshot-Artifacts:
|
||||
runs-on: macos-latest # We're creating these snapshot builds on macOS to be consistent with our release workflow's build process, which also takes place on macOS (due to code signing requirements).
|
||||
steps:
|
||||
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Restore bootstrap cache
|
||||
id: cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
${{ github.workspace }}/.tmp
|
||||
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}-${{ hashFiles('Makefile') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}-
|
||||
${{ runner.os }}-go-${{ env.GO_VERSION }}-
|
||||
|
||||
- name: Bootstrap project dependencies
|
||||
if: steps.bootstrap-cache.outputs.cache-hit != 'true'
|
||||
run: make bootstrap
|
||||
|
||||
- name: Import GPG key
|
||||
id: import_gpg
|
||||
uses: crazy-max/ghaction-import-gpg@v2
|
||||
env:
|
||||
GPG_PRIVATE_KEY: ${{ secrets.SIGNING_GPG_PRIVATE_KEY }}
|
||||
PASSPHRASE: ${{ secrets.SIGNING_GPG_PASSPHRASE }}
|
||||
|
||||
- name: GPG signing info
|
||||
run: |
|
||||
echo "fingerprint: ${{ steps.import_gpg.outputs.fingerprint }}"
|
||||
echo "keyid: ${{ steps.import_gpg.outputs.keyid }}"
|
||||
echo "name: ${{ steps.import_gpg.outputs.name }}"
|
||||
echo "email: ${{ steps.import_gpg.outputs.email }}"
|
||||
|
||||
- name: Build snapshot artifacts
|
||||
run: make snapshot
|
||||
env:
|
||||
GPG_PRIVATE_KEY: ${{ secrets.SIGNING_GPG_PRIVATE_KEY }}
|
||||
PASSPHRASE: ${{ secrets.SIGNING_GPG_PASSPHRASE }}
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: artifacts
|
||||
path: snapshot/**/*
|
||||
|
||||
- uses: 8398a7/action-slack@v3
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
fields: repo,workflow,job,commit,message,author
|
||||
text: The syft acceptance tests have failed tragically!
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }}
|
||||
if: ${{ failure() }}
|
||||
|
||||
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
|
||||
Acceptance-Linux:
|
||||
needs: [ Build-Snapshot-Artifacts ]
|
||||
# come Nov 30 2020 ubuntu-latest will be ubuntu-20.04, until then it needs to be explicitly referenced due to python 3.7 specific features being used
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: artifacts
|
||||
path: snapshot
|
||||
|
||||
- name: Run Acceptance Tests (Linux)
|
||||
run: make acceptance-linux
|
||||
|
||||
- uses: 8398a7/action-slack@v3
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
fields: repo,workflow,job,commit,message,author
|
||||
text: The syft acceptance tests have failed tragically!
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }}
|
||||
if: ${{ failure() }}
|
||||
|
||||
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
|
||||
Acceptance-Mac:
|
||||
needs: [ Build-Snapshot-Artifacts ]
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: artifacts
|
||||
path: snapshot
|
||||
|
||||
- name: Run Acceptance Tests (Mac)
|
||||
run: make acceptance-mac
|
||||
|
||||
- uses: 8398a7/action-slack@v3
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
fields: repo,workflow,job,commit,message,author
|
||||
text: The syft acceptance tests have failed tragically!
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }}
|
||||
if: ${{ failure() }}
|
||||
|
||||
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
|
||||
Inline-Compare:
|
||||
needs: [ Build-Snapshot-Artifacts ]
|
||||
# come Nov 30 2020 ubuntu-latest will be ubuntu-20.04, until then it needs to be explicitly referenced due to python 3.7 specific features being used
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Fingerprint inline-compare sources
|
||||
run: make compare-fingerprint
|
||||
|
||||
- name: Restore inline reports cache
|
||||
id: cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ github.workspace }}/test/inline-compare/inline-reports
|
||||
key: inline-reports-${{ hashFiles('**/inline-compare.fingerprint') }}
|
||||
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: artifacts
|
||||
path: snapshot
|
||||
|
||||
- name: Compare Anchore inline-scan results against snapshot build output
|
||||
run: make compare-snapshot
|
||||
|
||||
- uses: 8398a7/action-slack@v3
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
fields: repo,workflow,job,commit,message,author
|
||||
text: The syft acceptance tests have failed tragically!
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }}
|
||||
if: ${{ failure() }}
|
||||
@@ -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
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
name: 'Release'
|
||||
name: "Release"
|
||||
on:
|
||||
push:
|
||||
# take no actions on push to any branch...
|
||||
branches-ignore:
|
||||
- '**'
|
||||
- "**"
|
||||
# ... only act on release tags
|
||||
tags:
|
||||
- 'v*'
|
||||
- "v*"
|
||||
|
||||
env:
|
||||
GO_VERSION: "1.14.x"
|
||||
GO_VERSION: "1.16.x"
|
||||
|
||||
jobs:
|
||||
wait-for-checks:
|
||||
quality-gate:
|
||||
environment: release
|
||||
runs-on: ubuntu-latest # This OS choice is arbitrary. None of the steps in this job are specific to either Linux or macOS.
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
# we don't want to release commits that have been pushed and tagged, but not necessarily merged onto main
|
||||
@@ -30,17 +30,26 @@ jobs:
|
||||
id: static-analysis
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# This check name is defined as the circle-ci workflow name (in .circleci/config.yaml)
|
||||
checkName: "Static-Analysis (1.x, ubuntu-latest)"
|
||||
# This check name is defined as the github action job name (in .github/workflows/testing.yaml)
|
||||
checkName: "Static analysis"
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
- name: Check unit + integration results (latest go version)
|
||||
- name: Check unit test results
|
||||
uses: fountainhead/action-wait-for-check@v1.0.0
|
||||
id: unit-integration
|
||||
id: unit
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# This check name is defined as the circle-ci workflow name (in .circleci/config.yaml)
|
||||
checkName: "Tests (1.x, ubuntu-latest)"
|
||||
# This check name is defined as the github action job name (in .github/workflows/testing.yaml)
|
||||
checkName: "Unit tests"
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
- name: Check integration test results
|
||||
uses: fountainhead/action-wait-for-check@v1.0.0
|
||||
id: integration
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# This check name is defined as the github action job name (in .github/workflows/testing.yaml)
|
||||
checkName: "Integration tests"
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
- name: Check acceptance test results (linux)
|
||||
@@ -48,8 +57,8 @@ jobs:
|
||||
id: acceptance-linux
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# This check name is defined as the github action job name (in .github/workflows/acceptance-test.yaml)
|
||||
checkName: "Acceptance-Linux"
|
||||
# This check name is defined as the github action job name (in .github/workflows/testing.yaml)
|
||||
checkName: "Acceptance tests (Linux)"
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
- name: Check acceptance test results (mac)
|
||||
@@ -57,33 +66,35 @@ jobs:
|
||||
id: acceptance-mac
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# This check name is defined as the github action job name (in .github/workflows/acceptance-test.yaml)
|
||||
checkName: "Acceptance-Mac"
|
||||
# This check name is defined as the github action job name (in .github/workflows/testing.yaml)
|
||||
checkName: "Acceptance tests (Mac)"
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
- name: Check inline comparison test results
|
||||
- name: Check cli test results (linux)
|
||||
uses: fountainhead/action-wait-for-check@v1.0.0
|
||||
id: inline-compare
|
||||
id: cli-linux
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# This check name is defined as the github action job name (in .github/workflows/acceptance-test.yaml)
|
||||
checkName: "Inline-Compare"
|
||||
# This check name is defined as the github action job name (in .github/workflows/testing.yaml)
|
||||
checkName: "CLI tests (Linux)"
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
- name: Quality gate
|
||||
if: steps.static-analysis.outputs.conclusion != 'success' || steps.unit-integration.outputs.conclusion != 'success' || steps.inline-compare.outputs.conclusion != 'success' || steps.acceptance-linux.outputs.conclusion != 'success' || steps.acceptance-mac.outputs.conclusion != 'success'
|
||||
if: steps.static-analysis.outputs.conclusion != 'success' || steps.unit.outputs.conclusion != 'success' || steps.integration.outputs.conclusion != 'success' || steps.cli-linux.outputs.conclusion != 'success' || steps.acceptance-linux.outputs.conclusion != 'success' || steps.acceptance-mac.outputs.conclusion != 'success'
|
||||
run: |
|
||||
echo "Static Analysis Status: ${{ steps.static-analysis.conclusion }}"
|
||||
echo "Unit & Integration Test Status: ${{ steps.unit-integration.outputs.conclusion }}"
|
||||
echo "Unit Test Status: ${{ steps.unit.outputs.conclusion }}"
|
||||
echo "Integration Test Status: ${{ steps.integration.outputs.conclusion }}"
|
||||
echo "Acceptance Test (Linux) Status: ${{ steps.acceptance-linux.outputs.conclusion }}"
|
||||
echo "Acceptance Test (Mac) Status: ${{ steps.acceptance-mac.outputs.conclusion }}"
|
||||
echo "Inline Compare Status: ${{ steps.inline-compare.outputs.conclusion }}"
|
||||
echo "CLI Test (Linux) Status: ${{ steps.cli-linux.outputs.conclusion }}"
|
||||
false
|
||||
|
||||
release:
|
||||
needs: [ wait-for-checks ]
|
||||
needs: [quality-gate]
|
||||
runs-on: macos-latest # Due to our code signing process, it's vital that we run our release steps on macOS.
|
||||
steps:
|
||||
- uses: docker-practice/actions-setup-docker@v1
|
||||
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
@@ -93,19 +104,26 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# We are expecting this cache to have been created during the "Build-Snapshot-Artifacts" job in the "Acceptance" workflow.
|
||||
- name: Restore bootstrap cache
|
||||
id: cache
|
||||
uses: actions/cache@v2
|
||||
- name: Restore tool cache
|
||||
id: tool-cache
|
||||
uses: actions/cache@v2.1.3
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
${{ github.workspace }}/.tmp
|
||||
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}-${{ hashFiles('Makefile') }}
|
||||
path: ${{ github.workspace }}/.tmp
|
||||
key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }}
|
||||
|
||||
- name: Restore go cache
|
||||
id: go-cache
|
||||
uses: actions/cache@v2.1.3
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}-
|
||||
${{ runner.os }}-go-${{ env.GO_VERSION }}-
|
||||
|
||||
- name: (cache-miss) Bootstrap all project dependencies
|
||||
if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true'
|
||||
run: make bootstrap
|
||||
|
||||
- name: Import GPG key
|
||||
id: import_gpg
|
||||
uses: crazy-max/ghaction-import-gpg@v2
|
||||
@@ -123,6 +141,8 @@ jobs:
|
||||
- name: Build & publish release artifacts
|
||||
run: make release
|
||||
env:
|
||||
DOCKER_USERNAME: ${{ secrets.TOOLBOX_DOCKER_USER }}
|
||||
DOCKER_PASSWORD: ${{ secrets.TOOLBOX_DOCKER_PASS }}
|
||||
GITHUB_TOKEN: ${{ secrets.ANCHORE_GIT_READ_TOKEN }}
|
||||
GPG_PRIVATE_KEY: ${{ secrets.SIGNING_GPG_PRIVATE_KEY }}
|
||||
PASSPHRASE: ${{ secrets.SIGNING_GPG_PASSPHRASE }}
|
||||
@@ -134,6 +154,10 @@ jobs:
|
||||
AC_USERNAME: ${{ secrets.ENG_CI_APPLE_ID }} # Used during macOS notarization.
|
||||
AC_PASSWORD: ${{ secrets.ENG_CI_APPLE_ID_PASS }} # Used during macOS notarization.
|
||||
|
||||
- uses: anchore/sbom-action@v0
|
||||
with:
|
||||
artifact-name: sbom.spdx.json
|
||||
|
||||
- uses: 8398a7/action-slack@v3
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
name: 'Static Analysis + Unit + Integration'
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
pull_request:
|
||||
jobs:
|
||||
|
||||
Static-Analysis:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.x]
|
||||
platform: [ubuntu-latest]
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Restore bootstrap cache
|
||||
id: bootstrap-cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
${{ github.workspace }}/.tmp
|
||||
key: ${{ runner.os }}-go-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }}-${{ hashFiles('Makefile') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }}-
|
||||
${{ runner.os }}-go-${{ matrix.go-version }}-
|
||||
|
||||
- name: Bootstrap project dependencies
|
||||
if: steps.bootstrap-cache.outputs.cache-hit != 'true'
|
||||
run: make bootstrap
|
||||
|
||||
- name: Bootstrap CI dependencies
|
||||
run: make ci-bootstrap
|
||||
|
||||
- name: Run static analysis
|
||||
run: make static-analysis
|
||||
|
||||
Tests:
|
||||
strategy:
|
||||
matrix:
|
||||
# test the lower bounds of support, and the latest available
|
||||
go-version: [1.13.x, 1.x]
|
||||
platform: [ubuntu-latest]
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Restore bootstrap cache
|
||||
id: bootstrap-cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
${{ github.workspace }}/.tmp
|
||||
key: ${{ runner.os }}-go-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }}-${{ hashFiles('Makefile') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }}-
|
||||
${{ runner.os }}-go-${{ matrix.go-version }}-
|
||||
|
||||
- name: Bootstrap project dependencies
|
||||
if: steps.bootstrap-cache.outputs.cache-hit != 'true'
|
||||
run: make bootstrap
|
||||
|
||||
- name: Bootstrap CI dependencies
|
||||
run: make ci-bootstrap
|
||||
|
||||
- name: Build cache key for java test-fixture blobs (for unit tests)
|
||||
run: make java-packages-fingerprint
|
||||
|
||||
- name: Restore Java test-fixture cache
|
||||
id: unit-java-cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: syft/cataloger/java/test-fixtures/java-builds/packages
|
||||
key: ${{ runner.os }}-unit-java-cache-${{ hashFiles( 'syft/cataloger/java/test-fixtures/java-builds/packages.fingerprint' ) }}
|
||||
|
||||
- name: Run unit tests
|
||||
run: make unit
|
||||
|
||||
- name: Validate syft output against the CycloneDX schema
|
||||
run: make validate-cyclonedx-schema
|
||||
|
||||
- name: Build key for tar cache
|
||||
run: make integration-fingerprint
|
||||
|
||||
- name: Restore integration test cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ github.workspace }}/integration/test-fixtures/cache
|
||||
key: ${{ runner.os }}-integration-test-cache-${{ hashFiles('integration/test-fixtures/cache.fingerprint') }}
|
||||
|
||||
- name: Run integration tests
|
||||
run: make integration
|
||||
@@ -0,0 +1,345 @@
|
||||
name: "Validations"
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
GO_VERSION: "1.16.x"
|
||||
|
||||
jobs:
|
||||
|
||||
Static-Analysis:
|
||||
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
|
||||
name: "Static analysis"
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Restore tool cache
|
||||
id: tool-cache
|
||||
uses: actions/cache@v2.1.3
|
||||
with:
|
||||
path: ${{ github.workspace }}/.tmp
|
||||
key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }}
|
||||
|
||||
- name: Restore go cache
|
||||
id: go-cache
|
||||
uses: actions/cache@v2.1.3
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-${{ env.GO_VERSION }}-
|
||||
|
||||
- name: (cache-miss) Bootstrap all project dependencies
|
||||
if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true'
|
||||
run: make bootstrap
|
||||
|
||||
- name: Bootstrap CI environment dependencies
|
||||
run: make ci-bootstrap
|
||||
|
||||
- name: Run static analysis
|
||||
run: make static-analysis
|
||||
|
||||
Unit-Test:
|
||||
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
|
||||
name: "Unit tests"
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Restore tool cache
|
||||
id: tool-cache
|
||||
uses: actions/cache@v2.1.3
|
||||
with:
|
||||
path: ${{ github.workspace }}/.tmp
|
||||
key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }}
|
||||
|
||||
- name: Restore go cache
|
||||
id: go-cache
|
||||
uses: actions/cache@v2.1.3
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-${{ env.GO_VERSION }}-
|
||||
|
||||
- name: (cache-miss) Bootstrap all project dependencies
|
||||
if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true'
|
||||
run: make bootstrap
|
||||
|
||||
- name: Bootstrap CI environment dependencies
|
||||
run: make ci-bootstrap
|
||||
|
||||
- name: Build cache key for java test-fixture blobs (for unit tests)
|
||||
run: make java-packages-fingerprint
|
||||
|
||||
- name: Restore Java test-fixture cache
|
||||
id: unit-java-cache
|
||||
uses: actions/cache@v2.1.3
|
||||
with:
|
||||
path: syft/pkg/cataloger/java/test-fixtures/java-builds/packages
|
||||
key: ${{ runner.os }}-unit-java-cache-${{ hashFiles( 'syft/pkg/cataloger/java/test-fixtures/java-builds/packages.fingerprint' ) }}
|
||||
|
||||
- name: Run unit tests
|
||||
run: make unit
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: unit-test-results
|
||||
path: test/results/**/*
|
||||
|
||||
Integration-Test:
|
||||
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
|
||||
name: "Integration tests"
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Restore tool cache
|
||||
id: tool-cache
|
||||
uses: actions/cache@v2.1.3
|
||||
with:
|
||||
path: ${{ github.workspace }}/.tmp
|
||||
key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }}
|
||||
|
||||
- name: Restore go cache
|
||||
id: go-cache
|
||||
uses: actions/cache@v2.1.3
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-${{ env.GO_VERSION }}-
|
||||
|
||||
- name: (cache-miss) Bootstrap all project dependencies
|
||||
if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true'
|
||||
run: make bootstrap
|
||||
|
||||
- name: Bootstrap CI environment dependencies
|
||||
run: make ci-bootstrap
|
||||
|
||||
- name: Validate syft output against the CycloneDX schema
|
||||
run: make validate-cyclonedx-schema
|
||||
|
||||
- name: Build key for tar cache
|
||||
run: make integration-fingerprint
|
||||
|
||||
- name: Restore integration test cache
|
||||
uses: actions/cache@v2.1.3
|
||||
with:
|
||||
path: ${{ github.workspace }}/test/integration/test-fixtures/cache
|
||||
key: ${{ runner.os }}-integration-test-cache-${{ hashFiles('test/integration/test-fixtures/cache.fingerprint') }}
|
||||
|
||||
- name: Run integration tests
|
||||
run: make integration
|
||||
|
||||
Benchmark-Test:
|
||||
name: "Benchmark tests"
|
||||
runs-on: ubuntu-20.04
|
||||
# note: we want benchmarks to run on pull_request events in order to publish results to a sticky comment, and
|
||||
# we also want to run on push such that merges to main are recorded to the cache. For this reason we don't filter
|
||||
# the job by event.
|
||||
steps:
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Restore tool cache
|
||||
id: tool-cache
|
||||
uses: actions/cache@v2.1.3
|
||||
with:
|
||||
path: ${{ github.workspace }}/.tmp
|
||||
key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }}
|
||||
|
||||
- name: Restore go cache
|
||||
id: go-cache
|
||||
uses: actions/cache@v2.1.3
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-${{ env.GO_VERSION }}-
|
||||
|
||||
- name: (cache-miss) Bootstrap all project dependencies
|
||||
if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true'
|
||||
run: make bootstrap
|
||||
|
||||
- name: Bootstrap CI environment dependencies
|
||||
run: make ci-bootstrap
|
||||
|
||||
- name: Restore base benchmark result
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: test/results/benchmark-main.txt
|
||||
# use base sha for PR or new commit hash for main push in benchmark result key
|
||||
key: ${{ runner.os }}-bench-${{ (github.event.pull_request.base.sha != github.event.after) && github.event.pull_request.base.sha || github.event.after }}
|
||||
|
||||
- name: Run benchmark tests
|
||||
id: benchmark
|
||||
run: |
|
||||
REF_NAME=${GITHUB_REF##*/} make benchmark
|
||||
OUTPUT=$(make show-benchstat)
|
||||
OUTPUT="${OUTPUT//'%'/'%25'}" # URL encode all '%' characters
|
||||
OUTPUT="${OUTPUT//$'\n'/'%0A'}" # URL encode all '\n' characters
|
||||
OUTPUT="${OUTPUT//$'\r'/'%0D'}" # URL encode all '\r' characters
|
||||
echo "::set-output name=result::$OUTPUT"
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: benchmark-test-results
|
||||
path: test/results/**/*
|
||||
|
||||
- name: Update PR benchmark results comment
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
continue-on-error: true
|
||||
with:
|
||||
header: benchmark
|
||||
message: |
|
||||
### Benchmark Test Results
|
||||
|
||||
<details>
|
||||
<summary>Benchmark results from the latest changes vs base branch</summary>
|
||||
|
||||
```
|
||||
${{ steps.benchmark.outputs.result }}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
Build-Snapshot-Artifacts:
|
||||
name: "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: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
- name: Restore tool cache
|
||||
id: tool-cache
|
||||
uses: actions/cache@v2.1.3
|
||||
with:
|
||||
path: ${{ github.workspace }}/.tmp
|
||||
key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }}
|
||||
|
||||
- name: Restore go cache
|
||||
id: go-cache
|
||||
uses: actions/cache@v2.1.3
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-${{ env.GO_VERSION }}-
|
||||
|
||||
- name: (cache-miss) Bootstrap all project dependencies
|
||||
if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true'
|
||||
run: make bootstrap
|
||||
|
||||
- name: Build snapshot artifacts
|
||||
run: make snapshot
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: artifacts
|
||||
path: snapshot/**/*
|
||||
|
||||
Acceptance-Linux:
|
||||
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
|
||||
name: "Acceptance tests (Linux)"
|
||||
needs: [Build-Snapshot-Artifacts]
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: artifacts
|
||||
path: snapshot
|
||||
|
||||
- name: Run Acceptance Tests (Linux)
|
||||
run: make acceptance-linux
|
||||
|
||||
Acceptance-Mac:
|
||||
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
|
||||
name: "Acceptance tests (Mac)"
|
||||
needs: [Build-Snapshot-Artifacts]
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: artifacts
|
||||
path: snapshot
|
||||
|
||||
- name: Restore docker image cache
|
||||
id: go-cache
|
||||
uses: actions/cache@v2.1.3
|
||||
with:
|
||||
path: image.tar
|
||||
key: ${{ runner.os }}-${{ hashFiles('test/acceptance/mac.sh') }}
|
||||
|
||||
- name: Run Acceptance Tests (Mac)
|
||||
run: make acceptance-mac
|
||||
|
||||
Cli-Linux:
|
||||
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
|
||||
name: "CLI tests (Linux)"
|
||||
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
|
||||
id: go-cache
|
||||
uses: actions/cache@v2.1.3
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-${{ env.GO_VERSION }}-
|
||||
|
||||
- name: (cache-miss) Bootstrap go dependencies
|
||||
if: steps.go-cache.outputs.cache-hit != 'true'
|
||||
run: make bootstrap-go
|
||||
|
||||
- name: Build key for tar cache
|
||||
run: make cli-fingerprint
|
||||
|
||||
- name: Restore CLI test cache
|
||||
uses: actions/cache@v2.1.3
|
||||
with:
|
||||
path: ${{ github.workspace }}/test/cli/test-fixtures/cache
|
||||
key: ${{ runner.os }}-cli-test-cache-${{ hashFiles('test/cli/test-fixtures/cache.fingerprint') }}
|
||||
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: artifacts
|
||||
path: snapshot
|
||||
|
||||
- name: Run CLI Tests (Linux)
|
||||
run: make cli
|
||||
@@ -1,4 +1,5 @@
|
||||
CHANGELOG.md
|
||||
/test/results
|
||||
/dist
|
||||
/snapshot
|
||||
.server/
|
||||
|
||||
+5
-3
@@ -15,6 +15,7 @@ linters:
|
||||
- dogsled
|
||||
- dupl
|
||||
- errcheck
|
||||
- exportloopref
|
||||
- funlen
|
||||
- gocognit
|
||||
- goconst
|
||||
@@ -22,7 +23,6 @@ linters:
|
||||
- gocyclo
|
||||
- gofmt
|
||||
- goimports
|
||||
- golint
|
||||
- goprintffuncname
|
||||
- gosec
|
||||
- gosimple
|
||||
@@ -31,8 +31,8 @@ linters:
|
||||
- misspell
|
||||
- nakedret
|
||||
- nolintlint
|
||||
- revive
|
||||
- rowserrcheck
|
||||
- scopelint
|
||||
- staticcheck
|
||||
- structcheck
|
||||
- stylecheck
|
||||
@@ -49,11 +49,13 @@ linters:
|
||||
# - godot
|
||||
# - godox
|
||||
# - goerr113
|
||||
# - golint # deprecated
|
||||
# - gomnd # this is too aggressive
|
||||
# - interfacer # this is a good idea, but is no longer supported and is prone to false positives
|
||||
# - lll # without a way to specify per-line exception cases, this is not usable
|
||||
# - maligned # this is an excellent linter, but tricky to optimize and we are not sensitive to memory layout optimizations
|
||||
# - nestif
|
||||
# - prealloc # following this rule isn't consistently a good idea, as it sometimes forces unnecessary allocations that result in less idiomatic code
|
||||
# - scopelint # deprecated
|
||||
# - testpackage
|
||||
# - wsl
|
||||
# - wsl # this doens't have an auto-fixer yet and is pretty noisy (https://github.com/bombsimon/wsl/issues/90)
|
||||
|
||||
+72
-4
@@ -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
|
||||
@@ -90,3 +111,50 @@ brews:
|
||||
name: homebrew-syft
|
||||
homepage: *website
|
||||
description: *description
|
||||
|
||||
dockers:
|
||||
- image_templates:
|
||||
- "anchore/syft:latest"
|
||||
- "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}}"
|
||||
|
||||
- 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
|
||||
|
||||
+11
-6
@@ -1,12 +1,12 @@
|
||||
# Contributing to `syft`
|
||||
# Contributing to Syft
|
||||
|
||||
If you are looking to contribute to this project and want to open a Github pull request ("PR"), there are a few guidelines of what we are looking for in patches. Make sure you go through this document and ensure that your code proposal is aligned.
|
||||
If you are looking to contribute to this project and want to open a GitHub pull request ("PR"), there are a few guidelines of what we are looking for in patches. Make sure you go through this document and ensure that your code proposal is aligned.
|
||||
|
||||
## Sign your work
|
||||
## Sign off your work
|
||||
|
||||
The `sign-off` is an added line at the end of the explanation for the commit, certifying that you wrote it or otherwise have the right to submit it as an open-source patch. By submitting a contribution, you agree to be bound by the terms of the DCO Version 1.1 and Apache License Version 2.0.
|
||||
|
||||
Signing a commit certifies the below Developer's Certificate of Origin (DCO):
|
||||
Signing off a commit certifies the below Developer's Certificate of Origin (DCO):
|
||||
|
||||
```text
|
||||
Developer's Certificate of Origin 1.1
|
||||
@@ -51,13 +51,13 @@ $ git config --global user.name "John Doe"
|
||||
$ git config --global user.email "john.doe@example.com"
|
||||
```
|
||||
|
||||
Creating a signed commit is then possible with `-s`:
|
||||
Creating a signed-off commit is then possible with `-s` or `--signoff`:
|
||||
|
||||
```text
|
||||
$ git commit -s -m "this is a commit message"
|
||||
```
|
||||
|
||||
To double check if the commit was signed, look at the log output:
|
||||
To double-check that the commit was signed-off, look at the log output:
|
||||
|
||||
```text
|
||||
$ git log -1
|
||||
@@ -82,6 +82,11 @@ Date: Mon Aug 1 11:27:13 2020 -0400
|
||||
|
||||
[//]: # (TODO: PR title and description)
|
||||
|
||||
## Sign your commits
|
||||
|
||||
To ensure the authenticity and integrity of code contributions, **we require that all commits are signed**. Signing commits proves that your commits were truly created by you, as the holder of a private key.
|
||||
|
||||
Configuring git to sign your commits is a straightforward process. To get set up, see [GitHub's documentation on signing commits](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits).
|
||||
|
||||
## Test your changes
|
||||
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
FROM alpine:latest AS build
|
||||
|
||||
RUN apk --no-cache add ca-certificates
|
||||
|
||||
FROM scratch
|
||||
# needed for version check HTTPS request
|
||||
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
|
||||
# create the /tmp dir, which is needed for image content cache
|
||||
WORKDIR /tmp
|
||||
|
||||
COPY syft /
|
||||
|
||||
ARG BUILD_DATE
|
||||
ARG BUILD_VERSION
|
||||
ARG VCS_REF
|
||||
ARG VCS_URL
|
||||
|
||||
LABEL org.label-schema.schema-version="1.0"
|
||||
LABEL org.label-schema.build-date=$BUILD_DATE
|
||||
LABEL org.label-schema.name="syft"
|
||||
LABEL org.label-schema.description="CLI tool and library for generating a Software Bill of Materials from container images and filesystems"
|
||||
LABEL org.label-schema.vcs-url=$VCS_URL
|
||||
LABEL org.label-schema.vcs-ref=$VCS_REF
|
||||
LABEL org.label-schema.vendor="Anchore, Inc."
|
||||
LABEL org.label-schema.version=$BUILD_VERSION
|
||||
|
||||
ENTRYPOINT ["/syft"]
|
||||
@@ -1,9 +1,9 @@
|
||||
BIN = syft
|
||||
TEMPDIR = ./.tmp
|
||||
RESULTSDIR = $(TEMPDIR)/results
|
||||
COVER_REPORT = $(RESULTSDIR)/cover.report
|
||||
COVER_TOTAL = $(RESULTSDIR)/cover.total
|
||||
LINTCMD = $(TEMPDIR)/golangci-lint run --tests=false --config .golangci.yaml
|
||||
RESULTSDIR = test/results
|
||||
COVER_REPORT = $(RESULTSDIR)/unit-coverage-details.txt
|
||||
COVER_TOTAL = $(RESULTSDIR)/unit-coverage-summary.txt
|
||||
LINTCMD = $(TEMPDIR)/golangci-lint run --tests=false --timeout=2m --config .golangci.yaml
|
||||
ACC_TEST_IMAGE = centos:8.2.2004
|
||||
ACC_DIR = ./test/acceptance
|
||||
BOLD := $(shell tput -T linux bold)
|
||||
@@ -15,25 +15,28 @@ 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 := 68
|
||||
COVERAGE_THRESHOLD := 62
|
||||
# CI cache busting values; change these if you want CI to not use previous stored cache
|
||||
COMPARE_CACHE_BUSTER="f7e689d76a9"
|
||||
INTEGRATION_CACHE_BUSTER="789bacdf"
|
||||
INTEGRATION_CACHE_BUSTER="88738d2f"
|
||||
CLI_CACHE_BUSTER="9a2c03cf"
|
||||
BOOTSTRAP_CACHE="c7afb99ad"
|
||||
|
||||
## Build variables
|
||||
DISTDIR=./dist
|
||||
SNAPSHOTDIR=./snapshot
|
||||
GITTREESTATE=$(if $(shell git status --porcelain),dirty,clean)
|
||||
SNAPSHOT_CMD=$(shell realpath $(shell pwd)/$(SNAPSHOTDIR)/syft_linux_amd64/syft)
|
||||
OS := $(shell uname)
|
||||
|
||||
ifeq ($(OS),Darwin)
|
||||
SNAPSHOT_CMD=$(shell realpath $(shell pwd)/$(SNAPSHOTDIR)/$(BIN)-macos_darwin_amd64/$(BIN))
|
||||
else
|
||||
SNAPSHOT_CMD=$(shell realpath $(shell pwd)/$(SNAPSHOTDIR)/$(BIN)_linux_amd64/$(BIN))
|
||||
endif
|
||||
|
||||
ifeq "$(strip $(VERSION))" ""
|
||||
override VERSION = $(shell git describe --always --tags --dirty)
|
||||
endif
|
||||
|
||||
# used to generate the changelog from the second to last tag to the current tag (used in the release pipeline when the release tag is in place)
|
||||
LAST_TAG := $(shell git describe --abbrev=0 --tags $(shell git rev-list --tags --max-count=1))
|
||||
SECOND_TO_LAST_TAG := $(shell git describe --abbrev=0 --tags $(shell git rev-list --tags --skip=1 --max-count=1))
|
||||
|
||||
## Variable assertions
|
||||
|
||||
ifndef TEMPDIR
|
||||
@@ -56,6 +59,10 @@ ifndef SNAPSHOTDIR
|
||||
$(error SNAPSHOTDIR is not set)
|
||||
endif
|
||||
|
||||
ifndef REF_NAME
|
||||
REF_NAME = $(VERSION)
|
||||
endif
|
||||
|
||||
define title
|
||||
@printf '$(TITLE)$(1)$(RESET)\n'
|
||||
endef
|
||||
@@ -67,7 +74,7 @@ all: clean static-analysis test ## Run all linux-based checks (linting, license
|
||||
@printf '$(SUCCESS)All checks pass!$(RESET)\n'
|
||||
|
||||
.PHONY: test
|
||||
test: unit validate-cyclonedx-schema integration acceptance-linux ## Run all tests (currently unit, integration, and linux acceptance tests)
|
||||
test: unit validate-cyclonedx-schema integration benchmark acceptance-linux cli ## Run all tests (currently unit, integration, linux acceptance, and cli tests)
|
||||
|
||||
.PHONY: help
|
||||
help:
|
||||
@@ -81,22 +88,30 @@ ci-bootstrap:
|
||||
ci-bootstrap-mac:
|
||||
github_changelog_generator --version || sudo gem install github_changelog_generator
|
||||
|
||||
.PHONY: bootstrap
|
||||
bootstrap: ## Download and install all go dependencies (+ prep tooling in the ./tmp dir)
|
||||
$(call title,Bootstrapping dependencies)
|
||||
@pwd
|
||||
# prep temp dirs
|
||||
mkdir -p $(TEMPDIR)
|
||||
$(RESULTSDIR):
|
||||
mkdir -p $(RESULTSDIR)
|
||||
# install go dependencies
|
||||
|
||||
$(TEMPDIR):
|
||||
mkdir -p $(TEMPDIR)
|
||||
|
||||
.PHONY: bootstrap-tools
|
||||
bootstrap-tools: $(TEMPDIR)
|
||||
GO111MODULE=off GOBIN=$(shell realpath $(TEMPDIR)) go get -u golang.org/x/perf/cmd/benchstat
|
||||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(TEMPDIR)/ v1.42.1
|
||||
curl -sSfL https://raw.githubusercontent.com/wagoodman/go-bouncer/master/bouncer.sh | sh -s -- -b $(TEMPDIR)/ v0.2.0
|
||||
curl -sSfL https://raw.githubusercontent.com/anchore/chronicle/main/install.sh | sh -s -- -b $(TEMPDIR)/ v0.3.0
|
||||
.github/scripts/goreleaser-install.sh -b $(TEMPDIR)/ v0.177.0
|
||||
|
||||
.PHONY: bootstrap-go
|
||||
bootstrap-go:
|
||||
go mod download
|
||||
# install utilities
|
||||
[ -f "$(TEMPDIR)/golangci" ] || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(TEMPDIR)/ v1.26.0
|
||||
[ -f "$(TEMPDIR)/bouncer" ] || curl -sSfL https://raw.githubusercontent.com/wagoodman/go-bouncer/master/bouncer.sh | sh -s -- -b $(TEMPDIR)/ v0.2.0
|
||||
[ -f "$(TEMPDIR)/goreleaser" ] || curl -sfL https://install.goreleaser.com/github.com/goreleaser/goreleaser.sh | sh -s -- -b $(TEMPDIR)/ v0.140.0
|
||||
|
||||
.PHONY: bootstrap
|
||||
bootstrap: $(RESULTSDIR) bootstrap-go bootstrap-tools ## Download and install all go dependencies (+ prep tooling in the ./tmp dir)
|
||||
$(call title,Bootstrapping dependencies)
|
||||
|
||||
.PHONY: static-analysis
|
||||
static-analysis: lint check-licenses
|
||||
static-analysis: lint check-go-mod-tidy check-licenses
|
||||
|
||||
.PHONY: lint
|
||||
lint: ## Run gofmt + golangci lint checks
|
||||
@@ -117,51 +132,68 @@ lint-fix: ## Auto-format all source code + run golangci lint fixers
|
||||
$(call title,Running lint fixers)
|
||||
gofmt -w -s .
|
||||
$(LINTCMD) --fix
|
||||
go mod tidy
|
||||
|
||||
.PHONY: check-licenses
|
||||
check-licenses:
|
||||
check-licenses: ## Ensure transitive dependencies are compliant with the current license policy
|
||||
$(TEMPDIR)/bouncer check
|
||||
|
||||
check-go-mod-tidy:
|
||||
@ .github/scripts/go-mod-tidy-check.sh && echo "go.mod and go.sum are tidy!"
|
||||
|
||||
.PHONY: validate-cyclonedx-schema
|
||||
validate-cyclonedx-schema:
|
||||
cd schema/cyclonedx && make
|
||||
|
||||
.PHONY: unit
|
||||
unit: fixtures ## Run unit tests (with coverage)
|
||||
unit: $(RESULTSDIR) fixtures ## Run unit tests (with coverage)
|
||||
$(call title,Running unit tests)
|
||||
go test -coverprofile $(COVER_REPORT) $(shell go list ./... | grep -v anchore/syft/test)
|
||||
go test -coverprofile $(COVER_REPORT) $(shell go list ./... | grep -v anchore/syft/test)
|
||||
@go tool cover -func $(COVER_REPORT) | grep total | awk '{print substr($$3, 1, length($$3)-1)}' > $(COVER_TOTAL)
|
||||
@echo "Coverage: $$(cat $(COVER_TOTAL))"
|
||||
@if [ $$(echo "$$(cat $(COVER_TOTAL)) >= $(COVERAGE_THRESHOLD)" | bc -l) -ne 1 ]; then echo "$(RED)$(BOLD)Failed coverage quality gate (> $(COVERAGE_THRESHOLD)%)$(RESET)" && false; fi
|
||||
|
||||
.PHONY: benchmark
|
||||
benchmark: $(RESULTSDIR) ## Run benchmark tests and compare against the baseline (if available)
|
||||
$(call title,Running benchmark tests)
|
||||
go test -p 1 -run=^Benchmark -bench=. -count=5 -benchmem ./... | tee $(RESULTSDIR)/benchmark-$(REF_NAME).txt
|
||||
(test -s $(RESULTSDIR)/benchmark-main.txt && \
|
||||
$(TEMPDIR)/benchstat $(RESULTSDIR)/benchmark-main.txt $(RESULTSDIR)/benchmark-$(REF_NAME).txt || \
|
||||
$(TEMPDIR)/benchstat $(RESULTSDIR)/benchmark-$(REF_NAME).txt) \
|
||||
| tee $(RESULTSDIR)/benchstat.txt
|
||||
|
||||
.PHONY: show-benchstat
|
||||
show-benchstat:
|
||||
@cat $(RESULTSDIR)/benchstat.txt
|
||||
|
||||
.PHONY: integration
|
||||
integration: ## Run integration tests
|
||||
$(call title,Running integration tests)
|
||||
|
||||
go test -v ./test/integration
|
||||
|
||||
|
||||
# note: this is used by CI to determine if the integration test fixture cache (docker image tars) should be busted
|
||||
integration-fingerprint:
|
||||
find test/integration/test-fixtures/image-* -type f -exec md5sum {} + | awk '{print $1}' | sort | md5sum | tee test/integration/test-fixtures/cache.fingerprint && echo "$(INTEGRATION_CACHE_BUSTER)" >> test/integration/test-fixtures/cache.fingerprint
|
||||
|
||||
.PHONY: java-packages-fingerprint
|
||||
java-packages-fingerprint:
|
||||
@cd syft/cataloger/java/test-fixtures/java-builds && \
|
||||
@cd syft/pkg/cataloger/java/test-fixtures/java-builds && \
|
||||
make packages.fingerprint
|
||||
|
||||
.PHONY: fixtures
|
||||
fixtures:
|
||||
$(call title,Generating test fixtures)
|
||||
cd syft/cataloger/java/test-fixtures/java-builds && make
|
||||
cd syft/pkg/cataloger/java/test-fixtures/java-builds && make
|
||||
|
||||
.PHONY: generate-json-schema
|
||||
generate-json-schema: ## Generate a new json schema
|
||||
cd schema/json && go run generate.go
|
||||
|
||||
.PHONY: clear-test-cache
|
||||
clear-test-cache: ## Delete all test cache (built docker image tars)
|
||||
find . -type f -wholename "**/test-fixtures/cache/*.tar" -delete
|
||||
.PHONY: generate-license-list
|
||||
generate-license-list: ## Generate an updated spdx license list
|
||||
go generate ./internal/spdxlicense/...
|
||||
gofmt -s -w ./internal/spdxlicense
|
||||
|
||||
.PHONY: build
|
||||
build: $(SNAPSHOTDIR) ## Build release snapshot binaries and packages
|
||||
@@ -173,12 +205,14 @@ $(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) \
|
||||
$(TEMPDIR)/goreleaser release --skip-publish --rm-dist --snapshot --config $(TEMPDIR)/goreleaser.yaml
|
||||
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
|
||||
.PHONY: acceptance-mac
|
||||
acceptance-mac: $(SNAPSHOTDIR) ## Run acceptance tests on build snapshot binaries and packages (Mac)
|
||||
acceptance-mac: $(RESULTSDIR) $(SNAPSHOTDIR) ## Run acceptance tests on build snapshot binaries and packages (Mac)
|
||||
$(call title,Running acceptance test: Run on Mac)
|
||||
$(ACC_DIR)/mac.sh \
|
||||
$(SNAPSHOTDIR) \
|
||||
@@ -190,22 +224,8 @@ acceptance-mac: $(SNAPSHOTDIR) ## Run acceptance tests on build snapshot binarie
|
||||
.PHONY: acceptance-linux
|
||||
acceptance-linux: acceptance-test-deb-package-install acceptance-test-rpm-package-install ## Run acceptance tests on build snapshot binaries and packages (Linux)
|
||||
|
||||
# note: this is used by CI to determine if the inline-scan report cache should be busted for the inline-compare tests
|
||||
.PHONY: compare-fingerprint
|
||||
compare-fingerprint:
|
||||
find test/inline-compare/* -type f -exec md5sum {} + | grep -v '\-reports' | grep -v 'fingerprint' | awk '{print $1}' | sort | md5sum | tee test/inline-compare/inline-compare.fingerprint && echo "$(COMPARE_CACHE_BUSTER)" >> test/inline-compare/inline-compare.fingerprint
|
||||
|
||||
.PHONY: compare-snapshot
|
||||
compare-snapshot: $(SNAPSHOTDIR) ## Compare the reports of a run of a snapshot build of syft against inline-scan
|
||||
chmod 755 $(SNAPSHOT_CMD)
|
||||
@cd test/inline-compare && SYFT_CMD=$(SNAPSHOT_CMD) make
|
||||
|
||||
.PHONY: compare
|
||||
compare: ## Compare the reports of a run of a main-branch build of syft against inline-scan
|
||||
@cd test/inline-compare && make
|
||||
|
||||
.PHONY: acceptance-test-deb-package-install
|
||||
acceptance-test-deb-package-install: $(SNAPSHOTDIR)
|
||||
acceptance-test-deb-package-install: $(RESULTSDIR) $(SNAPSHOTDIR)
|
||||
$(call title,Running acceptance test: DEB install)
|
||||
$(ACC_DIR)/deb.sh \
|
||||
$(SNAPSHOTDIR) \
|
||||
@@ -214,7 +234,7 @@ acceptance-test-deb-package-install: $(SNAPSHOTDIR)
|
||||
$(RESULTSDIR)
|
||||
|
||||
.PHONY: acceptance-test-rpm-package-install
|
||||
acceptance-test-rpm-package-install: $(SNAPSHOTDIR)
|
||||
acceptance-test-rpm-package-install: $(RESULTSDIR) $(SNAPSHOTDIR)
|
||||
$(call title,Running acceptance test: RPM install)
|
||||
$(ACC_DIR)/rpm.sh \
|
||||
$(SNAPSHOTDIR) \
|
||||
@@ -222,56 +242,49 @@ acceptance-test-rpm-package-install: $(SNAPSHOTDIR)
|
||||
$(ACC_TEST_IMAGE) \
|
||||
$(RESULTSDIR)
|
||||
|
||||
.PHONY: changlog-release
|
||||
changelog-release:
|
||||
@echo "Last tag: $(SECOND_TO_LAST_TAG)"
|
||||
@echo "Current tag: $(VERSION)"
|
||||
@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)
|
||||
# note: this is used by CI to determine if the integration test fixture cache (docker image tars) should be busted
|
||||
cli-fingerprint:
|
||||
find test/cli/test-fixtures/image-* -type f -exec md5sum {} + | awk '{print $1}' | sort | md5sum | tee test/cli/test-fixtures/cache.fingerprint && echo "$(CLI_CACHE_BUSTER)" >> test/cli/test-fixtures/cache.fingerprint
|
||||
|
||||
@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)
|
||||
|
||||
@printf '\n$(BOLD)$(CYAN)Unreleased Changes (closed PRs and issues will not be in the final changelog)$(RESET)\n'
|
||||
.PHONY: cli
|
||||
cli: $(SNAPSHOTDIR) ## Run CLI tests
|
||||
chmod 755 "$(SNAPSHOT_CMD)"
|
||||
$(SNAPSHOT_CMD) version
|
||||
SYFT_BINARY_LOCATION='$(SNAPSHOT_CMD)' \
|
||||
go test -count=1 -v ./test/cli
|
||||
|
||||
.PHONY: changelog
|
||||
changelog: clean-changelog CHANGELOG.md
|
||||
@docker run -it --rm \
|
||||
-v $(shell pwd)/CHANGELOG.md:/CHANGELOG.md \
|
||||
rawkode/mdv \
|
||||
-t 748.5989 \
|
||||
/CHANGELOG.md
|
||||
|
||||
CHANGELOG.md:
|
||||
$(TEMPDIR)/chronicle -vv > CHANGELOG.md
|
||||
|
||||
.PHONY: release
|
||||
release: clean-dist ci-bootstrap-mac changelog-release ## Build and publish final binaries and packages. Intended to be run only on macOS.
|
||||
release: clean-dist CHANGELOG.md ## Build and publish final binaries and packages. Intended to be run only on macOS.
|
||||
$(call title,Publishing release artifacts)
|
||||
|
||||
# Prepare for macOS-specific signing process
|
||||
.github/scripts/mac-prepare-for-signing.sh
|
||||
|
||||
# login to docker
|
||||
# note: the previous step creates a new keychain, so it is important to reauth into docker.io
|
||||
@echo $${DOCKER_PASSWORD} | docker login docker.io -u $${DOCKER_USERNAME} --password-stdin
|
||||
|
||||
# create a config with the dist dir overridden
|
||||
echo "dist: $(DISTDIR)" > $(TEMPDIR)/goreleaser.yaml
|
||||
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 \
|
||||
@@ -283,8 +296,9 @@ release: clean-dist ci-bootstrap-mac changelog-release ## Build and publish fina
|
||||
# upload the version file that supports the application version update check (excluding pre-releases)
|
||||
.github/scripts/update-version-file.sh "$(DISTDIR)" "$(VERSION)"
|
||||
|
||||
|
||||
.PHONY: clean
|
||||
clean: clean-dist clean-snapshot ## Remove previous builds and result reports
|
||||
clean: clean-dist clean-snapshot clean-test-image-cache ## Remove previous builds, result reports, and test cache
|
||||
rm -rf $(RESULTSDIR)/*
|
||||
|
||||
.PHONY: clean-snapshot
|
||||
@@ -292,5 +306,32 @@ clean-snapshot:
|
||||
rm -rf $(SNAPSHOTDIR) $(TEMPDIR)/goreleaser.yaml
|
||||
|
||||
.PHONY: clean-dist
|
||||
clean-dist:
|
||||
clean-dist: clean-changelog
|
||||
rm -rf $(DISTDIR) $(TEMPDIR)/goreleaser.yaml
|
||||
|
||||
.PHONY: clean-changelog
|
||||
clean-changelog:
|
||||
rm -f CHANGELOG.md
|
||||
|
||||
clean-test-image-cache: clean-test-image-tar-cache clean-test-image-docker-cache
|
||||
|
||||
.PHONY: clear-test-image-tar-cache
|
||||
clean-test-image-tar-cache: ## Delete all test cache (built docker image tars)
|
||||
find . -type f -wholename "**/test-fixtures/cache/stereoscope-fixture-*.tar" -delete
|
||||
|
||||
.PHONY: clear-test-image-docker-cache
|
||||
clean-test-image-docker-cache: ## Purge all test docker images
|
||||
docker images --format '{{.ID}} {{.Repository}}' | grep stereoscope-fixture- | awk '{print $$1}' | uniq | xargs docker rmi --force
|
||||
|
||||
.PHONY: show-test-image-cache
|
||||
show-test-image-cache: ## Show all docker and image tar cache
|
||||
$(call title,Docker daemon cache)
|
||||
@docker images --format '{{.ID}} {{.Repository}}:{{.Tag}}' | grep stereoscope-fixture- | sort
|
||||
|
||||
$(call title,Tar cache)
|
||||
@find . -type f -wholename "**/test-fixtures/cache/stereoscope-fixture-*.tar" | sort
|
||||
|
||||
.PHONY: show-test-snapshots
|
||||
show-test-snapshots: ## Show all test snapshots
|
||||
$(call title,Test snapshots)
|
||||
@find . -type f -wholename "**/test-fixtures/snapshot/*" | sort
|
||||
|
||||
@@ -1,23 +1,59 @@
|
||||
# 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?query=workflow%3A%22Static+Analysis+%2B+Unit+%2B+Integration%22)
|
||||
[](https://github.com/anchore/syft/actions?query=workflow%3AAcceptance)
|
||||
[](https://github.com/anchore/syft/actions/workflows/validations.yaml)
|
||||
[](https://goreportcard.com/report/github.com/anchore/syft)
|
||||
[](https://github.com/anchore/syft/releases/latest)
|
||||
[](https://github.com/anchore/syft)
|
||||
[](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.
|
||||
A CLI tool and Go library for generating a Software Bill of Materials (SBOM) from container images and filesystems. Exceptional for vulnerability detection when used with a scanner tool like [Grype](https://github.com/anchore/grype).
|
||||
|
||||
### Join us at the virtual OSS meetup!
|
||||
|
||||
When: December 1, 2021 @ 11am-noon PT (2-3pm ET)
|
||||
|
||||
We’ve got security experts from VMware and Anchore who will share their tips, tricks, and lessons learned on securing containers using [Syft](https://github.com/anchore/syft), [Grype](https://github.com/anchore/grype), and [Cartographer](https://github.com/vmware-tanzu/cartographer).
|
||||
|
||||
Register now: https://get.anchore.com/anchore-oss-meetup-dec-1-2021/
|
||||
|
||||
---
|
||||
|
||||

|
||||
|
||||
**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)
|
||||
- Supports Docker and OCI image formats
|
||||
- Direct support for [Grype](https://github.com/anchore/grype), a fast and powerful vulnerability matcher.
|
||||
|
||||
|
||||
If you encounter an issue, please [let us know using the issue tracker](https://github.com/anchore/syft/issues).
|
||||
|
||||
## Installation
|
||||
|
||||
### Recommended
|
||||
```bash
|
||||
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
|
||||
```
|
||||
|
||||
...or, you can specify a release version and destination directory for the installation:
|
||||
|
||||
```
|
||||
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b <DESTINATION_DIR> <RELEASE_VERSION>
|
||||
```
|
||||
|
||||
### Homebrew
|
||||
```bash
|
||||
brew tap anchore/syft
|
||||
brew install syft
|
||||
```
|
||||
|
||||
**Note**: Currently, Syft is built only for macOS and Linux.
|
||||
|
||||
## Getting started
|
||||
|
||||
To generate an SBOM for a Docker or OCI image:
|
||||
@@ -25,49 +61,139 @@ To generate an SBOM for a Docker or OCI image:
|
||||
syft <image>
|
||||
```
|
||||
|
||||
**Note**: This is equivalent to specifying the `packages` subcommand:
|
||||
```
|
||||
syft packages <image>
|
||||
```
|
||||
|
||||
The above output includes only software that is visible in the container (i.e., the squashed representation of the image).
|
||||
To include software from all image layers in the SBOM, regardless of its presence in the final image, provide `--scope all-layers`:
|
||||
|
||||
```
|
||||
syft <image> --scope all-layers
|
||||
syft packages <image> --scope all-layers
|
||||
```
|
||||
|
||||
### Supported sources
|
||||
|
||||
Syft can generate a SBOM from a variety of sources:
|
||||
|
||||
```
|
||||
# catalog a container image archive (from the result of `docker image save ...`, `podman save ...`, or `skopeo copy` commands)
|
||||
syft path/to/image.tar
|
||||
syft packages path/to/image.tar
|
||||
|
||||
# catalog a directory
|
||||
syft path/to/dir
|
||||
syft packages path/to/dir
|
||||
```
|
||||
|
||||
Sources can be explicitly provided with a scheme:
|
||||
|
||||
```
|
||||
docker:yourrepo/yourimage:tag use images from the Docker daemon
|
||||
docker-archive:path/to/yourimage.tar use a tarball from disk for archives created from "docker save"
|
||||
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)
|
||||
```
|
||||
|
||||
### Output formats
|
||||
|
||||
The output format for Syft is configurable as well:
|
||||
|
||||
```
|
||||
syft <image> -o <format>
|
||||
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](https://cyclonedx.org/) specification.
|
||||
- `cyclonedx`: A XML report conforming to the [CycloneDX 1.2 specification](https://cyclonedx.org/specification/overview/).
|
||||
- `spdx`: A tag-value formatted report conforming to the [SPDX 2.2 specification](https://spdx.github.io/spdx-spec/).
|
||||
- `spdx-json`: A JSON report conforming to the [SPDX 2.2 JSON Schema](https://github.com/spdx/spdx-spec/blob/v2.2/schemas/spdx-schema.json).
|
||||
- `table`: A columnar summary (default).
|
||||
|
||||
## Installation
|
||||
## Private Registry Authentication
|
||||
|
||||
**Recommended (macOS and Linux)**
|
||||
```bash
|
||||
# install the latest version to /usr/local/bin
|
||||
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
|
||||
### 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).
|
||||
|
||||
# install a specific version into a specific dir
|
||||
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b <SOME_BIN_PATH> <RELEASE_VERSION>
|
||||
|
||||
An example `config.json` looks something like this:
|
||||
```
|
||||
// config.json
|
||||
{
|
||||
"auths": {
|
||||
"registry.example.com": {
|
||||
"username": "AzureDiamond",
|
||||
"password": "hunter2"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Homebrew (macOS)**
|
||||
```bash
|
||||
brew tap anchore/syft
|
||||
brew install syft
|
||||
```
|
||||
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
|
||||
|
||||
@@ -75,6 +201,8 @@ Configuration search paths:
|
||||
|
||||
- `.syft.yaml`
|
||||
- `.syft/config.yaml`
|
||||
- `<git repo root>/.syft.yaml`
|
||||
- `<git repo root>/.syft/config.yaml`
|
||||
- `~/.syft.yaml`
|
||||
- `<XDG_CONFIG_HOME>/syft/config.yaml`
|
||||
|
||||
@@ -85,18 +213,127 @@ Configuration options (example values are the default):
|
||||
# same as -o ; SYFT_OUTPUT env var
|
||||
output: "table"
|
||||
|
||||
# the search space to look for packages (options: all-layers, squashed)
|
||||
# same as -s ; SYFT_SCOPE env var
|
||||
scope: "squashed"
|
||||
|
||||
# suppress all output (except for the SBOM report)
|
||||
# same as -q ; SYFT_QUIET env var
|
||||
quiet: false
|
||||
|
||||
# same as --file; write output report to a file (default is to write to stdout)
|
||||
file: ""
|
||||
|
||||
# enable/disable checking for application updates on startup
|
||||
# same as SYFT_CHECK_FOR_APP_UPDATE env var
|
||||
check-for-app-update: true
|
||||
|
||||
# cataloging packages is exposed through the packages and power-user subcommands
|
||||
package:
|
||||
cataloger:
|
||||
# enable/disable cataloging of packages
|
||||
# SYFT_PACKAGE_CATALOGER_ENABLED env var
|
||||
enabled: true
|
||||
|
||||
# the search space to look for packages (options: all-layers, squashed)
|
||||
# same as -s ; SYFT_PACKAGE_CATALOGER_SCOPE env var
|
||||
scope: "squashed"
|
||||
|
||||
# cataloging file classifications is exposed through the power-user subcommand
|
||||
file-classification:
|
||||
cataloger:
|
||||
# enable/disable cataloging of file classifications
|
||||
# SYFT_FILE_CLASSIFICATION_CATALOGER_ENABLED env var
|
||||
enabled: true
|
||||
|
||||
# the search space to look for file classifications (options: all-layers, squashed)
|
||||
# SYFT_FILE_CLASSIFICATION_CATALOGER_SCOPE env var
|
||||
scope: "squashed"
|
||||
|
||||
# cataloging file contents is exposed through the power-user subcommand
|
||||
file-contents:
|
||||
cataloger:
|
||||
# enable/disable cataloging of secrets
|
||||
# SYFT_FILE_CONTENTS_CATALOGER_ENABLED env var
|
||||
enabled: true
|
||||
|
||||
# the search space to look for secrets (options: all-layers, squashed)
|
||||
# SYFT_FILE_CONTENTS_CATALOGER_SCOPE env var
|
||||
scope: "squashed"
|
||||
|
||||
# skip searching a file entirely if it is above the given size (default = 1MB; unit = bytes)
|
||||
# SYFT_FILE_CONTENTS_SKIP_FILES_ABOVE_SIZE env var
|
||||
skip-files-above-size: 1048576
|
||||
|
||||
# file globs for the cataloger to match on
|
||||
# SYFT_FILE_CONTENTS_GLOBS env var
|
||||
globs: []
|
||||
|
||||
# cataloging file metadata is exposed through the power-user subcommand
|
||||
file-metadata:
|
||||
cataloger:
|
||||
# enable/disable cataloging of file metadata
|
||||
# SYFT_FILE_METADATA_CATALOGER_ENABLED env var
|
||||
enabled: true
|
||||
|
||||
# the search space to look for file metadata (options: all-layers, squashed)
|
||||
# SYFT_FILE_METADATA_CATALOGER_SCOPE env var
|
||||
scope: "squashed"
|
||||
|
||||
# the file digest algorithms to use when cataloging files (options: "sha256", "md5", "sha1")
|
||||
# SYFT_FILE_METADATA_DIGESTS env var
|
||||
digests: ["sha256"]
|
||||
|
||||
# cataloging secrets is exposed through the power-user subcommand
|
||||
secrets:
|
||||
cataloger:
|
||||
# enable/disable cataloging of secrets
|
||||
# SYFT_SECRETS_CATALOGER_ENABLED env var
|
||||
enabled: true
|
||||
|
||||
# the search space to look for secrets (options: all-layers, squashed)
|
||||
# SYFT_SECRETS_CATALOGER_SCOPE env var
|
||||
scope: "all-layers"
|
||||
|
||||
# show extracted secret values in the final JSON report
|
||||
# SYFT_SECRETS_REVEAL_VALUES env var
|
||||
reveal-values: false
|
||||
|
||||
# skip searching a file entirely if it is above the given size (default = 1MB; unit = bytes)
|
||||
# SYFT_SECRETS_SKIP_FILES_ABOVE_SIZE env var
|
||||
skip-files-above-size: 1048576
|
||||
|
||||
# name-regex pairs to consider when searching files for secrets. Note: the regex must match single line patterns
|
||||
# but may also have OPTIONAL multiline capture groups. Regexes with a named capture group of "value" will
|
||||
# use the entire regex to match, but the secret value will be assumed to be entirely contained within the
|
||||
# "value" named capture group.
|
||||
additional-patterns: {}
|
||||
|
||||
# names to exclude from the secrets search, valid values are: "aws-access-key", "aws-secret-key", "pem-private-key",
|
||||
# "docker-config-auth", and "generic-api-key". Note: this does not consider any names introduced in the
|
||||
# "secrets.additional-patterns" config option.
|
||||
# SYFT_SECRETS_EXCLUDE_PATTERN_NAMES env var
|
||||
exclude-pattern-names: []
|
||||
|
||||
# options when pulling directly from a registry via the "registry:" scheme
|
||||
registry:
|
||||
# skip TLS verification when communicating with the registry
|
||||
# SYFT_REGISTRY_INSECURE_SKIP_TLS_VERIFY env var
|
||||
insecure-skip-tls-verify: false
|
||||
# use http instead of https when connecting to the registry
|
||||
# SYFT_REGISTRY_INSECURE_USE_HTTP env var
|
||||
insecure-use-http: false
|
||||
|
||||
# credentials for specific registries
|
||||
auth:
|
||||
- # the URL to the registry (e.g. "docker.io", "localhost:5000", etc.)
|
||||
# SYFT_REGISTRY_AUTH_AUTHORITY env var
|
||||
authority: ""
|
||||
# SYFT_REGISTRY_AUTH_USERNAME env var
|
||||
username: ""
|
||||
# SYFT_REGISTRY_AUTH_PASSWORD env var
|
||||
password: ""
|
||||
# note: token and username/password are mutually exclusive
|
||||
# SYFT_REGISTRY_AUTH_TOKEN env var
|
||||
token: ""
|
||||
- ... # note, more credentials can be provided via config file only
|
||||
|
||||
log:
|
||||
# use structured logging
|
||||
# same as SYFT_LOG_STRUCTURED env var
|
||||
@@ -110,11 +347,8 @@ log:
|
||||
# same as SYFT_LOG_FILE env var
|
||||
file: ""
|
||||
|
||||
# uploading package SBOM is exposed through the packages subcommand
|
||||
anchore:
|
||||
# (feature-preview) enable uploading of results to Anchore Enterprise automatically (supported on Enterprise 3.0+)
|
||||
# same as SYFT_ANCHORE_UPLOAD_ENABLED env var
|
||||
upload-enabled: false
|
||||
|
||||
# (feature-preview) the Anchore Enterprise Host or URL to upload results to (supported on Enterprise 3.0+)
|
||||
# same as -H ; SYFT_ANCHORE_HOST env var
|
||||
host: ""
|
||||
@@ -135,4 +369,4 @@ anchore:
|
||||
# same as -d ; SYFT_ANCHORE_DOCKERFILE env var
|
||||
dockerfile: ""
|
||||
|
||||
```
|
||||
```
|
||||
|
||||
+11
-6
@@ -87,13 +87,18 @@ This release process itself should be as automated as possible, and have only a
|
||||
|
||||
1. Tag the main branch with a full semantic-version, prefixed with a `v`. If there is a
|
||||
milestone with a partial version, the full version should be used for the git tag (e.g.
|
||||
with a Milestone of `v0.1` the tag should be `v0.1.0`).
|
||||
with a Milestone of `v0.1` the tag should be `v0.1.0`). You can determine the changes going
|
||||
into a release by running `make changelog-unreleased`. Use this change list to determine the
|
||||
release increment. After determining the release increment (major, minor, patch), create the tag.
|
||||
Given the above example the command to create the tag would be `git tag v0.1.0`.
|
||||
|
||||
1. Push the tag, the release pipeline will generate and publish all assets as well as a
|
||||
draft GitHub Release.
|
||||
1. Push the tag. Given the above example the command to push the tag would be `git push origin v0.1.0`.
|
||||
|
||||
1. A release admin must approve the release on the GitHub Actions release pipeline run page.
|
||||
Once approved, the release pipeline will generate all assets and draft a GitHub Release.
|
||||
|
||||
1. Promote the GitHub Release from draft to public. Note: since extra assets are made
|
||||
available immediately from previous steps (i.e. the brew formula) the release should
|
||||
only be in this state for a small amount of time (minutes).
|
||||
1. Navigate to the GitHub Release draft page to review the final changelog and publish the
|
||||
release. Once published, a release-follow-up pipeline will publish derivative artifacts
|
||||
(docker image to DockerHub, brew formula to the external homebrew git repo, etc).
|
||||
|
||||
1. If there is a release Milestone, close it.
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/version"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
)
|
||||
|
||||
func checkForApplicationUpdate() {
|
||||
if appConfig.CheckForAppUpdate {
|
||||
isAvailable, newVersion, err := version.IsUpdateAvailable()
|
||||
if err != nil {
|
||||
// this should never stop the application
|
||||
log.Errorf(err.Error())
|
||||
}
|
||||
if isAvailable {
|
||||
log.Infof("new version of %s is available: %s (current version is %s)", internal.ApplicationName, newVersion, version.FromBuild().Version)
|
||||
|
||||
bus.Publish(partybus.Event{
|
||||
Type: event.AppUpdateAvailable,
|
||||
Value: newVersion,
|
||||
})
|
||||
} else {
|
||||
log.Debugf("no new %s update available", internal.ApplicationName)
|
||||
}
|
||||
}
|
||||
}
|
||||
+38
-101
@@ -4,28 +4,27 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/anchore/stereoscope"
|
||||
"github.com/anchore/syft/internal/config"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/logger"
|
||||
"github.com/anchore/syft/syft"
|
||||
"github.com/anchore/syft/syft/presenter"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/gookit/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
)
|
||||
|
||||
var appConfig *config.Application
|
||||
var eventBus *partybus.Bus
|
||||
var eventSubscription *partybus.Subscription
|
||||
var cliOpts = config.CliOnlyOptions{}
|
||||
var (
|
||||
appConfig *config.Application
|
||||
eventBus *partybus.Bus
|
||||
eventSubscription *partybus.Subscription
|
||||
)
|
||||
|
||||
func init() {
|
||||
setGlobalCliOptions()
|
||||
|
||||
cobra.OnInitialize(
|
||||
initCmdAliasBindings,
|
||||
initAppConfig,
|
||||
initLogging,
|
||||
logAppConfig,
|
||||
@@ -35,109 +34,47 @@ func init() {
|
||||
|
||||
func Execute() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
fmt.Fprintln(os.Stderr, color.Red.Sprint(err.Error()))
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func setGlobalCliOptions() {
|
||||
rootCmd.PersistentFlags().StringVarP(&cliOpts.ConfigPath, "config", "c", "", "application config file")
|
||||
|
||||
// scan options
|
||||
flag := "scope"
|
||||
rootCmd.Flags().StringP(
|
||||
"scope", "s", source.SquashedScope.String(),
|
||||
fmt.Sprintf("selection of layers to catalog, options=%v", source.AllScopes))
|
||||
if err := viper.BindPFlag(flag, rootCmd.Flags().Lookup(flag)); err != nil {
|
||||
fmt.Printf("unable to bind flag '%s': %+v", flag, err)
|
||||
os.Exit(1)
|
||||
// we must setup the config-cli bindings first before the application configuration is parsed. However, this cannot
|
||||
// be done without determining what the primary command that the config options should be bound to since there are
|
||||
// shared concerns (the root-packages alias).
|
||||
func initCmdAliasBindings() {
|
||||
activeCmd, _, err := rootCmd.Find(os.Args[1:])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
setGlobalFormatOptions()
|
||||
setGlobalUploadOptions()
|
||||
}
|
||||
|
||||
func setGlobalFormatOptions() {
|
||||
// output & formatting options
|
||||
flag := "output"
|
||||
rootCmd.Flags().StringP(
|
||||
flag, "o", string(presenter.TablePresenter),
|
||||
fmt.Sprintf("report output formatter, options=%v", presenter.Options),
|
||||
)
|
||||
if err := viper.BindPFlag(flag, rootCmd.Flags().Lookup(flag)); err != nil {
|
||||
fmt.Printf("unable to bind flag '%s': %+v", flag, err)
|
||||
os.Exit(1)
|
||||
// enable all cataloger by default if power-user command is run
|
||||
if activeCmd == powerUserCmd {
|
||||
config.PowerUserCatalogerEnabledDefault()
|
||||
}
|
||||
|
||||
flag = "quiet"
|
||||
rootCmd.Flags().BoolP(
|
||||
flag, "q", false,
|
||||
"suppress all logging output",
|
||||
)
|
||||
if err := viper.BindPFlag(flag, rootCmd.Flags().Lookup(flag)); err != nil {
|
||||
fmt.Printf("unable to bind flag '%s': %+v", flag, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
rootCmd.Flags().CountVarP(&cliOpts.Verbosity, "verbose", "v", "increase verbosity (-v = info, -vv = debug)")
|
||||
}
|
||||
|
||||
func setGlobalUploadOptions() {
|
||||
flag := "host"
|
||||
rootCmd.Flags().StringP(
|
||||
flag, "H", "",
|
||||
"the hostname or URL of the Anchore Enterprise instance to upload to",
|
||||
)
|
||||
if err := viper.BindPFlag("anchore.host", rootCmd.Flags().Lookup(flag)); err != nil {
|
||||
fmt.Printf("unable to bind flag '%s': %+v", flag, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
flag = "username"
|
||||
rootCmd.Flags().StringP(
|
||||
flag, "u", "",
|
||||
"the username to authenticate against Anchore Enterprise",
|
||||
)
|
||||
if err := viper.BindPFlag("anchore.username", rootCmd.Flags().Lookup(flag)); err != nil {
|
||||
fmt.Printf("unable to bind flag '%s': %+v", flag, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
flag = "password"
|
||||
rootCmd.Flags().StringP(
|
||||
flag, "p", "",
|
||||
"the password to authenticate against Anchore Enterprise",
|
||||
)
|
||||
if err := viper.BindPFlag("anchore.password", rootCmd.Flags().Lookup(flag)); err != nil {
|
||||
fmt.Printf("unable to bind flag '%s': %+v", flag, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
flag = "dockerfile"
|
||||
rootCmd.Flags().StringP(
|
||||
flag, "d", "",
|
||||
"include dockerfile for upload to Anchore Enterprise",
|
||||
)
|
||||
if err := viper.BindPFlag("anchore.dockerfile", rootCmd.Flags().Lookup(flag)); err != nil {
|
||||
fmt.Printf("unable to bind flag '#{flag}': #{err}")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
flag = "overwrite-existing-image"
|
||||
rootCmd.Flags().Bool(
|
||||
flag, false,
|
||||
"overwrite an existing image during the upload to Anchore Enterprise",
|
||||
)
|
||||
if err := viper.BindPFlag("anchore.overwrite-existing-image", rootCmd.Flags().Lookup(flag)); err != nil {
|
||||
fmt.Printf("unable to bind flag '#{flag}': #{err}")
|
||||
os.Exit(1)
|
||||
switch activeCmd {
|
||||
case packagesCmd, rootCmd:
|
||||
// note: we need to lazily bind config options since they are shared between both the root command
|
||||
// and the packages command. Otherwise there will be global viper state that is in contention.
|
||||
// See for more details: https://github.com/spf13/viper/issues/233 . Additionally, the bindings must occur BEFORE
|
||||
// reading the application configuration, which implies that it must be an initializer (or rewrite the command
|
||||
// initialization structure against typical patterns used with cobra, which is somewhat extreme for a
|
||||
// temporary alias)
|
||||
if err = bindPackagesConfigOptions(activeCmd.Flags()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
default:
|
||||
// even though the root command or packages command is NOT being run, we still need default bindings
|
||||
// such that application config parsing passes.
|
||||
if err = bindPackagesConfigOptions(packagesCmd.Flags()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func initAppConfig() {
|
||||
cfgVehicle := viper.GetViper()
|
||||
wasHostnameSet := rootCmd.Flags().Changed("host")
|
||||
cfg, err := config.LoadApplicationConfig(cfgVehicle, cliOpts, wasHostnameSet)
|
||||
cfg, err := config.LoadApplicationConfig(viper.GetViper(), persistentOpts)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to load application config: \n\t%+v\n", err)
|
||||
os.Exit(1)
|
||||
@@ -163,7 +100,7 @@ func initLogging() {
|
||||
}
|
||||
|
||||
func logAppConfig() {
|
||||
log.Debugf("Application config:\n%+v", color.Magenta.Sprint(appConfig.String()))
|
||||
log.Debugf("application config:\n%+v", color.Magenta.Sprint(appConfig.String()))
|
||||
}
|
||||
|
||||
func initEventBus() {
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/client"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -63,3 +69,45 @@ $ syft completion fish > ~/.config/fish/completions/syft.fish
|
||||
func init() {
|
||||
rootCmd.AddCommand(completionCmd)
|
||||
}
|
||||
|
||||
func dockerImageValidArgsFunction(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
// Since we use ValidArgsFunction, Cobra will call this AFTER having parsed all flags and arguments provided
|
||||
dockerImageRepoTags, err := listLocalDockerImages(toComplete)
|
||||
if err != nil {
|
||||
// Indicates that an error occurred and completions should be ignored
|
||||
return []string{"completion failed"}, cobra.ShellCompDirectiveError
|
||||
}
|
||||
if len(dockerImageRepoTags) == 0 {
|
||||
return []string{"no docker images found"}, cobra.ShellCompDirectiveError
|
||||
}
|
||||
// ShellCompDirectiveDefault indicates that the shell will perform its default behavior after completions have
|
||||
// been provided (without implying other possible directives)
|
||||
return dockerImageRepoTags, cobra.ShellCompDirectiveDefault
|
||||
}
|
||||
|
||||
func listLocalDockerImages(prefix string) ([]string, error) {
|
||||
var repoTags = make([]string, 0)
|
||||
ctx := context.Background()
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
return repoTags, err
|
||||
}
|
||||
|
||||
// Only want to return tagged images
|
||||
imageListArgs := filters.NewArgs()
|
||||
imageListArgs.Add("dangling", "false")
|
||||
images, err := cli.ImageList(ctx, types.ImageListOptions{All: false, Filters: imageListArgs})
|
||||
if err != nil {
|
||||
return repoTags, err
|
||||
}
|
||||
|
||||
for _, image := range images {
|
||||
// image may have multiple tags
|
||||
for _, tag := range image.RepoTags {
|
||||
if strings.HasPrefix(tag, prefix) {
|
||||
repoTags = append(repoTags, tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
return repoTags, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/ui"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
)
|
||||
|
||||
// eventLoop listens to worker errors (from execution path), worker events (from a partybus subscription), and
|
||||
// signal interrupts. Is responsible for handling each event relative to a given UI an to coordinate eventing until
|
||||
// an eventual graceful exit.
|
||||
// nolint:funlen
|
||||
func eventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription *partybus.Subscription, cleanupFn func(), uxs ...ui.UI) error {
|
||||
defer cleanupFn()
|
||||
events := subscription.Events()
|
||||
var err error
|
||||
var ux ui.UI
|
||||
|
||||
if ux, err = setupUI(subscription.Unsubscribe, uxs...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var retErr error
|
||||
var forceTeardown bool
|
||||
|
||||
for {
|
||||
if workerErrs == nil && events == nil {
|
||||
break
|
||||
}
|
||||
select {
|
||||
case err, isOpen := <-workerErrs:
|
||||
if !isOpen {
|
||||
workerErrs = nil
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
// capture the error from the worker and unsubscribe to complete a graceful shutdown
|
||||
retErr = multierror.Append(retErr, err)
|
||||
_ = subscription.Unsubscribe()
|
||||
// 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 {
|
||||
events = nil
|
||||
continue
|
||||
}
|
||||
|
||||
if err := ux.Handle(e); err != nil {
|
||||
if errors.Is(err, partybus.ErrUnsubscribe) {
|
||||
events = nil
|
||||
} else {
|
||||
retErr = multierror.Append(retErr, err)
|
||||
// TODO: should we unsubscribe? should we try to halt execution? or continue?
|
||||
}
|
||||
}
|
||||
case <-signals:
|
||||
// ignore further results from any event source and exit ASAP, but ensure that all cache is cleaned up.
|
||||
// we ignore further errors since cleaning up the tmp directories will affect running catalogers that are
|
||||
// reading/writing from/to their nested temp dirs. This is acceptable since we are bailing without result.
|
||||
|
||||
// TODO: potential future improvement would be to pass context into workers with a cancel function that is
|
||||
// to the event loop. In this way we can have a more controlled shutdown even at the most nested levels
|
||||
// of processing.
|
||||
events = nil
|
||||
workerErrs = nil
|
||||
forceTeardown = true
|
||||
}
|
||||
}
|
||||
|
||||
if err := ux.Teardown(forceTeardown); err != nil {
|
||||
retErr = multierror.Append(retErr, err)
|
||||
}
|
||||
|
||||
return retErr
|
||||
}
|
||||
|
||||
// setupUI takes one or more UIs that responds to events and takes a event bus unsubscribe function for use
|
||||
// during teardown. With the given UIs, the first UI which the ui.Setup() function does not return an error
|
||||
// will be utilized in execution. Providing a set of UIs allows for the caller to provide graceful fallbacks
|
||||
// when there are environmental problem (e.g. unable to setup a TUI with the current TTY).
|
||||
func setupUI(unsubscribe func() error, uis ...ui.UI) (ui.UI, error) {
|
||||
for _, ux := range uis {
|
||||
if err := ux.Setup(unsubscribe); err != nil {
|
||||
log.Warnf("unable to setup given UI, falling back to alternative UI: %+v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
return ux, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unable to setup any UI")
|
||||
}
|
||||
@@ -0,0 +1,455 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/anchore/syft/internal/ui"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
)
|
||||
|
||||
var _ ui.UI = (*uiMock)(nil)
|
||||
|
||||
type uiMock struct {
|
||||
t *testing.T
|
||||
finalEvent partybus.Event
|
||||
unsubscribe func() error
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (u *uiMock) Setup(unsubscribe func() error) error {
|
||||
u.t.Logf("UI Setup called")
|
||||
u.unsubscribe = unsubscribe
|
||||
return u.Called(unsubscribe).Error(0)
|
||||
}
|
||||
|
||||
func (u *uiMock) Handle(event partybus.Event) error {
|
||||
u.t.Logf("UI Handle called: %+v", event.Type)
|
||||
if event == u.finalEvent {
|
||||
assert.NoError(u.t, u.unsubscribe())
|
||||
}
|
||||
return u.Called(event).Error(0)
|
||||
}
|
||||
|
||||
func (u *uiMock) Teardown(_ bool) error {
|
||||
u.t.Logf("UI Teardown called")
|
||||
return u.Called().Error(0)
|
||||
}
|
||||
|
||||
func Test_eventLoop_gracefulExit(t *testing.T) {
|
||||
test := func(t *testing.T) {
|
||||
|
||||
testBus := partybus.NewBus()
|
||||
subscription := testBus.Subscribe()
|
||||
t.Cleanup(testBus.Close)
|
||||
|
||||
finalEvent := partybus.Event{
|
||||
Type: event.PresenterReady,
|
||||
}
|
||||
|
||||
worker := func() <-chan error {
|
||||
ret := make(chan error)
|
||||
go func() {
|
||||
t.Log("worker running")
|
||||
// send an empty item (which is ignored) ensuring we've entered the select statement,
|
||||
// then close (a partial shutdown).
|
||||
ret <- nil
|
||||
t.Log("worker sent nothing")
|
||||
close(ret)
|
||||
t.Log("worker closed")
|
||||
// do the other half of the shutdown
|
||||
testBus.Publish(finalEvent)
|
||||
t.Log("worker published final event")
|
||||
}()
|
||||
return ret
|
||||
}
|
||||
|
||||
signaler := func() <-chan os.Signal {
|
||||
return nil
|
||||
}
|
||||
|
||||
ux := &uiMock{
|
||||
t: t,
|
||||
finalEvent: finalEvent,
|
||||
}
|
||||
|
||||
// ensure the mock sees at least the final event
|
||||
ux.On("Handle", finalEvent).Return(nil)
|
||||
// ensure the mock sees basic setup/teardown events
|
||||
ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
|
||||
ux.On("Teardown").Return(nil)
|
||||
|
||||
var cleanupCalled bool
|
||||
cleanupFn := func() {
|
||||
t.Log("cleanup called")
|
||||
cleanupCalled = true
|
||||
}
|
||||
|
||||
assert.NoError(t,
|
||||
eventLoop(
|
||||
worker(),
|
||||
signaler(),
|
||||
subscription,
|
||||
cleanupFn,
|
||||
ux,
|
||||
),
|
||||
)
|
||||
|
||||
assert.True(t, cleanupCalled, "cleanup function not called")
|
||||
ux.AssertExpectations(t)
|
||||
}
|
||||
|
||||
// if there is a bug, then there is a risk of the event loop never returning
|
||||
testWithTimeout(t, 5*time.Second, test)
|
||||
}
|
||||
|
||||
func Test_eventLoop_workerError(t *testing.T) {
|
||||
test := func(t *testing.T) {
|
||||
|
||||
testBus := partybus.NewBus()
|
||||
subscription := testBus.Subscribe()
|
||||
t.Cleanup(testBus.Close)
|
||||
|
||||
workerErr := fmt.Errorf("worker error")
|
||||
|
||||
worker := func() <-chan error {
|
||||
ret := make(chan error)
|
||||
go func() {
|
||||
t.Log("worker running")
|
||||
// send an empty item (which is ignored) ensuring we've entered the select statement,
|
||||
// then close (a partial shutdown).
|
||||
ret <- nil
|
||||
t.Log("worker sent nothing")
|
||||
ret <- workerErr
|
||||
t.Log("worker sent error")
|
||||
close(ret)
|
||||
t.Log("worker closed")
|
||||
// note: NO final event is fired
|
||||
}()
|
||||
return ret
|
||||
}
|
||||
|
||||
signaler := func() <-chan os.Signal {
|
||||
return nil
|
||||
}
|
||||
|
||||
ux := &uiMock{
|
||||
t: t,
|
||||
}
|
||||
|
||||
// ensure the mock sees basic setup/teardown events
|
||||
ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
|
||||
ux.On("Teardown").Return(nil)
|
||||
|
||||
var cleanupCalled bool
|
||||
cleanupFn := func() {
|
||||
t.Log("cleanup called")
|
||||
cleanupCalled = true
|
||||
}
|
||||
|
||||
// ensure we see an error returned
|
||||
assert.ErrorIs(t,
|
||||
eventLoop(
|
||||
worker(),
|
||||
signaler(),
|
||||
subscription,
|
||||
cleanupFn,
|
||||
ux,
|
||||
),
|
||||
workerErr,
|
||||
"should have seen a worker error, but did not",
|
||||
)
|
||||
|
||||
assert.True(t, cleanupCalled, "cleanup function not called")
|
||||
ux.AssertExpectations(t)
|
||||
}
|
||||
|
||||
// if there is a bug, then there is a risk of the event loop never returning
|
||||
testWithTimeout(t, 5*time.Second, test)
|
||||
}
|
||||
|
||||
func Test_eventLoop_unsubscribeError(t *testing.T) {
|
||||
test := func(t *testing.T) {
|
||||
|
||||
testBus := partybus.NewBus()
|
||||
subscription := testBus.Subscribe()
|
||||
t.Cleanup(testBus.Close)
|
||||
|
||||
finalEvent := partybus.Event{
|
||||
Type: event.PresenterReady,
|
||||
}
|
||||
|
||||
worker := func() <-chan error {
|
||||
ret := make(chan error)
|
||||
go func() {
|
||||
t.Log("worker running")
|
||||
// send an empty item (which is ignored) ensuring we've entered the select statement,
|
||||
// then close (a partial shutdown).
|
||||
ret <- nil
|
||||
t.Log("worker sent nothing")
|
||||
close(ret)
|
||||
t.Log("worker closed")
|
||||
// do the other half of the shutdown
|
||||
testBus.Publish(finalEvent)
|
||||
t.Log("worker published final event")
|
||||
}()
|
||||
return ret
|
||||
}
|
||||
|
||||
signaler := func() <-chan os.Signal {
|
||||
return nil
|
||||
}
|
||||
|
||||
ux := &uiMock{
|
||||
t: t,
|
||||
finalEvent: finalEvent,
|
||||
}
|
||||
|
||||
// ensure the mock sees at least the final event... note the unsubscribe error here
|
||||
ux.On("Handle", finalEvent).Return(partybus.ErrUnsubscribe)
|
||||
// ensure the mock sees basic setup/teardown events
|
||||
ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
|
||||
ux.On("Teardown").Return(nil)
|
||||
|
||||
var cleanupCalled bool
|
||||
cleanupFn := func() {
|
||||
t.Log("cleanup called")
|
||||
cleanupCalled = true
|
||||
}
|
||||
|
||||
// unsubscribe errors should be handled and ignored, not propagated. We are additionally asserting that
|
||||
// this case is handled as a controlled shutdown (this test should not timeout)
|
||||
assert.NoError(t,
|
||||
eventLoop(
|
||||
worker(),
|
||||
signaler(),
|
||||
subscription,
|
||||
cleanupFn,
|
||||
ux,
|
||||
),
|
||||
)
|
||||
|
||||
assert.True(t, cleanupCalled, "cleanup function not called")
|
||||
ux.AssertExpectations(t)
|
||||
}
|
||||
|
||||
// if there is a bug, then there is a risk of the event loop never returning
|
||||
testWithTimeout(t, 5*time.Second, test)
|
||||
}
|
||||
|
||||
func Test_eventLoop_handlerError(t *testing.T) {
|
||||
test := func(t *testing.T) {
|
||||
|
||||
testBus := partybus.NewBus()
|
||||
subscription := testBus.Subscribe()
|
||||
t.Cleanup(testBus.Close)
|
||||
|
||||
finalEvent := partybus.Event{
|
||||
Type: event.PresenterReady,
|
||||
Error: fmt.Errorf("unable to create presenter"),
|
||||
}
|
||||
|
||||
worker := func() <-chan error {
|
||||
ret := make(chan error)
|
||||
go func() {
|
||||
t.Log("worker running")
|
||||
// send an empty item (which is ignored) ensuring we've entered the select statement,
|
||||
// then close (a partial shutdown).
|
||||
ret <- nil
|
||||
t.Log("worker sent nothing")
|
||||
close(ret)
|
||||
t.Log("worker closed")
|
||||
// do the other half of the shutdown
|
||||
testBus.Publish(finalEvent)
|
||||
t.Log("worker published final event")
|
||||
}()
|
||||
return ret
|
||||
}
|
||||
|
||||
signaler := func() <-chan os.Signal {
|
||||
return nil
|
||||
}
|
||||
|
||||
ux := &uiMock{
|
||||
t: t,
|
||||
finalEvent: finalEvent,
|
||||
}
|
||||
|
||||
// ensure the mock sees at least the final event... note the event error is propagated
|
||||
ux.On("Handle", finalEvent).Return(finalEvent.Error)
|
||||
// ensure the mock sees basic setup/teardown events
|
||||
ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
|
||||
ux.On("Teardown").Return(nil)
|
||||
|
||||
var cleanupCalled bool
|
||||
cleanupFn := func() {
|
||||
t.Log("cleanup called")
|
||||
cleanupCalled = true
|
||||
}
|
||||
|
||||
// handle errors SHOULD propagate the event loop. We are additionally asserting that this case is
|
||||
// handled as a controlled shutdown (this test should not timeout)
|
||||
assert.ErrorIs(t,
|
||||
eventLoop(
|
||||
worker(),
|
||||
signaler(),
|
||||
subscription,
|
||||
cleanupFn,
|
||||
ux,
|
||||
),
|
||||
finalEvent.Error,
|
||||
"should have seen a event error, but did not",
|
||||
)
|
||||
|
||||
assert.True(t, cleanupCalled, "cleanup function not called")
|
||||
ux.AssertExpectations(t)
|
||||
}
|
||||
|
||||
// if there is a bug, then there is a risk of the event loop never returning
|
||||
testWithTimeout(t, 5*time.Second, test)
|
||||
}
|
||||
|
||||
func Test_eventLoop_signalsStopExecution(t *testing.T) {
|
||||
test := func(t *testing.T) {
|
||||
|
||||
testBus := partybus.NewBus()
|
||||
subscription := testBus.Subscribe()
|
||||
t.Cleanup(testBus.Close)
|
||||
|
||||
worker := func() <-chan error {
|
||||
// the worker will never return work and the event loop will always be waiting...
|
||||
return make(chan error)
|
||||
}
|
||||
|
||||
signaler := func() <-chan os.Signal {
|
||||
ret := make(chan os.Signal)
|
||||
go func() {
|
||||
ret <- syscall.SIGINT
|
||||
// note: we do NOT close the channel to ensure the event loop does not depend on that behavior to exit
|
||||
}()
|
||||
return ret
|
||||
}
|
||||
|
||||
ux := &uiMock{
|
||||
t: t,
|
||||
}
|
||||
|
||||
// ensure the mock sees basic setup/teardown events
|
||||
ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
|
||||
ux.On("Teardown").Return(nil)
|
||||
|
||||
var cleanupCalled bool
|
||||
cleanupFn := func() {
|
||||
t.Log("cleanup called")
|
||||
cleanupCalled = true
|
||||
}
|
||||
|
||||
assert.NoError(t,
|
||||
eventLoop(
|
||||
worker(),
|
||||
signaler(),
|
||||
subscription,
|
||||
cleanupFn,
|
||||
ux,
|
||||
),
|
||||
)
|
||||
|
||||
assert.True(t, cleanupCalled, "cleanup function not called")
|
||||
ux.AssertExpectations(t)
|
||||
}
|
||||
|
||||
// if there is a bug, then there is a risk of the event loop never returning
|
||||
testWithTimeout(t, 5*time.Second, test)
|
||||
}
|
||||
|
||||
func Test_eventLoop_uiTeardownError(t *testing.T) {
|
||||
test := func(t *testing.T) {
|
||||
|
||||
testBus := partybus.NewBus()
|
||||
subscription := testBus.Subscribe()
|
||||
t.Cleanup(testBus.Close)
|
||||
|
||||
finalEvent := partybus.Event{
|
||||
Type: event.PresenterReady,
|
||||
}
|
||||
|
||||
worker := func() <-chan error {
|
||||
ret := make(chan error)
|
||||
go func() {
|
||||
t.Log("worker running")
|
||||
// send an empty item (which is ignored) ensuring we've entered the select statement,
|
||||
// then close (a partial shutdown).
|
||||
ret <- nil
|
||||
t.Log("worker sent nothing")
|
||||
close(ret)
|
||||
t.Log("worker closed")
|
||||
// do the other half of the shutdown
|
||||
testBus.Publish(finalEvent)
|
||||
t.Log("worker published final event")
|
||||
}()
|
||||
return ret
|
||||
}
|
||||
|
||||
signaler := func() <-chan os.Signal {
|
||||
return nil
|
||||
}
|
||||
|
||||
ux := &uiMock{
|
||||
t: t,
|
||||
finalEvent: finalEvent,
|
||||
}
|
||||
|
||||
teardownError := fmt.Errorf("sorry, dave, the UI doesn't want to be torn down")
|
||||
|
||||
// ensure the mock sees at least the final event... note the event error is propagated
|
||||
ux.On("Handle", finalEvent).Return(nil)
|
||||
// ensure the mock sees basic setup/teardown events
|
||||
ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
|
||||
ux.On("Teardown").Return(teardownError)
|
||||
|
||||
var cleanupCalled bool
|
||||
cleanupFn := func() {
|
||||
t.Log("cleanup called")
|
||||
cleanupCalled = true
|
||||
}
|
||||
|
||||
// ensure we see an error returned
|
||||
assert.ErrorIs(t,
|
||||
eventLoop(
|
||||
worker(),
|
||||
signaler(),
|
||||
subscription,
|
||||
cleanupFn,
|
||||
ux,
|
||||
),
|
||||
teardownError,
|
||||
"should have seen a UI teardown error, but did not",
|
||||
)
|
||||
|
||||
assert.True(t, cleanupCalled, "cleanup function not called")
|
||||
ux.AssertExpectations(t)
|
||||
}
|
||||
|
||||
// if there is a bug, then there is a risk of the event loop never returning
|
||||
testWithTimeout(t, 5*time.Second, test)
|
||||
}
|
||||
|
||||
func testWithTimeout(t *testing.T, timeout time.Duration, test func(*testing.T)) {
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
test(t)
|
||||
done <- true
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-time.After(timeout):
|
||||
t.Fatal("test timed out")
|
||||
case <-done:
|
||||
}
|
||||
}
|
||||
+354
@@ -0,0 +1,354 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/anchore/stereoscope"
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/anchore"
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/formats"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/ui"
|
||||
"github.com/anchore/syft/internal/version"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/syft/format"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/pkg/profile"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
)
|
||||
|
||||
const (
|
||||
packagesExample = ` {{.appName}} {{.command}} alpine:latest a summary of discovered packages
|
||||
{{.appName}} {{.command}} alpine:latest -o json show all possible cataloging details
|
||||
{{.appName}} {{.command}} alpine:latest -o cyclonedx show a CycloneDX formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -o spdx show a SPDX 2.2 tag-value formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -o spdx-json show a SPDX 2.2 JSON formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -vv show verbose debug information
|
||||
|
||||
Supports the following image sources:
|
||||
{{.appName}} {{.command}} yourrepo/yourimage:tag defaults to using images from a Docker daemon. If Docker is not present, the image is pulled directly from the registry.
|
||||
{{.appName}} {{.command}} path/to/a/file/or/dir a Docker tar, OCI tar, OCI directory, or generic filesystem directory
|
||||
|
||||
You can also explicitly specify the scheme to use:
|
||||
{{.appName}} {{.command}} docker:yourrepo/yourimage:tag explicitly use the Docker daemon
|
||||
{{.appName}} {{.command}} docker-archive:path/to/yourimage.tar use a tarball from disk for archives created from "docker save"
|
||||
{{.appName}} {{.command}} oci-archive:path/to/yourimage.tar use a tarball from disk for OCI archives (from Skopeo or otherwise)
|
||||
{{.appName}} {{.command}} oci-dir:path/to/yourimage read directly from a path on disk for OCI layout directories (from Skopeo or otherwise)
|
||||
{{.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 format.Option
|
||||
packagesCmd = &cobra.Command{
|
||||
Use: "packages [SOURCE]",
|
||||
Short: "Generate a package SBOM",
|
||||
Long: "Generate a packaged-based Software Bill Of Materials (SBOM) from container images and filesystems",
|
||||
Example: internal.Tprintf(packagesExample, map[string]interface{}{
|
||||
"appName": internal.ApplicationName,
|
||||
"command": "packages",
|
||||
}),
|
||||
Args: validateInputArgs,
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
// set the presenter
|
||||
presenterOption := format.ParseOption(appConfig.Output)
|
||||
if presenterOption == format.UnknownFormatOption {
|
||||
return fmt.Errorf("bad --output value '%s'", appConfig.Output)
|
||||
}
|
||||
packagesPresenterOpt = presenterOption
|
||||
|
||||
if appConfig.Dev.ProfileCPU && appConfig.Dev.ProfileMem {
|
||||
return fmt.Errorf("cannot profile CPU and memory simultaneously")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if appConfig.Dev.ProfileCPU {
|
||||
defer profile.Start(profile.CPUProfile).Stop()
|
||||
} else if appConfig.Dev.ProfileMem {
|
||||
defer profile.Start(profile.MemProfile).Stop()
|
||||
}
|
||||
|
||||
return packagesExec(cmd, args)
|
||||
},
|
||||
ValidArgsFunction: dockerImageValidArgsFunction,
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
setPackageFlags(packagesCmd.Flags())
|
||||
|
||||
rootCmd.AddCommand(packagesCmd)
|
||||
}
|
||||
|
||||
func setPackageFlags(flags *pflag.FlagSet) {
|
||||
// Formatting & Input options //////////////////////////////////////////////
|
||||
|
||||
flags.StringP(
|
||||
"scope", "s", source.SquashedScope.String(),
|
||||
fmt.Sprintf("selection of layers to catalog, options=%v", source.AllScopes))
|
||||
|
||||
flags.StringP(
|
||||
"output", "o", string(format.TableOption),
|
||||
fmt.Sprintf("report output formatter, options=%v", format.AllOptions),
|
||||
)
|
||||
|
||||
flags.StringP(
|
||||
"file", "", "",
|
||||
"file to write the report output to (default is STDOUT)",
|
||||
)
|
||||
|
||||
// Upload options //////////////////////////////////////////////////////////
|
||||
flags.StringP(
|
||||
"host", "H", "",
|
||||
"the hostname or URL of the Anchore Enterprise instance to upload to",
|
||||
)
|
||||
|
||||
flags.StringP(
|
||||
"username", "u", "",
|
||||
"the username to authenticate against Anchore Enterprise",
|
||||
)
|
||||
|
||||
flags.StringP(
|
||||
"password", "p", "",
|
||||
"the password to authenticate against Anchore Enterprise",
|
||||
)
|
||||
|
||||
flags.StringP(
|
||||
"dockerfile", "d", "",
|
||||
"include dockerfile for upload to Anchore Enterprise",
|
||||
)
|
||||
|
||||
flags.Bool(
|
||||
"overwrite-existing-image", false,
|
||||
"overwrite an existing image during the upload to Anchore Enterprise",
|
||||
)
|
||||
|
||||
flags.Uint(
|
||||
"import-timeout", 30,
|
||||
"set a timeout duration (in seconds) for the upload to Anchore Enterprise",
|
||||
)
|
||||
}
|
||||
|
||||
func bindPackagesConfigOptions(flags *pflag.FlagSet) error {
|
||||
// Formatting & Input options //////////////////////////////////////////////
|
||||
|
||||
if err := viper.BindPFlag("package.cataloger.scope", flags.Lookup("scope")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := viper.BindPFlag("output", flags.Lookup("output")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := viper.BindPFlag("file", flags.Lookup("file")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Upload options //////////////////////////////////////////////////////////
|
||||
|
||||
if err := viper.BindPFlag("anchore.host", flags.Lookup("host")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := viper.BindPFlag("anchore.username", flags.Lookup("username")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := viper.BindPFlag("anchore.password", flags.Lookup("password")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := viper.BindPFlag("anchore.dockerfile", flags.Lookup("dockerfile")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := viper.BindPFlag("anchore.overwrite-existing-image", flags.Lookup("overwrite-existing-image")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := viper.BindPFlag("anchore.import-timeout", flags.Lookup("import-timeout")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateInputArgs(cmd *cobra.Command, args []string) error {
|
||||
if len(args) == 0 {
|
||||
// in the case that no arguments are given we want to show the help text and return with a non-0 return code.
|
||||
if err := cmd.Help(); err != nil {
|
||||
return fmt.Errorf("unable to display help: %w", err)
|
||||
}
|
||||
return fmt.Errorf("an image/directory argument is required")
|
||||
}
|
||||
|
||||
return cobra.MaximumNArgs(1)(cmd, args)
|
||||
}
|
||||
|
||||
func packagesExec(_ *cobra.Command, args []string) error {
|
||||
// could be an image or a directory, with or without a scheme
|
||||
userInput := args[0]
|
||||
|
||||
reporter, closer, err := reportWriter()
|
||||
defer func() {
|
||||
if err := closer(); err != nil {
|
||||
log.Warnf("unable to write to report destination: %+v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return eventLoop(
|
||||
packagesExecWorker(userInput),
|
||||
setupSignals(),
|
||||
eventSubscription,
|
||||
stereoscope.Cleanup,
|
||||
ui.Select(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)
|
||||
|
||||
tasks, err := tasks()
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
|
||||
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())
|
||||
if err != nil {
|
||||
errs <- fmt.Errorf("failed to determine image source: %w", err)
|
||||
return
|
||||
}
|
||||
if cleanup != nil {
|
||||
defer cleanup()
|
||||
}
|
||||
|
||||
s := sbom.SBOM{
|
||||
Source: src.Metadata,
|
||||
Descriptor: sbom.Descriptor{
|
||||
Name: internal.ApplicationName,
|
||||
Version: version.FromBuild().Version,
|
||||
Configuration: appConfig,
|
||||
},
|
||||
}
|
||||
|
||||
var relationships []<-chan artifact.Relationship
|
||||
for _, task := range tasks {
|
||||
c := make(chan artifact.Relationship)
|
||||
relationships = append(relationships, c)
|
||||
|
||||
go runTask(task, &s.Artifacts, src, c, errs)
|
||||
}
|
||||
s.Relationships = append(s.Relationships, mergeRelationships(relationships...)...)
|
||||
|
||||
if appConfig.Anchore.Host != "" {
|
||||
if err := runPackageSbomUpload(src, s); err != nil {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
bus.Publish(partybus.Event{
|
||||
Type: event.PresenterReady,
|
||||
Value: f.Presenter(s),
|
||||
})
|
||||
}()
|
||||
return errs
|
||||
}
|
||||
|
||||
func mergeRelationships(cs ...<-chan artifact.Relationship) (relationships []artifact.Relationship) {
|
||||
for _, c := range cs {
|
||||
for n := range c {
|
||||
relationships = append(relationships, n)
|
||||
}
|
||||
}
|
||||
|
||||
return relationships
|
||||
}
|
||||
|
||||
func runPackageSbomUpload(src *source.Source, s sbom.SBOM) error {
|
||||
log.Infof("uploading results to %s", appConfig.Anchore.Host)
|
||||
|
||||
if src.Metadata.Scheme != source.ImageScheme {
|
||||
return fmt.Errorf("unable to upload results: only images are supported")
|
||||
}
|
||||
|
||||
var dockerfileContents []byte
|
||||
if appConfig.Anchore.Dockerfile != "" {
|
||||
if _, err := os.Stat(appConfig.Anchore.Dockerfile); os.IsNotExist(err) {
|
||||
return fmt.Errorf("unable dockerfile=%q does not exist: %w", appConfig.Anchore.Dockerfile, err)
|
||||
}
|
||||
|
||||
fh, err := os.Open(appConfig.Anchore.Dockerfile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to open dockerfile=%q: %w", appConfig.Anchore.Dockerfile, err)
|
||||
}
|
||||
|
||||
dockerfileContents, err = ioutil.ReadAll(fh)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read dockerfile=%q: %w", appConfig.Anchore.Dockerfile, err)
|
||||
}
|
||||
}
|
||||
|
||||
c, err := anchore.NewClient(anchore.Configuration{
|
||||
BaseURL: appConfig.Anchore.Host,
|
||||
Username: appConfig.Anchore.Username,
|
||||
Password: appConfig.Anchore.Password,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create anchore client: %w", err)
|
||||
}
|
||||
|
||||
importCfg := anchore.ImportConfig{
|
||||
ImageMetadata: src.Image.Metadata,
|
||||
SBOM: s,
|
||||
Dockerfile: dockerfileContents,
|
||||
OverwriteExistingUpload: appConfig.Anchore.OverwriteExistingImage,
|
||||
Timeout: appConfig.Anchore.ImportTimeout,
|
||||
}
|
||||
|
||||
if err := c.Import(context.Background(), importCfg); err != nil {
|
||||
return fmt.Errorf("failed to upload results to host=%s: %+v", appConfig.Anchore.Host, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/anchore/stereoscope"
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/formats/syftjson"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/ui"
|
||||
"github.com/anchore/syft/internal/version"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/gookit/color"
|
||||
"github.com/pkg/profile"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
)
|
||||
|
||||
const powerUserExample = ` {{.appName}} {{.command}} <image>
|
||||
|
||||
DEPRECATED - THIS COMMAND WILL BE REMOVED in v1.0.0
|
||||
|
||||
Only image sources are supported (e.g. docker: , docker-archive: , oci: , etc.), the directory source (dir:) is not supported.
|
||||
|
||||
All behavior is controlled via application configuration and environment variables (see https://github.com/anchore/syft#configuration)
|
||||
`
|
||||
|
||||
var powerUserOpts = struct {
|
||||
configPath string
|
||||
}{}
|
||||
|
||||
var powerUserCmd = &cobra.Command{
|
||||
Use: "power-user [IMAGE]",
|
||||
Short: "Run bulk operations on container images",
|
||||
Example: internal.Tprintf(powerUserExample, map[string]interface{}{
|
||||
"appName": internal.ApplicationName,
|
||||
"command": "power-user",
|
||||
}),
|
||||
Args: validateInputArgs,
|
||||
Hidden: true,
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
if appConfig.Dev.ProfileCPU && appConfig.Dev.ProfileMem {
|
||||
return fmt.Errorf("cannot profile CPU and memory simultaneously")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if appConfig.Dev.ProfileCPU {
|
||||
defer profile.Start(profile.CPUProfile).Stop()
|
||||
} else if appConfig.Dev.ProfileMem {
|
||||
defer profile.Start(profile.MemProfile).Stop()
|
||||
}
|
||||
|
||||
return powerUserExec(cmd, args)
|
||||
},
|
||||
ValidArgsFunction: dockerImageValidArgsFunction,
|
||||
}
|
||||
|
||||
func init() {
|
||||
powerUserCmd.Flags().StringVarP(&powerUserOpts.configPath, "config", "c", "", "config file path with all power-user options")
|
||||
|
||||
rootCmd.AddCommand(powerUserCmd)
|
||||
}
|
||||
|
||||
func powerUserExec(_ *cobra.Command, args []string) error {
|
||||
// could be an image or a directory, with or without a scheme
|
||||
userInput := args[0]
|
||||
|
||||
reporter, closer, err := reportWriter()
|
||||
defer func() {
|
||||
if err := closer(); err != nil {
|
||||
log.Warnf("unable to write to report destination: %+v", err)
|
||||
}
|
||||
|
||||
// inform user at end of run that command will be removed
|
||||
deprecated := color.Style{color.Red, color.OpBold}.Sprint("DEPRECATED: This command will be removed in v1.0.0")
|
||||
fmt.Fprintln(os.Stderr, deprecated)
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return eventLoop(
|
||||
powerUserExecWorker(userInput),
|
||||
setupSignals(),
|
||||
eventSubscription,
|
||||
stereoscope.Cleanup,
|
||||
ui.Select(isVerbose(), appConfig.Quiet, reporter)...,
|
||||
)
|
||||
}
|
||||
func powerUserExecWorker(userInput string) <-chan error {
|
||||
errs := make(chan error)
|
||||
go func() {
|
||||
defer close(errs)
|
||||
|
||||
appConfig.Secrets.Cataloger.Enabled = true
|
||||
appConfig.FileMetadata.Cataloger.Enabled = true
|
||||
appConfig.FileContents.Cataloger.Enabled = true
|
||||
appConfig.FileClassification.Cataloger.Enabled = true
|
||||
tasks, err := tasks()
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
|
||||
checkForApplicationUpdate()
|
||||
|
||||
src, cleanup, err := source.New(userInput, appConfig.Registry.ToOptions())
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
if cleanup != nil {
|
||||
defer cleanup()
|
||||
}
|
||||
|
||||
s := sbom.SBOM{
|
||||
Source: src.Metadata,
|
||||
Descriptor: sbom.Descriptor{
|
||||
Name: internal.ApplicationName,
|
||||
Version: version.FromBuild().Version,
|
||||
Configuration: appConfig,
|
||||
},
|
||||
}
|
||||
|
||||
var relationships []<-chan artifact.Relationship
|
||||
for _, task := range tasks {
|
||||
c := make(chan artifact.Relationship)
|
||||
relationships = append(relationships, c)
|
||||
|
||||
go runTask(task, &s.Artifacts, src, c, errs)
|
||||
}
|
||||
|
||||
s.Relationships = append(s.Relationships, mergeRelationships(relationships...)...)
|
||||
|
||||
bus.Publish(partybus.Event{
|
||||
Type: event.PresenterReady,
|
||||
Value: syftjson.Format().Presenter(s),
|
||||
})
|
||||
}()
|
||||
|
||||
return errs
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
)
|
||||
|
||||
func reportWriter() (io.Writer, func() error, error) {
|
||||
nop := func() error { return nil }
|
||||
path := strings.TrimSpace(appConfig.File)
|
||||
|
||||
switch len(path) {
|
||||
case 0:
|
||||
return os.Stdout, nop, nil
|
||||
|
||||
default:
|
||||
reportFile, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
|
||||
if err != nil {
|
||||
return nil, nop, fmt.Errorf("unable to create report file: %w", err)
|
||||
}
|
||||
|
||||
return reportFile, func() error {
|
||||
log.Infof("report written to file=%q", path)
|
||||
|
||||
return reportFile.Close()
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
+29
-209
@@ -1,226 +1,46 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/profile"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/anchore"
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/ui"
|
||||
"github.com/anchore/syft/internal/version"
|
||||
"github.com/anchore/syft/syft"
|
||||
"github.com/anchore/syft/syft/distro"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/presenter"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/anchore/syft/internal/config"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var persistentOpts = config.CliOnlyOptions{}
|
||||
|
||||
// rootCmd is currently an alias for the packages command
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: fmt.Sprintf("%s [SOURCE]", internal.ApplicationName),
|
||||
Short: "A tool for generating a Software Bill Of Materials (PackageSBOM) from container images and filesystems",
|
||||
Long: internal.Tprintf(`
|
||||
Supports the following image sources:
|
||||
{{.appName}} yourrepo/yourimage:tag defaults to using images from a Docker daemon
|
||||
{{.appName}} path/to/yourproject a Docker tar, OCI tar, OCI directory, or generic filesystem directory
|
||||
|
||||
You can also explicitly specify the scheme to use:
|
||||
{{.appName}} docker:yourrepo/yourimage:tag explicitly use the Docker daemon
|
||||
{{.appName}} docker-archive:path/to/yourimage.tar use a tarball from disk for archives created from "docker save"
|
||||
{{.appName}} oci-archive:path/to/yourimage.tar use a tarball from disk for OCI archives (from Podman or otherwise)
|
||||
{{.appName}} oci-dir:path/to/yourimage read directly from a path on disk for OCI layout directories (from Skopeo or otherwise)
|
||||
{{.appName}} dir:path/to/yourproject read directly from a path on disk (any directory)
|
||||
`, map[string]interface{}{
|
||||
"appName": internal.ApplicationName,
|
||||
}),
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) == 0 {
|
||||
err := cmd.Help()
|
||||
if err != nil {
|
||||
log.Errorf(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if appConfig.Dev.ProfileCPU && appConfig.Dev.ProfileMem {
|
||||
log.Errorf("cannot profile CPU and memory simultaneously")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if appConfig.Dev.ProfileCPU {
|
||||
defer profile.Start(profile.CPUProfile).Stop()
|
||||
} else if appConfig.Dev.ProfileMem {
|
||||
defer profile.Start(profile.MemProfile).Stop()
|
||||
}
|
||||
|
||||
err := doRunCmd(cmd, args)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
},
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
// Since we use ValidArgsFunction, Cobra will call this AFTER having parsed all flags and arguments provided
|
||||
dockerImageRepoTags, err := ListLocalDockerImages(toComplete)
|
||||
if err != nil {
|
||||
// Indicates that an error occurred and completions should be ignored
|
||||
return []string{"completion failed"}, cobra.ShellCompDirectiveError
|
||||
}
|
||||
if len(dockerImageRepoTags) == 0 {
|
||||
return []string{"no docker images found"}, cobra.ShellCompDirectiveError
|
||||
}
|
||||
// ShellCompDirectiveDefault indicates that the shell will perform its default behavior after completions have
|
||||
// been provided (without implying other possible directives)
|
||||
return dockerImageRepoTags, cobra.ShellCompDirectiveDefault
|
||||
},
|
||||
Short: packagesCmd.Short,
|
||||
Long: packagesCmd.Long,
|
||||
Args: packagesCmd.Args,
|
||||
Example: packagesCmd.Example,
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
PreRunE: packagesCmd.PreRunE,
|
||||
RunE: packagesCmd.RunE,
|
||||
ValidArgsFunction: packagesCmd.ValidArgsFunction,
|
||||
}
|
||||
|
||||
func startWorker(userInput string) <-chan error {
|
||||
errs := make(chan error)
|
||||
go func() {
|
||||
defer close(errs)
|
||||
func init() {
|
||||
// set universal flags
|
||||
rootCmd.PersistentFlags().StringVarP(&persistentOpts.ConfigPath, "config", "c", "", "application config file")
|
||||
|
||||
if appConfig.CheckForAppUpdate {
|
||||
isAvailable, newVersion, err := version.IsUpdateAvailable()
|
||||
if err != nil {
|
||||
log.Errorf(err.Error())
|
||||
}
|
||||
if isAvailable {
|
||||
log.Infof("new version of %s is available: %s", internal.ApplicationName, newVersion)
|
||||
flag := "quiet"
|
||||
rootCmd.PersistentFlags().BoolP(
|
||||
flag, "q", false,
|
||||
"suppress all logging output",
|
||||
)
|
||||
|
||||
bus.Publish(partybus.Event{
|
||||
Type: event.AppUpdateAvailable,
|
||||
Value: newVersion,
|
||||
})
|
||||
} else {
|
||||
log.Debugf("no new %s update available", internal.ApplicationName)
|
||||
}
|
||||
}
|
||||
if err := viper.BindPFlag(flag, rootCmd.PersistentFlags().Lookup(flag)); err != nil {
|
||||
fmt.Printf("unable to bind flag '%s': %+v", flag, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
src, catalog, distro, err := syft.Catalog(userInput, appConfig.ScopeOpt)
|
||||
if err != nil {
|
||||
errs <- fmt.Errorf("failed to catalog input: %+v", err)
|
||||
return
|
||||
}
|
||||
rootCmd.PersistentFlags().CountVarP(&persistentOpts.Verbosity, "verbose", "v", "increase verbosity (-v = info, -vv = debug)")
|
||||
|
||||
if appConfig.Anchore.UploadEnabled {
|
||||
if err := doImport(src, src.Metadata, catalog, distro); err != nil {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
bus.Publish(partybus.Event{
|
||||
Type: event.CatalogerFinished,
|
||||
Value: presenter.GetPresenter(appConfig.PresenterOpt, src.Metadata, catalog, distro),
|
||||
})
|
||||
}()
|
||||
return errs
|
||||
}
|
||||
|
||||
func doImport(src source.Source, s source.Metadata, catalog *pkg.Catalog, d *distro.Distro) error {
|
||||
// TODO: ETUI element for this
|
||||
log.Infof("uploading results to %s", appConfig.Anchore.Host)
|
||||
|
||||
if src.Metadata.Scheme != source.ImageScheme {
|
||||
return fmt.Errorf("unable to upload results: only images are supported")
|
||||
}
|
||||
|
||||
var dockerfileContents []byte
|
||||
if appConfig.Anchore.Dockerfile != "" {
|
||||
if _, err := os.Stat(appConfig.Anchore.Dockerfile); os.IsNotExist(err) {
|
||||
return fmt.Errorf("unable to read dockerfile=%q: %w", appConfig.Anchore.Dockerfile, err)
|
||||
}
|
||||
|
||||
fh, err := os.Open(appConfig.Anchore.Dockerfile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to open dockerfile=%q: %w", appConfig.Anchore.Dockerfile, err)
|
||||
}
|
||||
|
||||
dockerfileContents, err = ioutil.ReadAll(fh)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read dockerfile=%q: %w", appConfig.Anchore.Dockerfile, err)
|
||||
}
|
||||
}
|
||||
|
||||
var scheme string
|
||||
var hostname = appConfig.Anchore.Host
|
||||
urlFields := strings.Split(hostname, "://")
|
||||
if len(urlFields) > 1 {
|
||||
scheme = urlFields[0]
|
||||
hostname = urlFields[1]
|
||||
}
|
||||
|
||||
c, err := anchore.NewClient(anchore.Configuration{
|
||||
Hostname: hostname,
|
||||
Username: appConfig.Anchore.Username,
|
||||
Password: appConfig.Anchore.Password,
|
||||
Scheme: scheme,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create anchore client: %+v", err)
|
||||
}
|
||||
|
||||
importCfg := anchore.ImportConfig{
|
||||
ImageMetadata: src.Image.Metadata,
|
||||
SourceMetadata: s,
|
||||
Catalog: catalog,
|
||||
Distro: d,
|
||||
Dockerfile: dockerfileContents,
|
||||
OverwriteExistingUpload: appConfig.Anchore.OverwriteExistingImage,
|
||||
}
|
||||
|
||||
if err := c.Import(context.Background(), importCfg); err != nil {
|
||||
return fmt.Errorf("failed to upload results to host=%s: %+v", appConfig.Anchore.Host, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func doRunCmd(_ *cobra.Command, args []string) error {
|
||||
userInput := args[0]
|
||||
errs := startWorker(userInput)
|
||||
ux := ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet)
|
||||
return ux(errs, eventSubscription)
|
||||
}
|
||||
|
||||
func ListLocalDockerImages(prefix string) ([]string, error) {
|
||||
var repoTags = make([]string, 0)
|
||||
ctx := context.Background()
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
return repoTags, err
|
||||
}
|
||||
|
||||
// Only want to return tagged images
|
||||
imageListArgs := filters.NewArgs()
|
||||
imageListArgs.Add("dangling", "false")
|
||||
images, err := cli.ImageList(ctx, types.ImageListOptions{All: false, Filters: imageListArgs})
|
||||
if err != nil {
|
||||
return repoTags, err
|
||||
}
|
||||
|
||||
for _, image := range images {
|
||||
// image may have multiple tags
|
||||
for _, tag := range image.RepoTags {
|
||||
if strings.HasPrefix(tag, prefix) {
|
||||
repoTags = append(repoTags, tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
return repoTags, nil
|
||||
// set common options that are not universal (package subcommand-alias specific)
|
||||
setPackageFlags(rootCmd.Flags())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func setupSignals() <-chan os.Signal {
|
||||
c := make(chan os.Signal, 1) // Note: A buffered channel is recommended for this; see https://golang.org/pkg/os/signal/#Notify
|
||||
|
||||
interruptions := []os.Signal{
|
||||
syscall.SIGINT,
|
||||
syscall.SIGTERM,
|
||||
}
|
||||
|
||||
signal.Notify(c, interruptions...)
|
||||
|
||||
return c
|
||||
}
|
||||
+231
@@ -0,0 +1,231 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"fmt"
|
||||
|
||||
"github.com/anchore/syft/syft"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
type task func(*sbom.Artifacts, *source.Source) ([]artifact.Relationship, error)
|
||||
|
||||
func tasks() ([]task, error) {
|
||||
var tasks []task
|
||||
|
||||
generators := []func() (task, error){
|
||||
generateCatalogPackagesTask,
|
||||
generateCatalogFileMetadataTask,
|
||||
generateCatalogFileDigestsTask,
|
||||
generateCatalogSecretsTask,
|
||||
generateCatalogFileClassificationsTask,
|
||||
generateCatalogContentsTask,
|
||||
}
|
||||
|
||||
for _, generator := range generators {
|
||||
task, err := generator()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if task != nil {
|
||||
tasks = append(tasks, task)
|
||||
}
|
||||
}
|
||||
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
func generateCatalogPackagesTask() (task, error) {
|
||||
if !appConfig.Package.Cataloger.Enabled {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
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 nil, err
|
||||
}
|
||||
|
||||
results.PackageCatalog = packageCatalog
|
||||
results.Distro = theDistro
|
||||
|
||||
return relationships, nil
|
||||
}
|
||||
|
||||
return task, nil
|
||||
}
|
||||
|
||||
func generateCatalogFileMetadataTask() (task, error) {
|
||||
if !appConfig.FileMetadata.Cataloger.Enabled {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
metadataCataloger := file.NewMetadataCataloger()
|
||||
|
||||
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
|
||||
resolver, err := src.FileResolver(appConfig.FileMetadata.Cataloger.ScopeOpt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, err := metadataCataloger.Catalog(resolver)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results.FileMetadata = result
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return task, nil
|
||||
}
|
||||
|
||||
func generateCatalogFileDigestsTask() (task, error) {
|
||||
if !appConfig.FileMetadata.Cataloger.Enabled {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
supportedHashAlgorithms := make(map[string]crypto.Hash)
|
||||
for _, h := range []crypto.Hash{
|
||||
crypto.MD5,
|
||||
crypto.SHA1,
|
||||
crypto.SHA256,
|
||||
} {
|
||||
supportedHashAlgorithms[file.DigestAlgorithmName(h)] = h
|
||||
}
|
||||
|
||||
var hashes []crypto.Hash
|
||||
for _, hashStr := range appConfig.FileMetadata.Digests {
|
||||
name := file.CleanDigestAlgorithmName(hashStr)
|
||||
hashObj, ok := supportedHashAlgorithms[name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unsupported hash algorithm: %s", hashStr)
|
||||
}
|
||||
hashes = append(hashes, hashObj)
|
||||
}
|
||||
|
||||
digestsCataloger, err := file.NewDigestsCataloger(hashes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
|
||||
resolver, err := src.FileResolver(appConfig.FileMetadata.Cataloger.ScopeOpt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, err := digestsCataloger.Catalog(resolver)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results.FileDigests = result
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return task, nil
|
||||
}
|
||||
|
||||
func generateCatalogSecretsTask() (task, error) {
|
||||
if !appConfig.Secrets.Cataloger.Enabled {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
patterns, err := file.GenerateSearchPatterns(file.DefaultSecretsPatterns, appConfig.Secrets.AdditionalPatterns, appConfig.Secrets.ExcludePatternNames)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
secretsCataloger, err := file.NewSecretsCataloger(patterns, appConfig.Secrets.RevealValues, appConfig.Secrets.SkipFilesAboveSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
|
||||
resolver, err := src.FileResolver(appConfig.Secrets.Cataloger.ScopeOpt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, err := secretsCataloger.Catalog(resolver)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results.Secrets = result
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return task, nil
|
||||
}
|
||||
|
||||
func generateCatalogFileClassificationsTask() (task, error) {
|
||||
if !appConfig.FileClassification.Cataloger.Enabled {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// TODO: in the future we could expose out the classifiers via configuration
|
||||
classifierCataloger, err := file.NewClassificationCataloger(file.DefaultClassifiers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
|
||||
resolver, err := src.FileResolver(appConfig.FileClassification.Cataloger.ScopeOpt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, err := classifierCataloger.Catalog(resolver)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results.FileClassifications = result
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return task, nil
|
||||
}
|
||||
|
||||
func generateCatalogContentsTask() (task, error) {
|
||||
if !appConfig.FileContents.Cataloger.Enabled {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
contentsCataloger, err := file.NewContentsCataloger(appConfig.FileContents.Globs, appConfig.FileContents.SkipFilesAboveSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
|
||||
resolver, err := src.FileResolver(appConfig.FileContents.Cataloger.ScopeOpt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, err := contentsCataloger.Catalog(resolver)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results.FileContents = result
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return task, nil
|
||||
}
|
||||
|
||||
func runTask(t task, a *sbom.Artifacts, src *source.Source, c chan<- artifact.Relationship, errs chan<- error) {
|
||||
defer close(c)
|
||||
|
||||
relationships, err := t(a, src)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
|
||||
for _, relationship := range relationships {
|
||||
c <- relationship
|
||||
}
|
||||
}
|
||||
+1
-2
@@ -8,7 +8,6 @@ import (
|
||||
"github.com/anchore/syft/internal"
|
||||
|
||||
"github.com/anchore/syft/internal/version"
|
||||
"github.com/anchore/syft/syft/presenter"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -21,7 +20,7 @@ var versionCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
func init() {
|
||||
versionCmd.Flags().StringVarP(&outputFormat, "output", "o", string(presenter.TextPresenter), "format to show version information (available=[text, json])")
|
||||
versionCmd.Flags().StringVarP(&outputFormat, "output", "o", "text", "format to show version information (available=[text, json])")
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,43 +1,58 @@
|
||||
module github.com/anchore/syft
|
||||
|
||||
go 1.14
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
|
||||
github.com/adrg/xdg v0.2.1
|
||||
github.com/alecthomas/jsonschema v0.0.0-20200530073317-71f438968921
|
||||
github.com/anchore/client-go v0.0.0-20201216213038-a486b838e238
|
||||
github.com/anchore/go-rpmdb v0.0.0-20201106153645-0043963c2e12
|
||||
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/stereoscope v0.0.0-20210201165248-e94c52b4052d
|
||||
github.com/anchore/packageurl-go v0.0.0-20210922164639-b3fa992ebd29
|
||||
github.com/anchore/stereoscope v0.0.0-20211116152349-7e4e1b56a15d
|
||||
// we are hinting brotli to latest due to warning when installing archiver v3:
|
||||
// go: warning: github.com/andybalholm/brotli@v1.0.1: retracted by module author: occasional panics and data corruption
|
||||
github.com/andybalholm/brotli v1.0.4 // indirect
|
||||
github.com/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
|
||||
github.com/docker/docker v20.10.10+incompatible
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/facebookincubator/nvdtools v0.1.4
|
||||
github.com/go-test/deep v1.0.7
|
||||
github.com/google/uuid v1.1.1
|
||||
github.com/google/go-cmp v0.5.6
|
||||
github.com/google/uuid v1.2.0
|
||||
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/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/mholt/archiver/v3 v3.5.1
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/mitchellh/mapstructure v1.3.1
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2
|
||||
github.com/mitchellh/mapstructure v1.4.1
|
||||
github.com/olekukonko/tablewriter v0.0.4
|
||||
github.com/package-url/packageurl-go v0.1.0
|
||||
github.com/pelletier/go-toml v1.8.0
|
||||
github.com/pelletier/go-toml v1.9.3
|
||||
github.com/pkg/profile v1.5.0
|
||||
github.com/scylladb/go-set v1.0.2
|
||||
github.com/sergi/go-diff v1.1.0
|
||||
github.com/sirupsen/logrus v1.6.0
|
||||
github.com/spf13/afero v1.2.2
|
||||
github.com/spf13/cobra v1.0.1-0.20200909172742-8a63648dd905
|
||||
github.com/spf13/viper v1.7.0
|
||||
github.com/wagoodman/go-partybus v0.0.0-20200526224238-eb215533f07d
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
github.com/spdx/tools-golang v0.1.0
|
||||
github.com/spf13/afero v1.6.0
|
||||
github.com/spf13/cobra v1.2.1
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/spf13/viper v1.8.1
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/vifraa/gopom v0.1.0
|
||||
github.com/wagoodman/go-partybus v0.0.0-20210627031916-db1f5573bbc5
|
||||
github.com/wagoodman/go-progress v0.0.0-20200731105512-1020f39e6240
|
||||
github.com/wagoodman/jotframe v0.0.0-20200730190914-3517092dd163
|
||||
github.com/wagoodman/jotframe v0.0.0-20211129225309-56b0d0a4aebb
|
||||
github.com/x-cray/logrus-prefixed-formatter v0.5.2
|
||||
github.com/xeipuuv/gojsonschema v1.2.0
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
|
||||
golang.org/x/mod v0.3.0
|
||||
gopkg.in/yaml.v2 v2.3.0
|
||||
golang.org/x/mod v0.4.2
|
||||
golang.org/x/net v0.0.0-20211111160137-58aab5ef257a
|
||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
@@ -2,7 +2,11 @@ package anchore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/anchore/client-go/pkg/external"
|
||||
"github.com/anchore/syft/internal"
|
||||
@@ -10,11 +14,10 @@ import (
|
||||
)
|
||||
|
||||
type Configuration struct {
|
||||
Hostname string
|
||||
BaseURL string
|
||||
Username string
|
||||
Password string
|
||||
UserAgent string
|
||||
Scheme string
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
@@ -29,16 +32,16 @@ func NewClient(cfg Configuration) (*Client, error) {
|
||||
cfg.UserAgent = fmt.Sprintf("%s / %s %s", internal.ApplicationName, versionInfo.Version, versionInfo.Platform)
|
||||
}
|
||||
|
||||
if cfg.Scheme == "" {
|
||||
cfg.Scheme = "https"
|
||||
baseURL, err := prepareBaseURLForClient(cfg.BaseURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create client: %w", err)
|
||||
}
|
||||
|
||||
return &Client{
|
||||
config: cfg,
|
||||
client: external.NewAPIClient(&external.Configuration{
|
||||
Host: cfg.Hostname,
|
||||
BasePath: baseURL,
|
||||
UserAgent: cfg.UserAgent,
|
||||
Scheme: cfg.Scheme,
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
@@ -56,3 +59,57 @@ func (c *Client) newRequestContext(parentContext context.Context) context.Contex
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
var ErrInvalidBaseURLInput = errors.New("invalid base URL input")
|
||||
|
||||
func prepareBaseURLForClient(baseURL string) (string, error) {
|
||||
if err := checkBaseURLInput(baseURL); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
scheme, urlWithoutScheme := splitSchemeFromURL(baseURL)
|
||||
|
||||
if scheme == "" {
|
||||
scheme = "http"
|
||||
}
|
||||
|
||||
urlWithoutScheme = path.Clean(urlWithoutScheme)
|
||||
|
||||
const requiredSuffix = "v1"
|
||||
if path.Base(urlWithoutScheme) != requiredSuffix {
|
||||
urlWithoutScheme = path.Join(urlWithoutScheme, requiredSuffix)
|
||||
}
|
||||
|
||||
preparedBaseURL := scheme + "://" + urlWithoutScheme
|
||||
return preparedBaseURL, nil
|
||||
}
|
||||
|
||||
func checkBaseURLInput(url string) error {
|
||||
if url == "" {
|
||||
return ErrInvalidBaseURLInput
|
||||
}
|
||||
|
||||
firstCharacter := rune(url[0])
|
||||
if !(unicode.IsLetter(firstCharacter)) {
|
||||
return ErrInvalidBaseURLInput
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func splitSchemeFromURL(url string) (scheme, urlWithoutScheme string) {
|
||||
if hasScheme(url) {
|
||||
urlParts := strings.SplitN(url, "://", 2)
|
||||
scheme = urlParts[0]
|
||||
urlWithoutScheme = urlParts[1]
|
||||
return
|
||||
}
|
||||
|
||||
return "", url
|
||||
}
|
||||
|
||||
func hasScheme(url string) bool {
|
||||
parts := strings.Split(url, "://")
|
||||
|
||||
return len(parts) > 1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
package anchore
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestHasScheme(t *testing.T) {
|
||||
cases := []struct {
|
||||
url string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
url: "http://localhost",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
url: "https://anchore.com:8443",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
url: "google.com",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
url: "",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range cases {
|
||||
t.Run(testCase.url, func(t *testing.T) {
|
||||
result := hasScheme(testCase.url)
|
||||
|
||||
if testCase.expected != result {
|
||||
t.Errorf("expected %t but got %t", testCase.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareBaseURLForClient(t *testing.T) {
|
||||
cases := []struct {
|
||||
inputURL string
|
||||
expectedURL string
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
inputURL: "",
|
||||
expectedURL: "",
|
||||
expectedErr: ErrInvalidBaseURLInput,
|
||||
},
|
||||
{
|
||||
inputURL: "localhost",
|
||||
expectedURL: "http://localhost/v1",
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
inputURL: "https://localhost",
|
||||
expectedURL: "https://localhost/v1",
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
inputURL: "https://localhost/",
|
||||
expectedURL: "https://localhost/v1",
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
inputURL: "https://localhost/v1/",
|
||||
expectedURL: "https://localhost/v1",
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
inputURL: "https://localhost/v1//",
|
||||
expectedURL: "https://localhost/v1",
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
inputURL: "http://something.com/platform/v1/services/anchore",
|
||||
expectedURL: "http://something.com/platform/v1/services/anchore/v1",
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
inputURL: "my-host:8228",
|
||||
expectedURL: "http://my-host:8228/v1",
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
inputURL: "v1/v1",
|
||||
expectedURL: "http://v1/v1",
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
inputURL: "/v1",
|
||||
expectedURL: "",
|
||||
expectedErr: ErrInvalidBaseURLInput,
|
||||
},
|
||||
{
|
||||
inputURL: "/imports/images",
|
||||
expectedURL: "",
|
||||
expectedErr: ErrInvalidBaseURLInput,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range cases {
|
||||
t.Run(testCase.inputURL, func(t *testing.T) {
|
||||
resultURL, err := prepareBaseURLForClient(testCase.inputURL)
|
||||
if err != testCase.expectedErr {
|
||||
t.Errorf("expected err to be '%v' but got '%v'", testCase.expectedErr, err)
|
||||
}
|
||||
|
||||
if resultURL != testCase.expectedURL {
|
||||
t.Errorf("expected URL to be '%v' but got '%v'", testCase.expectedURL, resultURL)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckBaseURLInput(t *testing.T) {
|
||||
cases := []struct {
|
||||
input string
|
||||
expected error
|
||||
}{
|
||||
{
|
||||
input: "",
|
||||
expected: ErrInvalidBaseURLInput,
|
||||
},
|
||||
{
|
||||
input: "x",
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
input: "localhost:8000",
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
input: ":80",
|
||||
expected: ErrInvalidBaseURLInput,
|
||||
},
|
||||
{
|
||||
input: "/v1",
|
||||
expected: ErrInvalidBaseURLInput,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range cases {
|
||||
t.Run(testCase.input, func(t *testing.T) {
|
||||
resultErr := checkBaseURLInput(testCase.input)
|
||||
|
||||
if testCase.expected != resultErr {
|
||||
t.Errorf("expected err to be '%v' but got '%v'", testCase.expected, resultErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitSchemeFromURL(t *testing.T) {
|
||||
cases := []struct {
|
||||
input string
|
||||
expectedScheme string
|
||||
expectedURLWithoutScheme string
|
||||
}{
|
||||
{
|
||||
input: "",
|
||||
expectedScheme: "",
|
||||
expectedURLWithoutScheme: "",
|
||||
},
|
||||
{
|
||||
input: "localhost",
|
||||
expectedScheme: "",
|
||||
expectedURLWithoutScheme: "localhost",
|
||||
},
|
||||
{
|
||||
input: "https://anchore.com/path",
|
||||
expectedScheme: "https",
|
||||
expectedURLWithoutScheme: "anchore.com/path",
|
||||
},
|
||||
{
|
||||
input: "tcp://host:1234",
|
||||
expectedScheme: "tcp",
|
||||
expectedURLWithoutScheme: "host:1234",
|
||||
},
|
||||
{
|
||||
input: "/hello",
|
||||
expectedScheme: "",
|
||||
expectedURLWithoutScheme: "/hello",
|
||||
},
|
||||
{
|
||||
input: "://host",
|
||||
expectedScheme: "",
|
||||
expectedURLWithoutScheme: "host",
|
||||
},
|
||||
{
|
||||
input: "http//localhost",
|
||||
expectedScheme: "",
|
||||
expectedURLWithoutScheme: "http//localhost",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range cases {
|
||||
t.Run(testCase.input, func(t *testing.T) {
|
||||
resultScheme, resultURLWithoutScheme := splitSchemeFromURL(testCase.input)
|
||||
|
||||
if testCase.expectedScheme != resultScheme {
|
||||
t.Errorf("expected scheme to be '%s' but got '%s'", testCase.expectedScheme, resultScheme)
|
||||
}
|
||||
|
||||
if testCase.expectedURLWithoutScheme != resultURLWithoutScheme {
|
||||
t.Errorf("expected urlWithoutScheme to be '%s' but got '%s'", testCase.expectedURLWithoutScheme, resultURLWithoutScheme)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+10
-13
@@ -6,26 +6,22 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/antihax/optional"
|
||||
|
||||
"github.com/anchore/client-go/pkg/external"
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/syft/distro"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
"github.com/antihax/optional"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
)
|
||||
|
||||
type ImportConfig struct {
|
||||
ImageMetadata image.Metadata
|
||||
SourceMetadata source.Metadata
|
||||
Catalog *pkg.Catalog
|
||||
Distro *distro.Distro
|
||||
SBOM sbom.SBOM
|
||||
Dockerfile []byte
|
||||
OverwriteExistingUpload bool
|
||||
Timeout uint
|
||||
}
|
||||
|
||||
func importProgress(source string) (*progress.Stage, *progress.Manual) {
|
||||
@@ -51,9 +47,10 @@ func importProgress(source string) (*progress.Stage, *progress.Manual) {
|
||||
|
||||
// nolint:funlen
|
||||
func (c *Client) Import(ctx context.Context, cfg ImportConfig) error {
|
||||
stage, prog := importProgress(c.config.Hostname)
|
||||
stage, prog := importProgress(c.config.BaseURL)
|
||||
|
||||
ctxWithTimeout, cancel := context.WithTimeout(ctx, time.Second*30)
|
||||
timeout := time.Duration(cfg.Timeout) * time.Second
|
||||
ctxWithTimeout, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
authedCtx := c.newRequestContext(ctxWithTimeout)
|
||||
@@ -71,19 +68,19 @@ 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, stage)
|
||||
packageDigest, err := importPackageSBOM(authedCtx, c.client.ImportsApi, sessionID, cfg.SBOM, stage)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to import Package SBOM: %w", err)
|
||||
}
|
||||
prog.N++
|
||||
|
||||
manifestDigest, err := importManifest(authedCtx, c.client.ImportsApi, sessionID, cfg.ImageMetadata.RawManifest, stage)
|
||||
manifestDigest, err := importManifest(authedCtx, c.client.ImportsApi, sessionID, cfg.SBOM.Source.ImageMetadata.RawManifest, stage)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to import Manifest: %w", err)
|
||||
}
|
||||
prog.N++
|
||||
|
||||
configDigest, err := importConfig(authedCtx, c.client.ImportsApi, sessionID, cfg.ImageMetadata.RawConfig, stage)
|
||||
configDigest, err := importConfig(authedCtx, c.client.ImportsApi, sessionID, cfg.SBOM.Source.ImageMetadata.RawConfig, stage)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to import Config: %w", err)
|
||||
}
|
||||
|
||||
@@ -8,26 +8,24 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
|
||||
"github.com/anchore/syft/internal/formats/syftjson"
|
||||
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
jsonPresenter "github.com/anchore/syft/syft/presenter/json"
|
||||
|
||||
"github.com/anchore/syft/syft/distro"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
|
||||
"github.com/anchore/client-go/pkg/external"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
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) (*external.ImagePackageManifest, error) {
|
||||
func packageSbomModel(s sbom.SBOM) (*external.ImagePackageManifest, error) {
|
||||
var buf bytes.Buffer
|
||||
pres := jsonPresenter.NewPresenter(catalog, s, d)
|
||||
err := pres.Present(&buf)
|
||||
|
||||
err := syftjson.Format().Presenter(s).Present(&buf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to serialize results: %w", err)
|
||||
}
|
||||
@@ -41,11 +39,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, stage *progress.Stage) (string, error) {
|
||||
func importPackageSBOM(ctx context.Context, api packageSBOMImportAPI, sessionID string, s sbom.SBOM, stage *progress.Stage) (string, error) {
|
||||
log.Debug("importing package SBOM")
|
||||
stage.Current = "package SBOM"
|
||||
|
||||
model, err := packageSbomModel(s, catalog, d)
|
||||
model, err := packageSbomModel(s)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to create PackageSBOM model: %w", err)
|
||||
}
|
||||
|
||||
@@ -9,18 +9,17 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
jsonPresenter "github.com/anchore/syft/syft/presenter/json"
|
||||
|
||||
"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 {
|
||||
@@ -38,7 +37,6 @@ func TestPackageSbomToModel(t *testing.T) {
|
||||
Scheme: source.ImageScheme,
|
||||
ImageMetadata: source.ImageMetadata{
|
||||
UserInput: "user-in",
|
||||
Scope: "scope!",
|
||||
Layers: []source.LayerMetadata{
|
||||
{
|
||||
MediaType: "layer-metadata-type!",
|
||||
@@ -61,8 +59,10 @@ func TestPackageSbomToModel(t *testing.T) {
|
||||
FoundBy: "foundBy",
|
||||
Locations: []source.Location{
|
||||
{
|
||||
RealPath: "path",
|
||||
FileSystemID: "layerID",
|
||||
Coordinates: source.Coordinates{
|
||||
RealPath: "path",
|
||||
FileSystemID: "layerID",
|
||||
},
|
||||
},
|
||||
},
|
||||
Licenses: []string{"license"},
|
||||
@@ -76,7 +76,15 @@ func TestPackageSbomToModel(t *testing.T) {
|
||||
|
||||
c := pkg.NewCatalog(p)
|
||||
|
||||
model, err := packageSbomModel(m, c, &d)
|
||||
sbomResult := sbom.SBOM{
|
||||
Artifacts: sbom.Artifacts{
|
||||
PackageCatalog: c,
|
||||
Distro: &d,
|
||||
},
|
||||
Source: m,
|
||||
}
|
||||
|
||||
model, err := packageSbomModel(sbomResult)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to generate model from source material: %+v", err)
|
||||
}
|
||||
@@ -88,25 +96,37 @@ 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 := jsonPresenter.NewPresenter(c, m, &d)
|
||||
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 jsonPresenter.Document
|
||||
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 jsonPresenter.Document
|
||||
var actualDoc syftjsonModel.Document
|
||||
if err := json.Unmarshal(modelJSON, &actualDoc); err != nil {
|
||||
t.Fatalf("unable to parse json doc: %+v", err)
|
||||
}
|
||||
|
||||
for _, d := range deep.Equal(actualDoc, expectedDoc) {
|
||||
if strings.HasSuffix(d, "<nil slice> != []") {
|
||||
// do not consider nil vs empty collection semantics as a "difference"
|
||||
continue
|
||||
}
|
||||
t.Errorf("diff: %+v", d)
|
||||
}
|
||||
}
|
||||
@@ -139,8 +159,10 @@ func TestPackageSbomImport(t *testing.T) {
|
||||
FoundBy: "foundBy",
|
||||
Locations: []source.Location{
|
||||
{
|
||||
RealPath: "path",
|
||||
FileSystemID: "layerID",
|
||||
Coordinates: source.Coordinates{
|
||||
RealPath: "path",
|
||||
FileSystemID: "layerID",
|
||||
},
|
||||
},
|
||||
},
|
||||
Licenses: []string{"license"},
|
||||
@@ -174,10 +196,9 @@ func TestPackageSbomImport(t *testing.T) {
|
||||
})
|
||||
|
||||
m := source.Metadata{
|
||||
Scheme: "a-schema",
|
||||
Scheme: source.ImageScheme,
|
||||
ImageMetadata: source.ImageMetadata{
|
||||
UserInput: "user-in",
|
||||
Scope: "scope!",
|
||||
Layers: nil,
|
||||
Size: 10,
|
||||
ManifestDigest: "sha256:digest!",
|
||||
@@ -188,7 +209,15 @@ func TestPackageSbomImport(t *testing.T) {
|
||||
|
||||
d, _ := distro.NewDistro(distro.CentOS, "8.0", "")
|
||||
|
||||
theModel, err := packageSbomModel(m, catalog, &d)
|
||||
sbomResult := sbom.SBOM{
|
||||
Artifacts: sbom.Artifacts{
|
||||
PackageCatalog: catalog,
|
||||
Distro: &d,
|
||||
},
|
||||
Source: m,
|
||||
}
|
||||
|
||||
theModel, err := packageSbomModel(sbomResult)
|
||||
if err != nil {
|
||||
t.Fatalf("could not get sbom model: %+v", err)
|
||||
}
|
||||
@@ -227,7 +256,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, &progress.Stage{})
|
||||
digest, err := importPackageSBOM(context.TODO(), test.api, sessionID, sbomResult, &progress.Stage{})
|
||||
|
||||
// validate error handling
|
||||
if err != nil && !test.expectsError {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package config
|
||||
|
||||
import "github.com/spf13/viper"
|
||||
|
||||
type anchore struct {
|
||||
// upload options
|
||||
Host string `yaml:"host" json:"host" mapstructure:"host"` // -H , hostname of the engine/enterprise instance to upload to (setting this value enables upload)
|
||||
Path string `yaml:"path" json:"path" mapstructure:"path"` // override the engine/enterprise API upload path
|
||||
// IMPORTANT: do not show the username in any YAML/JSON output (sensitive information)
|
||||
Username string `yaml:"-" json:"-" mapstructure:"username"` // -u , username to authenticate upload
|
||||
// IMPORTANT: do not show the password in any YAML/JSON output (sensitive information)
|
||||
Password string `yaml:"-" json:"-" mapstructure:"password"` // -p , password to authenticate upload
|
||||
Dockerfile string `yaml:"dockerfile" json:"dockerfile" mapstructure:"dockerfile"` // -d , dockerfile to attach for upload
|
||||
OverwriteExistingImage bool `yaml:"overwrite-existing-image" json:"overwrite-existing-image" mapstructure:"overwrite-existing-image"` // --overwrite-existing-image , if any of the SBOM components have already been uploaded this flag will ensure they are overwritten with the current upload
|
||||
ImportTimeout uint `yaml:"import-timeout" json:"import-timeout" mapstructure:"import-timeout"` // --import-timeout
|
||||
// , customize the number of seconds within which the SBOM import must be completed or canceled
|
||||
}
|
||||
|
||||
func (cfg anchore) loadDefaultValues(v *viper.Viper) {
|
||||
v.SetDefault("anchore.path", "")
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/adrg/xdg"
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/viper"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
var ErrApplicationConfigNotFound = fmt.Errorf("application config not found")
|
||||
|
||||
var catalogerEnabledDefault = false
|
||||
|
||||
type defaultValueLoader interface {
|
||||
loadDefaultValues(*viper.Viper)
|
||||
}
|
||||
|
||||
type parser interface {
|
||||
parseConfigValues() error
|
||||
}
|
||||
|
||||
// Application is the main syft application configuration.
|
||||
type Application struct {
|
||||
ConfigPath string `yaml:",omitempty" json:"configPath"` // the location where the application config was read from (either from -c or discovered while loading)
|
||||
Output string `yaml:"output" json:"output" mapstructure:"output"` // -o, the Presenter hint string to use for report formatting
|
||||
File string `yaml:"file" json:"file" mapstructure:"file"` // --file, the file to write report output to
|
||||
Quiet bool `yaml:"quiet" json:"quiet" mapstructure:"quiet"` // -q, indicates to not show any status output to stderr (ETUI or logging UI)
|
||||
CheckForAppUpdate bool `yaml:"check-for-app-update" json:"check-for-app-update" mapstructure:"check-for-app-update"` // whether to check for an application update on start up or not
|
||||
Anchore anchore `yaml:"anchore" json:"anchore" mapstructure:"anchore"` // options for interacting with Anchore Engine/Enterprise
|
||||
CliOptions CliOnlyOptions `yaml:"-" json:"-"` // all options only available through the CLI (not via env vars or config)
|
||||
Dev development `yaml:"dev" json:"dev" mapstructure:"dev"`
|
||||
Log logging `yaml:"log" json:"log" mapstructure:"log"` // all logging-related options
|
||||
Package packages `yaml:"package" json:"package" mapstructure:"package"`
|
||||
FileMetadata FileMetadata `yaml:"file-metadata" json:"file-metadata" mapstructure:"file-metadata"`
|
||||
FileClassification fileClassification `yaml:"file-classification" json:"file-classification" mapstructure:"file-classification"`
|
||||
FileContents fileContents `yaml:"file-contents" json:"file-contents" mapstructure:"file-contents"`
|
||||
Secrets secrets `yaml:"secrets" json:"secrets" mapstructure:"secrets"`
|
||||
Registry registry `yaml:"registry" json:"registry" mapstructure:"registry"`
|
||||
}
|
||||
|
||||
// PowerUserCatalogerEnabledDefault switches all catalogers to be enabled when running power-user command
|
||||
func PowerUserCatalogerEnabledDefault() {
|
||||
catalogerEnabledDefault = true
|
||||
}
|
||||
|
||||
func newApplicationConfig(v *viper.Viper, cliOpts CliOnlyOptions) *Application {
|
||||
config := &Application{
|
||||
CliOptions: cliOpts,
|
||||
}
|
||||
config.loadDefaultValues(v)
|
||||
return config
|
||||
}
|
||||
|
||||
// LoadApplicationConfig populates the given viper object with application configuration discovered on disk
|
||||
func LoadApplicationConfig(v *viper.Viper, cliOpts CliOnlyOptions) (*Application, error) {
|
||||
// the user may not have a config, and this is OK, we can use the default config + default cobra cli values instead
|
||||
config := newApplicationConfig(v, cliOpts)
|
||||
|
||||
if err := readConfig(v, cliOpts.ConfigPath); err != nil && !errors.Is(err, ErrApplicationConfigNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := v.Unmarshal(config); err != nil {
|
||||
return nil, fmt.Errorf("unable to parse config: %w", err)
|
||||
}
|
||||
config.ConfigPath = v.ConfigFileUsed()
|
||||
|
||||
if err := config.parseConfigValues(); err != nil {
|
||||
return nil, fmt.Errorf("invalid application config: %w", err)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// init loads the default configuration values into the viper instance (before the config values are read and parsed).
|
||||
func (cfg Application) loadDefaultValues(v *viper.Viper) {
|
||||
// set the default values for primitive fields in this struct
|
||||
v.SetDefault("check-for-app-update", true)
|
||||
|
||||
// for each field in the configuration struct, see if the field implements the defaultValueLoader interface and invoke it if it does
|
||||
value := reflect.ValueOf(cfg)
|
||||
for i := 0; i < value.NumField(); i++ {
|
||||
// note: the defaultValueLoader method receiver is NOT a pointer receiver.
|
||||
if loadable, ok := value.Field(i).Interface().(defaultValueLoader); ok {
|
||||
// the field implements defaultValueLoader, call it
|
||||
loadable.loadDefaultValues(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (cfg *Application) parseConfigValues() error {
|
||||
// parse application config options
|
||||
for _, optionFn := range []func() error{
|
||||
cfg.parseUploadOptions,
|
||||
cfg.parseLogLevelOption,
|
||||
} {
|
||||
if err := optionFn(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// parse nested config options
|
||||
// for each field in the configuration struct, see if the field implements the parser interface
|
||||
// note: the app config is a pointer, so we need to grab the elements explicitly (to traverse the address)
|
||||
value := reflect.ValueOf(cfg).Elem()
|
||||
for i := 0; i < value.NumField(); i++ {
|
||||
// note: since the interface method of parser is a pointer receiver we need to get the value of the field as a pointer.
|
||||
if parsable, ok := value.Field(i).Addr().Interface().(parser); ok {
|
||||
// the field implements parser, call it
|
||||
if err := parsable.parseConfigValues(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg *Application) parseUploadOptions() error {
|
||||
if cfg.Anchore.Host == "" && cfg.Anchore.Dockerfile != "" {
|
||||
return fmt.Errorf("cannot provide dockerfile option without enabling upload")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg *Application) parseLogLevelOption() error {
|
||||
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
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.Log.Level == "" {
|
||||
cfg.Log.Level = cfg.Log.LevelOpt.String()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg Application) String() string {
|
||||
// yaml is pretty human friendly (at least when compared to json)
|
||||
appCfgStr, err := yaml.Marshal(&cfg)
|
||||
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
return string(appCfgStr)
|
||||
}
|
||||
|
||||
// readConfig attempts to read the given config path from disk or discover an alternate store location
|
||||
// nolint:funlen
|
||||
func readConfig(v *viper.Viper, configPath string) error {
|
||||
var err error
|
||||
v.AutomaticEnv()
|
||||
v.SetEnvPrefix(internal.ApplicationName)
|
||||
// allow for nested options to be specified via environment variables
|
||||
// e.g. pod.context = APPNAME_POD_CONTEXT
|
||||
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
|
||||
|
||||
// use explicitly the given user config
|
||||
if configPath != "" {
|
||||
v.SetConfigFile(configPath)
|
||||
if err := v.ReadInConfig(); err != nil {
|
||||
return fmt.Errorf("unable to read application config=%q : %w", configPath, err)
|
||||
}
|
||||
// don't fall through to other options if the config path was explicitly provided
|
||||
return nil
|
||||
}
|
||||
|
||||
// start searching for valid configs in order...
|
||||
|
||||
// 1. look for .<appname>.yaml (in the current directory)
|
||||
v.AddConfigPath(".")
|
||||
v.SetConfigName("." + internal.ApplicationName)
|
||||
if exists, err := readViperConfig(v); exists || err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. look for .<appname>/config.yaml (in the current directory)
|
||||
v.AddConfigPath("." + internal.ApplicationName)
|
||||
v.SetConfigName("config")
|
||||
if exists, err := readViperConfig(v); exists || err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if root := gitRepoRoot(); root != "" {
|
||||
// 3. look for <git repo root>/.<appname>.yaml, where the repo root is defined by "git rev-parse --show-toplevel"
|
||||
v.AddConfigPath(root)
|
||||
v.SetConfigName("." + internal.ApplicationName)
|
||||
if exists, err := readViperConfig(v); exists {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Chdir(root)
|
||||
}
|
||||
|
||||
// 4. look for <git repo root>/.<appname>/config.yaml, where the repo root is defined by "git rev-parse --show-toplevel"
|
||||
v.AddConfigPath(filepath.Join(root, "."+internal.ApplicationName))
|
||||
v.SetConfigName("config")
|
||||
if exists, err := readViperConfig(v); exists {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Chdir(root)
|
||||
}
|
||||
}
|
||||
|
||||
// 5. look for ~/.<appname>.yaml
|
||||
home, err := homedir.Dir()
|
||||
if err == nil {
|
||||
v.AddConfigPath(home)
|
||||
v.SetConfigName("." + internal.ApplicationName)
|
||||
if exists, err := readViperConfig(v); exists || err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 6. look for <appname>/config.yaml in xdg locations (starting with xdg home config dir, then moving upwards)
|
||||
v.AddConfigPath(path.Join(xdg.ConfigHome, internal.ApplicationName))
|
||||
for _, dir := range xdg.ConfigDirs {
|
||||
v.AddConfigPath(path.Join(dir, internal.ApplicationName))
|
||||
}
|
||||
v.SetConfigName("config")
|
||||
if exists, err := readViperConfig(v); exists || err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ErrApplicationConfigNotFound
|
||||
}
|
||||
|
||||
func readViperConfig(v *viper.Viper) (bool, error) {
|
||||
err := v.ReadInConfig()
|
||||
if err == nil {
|
||||
return true, nil
|
||||
} else if errors.As(err, &viper.ConfigFileNotFoundError{}) {
|
||||
return false, nil
|
||||
}
|
||||
return true, fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err)
|
||||
}
|
||||
|
||||
func gitRepoRoot() string {
|
||||
root, err := exec.Command("git", "rev-parse", "--show-toplevel").Output()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
absRepoRoot, err := filepath.Abs(strings.TrimSpace(string(root)))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return absRepoRoot
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
type catalogerOptions struct {
|
||||
Enabled bool `yaml:"enabled" json:"enabled" mapstructure:"enabled"`
|
||||
Scope string `yaml:"scope" json:"scope" mapstructure:"scope"`
|
||||
ScopeOpt source.Scope `yaml:"-" json:"-"`
|
||||
}
|
||||
|
||||
func (cfg *catalogerOptions) parseConfigValues() error {
|
||||
scopeOption := source.ParseScope(cfg.Scope)
|
||||
if scopeOption == source.UnknownScope {
|
||||
return fmt.Errorf("bad scope value %q", cfg.Scope)
|
||||
}
|
||||
cfg.ScopeOpt = scopeOption
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package config
|
||||
|
||||
// CliOnlyOptions are options that are in the application config in memory, but are only exposed via CLI switches (not from unmarshaling a config file)
|
||||
type CliOnlyOptions struct {
|
||||
ConfigPath string // -c. where the read config is on disk
|
||||
Verbosity int // -v or -vv , controlling which UI (ETUI vs logging) and what the log level should be
|
||||
}
|
||||
@@ -1,228 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/adrg/xdg"
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/syft/presenter"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/viper"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// Application is the main syft application configuration.
|
||||
type Application struct {
|
||||
ConfigPath string `yaml:",omitempty"` // the location where the application config was read from (either from -c or discovered while loading)
|
||||
PresenterOpt presenter.Option `yaml:"-"` // -o, the native Presenter.Option to use for report formatting
|
||||
Output string `yaml:"output" mapstructure:"output"` // -o, the Presenter hint string to use for report formatting
|
||||
ScopeOpt source.Scope `yaml:"-"` // -s, the native source.Scope option to use for how to catalog the container image
|
||||
Scope string `yaml:"scope" mapstructure:"scope"` // -s, the source.Scope string hint for how to catalog the container image
|
||||
Quiet bool `yaml:"quiet" mapstructure:"quiet"` // -q, indicates to not show any status output to stderr (ETUI or logging UI)
|
||||
Log logging `yaml:"log" mapstructure:"log"` // all logging-related options
|
||||
CliOptions CliOnlyOptions `yaml:"-"` // all options only available through the CLI (not via env vars or config)
|
||||
Dev Development `mapstructure:"dev"`
|
||||
CheckForAppUpdate bool `yaml:"check-for-app-update" mapstructure:"check-for-app-update"` // whether to check for an application update on start up or not
|
||||
Anchore anchore `yaml:"anchore" mapstructure:"anchore"` // options for interacting with Anchore Engine/Enterprise
|
||||
}
|
||||
|
||||
// CliOnlyOptions are options that are in the application config in memory, but are only exposed via CLI switches (not from unmarshaling a config file)
|
||||
type CliOnlyOptions struct {
|
||||
ConfigPath string // -c. where the read config is on disk
|
||||
Verbosity int // -v or -vv , controlling which UI (ETUI vs logging) and what the log level should be
|
||||
}
|
||||
|
||||
// logging contains all logging-related configuration options available to the user via the application config.
|
||||
type logging struct {
|
||||
Structured bool `yaml:"structured" mapstructure:"structured"` // show all log entries as JSON formatted strings
|
||||
LevelOpt logrus.Level `yaml:"level"` // the native log level object used by the logger
|
||||
Level string `yaml:"-" mapstructure:"level"` // the log level string hint
|
||||
FileLocation string `yaml:"file" mapstructure:"file"` // the file path to write logs to
|
||||
}
|
||||
|
||||
type anchore struct {
|
||||
// upload options
|
||||
UploadEnabled bool `yaml:"upload-enabled" mapstructure:"upload-enabled"` // whether to upload results to Anchore Engine/Enterprise (defaults to "false" unless there is the presence of -h CLI option)
|
||||
Host string `yaml:"host" mapstructure:"host"` // -H , hostname of the engine/enterprise instance to upload to
|
||||
Path string `yaml:"path" mapstructure:"path"` // override the engine/enterprise API upload path
|
||||
Username string `yaml:"username" mapstructure:"username"` // -u , username to authenticate upload
|
||||
Password string `yaml:"password" mapstructure:"password"` // -p , password to authenticate upload
|
||||
Dockerfile string `yaml:"dockerfile" mapstructure:"dockerfile"` // -d , dockerfile to attach for upload
|
||||
OverwriteExistingImage bool `yaml:"overwrite-existing-image" mapstructure:"overwrite-existing-image"` // --overwrite-existing-image , if any of the SBOM components have already been uploaded this flag will ensure they are overwritten with the current upload
|
||||
}
|
||||
|
||||
type Development struct {
|
||||
ProfileCPU bool `mapstructure:"profile-cpu"`
|
||||
ProfileMem bool `mapstructure:"profile-mem"`
|
||||
}
|
||||
|
||||
// LoadApplicationConfig populates the given viper object with application configuration discovered on disk
|
||||
func LoadApplicationConfig(v *viper.Viper, cliOpts CliOnlyOptions, wasHostnameSet bool) (*Application, error) {
|
||||
// the user may not have a config, and this is OK, we can use the default config + default cobra cli values instead
|
||||
setNonCliDefaultValues(v)
|
||||
_ = readConfig(v, cliOpts.ConfigPath)
|
||||
|
||||
config := &Application{
|
||||
CliOptions: cliOpts,
|
||||
}
|
||||
|
||||
if err := v.Unmarshal(config); err != nil {
|
||||
return nil, fmt.Errorf("unable to parse config: %w", err)
|
||||
}
|
||||
config.ConfigPath = v.ConfigFileUsed()
|
||||
|
||||
if err := config.build(v, wasHostnameSet); err != nil {
|
||||
return nil, fmt.Errorf("invalid config: %w", err)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// build inflates simple config values into syft native objects (or other complex objects) after the config is fully read in.
|
||||
func (cfg *Application) build(v *viper.Viper, wasHostnameSet bool) error {
|
||||
// set the presenter
|
||||
presenterOption := presenter.ParseOption(cfg.Output)
|
||||
if presenterOption == presenter.UnknownPresenter {
|
||||
return fmt.Errorf("bad --output value '%s'", cfg.Output)
|
||||
}
|
||||
cfg.PresenterOpt = presenterOption
|
||||
|
||||
// set the source
|
||||
scopeOption := source.ParseScope(cfg.Scope)
|
||||
if scopeOption == source.UnknownScope {
|
||||
return fmt.Errorf("bad --scope value '%s'", cfg.Scope)
|
||||
}
|
||||
cfg.ScopeOpt = scopeOption
|
||||
|
||||
if cfg.Quiet {
|
||||
// TODO: this is bad: quiet option trumps all other logging options
|
||||
// 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")
|
||||
}
|
||||
|
||||
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.WarnLevel
|
||||
}
|
||||
}
|
||||
}
|
||||
// check if upload should be done relative to the CLI and config behavior
|
||||
if !v.IsSet("anchore.upload-enabled") && wasHostnameSet {
|
||||
// we know the user didn't specify to upload in the config file and a --hostname option was provided (so set upload)
|
||||
cfg.Anchore.UploadEnabled = true
|
||||
}
|
||||
|
||||
if !cfg.Anchore.UploadEnabled && cfg.Anchore.Dockerfile != "" {
|
||||
return fmt.Errorf("cannot provide dockerfile option without enabling upload")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg Application) String() string {
|
||||
// redact sensitive information
|
||||
if cfg.Anchore.Username != "" {
|
||||
cfg.Anchore.Username = "********"
|
||||
}
|
||||
|
||||
if cfg.Anchore.Password != "" {
|
||||
cfg.Anchore.Password = "********"
|
||||
}
|
||||
|
||||
// yaml is pretty human friendly (at least when compared to json)
|
||||
appCfgStr, err := yaml.Marshal(&cfg)
|
||||
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
return string(appCfgStr)
|
||||
}
|
||||
|
||||
// readConfig attempts to read the given config path from disk or discover an alternate store location
|
||||
func readConfig(v *viper.Viper, configPath string) error {
|
||||
v.AutomaticEnv()
|
||||
v.SetEnvPrefix(internal.ApplicationName)
|
||||
// allow for nested options to be specified via environment variables
|
||||
// e.g. pod.context = APPNAME_POD_CONTEXT
|
||||
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
|
||||
|
||||
// use explicitly the given user config
|
||||
if configPath != "" {
|
||||
v.SetConfigFile(configPath)
|
||||
if err := v.ReadInConfig(); err == nil {
|
||||
return nil
|
||||
}
|
||||
// don't fall through to other options if this fails
|
||||
return fmt.Errorf("unable to read config: %v", configPath)
|
||||
}
|
||||
|
||||
// start searching for valid configs in order...
|
||||
|
||||
// 1. look for .<appname>.yaml (in the current directory)
|
||||
v.AddConfigPath(".")
|
||||
v.SetConfigName(internal.ApplicationName)
|
||||
if err := v.ReadInConfig(); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 2. look for .<appname>/config.yaml (in the current directory)
|
||||
v.AddConfigPath("." + internal.ApplicationName)
|
||||
v.SetConfigName("config")
|
||||
if err := v.ReadInConfig(); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 3. look for ~/.<appname>.yaml
|
||||
home, err := homedir.Dir()
|
||||
if err == nil {
|
||||
v.AddConfigPath(home)
|
||||
v.SetConfigName("." + internal.ApplicationName)
|
||||
if err := v.ReadInConfig(); err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 4. look for <appname>/config.yaml in xdg locations (starting with xdg home config dir, then moving upwards)
|
||||
v.AddConfigPath(path.Join(xdg.ConfigHome, internal.ApplicationName))
|
||||
for _, dir := range xdg.ConfigDirs {
|
||||
v.AddConfigPath(path.Join(dir, internal.ApplicationName))
|
||||
}
|
||||
v.SetConfigName("config")
|
||||
if err := v.ReadInConfig(); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("application config not found")
|
||||
}
|
||||
|
||||
// setNonCliDefaultValues ensures that there are sane defaults for values that do not have CLI equivalent options (where there would already be a default value)
|
||||
func setNonCliDefaultValues(v *viper.Viper) {
|
||||
v.SetDefault("log.level", "")
|
||||
v.SetDefault("log.file", "")
|
||||
v.SetDefault("log.structured", false)
|
||||
v.SetDefault("check-for-app-update", true)
|
||||
v.SetDefault("dev.profile-cpu", false)
|
||||
v.SetDefault("dev.profile-mem", false)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package config
|
||||
|
||||
import "github.com/spf13/viper"
|
||||
|
||||
type development struct {
|
||||
ProfileCPU bool `yaml:"profile-cpu" json:"profile-cpu" mapstructure:"profile-cpu"`
|
||||
ProfileMem bool `yaml:"profile-mem" json:"profile-mem" mapstructure:"profile-mem"`
|
||||
}
|
||||
|
||||
func (cfg development) loadDefaultValues(v *viper.Viper) {
|
||||
v.SetDefault("dev.profile-cpu", false)
|
||||
v.SetDefault("dev.profile-mem", false)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type fileClassification struct {
|
||||
Cataloger catalogerOptions `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"`
|
||||
}
|
||||
|
||||
func (cfg fileClassification) loadDefaultValues(v *viper.Viper) {
|
||||
v.SetDefault("file-classification.cataloger.enabled", catalogerEnabledDefault)
|
||||
v.SetDefault("file-classification.cataloger.scope", source.SquashedScope)
|
||||
}
|
||||
|
||||
func (cfg *fileClassification) parseConfigValues() error {
|
||||
return cfg.Cataloger.parseConfigValues()
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/internal/file"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type fileContents struct {
|
||||
Cataloger catalogerOptions `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"`
|
||||
SkipFilesAboveSize int64 `yaml:"skip-files-above-size" json:"skip-files-above-size" mapstructure:"skip-files-above-size"`
|
||||
Globs []string `yaml:"globs" json:"globs" mapstructure:"globs"`
|
||||
}
|
||||
|
||||
func (cfg fileContents) loadDefaultValues(v *viper.Viper) {
|
||||
v.SetDefault("file-contents.cataloger.enabled", catalogerEnabledDefault)
|
||||
v.SetDefault("file-contents.cataloger.scope", source.SquashedScope)
|
||||
v.SetDefault("file-contents.skip-files-above-size", 1*file.MB)
|
||||
v.SetDefault("file-contents.globs", []string{})
|
||||
}
|
||||
|
||||
func (cfg *fileContents) parseConfigValues() error {
|
||||
return cfg.Cataloger.parseConfigValues()
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type FileMetadata struct {
|
||||
Cataloger catalogerOptions `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"`
|
||||
Digests []string `yaml:"digests" json:"digests" mapstructure:"digests"`
|
||||
}
|
||||
|
||||
func (cfg FileMetadata) loadDefaultValues(v *viper.Viper) {
|
||||
v.SetDefault("file-metadata.cataloger.enabled", catalogerEnabledDefault)
|
||||
v.SetDefault("file-metadata.cataloger.scope", source.SquashedScope)
|
||||
v.SetDefault("file-metadata.digests", []string{"sha256"})
|
||||
}
|
||||
|
||||
func (cfg *FileMetadata) parseConfigValues() error {
|
||||
return cfg.Cataloger.parseConfigValues()
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// logging contains all logging-related configuration options available to the user via the application config.
|
||||
type logging struct {
|
||||
Structured bool `yaml:"structured" json:"structured" mapstructure:"structured"` // show all log entries as JSON formatted strings
|
||||
LevelOpt logrus.Level `yaml:"-" json:"-"` // the native log level object used by the logger
|
||||
Level string `yaml:"level" json:"level" mapstructure:"level"` // the log level string hint
|
||||
FileLocation string `yaml:"file" json:"file-location" mapstructure:"file"` // the file path to write logs to
|
||||
}
|
||||
|
||||
func (cfg logging) loadDefaultValues(v *viper.Viper) {
|
||||
v.SetDefault("log.structured", false)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package config
|
||||
|
||||
import "github.com/spf13/viper"
|
||||
|
||||
type packages struct {
|
||||
Cataloger catalogerOptions `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"`
|
||||
}
|
||||
|
||||
func (cfg packages) loadDefaultValues(v *viper.Viper) {
|
||||
v.SetDefault("package.cataloger.enabled", true)
|
||||
}
|
||||
|
||||
func (cfg *packages) parseConfigValues() error {
|
||||
return cfg.Cataloger.parseConfigValues()
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type RegistryCredentials struct {
|
||||
Authority string `yaml:"authority" json:"authority" mapstructure:"authority"`
|
||||
// IMPORTANT: do not show the username in any YAML/JSON output (sensitive information)
|
||||
Username string `yaml:"-" json:"-" mapstructure:"username"`
|
||||
// IMPORTANT: do not show the password in any YAML/JSON output (sensitive information)
|
||||
Password string `yaml:"-" json:"-" mapstructure:"password"`
|
||||
// IMPORTANT: do not show the token in any YAML/JSON output (sensitive information)
|
||||
Token string `yaml:"-" json:"-" mapstructure:"token"`
|
||||
}
|
||||
|
||||
type registry struct {
|
||||
InsecureSkipTLSVerify bool `yaml:"insecure-skip-tls-verify" json:"insecure-skip-tls-verify" mapstructure:"insecure-skip-tls-verify"`
|
||||
InsecureUseHTTP bool `yaml:"insecure-use-http" json:"insecure-use-http" mapstructure:"insecure-use-http"`
|
||||
Auth []RegistryCredentials `yaml:"auth" json:"auth" mapstructure:"auth"`
|
||||
}
|
||||
|
||||
func (cfg registry) loadDefaultValues(v *viper.Viper) {
|
||||
v.SetDefault("registry.insecure-skip-tls-verify", false)
|
||||
v.SetDefault("registry.insecure-use-http", false)
|
||||
v.SetDefault("registry.auth", []RegistryCredentials{})
|
||||
}
|
||||
|
||||
// nolint: unparam
|
||||
func (cfg *registry) parseConfigValues() error {
|
||||
// there may be additional credentials provided by env var that should be appended to the set of credentials
|
||||
authority, username, password, token :=
|
||||
os.Getenv("SYFT_REGISTRY_AUTH_AUTHORITY"),
|
||||
os.Getenv("SYFT_REGISTRY_AUTH_USERNAME"),
|
||||
os.Getenv("SYFT_REGISTRY_AUTH_PASSWORD"),
|
||||
os.Getenv("SYFT_REGISTRY_AUTH_TOKEN")
|
||||
|
||||
if hasNonEmptyCredentials(username, password, token) {
|
||||
// note: we prepend the credentials such that the environment variables take precedence over on-disk configuration.
|
||||
cfg.Auth = append([]RegistryCredentials{
|
||||
{
|
||||
Authority: authority,
|
||||
Username: username,
|
||||
Password: password,
|
||||
Token: token,
|
||||
},
|
||||
}, cfg.Auth...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func hasNonEmptyCredentials(username, password, token string) bool {
|
||||
return password != "" && username != "" || token != ""
|
||||
}
|
||||
|
||||
func (cfg *registry) ToOptions() *image.RegistryOptions {
|
||||
var auth = make([]image.RegistryCredentials, len(cfg.Auth))
|
||||
for i, a := range cfg.Auth {
|
||||
auth[i] = image.RegistryCredentials{
|
||||
Authority: a.Authority,
|
||||
Username: a.Username,
|
||||
Password: a.Password,
|
||||
Token: a.Token,
|
||||
}
|
||||
}
|
||||
return &image.RegistryOptions{
|
||||
InsecureSkipTLSVerify: cfg.InsecureSkipTLSVerify,
|
||||
InsecureUseHTTP: cfg.InsecureUseHTTP,
|
||||
Credentials: auth,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestHasNonEmptyCredentials(t *testing.T) {
|
||||
tests := []struct {
|
||||
username, password, token string
|
||||
expected bool
|
||||
}{
|
||||
|
||||
{
|
||||
"", "", "",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"user", "", "",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"", "pass", "",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"", "pass", "tok",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"user", "", "tok",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"", "", "tok",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"user", "pass", "tok",
|
||||
true,
|
||||
},
|
||||
|
||||
{
|
||||
"user", "pass", "",
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprintf("%+v", test), func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, hasNonEmptyCredentials(test.username, test.password, test.token))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_registry_ToOptions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input registry
|
||||
expected image.RegistryOptions
|
||||
}{
|
||||
{
|
||||
name: "no registry options",
|
||||
input: registry{},
|
||||
expected: image.RegistryOptions{
|
||||
Credentials: []image.RegistryCredentials{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set InsecureSkipTLSVerify",
|
||||
input: registry{
|
||||
InsecureSkipTLSVerify: true,
|
||||
},
|
||||
expected: image.RegistryOptions{
|
||||
InsecureSkipTLSVerify: true,
|
||||
Credentials: []image.RegistryCredentials{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set InsecureUseHTTP",
|
||||
input: registry{
|
||||
InsecureUseHTTP: true,
|
||||
},
|
||||
expected: image.RegistryOptions{
|
||||
InsecureUseHTTP: true,
|
||||
Credentials: []image.RegistryCredentials{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set all bool options",
|
||||
input: registry{
|
||||
InsecureSkipTLSVerify: true,
|
||||
InsecureUseHTTP: true,
|
||||
},
|
||||
expected: image.RegistryOptions{
|
||||
InsecureSkipTLSVerify: true,
|
||||
InsecureUseHTTP: true,
|
||||
Credentials: []image.RegistryCredentials{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, &test.expected, test.input.ToOptions())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/internal/file"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type secrets struct {
|
||||
Cataloger catalogerOptions `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"`
|
||||
AdditionalPatterns map[string]string `yaml:"additional-patterns" json:"additional-patterns" mapstructure:"additional-patterns"`
|
||||
ExcludePatternNames []string `yaml:"exclude-pattern-names" json:"exclude-pattern-names" mapstructure:"exclude-pattern-names"`
|
||||
RevealValues bool `yaml:"reveal-values" json:"reveal-values" mapstructure:"reveal-values"`
|
||||
SkipFilesAboveSize int64 `yaml:"skip-files-above-size" json:"skip-files-above-size" mapstructure:"skip-files-above-size"`
|
||||
}
|
||||
|
||||
func (cfg secrets) loadDefaultValues(v *viper.Viper) {
|
||||
v.SetDefault("secrets.cataloger.enabled", catalogerEnabledDefault)
|
||||
v.SetDefault("secrets.cataloger.scope", source.AllLayersScope)
|
||||
v.SetDefault("secrets.reveal-values", false)
|
||||
v.SetDefault("secrets.skip-files-above-size", 1*file.MB)
|
||||
v.SetDefault("secrets.additional-patterns", map[string]string{})
|
||||
v.SetDefault("secrets.exclude-pattern-names", []string{})
|
||||
}
|
||||
|
||||
func (cfg *secrets) parseConfigValues() error {
|
||||
return cfg.Cataloger.parseConfigValues()
|
||||
}
|
||||
@@ -6,5 +6,5 @@ const (
|
||||
|
||||
// JSONSchemaVersion is the current schema version output by the JSON presenter
|
||||
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
|
||||
JSONSchemaVersion = "1.0.1"
|
||||
JSONSchemaVersion = "2.0.0"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
)
|
||||
|
||||
// CloseAndLogError closes the given io.Closer and reports any errors found as a warning in the log
|
||||
func CloseAndLogError(closer io.Closer, location string) {
|
||||
if err := closer.Close(); err != nil {
|
||||
log.Warnf("unable to close file for location=%q: %+v", location, err)
|
||||
}
|
||||
}
|
||||
|
||||
type ErrPath struct {
|
||||
Path string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e ErrPath) Error() string {
|
||||
return fmt.Sprintf("unable to observe contents of %+v: %v", e.Path, e.Err)
|
||||
}
|
||||
|
||||
func IsErrPath(err error) bool {
|
||||
_, ok := err.(ErrPath)
|
||||
return ok
|
||||
}
|
||||
|
||||
func IsErrPathPermission(err error) bool {
|
||||
pathErr, ok := err.(ErrPath)
|
||||
if ok {
|
||||
return os.IsPermission(pathErr.Err)
|
||||
}
|
||||
return ok
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -9,6 +8,8 @@ import (
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var expectedZipArchiveEntries = []string{
|
||||
@@ -18,35 +19,21 @@ var expectedZipArchiveEntries = []string{
|
||||
"nested.zip",
|
||||
}
|
||||
|
||||
// fatalIfError calls the supplied function. If the function returns a non-nil error, t.Fatal(err) is called.
|
||||
func fatalIfError(t *testing.T, fn func() error) {
|
||||
t.Helper()
|
||||
|
||||
if fn == nil {
|
||||
return
|
||||
}
|
||||
|
||||
err := fn()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// createZipArchive creates a new ZIP archive file at destinationArchivePath based on the directory found at
|
||||
// sourceDirPath.
|
||||
func createZipArchive(t *testing.T, sourceDirPath, destinationArchivePath string) error {
|
||||
func createZipArchive(t testing.TB, sourceDirPath, destinationArchivePath string) {
|
||||
t.Helper()
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get cwd: %+v", err)
|
||||
t.Fatalf("unable to get cwd: %+v", err)
|
||||
}
|
||||
|
||||
cmd := exec.Command("./generate-zip-fixture.sh", destinationArchivePath, path.Base(sourceDirPath))
|
||||
cmd := exec.Command("./generate-zip-fixture-from-source-dir.sh", destinationArchivePath, path.Base(sourceDirPath))
|
||||
cmd.Dir = filepath.Join(cwd, "test-fixtures")
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("unable to start generate zip fixture script: %+v", err)
|
||||
t.Fatalf("unable to start generate zip fixture script: %+v", err)
|
||||
}
|
||||
|
||||
if err := cmd.Wait(); err != nil {
|
||||
@@ -59,61 +46,62 @@ func createZipArchive(t *testing.T, sourceDirPath, destinationArchivePath string
|
||||
// an ExitStatus() method with the same signature.
|
||||
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
|
||||
if status.ExitStatus() != 0 {
|
||||
return fmt.Errorf("failed to generate fixture: rc=%d", status.ExitStatus())
|
||||
t.Fatalf("failed to generate fixture: rc=%d", status.ExitStatus())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("unable to get generate fixture script result: %+v", err)
|
||||
t.Fatalf("unable to get generate fixture script result: %+v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func assertNoError(t testing.TB, fn func() error) func() {
|
||||
return func() {
|
||||
assert.NoError(t, fn())
|
||||
}
|
||||
}
|
||||
|
||||
// setupZipFileTest encapsulates common test setup work for zip file tests. It returns a cleanup function,
|
||||
// which should be called (typically deferred) by the caller, the path of the created zip archive, and an error,
|
||||
// which should trigger a fatal test failure in the consuming test. The returned cleanup function will never be nil
|
||||
// (even if there's an error), and it should always be called.
|
||||
func setupZipFileTest(t *testing.T, sourceDirPath string) (func() error, string, error) {
|
||||
func setupZipFileTest(t testing.TB, sourceDirPath string) string {
|
||||
t.Helper()
|
||||
|
||||
// Keep track of any needed cleanup work as we go
|
||||
var cleanupFns []func() error
|
||||
cleanup := func(fns []func() error) func() error {
|
||||
return func() error {
|
||||
for _, fn := range fns {
|
||||
err := fn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
archivePrefix, err := ioutil.TempFile("", "syft-ziputil-archive-TEST-")
|
||||
if err != nil {
|
||||
return cleanup(cleanupFns), "", fmt.Errorf("unable to create tempfile: %+v", err)
|
||||
t.Fatalf("unable to create tempfile: %+v", err)
|
||||
}
|
||||
cleanupFns = append(cleanupFns, func() error { return os.Remove(archivePrefix.Name()) })
|
||||
|
||||
t.Cleanup(
|
||||
assertNoError(t,
|
||||
func() error {
|
||||
return os.Remove(archivePrefix.Name())
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
destinationArchiveFilePath := archivePrefix.Name() + ".zip"
|
||||
t.Logf("archive path: %s", destinationArchiveFilePath)
|
||||
err = createZipArchive(t, sourceDirPath, destinationArchiveFilePath)
|
||||
cleanupFns = append(cleanupFns, func() error { return os.Remove(destinationArchiveFilePath) })
|
||||
if err != nil {
|
||||
return cleanup(cleanupFns), "", err
|
||||
}
|
||||
createZipArchive(t, sourceDirPath, destinationArchiveFilePath)
|
||||
|
||||
t.Cleanup(
|
||||
assertNoError(t,
|
||||
func() error {
|
||||
return os.Remove(destinationArchiveFilePath)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return cleanup(cleanupFns), "", fmt.Errorf("unable to get cwd: %+v", err)
|
||||
t.Fatalf("unable to get cwd: %+v", err)
|
||||
}
|
||||
|
||||
t.Logf("running from: %s", cwd)
|
||||
|
||||
return cleanup(cleanupFns), destinationArchiveFilePath, nil
|
||||
return destinationArchiveFilePath
|
||||
}
|
||||
|
||||
// TODO: Consider moving any non-git asset generation to a task (e.g. make) that's run ahead of running go tests.
|
||||
@@ -121,11 +109,7 @@ func ensureNestedZipExists(t *testing.T, sourceDirPath string) error {
|
||||
t.Helper()
|
||||
|
||||
nestedArchiveFilePath := path.Join(sourceDirPath, "nested.zip")
|
||||
err := createZipArchive(t, sourceDirPath, nestedArchiveFilePath)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create nested archive for test fixture: %+v", err)
|
||||
}
|
||||
createZipArchive(t, sourceDirPath, nestedArchiveFilePath)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,23 +1,36 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
)
|
||||
|
||||
// ZipFileManifest is a collection of paths and their file metadata.
|
||||
type ZipFileManifest map[string]os.FileInfo
|
||||
|
||||
// newZipManifest creates an empty ZipFileManifest.
|
||||
func newZipManifest() ZipFileManifest {
|
||||
return make(ZipFileManifest)
|
||||
// NewZipFileManifest creates and returns a new ZipFileManifest populated with path and metadata from the given zip archive path.
|
||||
func NewZipFileManifest(archivePath string) (ZipFileManifest, error) {
|
||||
zipReader, err := OpenZip(archivePath)
|
||||
manifest := make(ZipFileManifest)
|
||||
if err != nil {
|
||||
return manifest, fmt.Errorf("unable to open zip archive (%s): %w", archivePath, err)
|
||||
}
|
||||
defer func() {
|
||||
err = zipReader.Close()
|
||||
if err != nil {
|
||||
log.Warnf("unable to close zip archive (%s): %+v", archivePath, err)
|
||||
}
|
||||
}()
|
||||
|
||||
for _, file := range zipReader.Reader.File {
|
||||
manifest.Add(file.Name, file.FileInfo())
|
||||
}
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
// Add a new path and it's file metadata to the collection.
|
||||
@@ -47,26 +60,6 @@ func (z ZipFileManifest) GlobMatch(patterns ...string) []string {
|
||||
return results
|
||||
}
|
||||
|
||||
// NewZipFileManifest creates and returns a new ZipFileManifest populated with path and metadata from the given zip archive path.
|
||||
func NewZipFileManifest(archivePath string) (ZipFileManifest, error) {
|
||||
zipReader, err := zip.OpenReader(archivePath)
|
||||
manifest := newZipManifest()
|
||||
if err != nil {
|
||||
return manifest, fmt.Errorf("unable to open zip archive (%s): %w", archivePath, err)
|
||||
}
|
||||
defer func() {
|
||||
err = zipReader.Close()
|
||||
if err != nil {
|
||||
log.Errorf("unable to close zip archive (%s): %+v", archivePath, err)
|
||||
}
|
||||
}()
|
||||
|
||||
for _, file := range zipReader.Reader.File {
|
||||
manifest.Add(file.Name, file.FileInfo())
|
||||
}
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
// normalizeZipEntryName takes the given path entry and ensures it is prefixed with "/".
|
||||
func normalizeZipEntryName(entry string) string {
|
||||
if !strings.HasPrefix(entry, "/") {
|
||||
|
||||
@@ -19,11 +19,7 @@ func TestNewZipFileManifest(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cleanup, archiveFilePath, err := setupZipFileTest(t, sourceDirPath)
|
||||
defer fatalIfError(t, cleanup)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
archiveFilePath := setupZipFileTest(t, sourceDirPath)
|
||||
|
||||
actual, err := NewZipFileManifest(archiveFilePath)
|
||||
if err != nil {
|
||||
@@ -63,12 +59,7 @@ func TestZipFileManifest_GlobMatch(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cleanup, archiveFilePath, err := setupZipFileTest(t, sourceDirPath)
|
||||
//goland:noinspection GoNilness
|
||||
defer fatalIfError(t, cleanup)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
archiveFilePath := setupZipFileTest(t, sourceDirPath)
|
||||
|
||||
z, err := NewZipFileManifest(archiveFilePath)
|
||||
if err != nil {
|
||||
|
||||
@@ -24,6 +24,15 @@ const (
|
||||
|
||||
const perFileReadLimit = 2 * GB
|
||||
|
||||
type errZipSlipDetected struct {
|
||||
Prefix string
|
||||
JoinArgs []string
|
||||
}
|
||||
|
||||
func (e *errZipSlipDetected) Error() string {
|
||||
return fmt.Sprintf("paths are not allowed to resolve outside of the root prefix (%q). Destination: %q", e.Prefix, e.JoinArgs)
|
||||
}
|
||||
|
||||
type zipTraversalRequest map[string]struct{}
|
||||
|
||||
func newZipTraverseRequest(paths ...string) zipTraversalRequest {
|
||||
@@ -38,7 +47,7 @@ func newZipTraverseRequest(paths ...string) zipTraversalRequest {
|
||||
func TraverseFilesInZip(archivePath string, visitor func(*zip.File) error, paths ...string) error {
|
||||
request := newZipTraverseRequest(paths...)
|
||||
|
||||
zipReader, err := zip.OpenReader(archivePath)
|
||||
zipReader, err := OpenZip(archivePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to open zip archive (%s): %w", archivePath, err)
|
||||
}
|
||||
@@ -169,17 +178,12 @@ func ContentsFromZip(archivePath string, paths ...string) (map[string]string, er
|
||||
// UnzipToDir extracts a zip archive to a target directory.
|
||||
func UnzipToDir(archivePath, targetDir string) error {
|
||||
visitor := func(file *zip.File) error {
|
||||
// the zip-slip attack protection is still being erroneously detected
|
||||
// nolint:gosec
|
||||
expandedFilePath := filepath.Clean(filepath.Join(targetDir, file.Name))
|
||||
|
||||
// protect against zip slip attacks (traversing unintended parent paths from maliciously crafted relative-path entries)
|
||||
if !strings.HasPrefix(expandedFilePath, filepath.Clean(targetDir)+string(os.PathSeparator)) {
|
||||
return fmt.Errorf("potential zip slip attack: %q", expandedFilePath)
|
||||
joinedPath, err := safeJoin(targetDir, file.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := extractSingleFile(file, expandedFilePath, archivePath)
|
||||
if err != nil {
|
||||
if err = extractSingleFile(file, joinedPath, archivePath); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -188,6 +192,20 @@ func UnzipToDir(archivePath, targetDir string) error {
|
||||
return TraverseFilesInZip(archivePath, visitor)
|
||||
}
|
||||
|
||||
// safeJoin ensures that any destinations do not resolve to a path above the prefix path.
|
||||
func safeJoin(prefix string, dest ...string) (string, error) {
|
||||
joinResult := filepath.Join(append([]string{prefix}, dest...)...)
|
||||
cleanJoinResult := filepath.Clean(joinResult)
|
||||
if !strings.HasPrefix(cleanJoinResult, filepath.Clean(prefix)) {
|
||||
return "", &errZipSlipDetected{
|
||||
Prefix: prefix,
|
||||
JoinArgs: dest,
|
||||
}
|
||||
}
|
||||
// why not return the clean path? the called may not be expected it from what should only be a join operation.
|
||||
return joinResult, nil
|
||||
}
|
||||
|
||||
func extractSingleFile(file *zip.File, expandedFilePath, archivePath string) error {
|
||||
zippedFile, err := file.Open()
|
||||
if err != nil {
|
||||
|
||||
@@ -3,6 +3,8 @@ package file
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
@@ -12,6 +14,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/go-test/deep"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func equal(r1, r2 io.Reader) (bool, error) {
|
||||
@@ -41,14 +44,12 @@ func TestUnzipToDir(t *testing.T) {
|
||||
|
||||
goldenRootDir := filepath.Join(cwd, "test-fixtures")
|
||||
sourceDirPath := path.Join(goldenRootDir, "zip-source")
|
||||
cleanup, archiveFilePath, err := setupZipFileTest(t, sourceDirPath)
|
||||
defer fatalIfError(t, cleanup)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
archiveFilePath := setupZipFileTest(t, sourceDirPath)
|
||||
|
||||
unzipDestinationDir, err := ioutil.TempDir("", "syft-ziputil-contents-TEST-")
|
||||
defer os.RemoveAll(unzipDestinationDir)
|
||||
t.Cleanup(assertNoError(t, func() error {
|
||||
return os.RemoveAll(unzipDestinationDir)
|
||||
}))
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create tempdir: %+v", err)
|
||||
}
|
||||
@@ -124,41 +125,119 @@ func TestUnzipToDir(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestContentsFromZip(t *testing.T) {
|
||||
archivePrefix, err := ioutil.TempFile("", "syft-ziputil-archive-TEST-")
|
||||
tests := []struct {
|
||||
name string
|
||||
archivePrep func(tb testing.TB) string
|
||||
}{
|
||||
{
|
||||
name: "standard, non-nested zip",
|
||||
archivePrep: prepZipSourceFixture,
|
||||
},
|
||||
{
|
||||
name: "zip with prepended bytes",
|
||||
archivePrep: prependZipSourceFixtureWithString(t, "junk at the beginning of the file..."),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
archivePath := test.archivePrep(t)
|
||||
expected := zipSourceFixtureExpectedContents()
|
||||
|
||||
var paths []string
|
||||
for p := range expected {
|
||||
paths = append(paths, p)
|
||||
}
|
||||
|
||||
actual, err := ContentsFromZip(archivePath, paths...)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to extract from unzip archive: %+v", err)
|
||||
}
|
||||
|
||||
assertZipSourceFixtureContents(t, actual, expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func prependZipSourceFixtureWithString(tb testing.TB, value string) func(tb testing.TB) string {
|
||||
if len(value) == 0 {
|
||||
tb.Fatalf("no bytes given to prefix")
|
||||
}
|
||||
return func(t testing.TB) string {
|
||||
archivePath := prepZipSourceFixture(t)
|
||||
|
||||
// create a temp file
|
||||
tmpFile, err := ioutil.TempFile("", "syft-ziputil-prependZipSourceFixtureWithString-")
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create tempfile: %+v", err)
|
||||
}
|
||||
defer tmpFile.Close()
|
||||
|
||||
// write value to the temp file
|
||||
if _, err := tmpFile.WriteString(value); err != nil {
|
||||
t.Fatalf("unable to write to tempfile: %+v", err)
|
||||
}
|
||||
|
||||
// open the original archive
|
||||
sourceFile, err := os.Open(archivePath)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to read source file: %+v", err)
|
||||
}
|
||||
|
||||
// copy all contents from the archive to the temp file
|
||||
if _, err := io.Copy(tmpFile, sourceFile); err != nil {
|
||||
t.Fatalf("unable to copy source to dest: %+v", err)
|
||||
}
|
||||
|
||||
sourceFile.Close()
|
||||
|
||||
// remove the original archive and replace it with the temp file
|
||||
if err := os.Remove(archivePath); err != nil {
|
||||
t.Fatalf("unable to remove original source archive (%q): %+v", archivePath, err)
|
||||
}
|
||||
|
||||
if err := os.Rename(tmpFile.Name(), archivePath); err != nil {
|
||||
t.Fatalf("unable to move new archive to old path (%q): %+v", tmpFile.Name(), err)
|
||||
}
|
||||
|
||||
return archivePath
|
||||
}
|
||||
}
|
||||
|
||||
func prepZipSourceFixture(t testing.TB) string {
|
||||
t.Helper()
|
||||
archivePrefix, err := ioutil.TempFile("", "syft-ziputil-prepZipSourceFixture-")
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create tempfile: %+v", err)
|
||||
}
|
||||
defer os.Remove(archivePrefix.Name())
|
||||
|
||||
t.Cleanup(func() {
|
||||
assert.NoError(t, os.Remove(archivePrefix.Name()))
|
||||
})
|
||||
|
||||
// the zip utility will add ".zip" to the end of the given name
|
||||
archivePath := archivePrefix.Name() + ".zip"
|
||||
defer os.Remove(archivePath)
|
||||
|
||||
t.Cleanup(func() {
|
||||
assert.NoError(t, os.Remove(archivePath))
|
||||
})
|
||||
|
||||
t.Logf("archive path: %s", archivePath)
|
||||
|
||||
err = createZipArchive(t, "zip-source", archivePrefix.Name())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
createZipArchive(t, "zip-source", archivePrefix.Name())
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Errorf("unable to get cwd: %+v", err)
|
||||
}
|
||||
return archivePath
|
||||
}
|
||||
|
||||
t.Logf("running from: %s", cwd)
|
||||
|
||||
aFilePath := filepath.Join("some-dir", "a-file.txt")
|
||||
bFilePath := filepath.Join("b-file.txt")
|
||||
|
||||
expected := map[string]string{
|
||||
aFilePath: "A file! nice!",
|
||||
bFilePath: "B file...",
|
||||
}
|
||||
|
||||
actual, err := ContentsFromZip(archivePath, aFilePath, bFilePath)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to extract from unzip archive: %+v", err)
|
||||
func zipSourceFixtureExpectedContents() map[string]string {
|
||||
return map[string]string{
|
||||
filepath.Join("some-dir", "a-file.txt"): "A file! nice!",
|
||||
filepath.Join("b-file.txt"): "B file...",
|
||||
}
|
||||
}
|
||||
|
||||
func assertZipSourceFixtureContents(t testing.TB, actual map[string]string, expected map[string]string) {
|
||||
t.Helper()
|
||||
diffs := deep.Equal(actual, expected)
|
||||
if len(diffs) > 0 {
|
||||
for _, d := range diffs {
|
||||
@@ -173,3 +252,79 @@ func TestContentsFromZip(t *testing.T) {
|
||||
t.Errorf("full result: %s", string(b))
|
||||
}
|
||||
}
|
||||
|
||||
// looks like there isn't a helper for this yet? https://github.com/stretchr/testify/issues/497
|
||||
func assertErrorAs(expectedErr interface{}) assert.ErrorAssertionFunc {
|
||||
return func(t assert.TestingT, actualErr error, i ...interface{}) bool {
|
||||
return errors.As(actualErr, &expectedErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSafeJoin(t *testing.T) {
|
||||
tests := []struct {
|
||||
prefix string
|
||||
args []string
|
||||
expected string
|
||||
errAssertion assert.ErrorAssertionFunc
|
||||
}{
|
||||
// go cases...
|
||||
{
|
||||
prefix: "/a/place",
|
||||
args: []string{
|
||||
"somewhere/else",
|
||||
},
|
||||
expected: "/a/place/somewhere/else",
|
||||
errAssertion: assert.NoError,
|
||||
},
|
||||
{
|
||||
prefix: "/a/place",
|
||||
args: []string{
|
||||
"somewhere/../else",
|
||||
},
|
||||
expected: "/a/place/else",
|
||||
errAssertion: assert.NoError,
|
||||
},
|
||||
{
|
||||
prefix: "/a/../place",
|
||||
args: []string{
|
||||
"somewhere/else",
|
||||
},
|
||||
expected: "/place/somewhere/else",
|
||||
errAssertion: assert.NoError,
|
||||
},
|
||||
// zip slip examples....
|
||||
{
|
||||
prefix: "/a/place",
|
||||
args: []string{
|
||||
"../../../etc/passwd",
|
||||
},
|
||||
expected: "",
|
||||
errAssertion: assertErrorAs(&errZipSlipDetected{}),
|
||||
},
|
||||
{
|
||||
prefix: "/a/place",
|
||||
args: []string{
|
||||
"../",
|
||||
"../",
|
||||
},
|
||||
expected: "",
|
||||
errAssertion: assertErrorAs(&errZipSlipDetected{}),
|
||||
},
|
||||
{
|
||||
prefix: "/a/place",
|
||||
args: []string{
|
||||
"../",
|
||||
},
|
||||
expected: "",
|
||||
errAssertion: assertErrorAs(&errZipSlipDetected{}),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprintf("%+v:%+v", test.prefix, test.args), func(t *testing.T) {
|
||||
actual, err := safeJoin(test.prefix, test.args...)
|
||||
test.errAssertion(t, err)
|
||||
assert.Equal(t, test.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// directoryEndLen, readByf, directoryEnd, and findSignatureInBlock were copied from the golang stdlib, specifically:
|
||||
// - https://github.com/golang/go/blob/go1.16.4/src/archive/zip/struct.go
|
||||
// - https://github.com/golang/go/blob/go1.16.4/src/archive/zip/reader.go
|
||||
// findArchiveStartOffset is derived from the same stdlib utils, specifically the readDirectoryEnd function.
|
||||
|
||||
const directoryEndLen = 22
|
||||
|
||||
// ZipReadCloser is a drop-in replacement for zip.ReadCloser (from zip.OpenReader) that additionally considers zips
|
||||
// that have bytes prefixed to the front of the archive (common with self-extracting jars).
|
||||
type ZipReadCloser struct {
|
||||
*zip.Reader
|
||||
io.Closer
|
||||
}
|
||||
|
||||
// OpenZip provides a ZipReadCloser for the given filepath.
|
||||
func OpenZip(filepath string) (*ZipReadCloser, error) {
|
||||
f, err := os.Open(filepath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// some archives may have bytes prepended to the front of the archive, such as with self executing JARs. We first
|
||||
// need to find the start of the archive and keep track of this offset.
|
||||
offset, err := findArchiveStartOffset(f, fi.Size())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot find beginning of zip archive=%q : %w", filepath, err)
|
||||
}
|
||||
|
||||
if _, err := f.Seek(0, io.SeekStart); err != nil {
|
||||
return nil, fmt.Errorf("unable to seek to beginning of archive: %w", err)
|
||||
}
|
||||
|
||||
size := fi.Size() - int64(offset)
|
||||
|
||||
r, err := zip.NewReader(io.NewSectionReader(f, int64(offset), size), size)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to open ZipReadCloser @ %q: %w", filepath, err)
|
||||
}
|
||||
|
||||
return &ZipReadCloser{
|
||||
Reader: r,
|
||||
Closer: f,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type readBuf []byte
|
||||
|
||||
func (b *readBuf) uint16() uint16 {
|
||||
v := binary.LittleEndian.Uint16(*b)
|
||||
*b = (*b)[2:]
|
||||
return v
|
||||
}
|
||||
|
||||
func (b *readBuf) uint32() uint32 {
|
||||
v := binary.LittleEndian.Uint32(*b)
|
||||
*b = (*b)[4:]
|
||||
return v
|
||||
}
|
||||
|
||||
type directoryEnd struct {
|
||||
diskNbr uint32 // unused
|
||||
dirDiskNbr uint32 // unused
|
||||
dirRecordsThisDisk uint64 // unused
|
||||
directoryRecords uint64
|
||||
directorySize uint64
|
||||
directoryOffset uint64 // relative to file
|
||||
}
|
||||
|
||||
// note: this is derived from readDirectoryEnd within the archive/zip package
|
||||
func findArchiveStartOffset(r io.ReaderAt, size int64) (startOfArchive uint64, err error) {
|
||||
// look for directoryEndSignature in the last 1k, then in the last 65k
|
||||
var buf []byte
|
||||
var directoryEndOffset int64
|
||||
for i, bLen := range []int64{1024, 65 * 1024} {
|
||||
if bLen > size {
|
||||
bLen = size
|
||||
}
|
||||
buf = make([]byte, int(bLen))
|
||||
if _, err := r.ReadAt(buf, size-bLen); err != nil && err != io.EOF {
|
||||
return 0, err
|
||||
}
|
||||
if p := findSignatureInBlock(buf); p >= 0 {
|
||||
buf = buf[p:]
|
||||
directoryEndOffset = size - bLen + int64(p)
|
||||
break
|
||||
}
|
||||
if i == 1 || bLen == size {
|
||||
return 0, zip.ErrFormat
|
||||
}
|
||||
}
|
||||
|
||||
if buf == nil {
|
||||
// we were unable to find the directoryEndSignature block
|
||||
return 0, zip.ErrFormat
|
||||
}
|
||||
|
||||
// read header into struct
|
||||
b := readBuf(buf[4:]) // skip signature
|
||||
d := &directoryEnd{
|
||||
diskNbr: uint32(b.uint16()),
|
||||
dirDiskNbr: uint32(b.uint16()),
|
||||
dirRecordsThisDisk: uint64(b.uint16()),
|
||||
directoryRecords: uint64(b.uint16()),
|
||||
directorySize: uint64(b.uint32()),
|
||||
directoryOffset: uint64(b.uint32()),
|
||||
}
|
||||
// Calculate where the zip data actually begins
|
||||
startOfArchive = uint64(directoryEndOffset) - d.directorySize - d.directoryOffset
|
||||
|
||||
// These values mean that the file can be a zip64 file
|
||||
if d.directoryRecords == 0xffff || d.directorySize == 0xffff || d.directoryOffset == 0xffffffff {
|
||||
startOfArchive = 0 // Prefixed data not supported
|
||||
}
|
||||
|
||||
// Make sure directoryOffset points to somewhere in our file.
|
||||
if o := int64(d.directoryOffset); o < 0 || o >= size {
|
||||
return 0, zip.ErrFormat
|
||||
}
|
||||
return startOfArchive, nil
|
||||
}
|
||||
|
||||
func findSignatureInBlock(b []byte) int {
|
||||
for i := len(b) - directoryEndLen; i >= 0; i-- {
|
||||
// defined from directoryEndSignature
|
||||
if b[i] == 'P' && b[i+1] == 'K' && b[i+2] == 0x05 && b[i+3] == 0x06 {
|
||||
// n is length of comment
|
||||
n := int(b[i+directoryEndLen-2]) | int(b[i+directoryEndLen-1])<<8
|
||||
if n+directoryEndLen+i <= len(b) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFindArchiveStartOffset(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
archivePrep func(tb testing.TB) string
|
||||
expected uint64
|
||||
}{
|
||||
{
|
||||
name: "standard, non-nested zip",
|
||||
archivePrep: prepZipSourceFixture,
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
name: "zip with prepended bytes",
|
||||
archivePrep: prependZipSourceFixtureWithString(t, "junk at the beginning of the file..."),
|
||||
expected: 36,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
archivePath := test.archivePrep(t)
|
||||
f, err := os.Open(archivePath)
|
||||
if err != nil {
|
||||
t.Fatalf("could not open archive %q: %+v", archivePath, err)
|
||||
}
|
||||
fi, err := os.Stat(f.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("unable to stat archive: %+v", err)
|
||||
}
|
||||
|
||||
actual, err := findArchiveStartOffset(f, fi.Size())
|
||||
if err != nil {
|
||||
t.Fatalf("unable to find offset: %+v", err)
|
||||
}
|
||||
assert.Equal(t, test.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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,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,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,41 @@
|
||||
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"
|
||||
case pkg.PhpComposerPkg:
|
||||
answer = "acquired package info from PHP composer 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,143 @@
|
||||
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{
|
||||
source.NewVirtualLocation("/a-place", "/b-place"),
|
||||
source.NewVirtualLocation("/c-place", "/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",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: pkg.Package{
|
||||
Type: pkg.PhpComposerPkg,
|
||||
},
|
||||
expected: []string{
|
||||
"from PHP composer 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?)")
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
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/sbom"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/sergi/go-diff/diffmatchpatch"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type redactor func(s []byte) []byte
|
||||
|
||||
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
|
||||
if updateSnapshot {
|
||||
imagetest.UpdateGoldenFixtureImage(t, testImage)
|
||||
}
|
||||
|
||||
err := pres.Present(&buffer)
|
||||
assert.NoError(t, err)
|
||||
actual := buffer.Bytes()
|
||||
|
||||
// replace the expected snapshot contents with the current presenter contents
|
||||
if updateSnapshot {
|
||||
testutils.UpdateGoldenFileContents(t, actual)
|
||||
}
|
||||
|
||||
var expected = testutils.GetGoldenFileContents(t)
|
||||
|
||||
// remove dynamic values, which should be tested independently
|
||||
for _, r := range redactors {
|
||||
actual = r(actual)
|
||||
expected = r(expected)
|
||||
}
|
||||
|
||||
// assert that the golden file snapshot matches the actual contents
|
||||
if !bytes.Equal(expected, actual) {
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(string(expected), string(actual), true)
|
||||
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
|
||||
}
|
||||
}
|
||||
|
||||
func AssertPresenterAgainstGoldenSnapshot(t *testing.T, pres presenter.Presenter, updateSnapshot bool, redactors ...redactor) {
|
||||
var buffer bytes.Buffer
|
||||
|
||||
err := pres.Present(&buffer)
|
||||
assert.NoError(t, err)
|
||||
actual := buffer.Bytes()
|
||||
|
||||
// replace the expected snapshot contents with the current presenter contents
|
||||
if updateSnapshot {
|
||||
testutils.UpdateGoldenFileContents(t, actual)
|
||||
}
|
||||
|
||||
var expected = testutils.GetGoldenFileContents(t)
|
||||
|
||||
// remove dynamic values, which should be tested independently
|
||||
for _, r := range redactors {
|
||||
actual = r(actual)
|
||||
expected = r(expected)
|
||||
}
|
||||
|
||||
if !bytes.Equal(expected, actual) {
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(string(expected), string(actual), true)
|
||||
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
|
||||
}
|
||||
}
|
||||
|
||||
func ImageInput(t testing.TB, testImage string, options ...ImageOption) sbom.SBOM {
|
||||
t.Helper()
|
||||
catalog := pkg.NewCatalog()
|
||||
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,
|
||||
Descriptor: sbom.Descriptor{
|
||||
Name: "syft",
|
||||
Version: "v0.42.0-bogus",
|
||||
// the application configuration should be persisted here, however, we do not want to import
|
||||
// the application configuration in this package (it's reserved only for ingestion by the cmd package)
|
||||
Configuration: map[string]string{
|
||||
"config-key": "config-value",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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{
|
||||
Name: "package-1",
|
||||
Version: "1.0.1",
|
||||
Locations: []source.Location{
|
||||
source.NewLocationFromImage(string(ref1.RealPath), *ref1, img),
|
||||
},
|
||||
Type: pkg.PythonPkg,
|
||||
FoundBy: "the-cataloger-1",
|
||||
Language: pkg.Python,
|
||||
MetadataType: pkg.PythonPackageMetadataType,
|
||||
Licenses: []string{"MIT"},
|
||||
Metadata: pkg.PythonPackageMetadata{
|
||||
Name: "package-1",
|
||||
Version: "1.0.1",
|
||||
},
|
||||
PURL: "a-purl-1",
|
||||
CPEs: []pkg.CPE{
|
||||
pkg.MustCPE("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*"),
|
||||
},
|
||||
})
|
||||
catalog.Add(pkg.Package{
|
||||
Name: "package-2",
|
||||
Version: "2.0.1",
|
||||
Locations: []source.Location{
|
||||
source.NewLocationFromImage(string(ref2.RealPath), *ref2, img),
|
||||
},
|
||||
Type: pkg.DebPkg,
|
||||
FoundBy: "the-cataloger-2",
|
||||
MetadataType: pkg.DpkgMetadataType,
|
||||
Metadata: pkg.DpkgMetadata{
|
||||
Package: "package-2",
|
||||
Version: "2.0.1",
|
||||
},
|
||||
PURL: "a-purl-2",
|
||||
CPEs: []pkg.CPE{
|
||||
pkg.MustCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func DirectoryInput(t testing.TB) sbom.SBOM {
|
||||
catalog := newDirectoryCatalog()
|
||||
|
||||
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 sbom.SBOM{
|
||||
Artifacts: sbom.Artifacts{
|
||||
PackageCatalog: catalog,
|
||||
Distro: &dist,
|
||||
},
|
||||
Source: src.Metadata,
|
||||
Descriptor: sbom.Descriptor{
|
||||
Name: "syft",
|
||||
Version: "v0.42.0-bogus",
|
||||
// the application configuration should be persisted here, however, we do not want to import
|
||||
// the application configuration in this package (it's reserved only for ingestion by the cmd package)
|
||||
Configuration: map[string]string{
|
||||
"config-key": "config-value",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newDirectoryCatalog() *pkg.Catalog {
|
||||
catalog := pkg.NewCatalog()
|
||||
|
||||
// populate catalog with test data
|
||||
catalog.Add(pkg.Package{
|
||||
Name: "package-1",
|
||||
Version: "1.0.1",
|
||||
Type: pkg.PythonPkg,
|
||||
FoundBy: "the-cataloger-1",
|
||||
Locations: []source.Location{
|
||||
source.NewLocation("/some/path/pkg1"),
|
||||
},
|
||||
Language: pkg.Python,
|
||||
MetadataType: pkg.PythonPackageMetadataType,
|
||||
Licenses: []string{"MIT"},
|
||||
Metadata: pkg.PythonPackageMetadata{
|
||||
Name: "package-1",
|
||||
Version: "1.0.1",
|
||||
Files: []pkg.PythonFileRecord{
|
||||
{
|
||||
Path: "/some/path/pkg1/dependencies/foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
PURL: "a-purl-2",
|
||||
CPEs: []pkg.CPE{
|
||||
pkg.MustCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"),
|
||||
},
|
||||
})
|
||||
catalog.Add(pkg.Package{
|
||||
Name: "package-2",
|
||||
Version: "2.0.1",
|
||||
Type: pkg.DebPkg,
|
||||
FoundBy: "the-cataloger-2",
|
||||
Locations: []source.Location{
|
||||
source.NewLocation("/some/path/pkg1"),
|
||||
},
|
||||
MetadataType: pkg.DpkgMetadataType,
|
||||
Metadata: pkg.DpkgMetadata{
|
||||
Package: "package-2",
|
||||
Version: "2.0.1",
|
||||
},
|
||||
PURL: "a-purl-2",
|
||||
CPEs: []pkg.CPE{
|
||||
pkg.MustCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"),
|
||||
},
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
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) {
|
||||
testutils.AssertPresenterAgainstGoldenSnapshot(t,
|
||||
Format().Presenter(testutils.DirectoryInput(t)),
|
||||
*updateCycloneDx,
|
||||
cycloneDxRedactor,
|
||||
)
|
||||
}
|
||||
|
||||
func TestCycloneDxImagePresenter(t *testing.T) {
|
||||
testImage := "image-simple"
|
||||
testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
|
||||
Format().Presenter(testutils.ImageInput(t, testImage)),
|
||||
testImage,
|
||||
*updateCycloneDx,
|
||||
cycloneDxRedactor,
|
||||
)
|
||||
}
|
||||
|
||||
func cycloneDxRedactor(s []byte) []byte {
|
||||
serialPattern := regexp.MustCompile(`serialNumber="[a-zA-Z0-9\-:]+"`)
|
||||
rfc3339Pattern := regexp.MustCompile(`([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(([Zz])|([\+|\-]([01][0-9]|2[0-3]):[0-5][0-9]))`)
|
||||
|
||||
for _, pattern := range []*regexp.Regexp{serialPattern, rfc3339Pattern} {
|
||||
s = pattern.ReplaceAll(s, []byte("redacted"))
|
||||
}
|
||||
return s
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
+1
-8
@@ -1,4 +1,4 @@
|
||||
package cyclonedx
|
||||
package model
|
||||
|
||||
import "encoding/xml"
|
||||
|
||||
@@ -18,10 +18,3 @@ type Component struct {
|
||||
// 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.)
|
||||
}
|
||||
|
||||
// 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,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
|
||||
}
|
||||
+9
-10
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" version="1" serialNumber="urn:uuid:544b9f08-b39d-4d0c-8723-32609567e8ed">
|
||||
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" version="1" serialNumber="urn:uuid:5404937f-72d6-44a2-8e9b-954305ecb4f6">
|
||||
<metadata>
|
||||
<timestamp>2020-12-28T13:56:29-05:00</timestamp>
|
||||
<timestamp>2021-06-23T13:40:33-04:00</timestamp>
|
||||
<tools>
|
||||
<tool>
|
||||
<vendor>anchore</vendor>
|
||||
@@ -16,20 +16,19 @@
|
||||
</metadata>
|
||||
<components>
|
||||
<component type="library">
|
||||
<name>package1</name>
|
||||
<name>package-1</name>
|
||||
<version>1.0.1</version>
|
||||
</component>
|
||||
<component type="library">
|
||||
<name>package2</name>
|
||||
<version>2.0.1</version>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>MIT</name>
|
||||
</license>
|
||||
<license>
|
||||
<name>Apache-v2</name>
|
||||
</license>
|
||||
</licenses>
|
||||
<purl>a-purl-2</purl>
|
||||
</component>
|
||||
<component type="library">
|
||||
<name>package-2</name>
|
||||
<version>2.0.1</version>
|
||||
<purl>a-purl-2</purl>
|
||||
</component>
|
||||
</components>
|
||||
</bom>
|
||||
+9
-12
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" version="1" serialNumber="urn:uuid:e1515688-2fd6-422a-acb2-97405a44cdfe">
|
||||
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" version="1" serialNumber="urn:uuid:e34bad2e-cd27-483c-86dc-f4e26d6103b0">
|
||||
<metadata>
|
||||
<timestamp>2020-12-28T13:56:29-05:00</timestamp>
|
||||
<timestamp>2021-06-23T13:40:33-04:00</timestamp>
|
||||
<tools>
|
||||
<tool>
|
||||
<vendor>anchore</vendor>
|
||||
@@ -16,22 +16,19 @@
|
||||
</metadata>
|
||||
<components>
|
||||
<component type="library">
|
||||
<name>package1</name>
|
||||
<name>package-1</name>
|
||||
<version>1.0.1</version>
|
||||
<purl>the-purl-1</purl>
|
||||
</component>
|
||||
<component type="library">
|
||||
<name>package2</name>
|
||||
<version>2.0.1</version>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>MIT</name>
|
||||
</license>
|
||||
<license>
|
||||
<name>Apache-v2</name>
|
||||
</license>
|
||||
</licenses>
|
||||
<purl>the-purl-2</purl>
|
||||
<purl>a-purl-1</purl>
|
||||
</component>
|
||||
<component type="library">
|
||||
<name>package-2</name>
|
||||
<version>2.0.1</version>
|
||||
<purl>a-purl-2</purl>
|
||||
</component>
|
||||
</components>
|
||||
</bom>
|
||||
@@ -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,21 @@
|
||||
package spdx22json
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
const anchoreNamespace = "https://anchore.com/syft"
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
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) {
|
||||
testutils.AssertPresenterAgainstGoldenSnapshot(t,
|
||||
Format().Presenter(testutils.DirectoryInput(t)),
|
||||
*updateSpdxJson,
|
||||
spdxJsonRedactor,
|
||||
)
|
||||
}
|
||||
|
||||
func TestSPDXJSONImagePresenter(t *testing.T) {
|
||||
testImage := "image-simple"
|
||||
testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
|
||||
Format().Presenter(testutils.ImageInput(t, testImage, testutils.FromSnapshot())),
|
||||
testImage,
|
||||
*updateSpdxJson,
|
||||
spdxJsonRedactor,
|
||||
)
|
||||
}
|
||||
|
||||
func spdxJsonRedactor(s []byte) []byte {
|
||||
// each SBOM reports the time it was generated, which is not useful during snapshot testing
|
||||
s = regexp.MustCompile(`"created": .*`).ReplaceAll(s, []byte("redacted"))
|
||||
|
||||
// each SBOM reports a unique documentNamespace when generated, this is not useful for snapshot testing
|
||||
s = regexp.MustCompile(`"documentNamespace": .*`).ReplaceAll(s, []byte("redacted"))
|
||||
|
||||
// the license list will be updated periodically, the value here should not be directly tested in snapshot tests
|
||||
return regexp.MustCompile(`"licenseListVersion": .*`).ReplaceAll(s, []byte("redacted"))
|
||||
}
|
||||
@@ -0,0 +1,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,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type AnnotationType string
|
||||
|
||||
const (
|
||||
ReviewerAnnotationType AnnotationType = "REVIEWER"
|
||||
OtherAnnotationType AnnotationType = "OTHER"
|
||||
)
|
||||
|
||||
type Annotation struct {
|
||||
// Identify when the comment was made. This is to be specified according to the combined date and time in the
|
||||
// UTC format, as specified in the ISO 8601 standard.
|
||||
AnnotationDate time.Time `json:"annotationDate"`
|
||||
// Type of the annotation
|
||||
AnnotationType AnnotationType `json:"annotationType"`
|
||||
// This field identifies the person, organization or tool that has commented on a file, package, or the entire document.
|
||||
Annotator string `json:"annotator"`
|
||||
Comment string `json:"comment"`
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package model
|
||||
|
||||
type Checksum struct {
|
||||
// Identifies the algorithm used to produce the subject Checksum. One of: "SHA256", "SHA1", "SHA384", "MD2", "MD4", "SHA512", "MD6", "MD5", "SHA224"
|
||||
Algorithm string `json:"algorithm"`
|
||||
ChecksumValue string `json:"checksumValue"`
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type CreationInfo struct {
|
||||
Comment string `json:"comment,omitempty"`
|
||||
// Identify when the SPDX file was originally created. The date is to be specified according to combined date and
|
||||
// time in UTC format as specified in ISO 8601 standard. This field is distinct from the fields in section 8,
|
||||
// which involves the addition of information during a subsequent review.
|
||||
Created time.Time `json:"created"`
|
||||
// Identify who (or what, in the case of a tool) created the SPDX file. If the SPDX file was created by an
|
||||
// individual, indicate the person's name. If the SPDX file was created on behalf of a company or organization,
|
||||
// indicate the entity name. If the SPDX file was created using a software tool, indicate the name and version
|
||||
// for that tool. If multiple participants or tools were involved, use multiple instances of this field. Person
|
||||
// name or organization name may be designated as “anonymous” if appropriate.
|
||||
Creators []string `json:"creators"`
|
||||
// An optional field for creators of the SPDX file to provide the version of the SPDX License List used when the SPDX file was created.
|
||||
LicenseListVersion string `json:"licenseListVersion"`
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package model
|
||||
|
||||
// derived from:
|
||||
// - https://spdx.github.io/spdx-spec/appendix-III-RDF-data-model-implementation-and-identifier-syntax/
|
||||
// - https://github.com/spdx/spdx-spec/blob/v2.2/schemas/spdx-schema.json
|
||||
// - https://github.com/spdx/spdx-spec/tree/v2.2/ontology
|
||||
|
||||
type Document struct {
|
||||
Element
|
||||
SPDXVersion string `json:"spdxVersion"`
|
||||
// One instance is required for each SPDX file produced. It provides the necessary information for forward
|
||||
// and backward compatibility for processing tools.
|
||||
CreationInfo CreationInfo `json:"creationInfo"`
|
||||
// 2.2: Data License; should be "CC0-1.0"
|
||||
// Cardinality: mandatory, one
|
||||
// License expression for dataLicense. Compliance with the SPDX specification includes populating the SPDX
|
||||
// fields therein with data related to such fields (\"SPDX-Metadata\"). The SPDX specification contains numerous
|
||||
// fields where an SPDX document creator may provide relevant explanatory text in SPDX-Metadata. Without
|
||||
// opining on the lawfulness of \"database rights\" (in jurisdictions where applicable), such explanatory text
|
||||
// is copyrightable subject matter in most Berne Convention countries. By using the SPDX specification, or any
|
||||
// portion hereof, you hereby agree that any copyright rights (as determined by your jurisdiction) in any
|
||||
// SPDX-Metadata, including without limitation explanatory text, shall be subject to the terms of the Creative
|
||||
// Commons CC0 1.0 Universal license. For SPDX-Metadata not containing any copyright rights, you hereby agree
|
||||
// and acknowledge that the SPDX-Metadata is provided to you \"as-is\" and without any representations or
|
||||
// warranties of any kind concerning the SPDX-Metadata, express, implied, statutory or otherwise, including
|
||||
// without limitation warranties of title, merchantability, fitness for a particular purpose, non-infringement,
|
||||
// or the absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not
|
||||
// discoverable, all to the greatest extent permissible under applicable law.
|
||||
DataLicense string `json:"dataLicense"`
|
||||
// Information about an external SPDX document reference including the checksum. This allows for verification of the external references.
|
||||
ExternalDocumentRefs []ExternalDocumentRef `json:"externalDocumentRefs,omitempty"`
|
||||
// Indicates that a particular ExtractedLicensingInfo was defined in the subject SpdxDocument.
|
||||
HasExtractedLicensingInfos []HasExtractedLicensingInfo `json:"hasExtractedLicensingInfos,omitempty"`
|
||||
// note: found in example documents from SPDX, but not in the JSON schema. See https://spdx.github.io/spdx-spec/2-document-creation-information/#25-spdx-document-namespace
|
||||
DocumentNamespace string `json:"documentNamespace"`
|
||||
// note: found in example documents from SPDX, but not in the JSON schema
|
||||
// DocumentDescribes []string `json:"documentDescribes"`
|
||||
Packages []Package `json:"packages"`
|
||||
// Files referenced in the SPDX document
|
||||
Files []File `json:"files,omitempty"`
|
||||
// Snippets referenced in the SPDX document
|
||||
Snippets []Snippet `json:"snippets,omitempty"`
|
||||
// Relationships referenced in the SPDX document
|
||||
Relationships []Relationship `json:"relationships,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package model
|
||||
|
||||
type Element struct {
|
||||
SPDXID string `json:"SPDXID"`
|
||||
// Identify name of this SpdxElement.
|
||||
Name string `json:"name"`
|
||||
// Relationships referenced in the SPDX document
|
||||
Relationships []Relationship `json:"relationships,omitempty"`
|
||||
// Provide additional information about an SpdxElement.
|
||||
Annotations []Annotation `json:"annotations,omitempty"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user