mirror of
https://github.com/anchore/syft.git
synced 2026-10-12 06:07:20 +02:00
Compare commits
90
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e415bb21e7 | ||
|
|
748cfbf006 | ||
|
|
8bc5d84481 | ||
|
|
68b7ad9770 | ||
|
|
1aeda6bb50 | ||
|
|
f24bbc1838 | ||
|
|
cb3e73e308 | ||
|
|
f157d7a862 | ||
|
|
028cd9e27e | ||
|
|
5549939cc6 | ||
|
|
47ea910868 | ||
|
|
a7db43f5ec | ||
|
|
cc2c0e57a0 | ||
|
|
5253da4b36 | ||
|
|
c0b547bdb2 | ||
|
|
cffcaf5984 | ||
|
|
7f9edf346a | ||
|
|
a644a45ef4 | ||
|
|
283db88dc4 | ||
|
|
03e193e577 | ||
|
|
069aa68b63 | ||
|
|
9240860f44 | ||
|
|
4231f38fa2 | ||
|
|
752b03b2d6 | ||
|
|
99c3339810 | ||
|
|
f4734d28b3 | ||
|
|
6ef3e45ffc | ||
|
|
ee0a1d172c | ||
|
|
b9b3ccecf9 | ||
|
|
95271fb10d | ||
|
|
93d2d57cd3 | ||
|
|
7cd3201fe9 | ||
|
|
c7cf8b0b26 | ||
|
|
44a6e00f7a | ||
|
|
6c8102bf28 | ||
|
|
7789506dc6 | ||
|
|
fa03723617 | ||
|
|
2946813a74 | ||
|
|
003d28ad48 | ||
|
|
9d9669e62f | ||
|
|
39737a2825 | ||
|
|
f2617285d0 | ||
|
|
07d3c9af52 | ||
|
|
5123f073c7 | ||
|
|
991af0d857 | ||
|
|
a86dd3704e | ||
|
|
4af32c5bee | ||
|
|
640099ce2e | ||
|
|
b2ab4671b9 | ||
|
|
ad322b3314 | ||
|
|
4a8a9ce290 | ||
|
|
635904fcb6 | ||
|
|
1e75cb0418 | ||
|
|
afc0c1acd9 | ||
|
|
edac8c7bf7 | ||
|
|
d2f28e0eb1 | ||
|
|
1d1a7de314 | ||
|
|
24cd39089a | ||
|
|
bb3d713b97 | ||
|
|
c89131bcf3 | ||
|
|
6c306efed2 | ||
|
|
99bb93d0fe | ||
|
|
7eea98fcc5 | ||
|
|
256e85bc12 | ||
|
|
738b3b60a5 | ||
|
|
20c1d14f6e | ||
|
|
4b16737b2f | ||
|
|
e6b5872bc8 | ||
|
|
641c44f449 | ||
|
|
55c7f3d1e7 | ||
|
|
52d2e62cdc | ||
|
|
51c6eb30f5 | ||
|
|
e1e9ccb401 | ||
|
|
220f3a24fd | ||
|
|
ca032434b3 | ||
|
|
5ab872c732 | ||
|
|
a04fa68539 | ||
|
|
76f8205936 | ||
|
|
ca081ae5e0 | ||
|
|
8f96adacfb | ||
|
|
8f292958a6 | ||
|
|
23e1bd3984 | ||
|
|
ac024f94a4 | ||
|
|
0ed2842123 | ||
|
|
b78c90d018 | ||
|
|
2c62651c82 | ||
|
|
5519a25035 | ||
|
|
950ad681a0 | ||
|
|
1e338502ff | ||
|
|
37251f6179 |
@@ -6,5 +6,6 @@ SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
. "$SCRIPT_DIR"/utils.sh
|
||||
|
||||
# cleanup any dev certs left behind
|
||||
. "$SCRIPT_DIR"/setup-import-cert.sh # defines KEYCHAIN_NAME and KEYCHAIN_PATH
|
||||
. "$SCRIPT_DIR"/setup-dev.sh
|
||||
cleanup_signing
|
||||
cleanup_dev_signing
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
set -ue
|
||||
|
||||
set +xu
|
||||
if [ -z "$AC_USERNAME" ]; then
|
||||
@@ -11,58 +10,33 @@ if [ -z "$AC_PASSWORD" ]; then
|
||||
fi
|
||||
set -u
|
||||
|
||||
# repackage [archive-path]
|
||||
#
|
||||
# returns an archive compatible for Apple's notarization process, repackaging the input archive as needed
|
||||
#
|
||||
repackage() {
|
||||
archive=$1
|
||||
|
||||
case "$archive" in
|
||||
*.tar.gz)
|
||||
new_archive=${archive%.tar.gz}.zip
|
||||
(
|
||||
tmp_dir=$(mktemp -d)
|
||||
cd "$tmp_dir"
|
||||
# redirect stdout to stderr to preserve the return value
|
||||
tar xzf "$archive" && zip "$new_archive" ./* 1>&2
|
||||
rm -rf "$tmp_dir"
|
||||
)
|
||||
echo "$new_archive"
|
||||
;;
|
||||
*.zip)
|
||||
echo "$archive"
|
||||
;;
|
||||
*) return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# notarize [archive-path]
|
||||
#
|
||||
notarize() {
|
||||
archive_path=$1
|
||||
binary_path=$1
|
||||
archive_path=${binary_path}-archive-for-notarization.zip
|
||||
|
||||
title "notarizing binaries found in the release archive"
|
||||
title "archiving release binary into ${archive_path}"
|
||||
|
||||
payload_archive_path=$(repackage "$archive_path")
|
||||
if [ "$?" != "0" ]; then
|
||||
exit_with_error "cannot prepare payload for notarization: $archive_path"
|
||||
fi
|
||||
parent=$(dirname "$binary_path")
|
||||
(
|
||||
cd "${parent}" && zip "${archive_path}" "$(basename ${binary_path})"
|
||||
)
|
||||
|
||||
if [ ! -f "$payload_archive_path" ]; then
|
||||
exit_with_error "cannot find payload for notarization: $payload_archive_path"
|
||||
if [ ! -f "$archive_path" ]; then
|
||||
exit_with_error "cannot find payload for notarization: $archive_path"
|
||||
fi
|
||||
|
||||
# install gon
|
||||
which gon || (brew tap mitchellh/gon && brew install mitchellh/gon/gon)
|
||||
which gon || (go install github.com/mitchellh/gon/cmd/gon@latest)
|
||||
|
||||
# create config (note: json via stdin with gon is broken, can only use HCL from file)
|
||||
tmp_file=$(mktemp).hcl
|
||||
hcl_file=$(mktemp).hcl
|
||||
|
||||
cat <<EOF > "$tmp_file"
|
||||
cat <<EOF > "$hcl_file"
|
||||
notarize {
|
||||
path = "$payload_archive_path"
|
||||
path = "$archive_path"
|
||||
bundle_id = "com.anchore.toolbox.syft"
|
||||
}
|
||||
|
||||
@@ -72,14 +46,8 @@ apple_id {
|
||||
}
|
||||
EOF
|
||||
|
||||
gon -log-level info "$tmp_file"
|
||||
gon -log-level info "$hcl_file"
|
||||
|
||||
result="$?"
|
||||
|
||||
rm "$tmp_file"
|
||||
|
||||
if [ "$result" -ne "0" ]; then
|
||||
exit_with_error "notarization failed"
|
||||
fi
|
||||
rm "${hcl_file}" "${archive_path}"
|
||||
}
|
||||
|
||||
|
||||
@@ -20,12 +20,6 @@ P12_FILE=$FILE_PREFIX.p12
|
||||
|
||||
EXT_SECTION=codesign_reqext
|
||||
|
||||
## Keychain material
|
||||
|
||||
KEYCHAIN_NAME=$NAME
|
||||
KEYCHAIN_PATH=$HOME/Library/Keychains/$KEYCHAIN_NAME-db
|
||||
KEYCHAIN_PASSWORD="topsykretts"
|
||||
|
||||
# setup_signing
|
||||
#
|
||||
# preps the MAC_SIGNING_IDENTITY env var for use in the signing process, using ephemeral developer certificate material
|
||||
@@ -46,7 +40,7 @@ function setup_signing() {
|
||||
mkdir -p "${DIR}"
|
||||
|
||||
# configure the openssl extensions
|
||||
cat << EOF > $EXT_FILE
|
||||
cat << EOF > "$EXT_FILE"
|
||||
[ req ]
|
||||
default_bits = 2048 # RSA key size
|
||||
encrypt_key = yes # Protect private key
|
||||
@@ -105,7 +99,7 @@ EOF
|
||||
-extensions $EXT_SECTION
|
||||
|
||||
commentary "verify the certificate: we should see our extensions"
|
||||
openssl x509 -text -noout -in $CERT_FILE | grep -A1 'X509v3' || exit_with_error "could not find x509 extensions in certificate"
|
||||
openssl x509 -text -noout -in "$CERT_FILE" | grep -A1 'X509v3' || exit_with_error "could not find x509 extensions in certificate"
|
||||
|
||||
title "export cert and private key to .p12 file"
|
||||
# note: this step may be entirely optional, however, I found it useful to follow the prod path which goes the route of using a p12
|
||||
@@ -117,55 +111,18 @@ EOF
|
||||
-passin "pass:$KEY_PASSWORD" \
|
||||
-passout "pass:$P12_PASSWORD"
|
||||
|
||||
|
||||
title "create the dev keychain"
|
||||
|
||||
# delete the keychain if it already exists
|
||||
if [ -f "$(KEYCHAIN_PATH)" ]; then
|
||||
security delete-keychain "$KEYCHAIN_NAME" &> /dev/null
|
||||
if [ -f "${KEYCHAIN_PATH}" ]; then
|
||||
cleanup_dev_signing
|
||||
fi
|
||||
|
||||
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME"
|
||||
|
||||
set +e
|
||||
if ! security verify-cert -k "$KEYCHAIN_PATH" -c "$CERT_FILE" &> /dev/null; then
|
||||
set -e
|
||||
title "import the cert into the dev keychain if it is not already trusted by the system"
|
||||
|
||||
security import "$P12_FILE" -P $P12_PASSWORD -f pkcs12 -k "$KEYCHAIN_PATH" -T /usr/bin/codesign
|
||||
|
||||
# note: set the partition list for this certificate's private key to include "apple-tool:" and "apple:" allows the codesign command to access this keychain item without an interactive user prompt.
|
||||
security set-key-partition-list -S "apple-tool:,apple:,codesign:" -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||||
|
||||
# note: add-trusted-cert requires user interaction
|
||||
commentary "adding the developer certificate as a trusted certificate... (requires user interaction)"
|
||||
security add-trusted-cert -d -r trustRoot -k "$KEYCHAIN_PATH" "$CERT_FILE"
|
||||
else
|
||||
set -e
|
||||
commentary "...dev cert has already been imported onto the dev keychain"
|
||||
fi
|
||||
|
||||
# remove any generated cert material since the keychain now has all of this material loaded
|
||||
rm -rf "${DIR}"
|
||||
|
||||
commentary "make certain there are identities that can be used for code signing"
|
||||
security find-identity -p codesigning "$KEYCHAIN_PATH" | grep -C 30 "$IDENTITY" || exit_with_error "could not find identity that can be used with codesign"
|
||||
|
||||
title "add the dev keychain to the search path for codesign"
|
||||
add_keychain $KEYCHAIN_NAME
|
||||
|
||||
commentary "verify the keychain actually shows up"
|
||||
security list-keychains | grep "$KEYCHAIN_NAME" || exit_with_error "could not find new keychain"
|
||||
|
||||
export MAC_SIGNING_IDENTITY=$IDENTITY
|
||||
commentary "setting MAC_SIGNING_IDENTITY=${IDENTITY}"
|
||||
|
||||
import_signing_certificate "$P12_FILE" "$P12_PASSWORD" "$IDENTITY"
|
||||
}
|
||||
|
||||
function cleanup_signing() {
|
||||
function cleanup_dev_signing() {
|
||||
title "delete the dev keychain and all certificate material"
|
||||
set -xue
|
||||
security delete-keychain "$KEYCHAIN_NAME"
|
||||
rm -f "$KEYCHAIN_PATH"
|
||||
rm -rf "${DIR}"
|
||||
security delete-keychain "$KEYCHAIN_NAME" || true
|
||||
rm -f "$KEYCHAIN_PATH" || true
|
||||
rm -rf "${DIR}" || true
|
||||
}
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eu
|
||||
|
||||
KEYCHAIN_NAME="syft-ephemeral-keychain"
|
||||
KEYCHAIN_PATH="$HOME/Library/Keychains/${KEYCHAIN_NAME}-db"
|
||||
|
||||
# import_signing_certificate
|
||||
#
|
||||
# imports a cert from a p12 file into a keychain used for codesigning
|
||||
#
|
||||
function import_signing_certificate() {
|
||||
p12_file=$1
|
||||
p12_password=$2
|
||||
identity=$3
|
||||
|
||||
keychain_password="$(openssl rand -base64 100)"
|
||||
|
||||
title "create the a new keychain"
|
||||
|
||||
security create-keychain -p "$keychain_password" "$KEYCHAIN_NAME"
|
||||
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
|
||||
security unlock-keychain -p "$keychain_password" "$KEYCHAIN_PATH"
|
||||
|
||||
if [ ! -f "$KEYCHAIN_PATH" ]; then
|
||||
exit_with_error "cannot find keychain '$KEYCHAIN_PATH'"
|
||||
fi
|
||||
|
||||
set +e
|
||||
if ! security verify-cert -k "$KEYCHAIN_PATH" -c "$p12_file" &> /dev/null; then
|
||||
set -e
|
||||
title "import the cert into the new keychain if it is not already trusted by the system"
|
||||
|
||||
# '-t cert' is vital since it side-steps the need for user interaction with "security add-trusted-cert" (which has wider security implications)
|
||||
security import "$p12_file" -P "$p12_password" -t cert -f pkcs12 -k "$KEYCHAIN_PATH" -T /usr/bin/codesign
|
||||
|
||||
# note: set the partition list for this certificate's private key to include "apple-tool:" and "apple:" allows the codesign command to access this keychain item without an interactive user prompt.
|
||||
security set-key-partition-list -S "apple-tool:,apple:,codesign:" -s -k "$keychain_password" "$KEYCHAIN_PATH"
|
||||
else
|
||||
set -e
|
||||
commentary "...cert has already been imported onto the new keychain"
|
||||
fi
|
||||
|
||||
commentary "make certain there are identities that can be used for code signing"
|
||||
security find-identity -p codesigning "$KEYCHAIN_PATH" | grep -C 30 "$identity" || exit_with_error "could not find identity that can be used with codesign"
|
||||
|
||||
title "add the new keychain to the search path for codesign"
|
||||
add_keychain "$KEYCHAIN_NAME"
|
||||
|
||||
commentary "verify the new keychain can be found by the security sub-system"
|
||||
security list-keychains | grep "$KEYCHAIN_NAME" || exit_with_error "could not find new keychain"
|
||||
|
||||
export MAC_SIGNING_IDENTITY=$identity
|
||||
commentary "setting MAC_SIGNING_IDENTITY=${identity}"
|
||||
|
||||
}
|
||||
@@ -3,6 +3,8 @@ set -eu
|
||||
|
||||
assert_in_ci
|
||||
|
||||
IDENTITY="Developer ID Application: ANCHORE, INC. (9MJHKYX5AT)"
|
||||
|
||||
set +xu
|
||||
if [ -z "$APPLE_DEVELOPER_ID_CERT" ]; then
|
||||
exit_with_error "APPLE_DEVELOPER_ID_CERT not set"
|
||||
@@ -19,6 +21,14 @@ fi
|
||||
if [ -z "$DOCKER_PASSWORD" ]; then
|
||||
exit_with_error "DOCKER_PASSWORD not set"
|
||||
fi
|
||||
|
||||
if [ -z "$GHCR_USERNAME" ]; then
|
||||
exit_with_error "GHCR_USERNAME not set"
|
||||
fi
|
||||
|
||||
if [ -z "$GHCR_PASSWORD" ]; then
|
||||
exit_with_error "GHCR_PASSWORD not set"
|
||||
fi
|
||||
set -u
|
||||
|
||||
# setup_signing
|
||||
@@ -29,28 +39,15 @@ setup_signing() {
|
||||
title "setting up production certificate material"
|
||||
|
||||
# Write signing certificate to disk from environment variable.
|
||||
cert_file="$HOME/developer_id_certificate.p12"
|
||||
echo -n "$APPLE_DEVELOPER_ID_CERT" | base64 --decode > "$cert_file"
|
||||
p12_file="$HOME/developer_id_certificate.p12"
|
||||
echo -n "$APPLE_DEVELOPER_ID_CERT" | base64 --decode > "$p12_file"
|
||||
|
||||
# In order to have all keychain interactions avoid an interactive user prompt, we need to control the password for the keychain in question, which means we need to create a new keychain into which we'll import the signing certificate and from which we'll later access this certificate during code signing.
|
||||
ephemeral_keychain="ci-ephemeral-keychain"
|
||||
ephemeral_keychain_password="$(openssl rand -base64 100)"
|
||||
security create-keychain -p "${ephemeral_keychain_password}" "${ephemeral_keychain}"
|
||||
|
||||
# Import signing certificate into the keychain. (This is a pre-requisite for gon, which is invoked via goreleaser.)
|
||||
ephemeral_keychain_full_path="$HOME/Library/Keychains/${ephemeral_keychain}-db"
|
||||
security import "${cert_file}" -k "${ephemeral_keychain_full_path}" -P "${APPLE_DEVELOPER_ID_CERT_PASS}" -T "$(command -v codesign)"
|
||||
|
||||
# Setting the partition list for this certificate's private key to include "apple-tool:" and "apple:" allows the codesign command to access this keychain item without an interactive user prompt. (codesign is invoked by gon.)
|
||||
security set-key-partition-list -S "apple-tool:,apple:" -s -k "${ephemeral_keychain_password}" "${ephemeral_keychain_full_path}"
|
||||
import_signing_certificate "$p12_file" "$APPLE_DEVELOPER_ID_CERT_PASS" "$IDENTITY"
|
||||
|
||||
# Make this new keychain the user's default keychain, so that codesign will be able to find this certificate when we specify it during signing.
|
||||
security default-keychain -d "user" -s "${ephemeral_keychain_full_path}"
|
||||
|
||||
# TODO: extract this from the certificate material itself
|
||||
export MAC_SIGNING_IDENTITY="Developer ID Application: ANCHORE, INC. (9MJHKYX5AT)"
|
||||
commentary "setting MAC_SIGNING_IDENTITY=${MAC_SIGNING_IDENTITY}"
|
||||
security default-keychain -d "user" -s "${KEYCHAIN_PATH}"
|
||||
|
||||
commentary "log into docker -- required for publishing (since the default keychain has now been replaced)"
|
||||
echo "${DOCKER_PASSWORD}" | docker login docker.io -u "${DOCKER_USERNAME}" --password-stdin
|
||||
echo "${GHCR_PASSWORD}" | docker login ghcr.io -u "${GHCR_USERNAME}" --password-stdin
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eu
|
||||
set -eu -o pipefail
|
||||
|
||||
IS_SNAPSHOT="$1"
|
||||
|
||||
## grab utilities
|
||||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
. "$SCRIPT_DIR"/utils.sh
|
||||
mkdir -p "$SCRIPT_DIR/log"
|
||||
|
||||
main() {
|
||||
# defines KEYCHAIN_NAME and KEYCHAIN_PATH
|
||||
. "$SCRIPT_DIR"/setup-import-cert.sh
|
||||
|
||||
case "$IS_SNAPSHOT" in
|
||||
|
||||
@@ -33,17 +36,13 @@ main() {
|
||||
echo -n "$MAC_SIGNING_IDENTITY" > "$SCRIPT_DIR/$SIGNING_IDENTITY_FILENAME"
|
||||
}
|
||||
|
||||
set +u
|
||||
if [ -z "$SCRIPT" ]
|
||||
then
|
||||
# capture all output from a subshell to log output additionally to a file (as well as the terminal)
|
||||
( (
|
||||
set +u
|
||||
if [ -n "$SKIP_SIGNING" ]; then
|
||||
commentary "skipping signing setup..."
|
||||
else
|
||||
set -u
|
||||
# log all output
|
||||
mkdir -p "$SCRIPT_DIR/log"
|
||||
/usr/bin/script "$SCRIPT_DIR/log/setup.txt" /bin/bash -c "$0 $*"
|
||||
exit $?
|
||||
elif [ -n "$SKIP_SIGNING" ]; then
|
||||
commentary "skipping signing setup..."
|
||||
else
|
||||
set -u
|
||||
main
|
||||
fi
|
||||
main
|
||||
fi
|
||||
) 2>&1) | tee "$SCRIPT_DIR/log/setup.txt"
|
||||
@@ -1,12 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eu
|
||||
set -eu -o pipefail
|
||||
|
||||
ARCHIVE_PATH="$1"
|
||||
BINARY_PATH="$1"
|
||||
IS_SNAPSHOT="$2"
|
||||
TARGET_NAME="$3"
|
||||
|
||||
## grab utilities
|
||||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
. "$SCRIPT_DIR"/utils.sh
|
||||
mkdir -p "$SCRIPT_DIR/log"
|
||||
|
||||
|
||||
# sign_binary [binary-path] [signing-identity]
|
||||
@@ -25,13 +27,15 @@ sign_binary() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
set -x
|
||||
|
||||
codesign \
|
||||
-s "$identity" \
|
||||
-f \
|
||||
--verbose=4 \
|
||||
--timestamp \
|
||||
--options runtime \
|
||||
$exe_path
|
||||
"$exe_path"
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
exit_with_error "signing failed"
|
||||
@@ -39,57 +43,15 @@ sign_binary() {
|
||||
|
||||
codesign --verify "$exe_path" --verbose=4
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
exit_with_error "signing verification failed"
|
||||
fi
|
||||
}
|
||||
|
||||
# sign_binaries_in_archive [archive-abs-path] [signing-identity]
|
||||
#
|
||||
# signs all binaries within an archive (there must be at least one)
|
||||
#
|
||||
sign_binaries_in_archive() {
|
||||
archive_abs_path=$1
|
||||
identity=$2
|
||||
|
||||
scratch_path=$(mktemp -d)
|
||||
trap "rm -rf -- $scratch_path" EXIT
|
||||
|
||||
title "getting contents from the release archive: $archive_abs_path"
|
||||
tar -C "$scratch_path" -xvf "$archive_abs_path"
|
||||
|
||||
# invalidate the current archive, we only want an asset with signed binaries from this point forward
|
||||
rm "$archive_abs_path"
|
||||
|
||||
title "signing binaries found in the release archive"
|
||||
|
||||
discovered_binaries=0
|
||||
tmp_pipe=$(mktemp -ut pipe.XXX)
|
||||
mkfifo "$tmp_pipe"
|
||||
|
||||
find "$scratch_path" -perm +111 -type f > "$tmp_pipe" &
|
||||
|
||||
while IFS= read -r binary; do
|
||||
sign_binary "$binary" "$identity"
|
||||
((discovered_binaries++))
|
||||
done < "$tmp_pipe"
|
||||
|
||||
rm "$tmp_pipe"
|
||||
|
||||
if [ "$discovered_binaries" = "0" ]; then
|
||||
exit_with_error "found no binaries to sign"
|
||||
fi
|
||||
|
||||
title "recreating the release archive: $archive_abs_path"
|
||||
(cd "$scratch_path" && tar -czvf "$archive_abs_path" .)
|
||||
set +x
|
||||
}
|
||||
|
||||
|
||||
main() {
|
||||
archive_abs_path=$(realpath "$ARCHIVE_PATH")
|
||||
binary_abs_path=$(realpath "$BINARY_PATH")
|
||||
|
||||
if [ ! -f "$archive_abs_path" ]; then
|
||||
echo "archive does not exist: $archive_abs_path"
|
||||
if [ ! -f "$binary_abs_path" ]; then
|
||||
echo "archive does not exist: $binary_abs_path"
|
||||
fi
|
||||
|
||||
case "$IS_SNAPSHOT" in
|
||||
@@ -114,27 +76,23 @@ main() {
|
||||
MAC_SIGNING_IDENTITY=$(cat "$SCRIPT_DIR/$SIGNING_IDENTITY_FILENAME")
|
||||
|
||||
# sign all of the binaries in the archive and recreate the input archive with the signed binaries
|
||||
sign_binaries_in_archive "$archive_abs_path" "$MAC_SIGNING_IDENTITY"
|
||||
sign_binary "$binary_abs_path" "$MAC_SIGNING_IDENTITY"
|
||||
|
||||
# send all of the binaries off to apple to bless
|
||||
if $perform_notarization ; then
|
||||
notarize "$archive_abs_path"
|
||||
notarize "$binary_abs_path"
|
||||
else
|
||||
commentary "skipping notarization..."
|
||||
fi
|
||||
}
|
||||
|
||||
set +u
|
||||
if [ -z "$SCRIPT" ]
|
||||
then
|
||||
# capture all output from a subshell to log output additionally to a file (as well as the terminal)
|
||||
( (
|
||||
set +u
|
||||
if [ -n "$SKIP_SIGNING" ]; then
|
||||
commentary "skipping signing..."
|
||||
else
|
||||
set -u
|
||||
# log all output
|
||||
mkdir -p "$SCRIPT_DIR/log"
|
||||
/usr/bin/script "$SCRIPT_DIR/log/signing-$(basename $ARCHIVE_PATH).txt" /bin/bash -c "$0 $*"
|
||||
exit $?
|
||||
elif [ -n "$SKIP_SIGNING" ]; then
|
||||
commentary "skipping signing..."
|
||||
else
|
||||
set -u
|
||||
main
|
||||
fi
|
||||
main
|
||||
fi
|
||||
) 2>&1) | tee "$SCRIPT_DIR/log/signing-$(basename $BINARY_PATH)-$TARGET_NAME.txt"
|
||||
@@ -7,10 +7,10 @@ name: "CodeQL Security Scan"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [main]
|
||||
branches:
|
||||
# only run when there are pushes to the main branch (not on PRs)
|
||||
- main
|
||||
|
||||
schedule:
|
||||
- cron: '0 0 * * 3'
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ on:
|
||||
- "v*"
|
||||
|
||||
env:
|
||||
GO_VERSION: "1.16.x"
|
||||
GO_VERSION: "1.18.x"
|
||||
|
||||
jobs:
|
||||
quality-gate:
|
||||
@@ -94,6 +94,8 @@ jobs:
|
||||
needs: [quality-gate]
|
||||
# due to our code signing process, it's vital that we run our release steps on macOS
|
||||
runs-on: macos-latest
|
||||
permissions:
|
||||
packages: write
|
||||
steps:
|
||||
- uses: docker-practice/actions-setup-docker@v1
|
||||
|
||||
@@ -128,6 +130,8 @@ jobs:
|
||||
- name: Build & publish release artifacts
|
||||
run: make release
|
||||
env:
|
||||
GHCR_USERNAME: ${{ github.actor }}
|
||||
GHCR_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
|
||||
DOCKER_USERNAME: ${{ secrets.TOOLBOX_DOCKER_USER }}
|
||||
DOCKER_PASSWORD: ${{ secrets.TOOLBOX_DOCKER_PASS }}
|
||||
# we use a different token than GITHUB_SECRETS to additionally allow updating the homebrew repos
|
||||
@@ -142,10 +146,12 @@ jobs:
|
||||
AC_PASSWORD: ${{ secrets.ENG_CI_APPLE_ID_PASS }}
|
||||
|
||||
- uses: anchore/sbom-action@v0
|
||||
continue-on-error: true
|
||||
with:
|
||||
artifact-name: sbom.spdx.json
|
||||
|
||||
- uses: 8398a7/action-slack@v3
|
||||
continue-on-error: true
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
fields: repo,workflow,action,eventName
|
||||
|
||||
@@ -2,10 +2,13 @@ name: "Validations"
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
GO_VERSION: "1.16.x"
|
||||
GO_VERSION: "1.18.x"
|
||||
GO_STABLE_VERSION: true
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -17,6 +20,7 @@ jobs:
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
stable: ${{ env.GO_STABLE_VERSION }}
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
@@ -54,6 +58,7 @@ jobs:
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
stable: ${{ env.GO_STABLE_VERSION }}
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
@@ -90,6 +95,16 @@ jobs:
|
||||
path: syft/pkg/cataloger/java/test-fixtures/java-builds/packages
|
||||
key: ${{ runner.os }}-unit-java-cache-${{ hashFiles( 'syft/pkg/cataloger/java/test-fixtures/java-builds/packages.fingerprint' ) }}
|
||||
|
||||
- name: Build cache key for go binary test-fixture blobs (for unit tests)
|
||||
run: make go-binaries-fingerprint
|
||||
|
||||
- name: Restore Go binary test-fixture cache
|
||||
id: unit-go-binary-cache
|
||||
uses: actions/cache@v2.1.3
|
||||
with:
|
||||
path: syft/pkg/cataloger/golang/test-fixtures/archs/binaries
|
||||
key: ${{ runner.os }}-unit-go-binaries-cache-${{ hashFiles( 'syft/pkg/cataloger/golang/test-fixtures/archs/binaries.fingerprint' ) }}
|
||||
|
||||
- name: Run unit tests
|
||||
run: make unit
|
||||
|
||||
@@ -106,6 +121,7 @@ jobs:
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
stable: ${{ env.GO_STABLE_VERSION }}
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
@@ -157,6 +173,7 @@ jobs:
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
stable: ${{ env.GO_STABLE_VERSION }}
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
@@ -229,6 +246,7 @@ jobs:
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
stable: ${{ env.GO_STABLE_VERSION }}
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
@@ -336,6 +354,7 @@ jobs:
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
stable: ${{ env.GO_STABLE_VERSION }}
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
@@ -348,9 +367,16 @@ jobs:
|
||||
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: Restore tool cache
|
||||
id: tool-cache
|
||||
uses: actions/cache@v2.1.3
|
||||
with:
|
||||
path: ${{ github.workspace }}/.tmp
|
||||
key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }}
|
||||
|
||||
- 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 key for tar cache
|
||||
run: make cli-fingerprint
|
||||
|
||||
+5
-1
@@ -36,4 +36,8 @@ bin/
|
||||
# macOS Finder metadata
|
||||
.DS_STORE
|
||||
|
||||
*.profile
|
||||
*.profile
|
||||
|
||||
# attestation
|
||||
cosign.key
|
||||
cosign.pub
|
||||
|
||||
+22
-18
@@ -41,6 +41,12 @@ builds:
|
||||
mod_timestamp: *build-timestamp
|
||||
env: *build-env
|
||||
ldflags: *build-ldflags
|
||||
hooks:
|
||||
post:
|
||||
# we must have signing as a build hook instead of the signs section. The signs section must register a new
|
||||
# asset, where we want to replace an existing asset. A post-build hook has the advantage of not needing to
|
||||
# unpackage and repackage a tar.gz with a signed binary
|
||||
- ./.github/scripts/apple-signing/sign.sh "{{ .Path }}" "{{ .IsSnapshot }}" "{{ .Target }}"
|
||||
|
||||
- id: windows-build
|
||||
binary: syft
|
||||
@@ -67,16 +73,6 @@ archives:
|
||||
builds:
|
||||
- windows-build
|
||||
|
||||
signs:
|
||||
- artifacts: archive
|
||||
ids:
|
||||
- darwin-archives
|
||||
signature: "${artifact}"
|
||||
cmd: ./.github/scripts/apple-signing/sign.sh
|
||||
args:
|
||||
- "${artifact}"
|
||||
- "{{ .IsSnapshot }}"
|
||||
|
||||
nfpms:
|
||||
- license: "Apache 2.0"
|
||||
maintainer: "Anchore, Inc"
|
||||
@@ -103,6 +99,11 @@ dockers:
|
||||
- "anchore/syft:{{ .Tag }}-amd64"
|
||||
- "anchore/syft:v{{ .Major }}-amd64"
|
||||
- "anchore/syft:v{{ .Major }}.{{ .Minor }}-amd64"
|
||||
- "ghcr.io/anchore/syft:latest"
|
||||
- "ghcr.io/anchore/syft:{{ .Tag }}-amd64"
|
||||
- "ghcr.io/anchore/syft:v{{ .Major }}-amd64"
|
||||
- "ghcr.io/anchore/syft:v{{ .Major }}.{{ .Minor }}-amd64"
|
||||
goarch: amd64
|
||||
dockerfile: Dockerfile
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
@@ -116,6 +117,9 @@ dockers:
|
||||
- "anchore/syft:{{ .Tag }}-arm64v8"
|
||||
- "anchore/syft:v{{ .Major }}-arm64v8"
|
||||
- "anchore/syft:v{{ .Major }}.{{ .Minor }}-arm64v8"
|
||||
- "ghcr.io/anchore/syft:{{ .Tag }}-arm64v8"
|
||||
- "ghcr.io/anchore/syft:v{{ .Major }}-arm64v8"
|
||||
- "ghcr.io/anchore/syft:v{{ .Major }}.{{ .Minor }}-arm64v8"
|
||||
goarch: arm64
|
||||
dockerfile: Dockerfile
|
||||
use: buildx
|
||||
@@ -129,17 +133,17 @@ dockers:
|
||||
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
|
||||
- name_template: ghcr.io/anchore/syft:{{ .Tag }}
|
||||
image_templates:
|
||||
- ghcr.io/anchore/syft:v{{ .Major }}.{{ .Minor }}-amd64
|
||||
- ghcr.io/anchore/syft:v{{ .Major }}.{{ .Minor }}-arm64v8
|
||||
- name_template: ghcr.io/anchore/syft:latest
|
||||
image_templates:
|
||||
- ghcr.io/anchore/syft:v{{ .Major }}.{{ .Minor }}-amd64
|
||||
- ghcr.io/anchore/syft:v{{ .Major }}.{{ .Minor }}-arm64v8
|
||||
|
||||
@@ -82,12 +82,6 @@ Date: Mon Aug 1 11:27:13 2020 -0400
|
||||
|
||||
[//]: # (TODO: PR title and description)
|
||||
|
||||
## Sign your commits
|
||||
|
||||
To ensure the authenticity and integrity of code contributions, **we require that all commits are signed**. Signing commits proves that your commits were truly created by you, as the holder of a private key.
|
||||
|
||||
Configuring git to sign your commits is a straightforward process. To get set up, see [GitHub's documentation on signing commits](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits).
|
||||
|
||||
## Test your changes
|
||||
|
||||
This project has a `Makefile` which includes many helpers running both unit and integration tests. Although PRs will have automatic checks for these, it is useful to run them locally, ensuring they pass before submitting changes. Ensure you've bootstrapped once before running tests:
|
||||
|
||||
+4
-1
@@ -24,6 +24,9 @@ LABEL org.opencontainers.image.revision=$VCS_REF
|
||||
LABEL org.opencontainers.image.vendor="Anchore, Inc."
|
||||
LABEL org.opencontainers.image.version=$BUILD_VERSION
|
||||
LABEL org.opencontainers.image.licenses="Apache-2.0"
|
||||
|
||||
LABEL io.artifacthub.package.readme-url="https://raw.githubusercontent.com/anchore/syft/main/README.md"
|
||||
LABEL io.artifacthub.package.logo-url="https://user-images.githubusercontent.com/5199289/136844524-1527b09f-c5cb-4aa9-be54-5aa92a6086c1.png"
|
||||
LABEL io.artifacthub.package.license="Apache-2.0"
|
||||
|
||||
|
||||
ENTRYPOINT ["/syft"]
|
||||
|
||||
@@ -3,9 +3,10 @@ TEMPDIR = ./.tmp
|
||||
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
|
||||
LINTCMD = $(TEMPDIR)/golangci-lint run --tests=false --timeout=4m --config .golangci.yaml
|
||||
RELEASE_CMD=$(TEMPDIR)/goreleaser release --rm-dist
|
||||
SNAPSHOT_CMD=$(RELEASE_CMD) --skip-publish --snapshot
|
||||
VERSION=$(shell git describe --dirty --always --tags)
|
||||
COMPARE_TEST_IMAGE = centos:8.2.2004
|
||||
COMPARE_DIR = ./test/compare
|
||||
|
||||
@@ -24,7 +25,7 @@ COVERAGE_THRESHOLD := 62
|
||||
|
||||
# CI cache busting values; change these if you want CI to not use previous stored cache
|
||||
INTEGRATION_CACHE_BUSTER="894d8ca"
|
||||
CLI_CACHE_BUSTER="894d8ca"
|
||||
CLI_CACHE_BUSTER="e5cdfd8"
|
||||
BOOTSTRAP_CACHE="c7afb99ad"
|
||||
|
||||
## Build variables
|
||||
@@ -55,6 +56,10 @@ ifndef SNAPSHOTDIR
|
||||
$(error SNAPSHOTDIR is not set)
|
||||
endif
|
||||
|
||||
ifndef VERSION
|
||||
$(error VERSION is not set)
|
||||
endif
|
||||
|
||||
ifndef REF_NAME
|
||||
REF_NAME = $(VERSION)
|
||||
endif
|
||||
@@ -101,11 +106,12 @@ $(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/golangci/golangci-lint/master/install.sh | sh -s -- -b $(TEMPDIR)/ v1.45.0
|
||||
curl -sSfL https://raw.githubusercontent.com/wagoodman/go-bouncer/master/bouncer.sh | sh -s -- -b $(TEMPDIR)/ v0.3.0
|
||||
curl -sSfL https://raw.githubusercontent.com/anchore/chronicle/main/install.sh | sh -s -- -b $(TEMPDIR)/ v0.3.0
|
||||
.github/scripts/goreleaser-install.sh -d -b $(TEMPDIR)/ v1.3.1
|
||||
.github/scripts/goreleaser-install.sh -d -b $(TEMPDIR)/ v1.4.1
|
||||
GOBIN="$(shell realpath $(TEMPDIR))" go install github.com/neilpa/yajsv@v1.4.0
|
||||
GOBIN="$(shell realpath $(TEMPDIR))" go install github.com/sigstore/cosign/cmd/cosign@v1.5.1
|
||||
|
||||
.PHONY: bootstrap-go
|
||||
bootstrap-go:
|
||||
@@ -116,7 +122,7 @@ bootstrap: $(RESULTSDIR) bootstrap-go bootstrap-tools ## Download and install al
|
||||
$(call title,Bootstrapping dependencies)
|
||||
|
||||
.PHONY: static-analysis
|
||||
static-analysis: lint check-go-mod-tidy check-licenses
|
||||
static-analysis: check-go-mod-tidy check-licenses lint
|
||||
|
||||
.PHONY: lint
|
||||
lint: ## Run gofmt + golangci lint checks
|
||||
@@ -208,6 +214,12 @@ java-packages-fingerprint:
|
||||
cd syft/pkg/cataloger/java/test-fixtures/java-builds && \
|
||||
make packages.fingerprint
|
||||
|
||||
.PHONY: go-binaries-fingerprint
|
||||
go-binaries-fingerprint:
|
||||
$(call title,Go binaries test fixture fingerprint)
|
||||
cd syft/pkg/cataloger/golang/test-fixtures/archs && \
|
||||
make binaries.fingerprint
|
||||
|
||||
.PHONY: fixtures
|
||||
fixtures:
|
||||
$(call title,Generating test fixtures)
|
||||
@@ -316,14 +328,17 @@ release: clean-dist CHANGELOG.md ## Build and publish final binaries and packag
|
||||
|
||||
rm -f .github/scripts/apple-signing/log/*.txt
|
||||
|
||||
# note: notarization cannot be done in parallel, thus --parallelism 1
|
||||
bash -c "\
|
||||
$(RELEASE_CMD) \
|
||||
--config $(TEMPDIR)/goreleaser.yaml \
|
||||
--parallelism 1 \
|
||||
--release-notes <(cat CHANGELOG.md)\
|
||||
|| cat .github/scripts/apple-signing/log/*.txt && false"
|
||||
|| (cat .github/scripts/apple-signing/log/*.txt && false)"
|
||||
|
||||
cat .github/scripts/apple-signing/log/*.txt
|
||||
|
||||
# TODO: turn this into a post-release hook
|
||||
# upload the version file that supports the application version update check (excluding pre-releases)
|
||||
.github/scripts/update-version-file.sh "$(DISTDIR)" "$(VERSION)"
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
[](https://github.com/anchore/syft/blob/main/LICENSE)
|
||||
[](https://anchore.com/slack)
|
||||
|
||||
A CLI tool and Go library for generating a Software Bill of Materials (SBOM) from container images and filesystems. Exceptional for vulnerability detection when used with a scanner tool like [Grype](https://github.com/anchore/grype).
|
||||
A CLI tool and Go library for generating a Software Bill of Materials (SBOM) from container images and filesystems. Exceptional for vulnerability detection when used with a scanner like [Grype](https://github.com/anchore/grype).
|
||||
|
||||
### Join our community meetings!
|
||||
|
||||
@@ -20,23 +20,37 @@ A CLI tool and Go library for generating a Software Bill of Materials (SBOM) fro
|
||||

|
||||
|
||||
## 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/PAR/SAR, Jenkins plugins JPI/HPI, Go modules, PHP Composer)
|
||||
- Linux distribution identification (supports Alpine, BusyBox, CentOS/RedHat, Debian/Ubuntu flavored distributions)
|
||||
- Supports Docker and OCI image formats
|
||||
- Direct support for [Grype](https://github.com/anchore/grype), a fast and powerful vulnerability matcher.
|
||||
- Generates SBOMs for container images, filesystems, archives, and more to discover packages and libraries
|
||||
- Supports OCI and Docker image formats
|
||||
- Linux distribution identification
|
||||
- Works seamlessly with [Grype](https://github.com/anchore/grype) (a fast, modern vulnerability scanner)
|
||||
- Able to create signed SBOM attestations using the [in-toto specification](https://github.com/in-toto/attestation/blob/main/spec/README.md)
|
||||
|
||||
### Supported Ecosystems
|
||||
|
||||
If you encounter an issue, please [let us know using the issue tracker](https://github.com/anchore/syft/issues).
|
||||
- Alpine (apk)
|
||||
- Dart (pubs)
|
||||
- Debian (dpkg)
|
||||
- Go (go.mod, Go binaries)
|
||||
- Java (jar, ear, war, par, sar)
|
||||
- JavaScript (npm, yarn)
|
||||
- Jenkins Plugins (jpi, hpi)
|
||||
- PHP (composer)
|
||||
- Python (wheel, egg, poetry, requirements.txt)
|
||||
- Red Hat (rpm)
|
||||
- Ruby (gem)
|
||||
- Rust (cargo.lock)
|
||||
|
||||
## Installation
|
||||
|
||||
**Note**: Currently, Syft is built only for Linux, macOS and Windows.
|
||||
|
||||
### 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:
|
||||
... 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>
|
||||
@@ -48,43 +62,63 @@ brew tap anchore/syft
|
||||
brew install syft
|
||||
```
|
||||
|
||||
**Note**: Currently, Syft is built only for macOS and Linux.
|
||||
### Nix
|
||||
|
||||
**Note**: Nix packaging of Syft is [community maintained](https://github.com/NixOS/nixpkgs/blob/master/pkgs/tools/admin/syft/default.nix). Syft is currently only in the [unstable channel](https://nixos.wiki/wiki/Nix_channels#The_official_channels) awaiting the `22.05` release
|
||||
|
||||
```bash
|
||||
nix-env -i syft
|
||||
```
|
||||
|
||||
... or, just try it out in an ephemeral nix shell:
|
||||
|
||||
```bash
|
||||
nix-shell -p syft
|
||||
```
|
||||
|
||||
## Getting started
|
||||
|
||||
To generate an SBOM for a Docker or OCI image:
|
||||
#### SBOM
|
||||
|
||||
To generate an SBOM for a container 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`:
|
||||
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 packages <image> --scope all-layers
|
||||
syft <image> --scope all-layers
|
||||
```
|
||||
|
||||
#### SBOM attestation
|
||||
|
||||
To generate an SBOM attestation for a container image:
|
||||
```
|
||||
syft attest --output [FORMAT] --key [KEY] [SOURCE] [flags]
|
||||
```
|
||||
|
||||
The above output is in the form of the [DSSE envelope](https://github.com/secure-systems-lab/dsse/blob/master/envelope.md#dsse-envelope).
|
||||
The payload is a base64 encoded `in-toto` statement with the generated SBOM as the predicate. For details on workflows using this command see [here](#adding-an-sbom-to-an-image-as-an-attestation-using-syft).
|
||||
|
||||
### 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 packages path/to/image.tar
|
||||
syft path/to/image.tar
|
||||
|
||||
# catalog a directory
|
||||
syft packages path/to/dir
|
||||
syft path/to/dir
|
||||
```
|
||||
|
||||
Sources can be explicitly provided with a scheme:
|
||||
|
||||
```
|
||||
docker:yourrepo/yourimage:tag use images from the Docker daemon
|
||||
podman:yourrepo/yourimage:tag use images from the Podman 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)
|
||||
@@ -116,7 +150,7 @@ The output format for Syft is configurable as well using the
|
||||
`-o` (or `--output`) option:
|
||||
|
||||
```
|
||||
syft packages <image> -o <format>
|
||||
syft <image> -o <format>
|
||||
```
|
||||
|
||||
Where the `formats` available are:
|
||||
@@ -134,20 +168,16 @@ Syft can also output _multiple_ files in differing formats by appending
|
||||
`=<file>` to the option, for example to output Syft JSON and SPDX JSON:
|
||||
|
||||
```shell
|
||||
syft packages <image> -o json=sbom.syft.json -o spdx-json=sbom.spdx.json
|
||||
syft <image> -o json=sbom.syft.json -o spdx-json=sbom.spdx.json
|
||||
```
|
||||
|
||||
## Private Registry Authentication
|
||||
|
||||
### Local Docker Credentials
|
||||
When a container runtime is not present, Syft can still utilize credentials configured in common credential sources (such as `~/.docker/config.json`).
|
||||
It will pull images from private registries using these credentials. The config file is where your credentials are stored when authenticating with private registries via some command like `docker login`.
|
||||
For more information see the `go-containerregistry` [documentation](https://github.com/google/go-containerregistry/tree/main/pkg/authn).
|
||||
|
||||
When a container runtime is not present, Syft can still utilize credentials configured in common credential sources (such as `~/.docker/config.json`). It will pull images from private registries using these credentials. The config file is where your credentials are stored when authenticating with private registries via some command like `docker login`. For more information see the `go-containerregistry` [documentation](https://github.com/google/go-containerregistry/tree/main/pkg/authn).
|
||||
|
||||
An example `config.json` looks something like this:
|
||||
```
|
||||
// config.json
|
||||
```json
|
||||
{
|
||||
"auths": {
|
||||
"registry.example.com": {
|
||||
@@ -160,15 +190,17 @@ An example `config.json` looks something like this:
|
||||
|
||||
You can run the following command as an example. It details the mount/environment configuration a container needs to access a private registry:
|
||||
|
||||
`docker run -v ./config.json:/config/config.json -e "DOCKER_CONFIG=/config" anchore/syft:latest <private_image>`
|
||||
|
||||
```
|
||||
docker run -v ./config.json:/config/config.json -e "DOCKER_CONFIG=/config" anchore/syft:latest <private_image>
|
||||
```
|
||||
|
||||
### Docker Credentials in Kubernetes
|
||||
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.
|
||||
```
|
||||
|
||||
Here's a simple workflow to mount this config file as a secret into a container on Kubernetes.
|
||||
|
||||
1. Create a secret. The value of `config.json` is important. It refers to the specification detailed [here](https://github.com/google/go-containerregistry/tree/main/pkg/authn#the-config-file). Below this section is the `secret.yaml` file that the pod configuration will consume as a volume. The key `config.json` is important. It will end up being the name of the file when mounted into the pod.
|
||||
|
||||
```yaml
|
||||
# secret.yaml
|
||||
|
||||
apiVersion: v1
|
||||
@@ -183,15 +215,15 @@ The key `config.json` is important. It will end up being the name of the file wh
|
||||
`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.
|
||||
```
|
||||
2. Create your pod running syft. The env `DOCKER_CONFIG` is important because it advertises where to look for the credential file. In the below example, setting `DOCKER_CONFIG=/config` informs syft that credentials can be found at `/config/config.json`. This is why we used `config.json` as the key for our secret. When mounted into containers the secrets' key is used as the filename. The `volumeMounts` section mounts our secret to `/config`. The `volumes` section names our volume and leverages the secret we created in step one.
|
||||
|
||||
```yaml
|
||||
# pod.yaml
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: syft-k8s-usage
|
||||
spec:
|
||||
containers:
|
||||
- image: anchore/syft:latest
|
||||
@@ -214,10 +246,9 @@ The `volumeMounts` section mounts our secret to `/config`. The `volumes` section
|
||||
`kubectl apply -f pod.yaml`
|
||||
|
||||
|
||||
3. The user can now run `kubectl logs syft-private-registry-demo`. The logs should show the syft analysis for the `<private_image>` provided in the pod configuration.
|
||||
3. The user can now run `kubectl logs syft-private-registry-demo`. The logs should show the Syft analysis for the `<private_image>` provided in the pod configuration.
|
||||
|
||||
Using the above information, users should be able to configure private registry access without having to do so in the `grype` or `syft` configuration files.
|
||||
They will also not be dependent on a docker daemon, (or some other runtime software) for registry configuration and access.
|
||||
Using the above information, users should be able to configure private registry access without having to do so in the `grype` or `syft` configuration files. They will also not be dependent on a Docker daemon, (or some other runtime software) for registry configuration and access.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -254,7 +285,11 @@ check-for-app-update: true
|
||||
# exclude:
|
||||
# - "/etc/**"
|
||||
# - "./out/**/*.json"
|
||||
exclude:
|
||||
exclude: []
|
||||
|
||||
# os and/or architecture to use when referencing container images (e.g. "windows/armv6" or "arm64")
|
||||
# same as --platform; SYFT_PLATFORM env var
|
||||
platform: ""
|
||||
|
||||
# cataloging packages is exposed through the packages and power-user subcommands
|
||||
package:
|
||||
@@ -378,6 +413,16 @@ registry:
|
||||
token: ""
|
||||
# - ... # note, more credentials can be provided via config file only
|
||||
|
||||
# generate an attested SBOM
|
||||
attest:
|
||||
# path to the private key file to use for attestation
|
||||
# SYFT_ATTEST_KEY env var
|
||||
key: "cosign.key"
|
||||
|
||||
# password to decrypt to given private key
|
||||
# SYFT_ATTEST_PASSWORD env var, additionally responds to COSIGN_PASSWORD
|
||||
password: ""
|
||||
|
||||
log:
|
||||
# use structured logging
|
||||
# same as SYFT_LOG_STRUCTURED env var
|
||||
@@ -414,30 +459,31 @@ anchore:
|
||||
dockerfile: ""
|
||||
```
|
||||
|
||||
### Adding an SBOM to an image as an attestation
|
||||
SBOMs themselves can serve as input to different analysis tools. The Anchore organization offers the vulnerability scanner
|
||||
[grype](https://github.com/anchore/grype) as one such tool.
|
||||
One of the foundational approaches to "trust" between tools is for producers to use the artifacts generated by syft as attestations to their images.
|
||||
The SBOM output of syft can be used with the [cosign](https://github.com/sigstore/cosign) tool to generate an attestation that is attached to a signed image.
|
||||
### Adding an SBOM to an image as an attestation using Syft
|
||||
|
||||
#### Example attest
|
||||
Note for the following example replace `test/image:latest` with an image you own. You should also have push access to
|
||||
its remote reference. Replace $MY_PRIVATE_KEY with a private key you own or have generated with cosign.
|
||||
`syft attest --output [FORMAT] --key [KEY] [SOURCE] [flags]`
|
||||
|
||||
SBOMs themselves can serve as input to different analysis tools. [Grype](https://github.com/anchore/grype), a vulnerability scanner CLI tool from Anchore, is one such tool. Publishers of container images can use attestations to enable their consumers to trust Syft-generated SBOM descriptions of those container images. To create and provide these attestations, image publishers can run `syft attest` in conjunction with the [cosign](https://github.com/sigstore/cosign) tool to attach SBOM attestations to their images.
|
||||
|
||||
#### Example attestation
|
||||
Note for the following example replace `docker.io/image:latest` with an image you own. You should also have push access to
|
||||
its remote reference. Replace `$MY_PRIVATE_KEY` with a private key you own or have generated with cosign.
|
||||
|
||||
```bash
|
||||
cosign sign --key $MY_PRIVATE_KEY test/image:latest
|
||||
syft test/image:latest -o json > test_latest_sbom.json
|
||||
cosign attest --predicate test_latest_sbom.json --key $MY_PRIVATE_KEY test/image:latest
|
||||
syft attest --key $MY_PRIVATE_KEY docker.io/image:latest > image_latest_sbom_attestation.json
|
||||
cosign attach attestation --attestation image_latest_sbom_attestation.json docker.io/image:latest
|
||||
```
|
||||
|
||||
Verify the new attestation exists on your image
|
||||
Verify the new attestation exists on your image.
|
||||
|
||||
```bash
|
||||
cosign verify-attestation -key $MY_PUBLIC_KEY test/image:latest | jq '.payload |= @base64d | .payload | fromjson | .predicate.Data | fromjson | .'
|
||||
cosign verify-attestation -key $MY_PUBLIC_KEY docker.io/image:latest | jq '.payload | @base64d | .payload | fromjson | .predicate'
|
||||
```
|
||||
|
||||
You should see this output along with the attached SBOM.
|
||||
You should see this output along with the attached SBOM:
|
||||
|
||||
```
|
||||
Verification for test/image:latest --
|
||||
Verification for docker.io/image:latest --
|
||||
The following checks were performed on each of these signatures:
|
||||
- The cosign claims were validated
|
||||
- The signatures were verified against the specified public key
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# See documentation here: https://github.com/artifacthub/hub/blob/v1.6.0/docs/metadata/artifacthub-repo.yml
|
||||
|
||||
owners:
|
||||
- name: wagoodman
|
||||
email: wagoodman@gmail.com
|
||||
+322
@@ -0,0 +1,322 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/internal/formats/cyclonedxjson"
|
||||
"github.com/anchore/syft/internal/formats/spdx22json"
|
||||
"github.com/anchore/syft/internal/formats/syftjson"
|
||||
|
||||
"github.com/anchore/stereoscope"
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/ui"
|
||||
"github.com/anchore/syft/syft"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/in-toto/in-toto-golang/in_toto"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/pkg/profile"
|
||||
"github.com/sigstore/cosign/cmd/cosign/cli/sign"
|
||||
"github.com/sigstore/cosign/pkg/cosign"
|
||||
"github.com/sigstore/cosign/pkg/cosign/attestation"
|
||||
"github.com/sigstore/sigstore/pkg/signature/dsse"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
|
||||
signatureoptions "github.com/sigstore/sigstore/pkg/signature/options"
|
||||
)
|
||||
|
||||
const (
|
||||
attestExample = ` {{.appName}} {{.command}} --output [FORMAT] --key [KEY] alpine:latest
|
||||
|
||||
Supports the following image sources:
|
||||
{{.appName}} {{.command}} --key [KEY] yourrepo/yourimage:tag defaults to using images from a Docker daemon. If Docker is not present, the image is pulled directly from the registry.
|
||||
{{.appName}} {{.command}} --key [KEY] path/to/a/file/or/dir only for OCI tar or OCI directory
|
||||
|
||||
`
|
||||
attestSchemeHelp = "\n" + indent + schemeHelpHeader + "\n" + imageSchemeHelp
|
||||
|
||||
attestHelp = attestExample + attestSchemeHelp
|
||||
|
||||
intotoJSONDsseType = `application/vnd.in-toto+json`
|
||||
)
|
||||
|
||||
var attestFormats = []sbom.FormatID{
|
||||
syftjson.ID,
|
||||
spdx22json.ID,
|
||||
cyclonedxjson.ID,
|
||||
}
|
||||
|
||||
var (
|
||||
attestCmd = &cobra.Command{
|
||||
Use: "attest --output [FORMAT] --key [KEY] [SOURCE]",
|
||||
Short: "Generate a package SBOM as an attestation for the given [SOURCE] container image",
|
||||
Long: "Generate a packaged-based Software Bill Of Materials (SBOM) from a container image as the predicate of an in-toto attestation",
|
||||
Example: internal.Tprintf(attestHelp, map[string]interface{}{
|
||||
"appName": internal.ApplicationName,
|
||||
"command": "attest",
|
||||
}),
|
||||
Args: validateInputArgs,
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
PreRunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
if appConfig.Dev.ProfileCPU && appConfig.Dev.ProfileMem {
|
||||
return fmt.Errorf("cannot profile CPU and memory simultaneously")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if appConfig.Dev.ProfileCPU {
|
||||
defer profile.Start(profile.CPUProfile).Stop()
|
||||
} else if appConfig.Dev.ProfileMem {
|
||||
defer profile.Start(profile.MemProfile).Stop()
|
||||
}
|
||||
|
||||
return attestExec(cmd.Context(), cmd, args)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func fetchPassword(_ bool) (b []byte, err error) {
|
||||
potentiallyPipedInput, err := internal.IsPipedInput()
|
||||
if err != nil {
|
||||
log.Warnf("unable to determine if there is piped input: %+v", err)
|
||||
}
|
||||
switch {
|
||||
case appConfig.Attest.Password != "":
|
||||
return []byte(appConfig.Attest.Password), nil
|
||||
case potentiallyPipedInput:
|
||||
// handle piped in passwords
|
||||
pwBytes, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get password from stdin: %w", err)
|
||||
}
|
||||
// be resilient to input that may have newline characters (in case someone is using echo without -n)
|
||||
cleanPw := strings.TrimRight(string(pwBytes), "\n")
|
||||
return []byte(cleanPw), nil
|
||||
case internal.IsTerminal():
|
||||
return cosign.GetPassFromTerm(false)
|
||||
}
|
||||
return nil, errors.New("no method available to fetch password")
|
||||
}
|
||||
|
||||
func selectPassFunc(keypath string) (cosign.PassFunc, error) {
|
||||
keyContents, err := os.ReadFile(keypath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var fn cosign.PassFunc = func(bool) (b []byte, err error) { return nil, nil }
|
||||
|
||||
_, err = cosign.LoadPrivateKey(keyContents, nil)
|
||||
if err != nil {
|
||||
fn = fetchPassword
|
||||
}
|
||||
|
||||
return fn, nil
|
||||
}
|
||||
|
||||
func attestExec(ctx context.Context, _ *cobra.Command, args []string) error {
|
||||
// can only be an image for attestation or OCI DIR
|
||||
userInput := args[0]
|
||||
si, err := source.ParseInput(userInput, appConfig.Platform, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not generate source input for attest command: %w", err)
|
||||
}
|
||||
|
||||
switch si.Scheme {
|
||||
case source.ImageScheme, source.UnknownScheme:
|
||||
// at this point we know that it cannot be dir: or file: schemes, so we will assume that the unknown scheme could represent an image
|
||||
si.Scheme = source.ImageScheme
|
||||
default:
|
||||
return fmt.Errorf("attest command can only be used with image sources but discovered %q when given %q", si.Scheme, userInput)
|
||||
}
|
||||
|
||||
// if the original detection was from a local daemon we want to short circuit
|
||||
// that and attempt to generate the image source from a registry source instead
|
||||
switch si.ImageSource {
|
||||
case image.UnknownSource, image.OciRegistrySource:
|
||||
si.ImageSource = image.OciRegistrySource
|
||||
default:
|
||||
return fmt.Errorf("attest command can only be used with image sources fetch directly from the registry, but discovered an image source of %q when given %q", si.ImageSource, userInput)
|
||||
}
|
||||
|
||||
if len(appConfig.Outputs) > 1 {
|
||||
return fmt.Errorf("unable to generate attestation for more than one output")
|
||||
}
|
||||
|
||||
format := syft.FormatByName(appConfig.Outputs[0])
|
||||
predicateType := formatPredicateType(format)
|
||||
if predicateType == "" {
|
||||
return fmt.Errorf("could not produce attestation predicate for given format: %q. Available formats: %+v", formatAliases(format.ID()), formatAliases(attestFormats...))
|
||||
}
|
||||
|
||||
passFunc, err := selectPassFunc(appConfig.Attest.Key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ko := sign.KeyOpts{
|
||||
KeyRef: appConfig.Attest.Key,
|
||||
PassFunc: passFunc,
|
||||
}
|
||||
|
||||
sv, err := sign.SignerFromKeyOpts(ctx, "", ko)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer sv.Close()
|
||||
|
||||
return eventLoop(
|
||||
attestationExecWorker(*si, format, predicateType, sv),
|
||||
setupSignals(),
|
||||
eventSubscription,
|
||||
stereoscope.Cleanup,
|
||||
ui.Select(isVerbose(), appConfig.Quiet)...,
|
||||
)
|
||||
}
|
||||
|
||||
func attestationExecWorker(sourceInput source.Input, format sbom.Format, predicateType string, sv *sign.SignerVerifier) <-chan error {
|
||||
errs := make(chan error)
|
||||
go func() {
|
||||
defer close(errs)
|
||||
|
||||
src, cleanup, err := source.NewFromRegistry(sourceInput, appConfig.Registry.ToOptions(), appConfig.Exclusions)
|
||||
if cleanup != nil {
|
||||
defer cleanup()
|
||||
}
|
||||
if err != nil {
|
||||
errs <- fmt.Errorf("failed to construct source from user input %q: %w", sourceInput.UserInput, err)
|
||||
return
|
||||
}
|
||||
|
||||
s, err := generateSBOM(src, errs)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
|
||||
sbomBytes, err := syft.Encode(*s, format)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
|
||||
err = generateAttestation(sbomBytes, src, sv, predicateType)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
}()
|
||||
return errs
|
||||
}
|
||||
|
||||
func formatPredicateType(format sbom.Format) string {
|
||||
switch format.ID() {
|
||||
case spdx22json.ID:
|
||||
return in_toto.PredicateSPDX
|
||||
case cyclonedxjson.ID:
|
||||
// Tentative see https://github.com/in-toto/attestation/issues/82
|
||||
return "https://cyclonedx.org/bom"
|
||||
case syftjson.ID:
|
||||
return "https://syft.dev/bom"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func findValidDigest(digests []string) string {
|
||||
// since we are only using the OCI repo provider for this source we are safe that this is only 1 value
|
||||
// see https://github.com/anchore/stereoscope/blob/25ebd49a842b5ac0a20c2e2b4b81335b64ad248c/pkg/image/oci/registry_provider.go#L57-L63
|
||||
split := strings.Split(digests[0], "sha256:")
|
||||
return split[1]
|
||||
}
|
||||
|
||||
func generateAttestation(predicate []byte, src *source.Source, sv *sign.SignerVerifier, predicateType string) error {
|
||||
switch len(src.Image.Metadata.RepoDigests) {
|
||||
case 0:
|
||||
return fmt.Errorf("cannot generate attestation since no repo digests were found; make sure you're passing an OCI registry source for the attest command")
|
||||
case 1:
|
||||
default:
|
||||
return fmt.Errorf("cannot generate attestation since multiple repo digests were found for the image: %+v", src.Image.Metadata.RepoDigests)
|
||||
}
|
||||
|
||||
wrapped := dsse.WrapSigner(sv, intotoJSONDsseType)
|
||||
|
||||
sh, err := attestation.GenerateStatement(attestation.GenerateOpts{
|
||||
Predicate: bytes.NewBuffer(predicate),
|
||||
Type: predicateType,
|
||||
Digest: findValidDigest(src.Image.Metadata.RepoDigests),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(sh)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
signedPayload, err := wrapped.SignMessage(bytes.NewReader(payload), signatureoptions.WithContext(context.Background()))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to sign SBOM")
|
||||
}
|
||||
|
||||
bus.Publish(partybus.Event{
|
||||
Type: event.Exit,
|
||||
Value: func() error {
|
||||
_, err := os.Stdout.Write(signedPayload)
|
||||
return err
|
||||
},
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
setAttestFlags(attestCmd.Flags())
|
||||
if err := bindAttestConfigOptions(attestCmd.Flags()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
rootCmd.AddCommand(attestCmd)
|
||||
}
|
||||
|
||||
func setAttestFlags(flags *pflag.FlagSet) {
|
||||
// key options
|
||||
flags.StringP("key", "", "cosign.key",
|
||||
"path to the private key file to use for attestation",
|
||||
)
|
||||
|
||||
// in-toto attestations only support JSON predicates, so not all SBOM formats that syft can output are supported
|
||||
flags.StringP(
|
||||
"output", "o", formatAliases(syftjson.ID)[0],
|
||||
fmt.Sprintf("the SBOM format encapsulated within the attestation, available options=%v", formatAliases(attestFormats...)),
|
||||
)
|
||||
|
||||
flags.StringP(
|
||||
"platform", "", "",
|
||||
"an optional platform specifier for container image sources (e.g. 'linux/arm64', 'linux/arm64/v8', 'arm64', 'linux')",
|
||||
)
|
||||
}
|
||||
|
||||
func bindAttestConfigOptions(flags *pflag.FlagSet) error {
|
||||
// note: output is not included since this configuration option is shared between multiple subcommands
|
||||
|
||||
if err := viper.BindPFlag("attest.key", flags.Lookup("key")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+32
-8
@@ -6,10 +6,12 @@ import (
|
||||
"os"
|
||||
"sort"
|
||||
|
||||
"github.com/anchore/go-logger/logrus"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/anchore/stereoscope"
|
||||
"github.com/anchore/syft/internal/config"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/logger"
|
||||
"github.com/anchore/syft/internal/version"
|
||||
"github.com/anchore/syft/syft"
|
||||
"github.com/gookit/color"
|
||||
@@ -57,6 +59,7 @@ func initCmdAliasBindings() {
|
||||
config.PowerUserCatalogerEnabledDefault()
|
||||
}
|
||||
|
||||
// set bindings based on the packages alias
|
||||
switch activeCmd {
|
||||
case packagesCmd, rootCmd:
|
||||
// note: we need to lazily bind config options since they are shared between both the root command
|
||||
@@ -68,6 +71,16 @@ func initCmdAliasBindings() {
|
||||
if err = bindPackagesConfigOptions(activeCmd.Flags()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
case attestCmd:
|
||||
// the --output and --platform options are independently defined flags, but a shared config option
|
||||
if err = bindSharedConfigOption(attestCmd.Flags()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// even though the root command or packages command is NOT being run, we still need default bindings
|
||||
// such that application config parsing passes.
|
||||
if err = bindExclusivePackagesConfigOptions(packagesCmd.Flags()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
default:
|
||||
// even though the root command or packages command is NOT being run, we still need default bindings
|
||||
// such that application config parsing passes.
|
||||
@@ -77,6 +90,18 @@ func initCmdAliasBindings() {
|
||||
}
|
||||
}
|
||||
|
||||
func bindSharedConfigOption(flags *pflag.FlagSet) error {
|
||||
if err := viper.BindPFlag("output", flags.Lookup("output")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := viper.BindPFlag("platform", flags.Lookup("platform")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initAppConfig() {
|
||||
cfg, err := config.LoadApplicationConfig(viper.GetViper(), persistentOpts)
|
||||
if err != nil {
|
||||
@@ -88,7 +113,7 @@ func initAppConfig() {
|
||||
}
|
||||
|
||||
func initLogging() {
|
||||
cfg := logrus.Config{
|
||||
cfg := logger.LogrusConfig{
|
||||
EnableConsole: (appConfig.Log.FileLocation == "" || appConfig.CliOptions.Verbosity > 0) && !appConfig.Quiet,
|
||||
EnableFile: appConfig.Log.FileLocation != "",
|
||||
Level: appConfig.Log.LevelOpt,
|
||||
@@ -96,12 +121,11 @@ func initLogging() {
|
||||
FileLocation: appConfig.Log.FileLocation,
|
||||
}
|
||||
|
||||
logger, err := logrus.New(cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
syft.SetLogger(logger)
|
||||
stereoscope.SetLogger(logger.Nested("from-lib", "stereoscope"))
|
||||
logWrapper := logger.NewLogrusLogger(cfg)
|
||||
syft.SetLogger(logWrapper)
|
||||
stereoscope.SetLogger(&logger.LogrusNestedLogger{
|
||||
Logger: logWrapper.Logger.WithField("from-lib", "stereoscope"),
|
||||
})
|
||||
}
|
||||
|
||||
func logAppConfig() {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/syft"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
func formatAliases(ids ...sbom.FormatID) (aliases []string) {
|
||||
for _, id := range ids {
|
||||
switch id {
|
||||
case syft.JSONFormatID:
|
||||
aliases = append(aliases, "syft-json")
|
||||
case syft.TextFormatID:
|
||||
aliases = append(aliases, "text")
|
||||
case syft.TableFormatID:
|
||||
aliases = append(aliases, "table")
|
||||
case syft.SPDXJSONFormatID:
|
||||
aliases = append(aliases, "spdx-json")
|
||||
case syft.SPDXTagValueFormatID:
|
||||
aliases = append(aliases, "spdx-tag-value")
|
||||
case syft.CycloneDxXMLFormatID:
|
||||
aliases = append(aliases, "cyclonedx-xml")
|
||||
case syft.CycloneDxJSONFormatID:
|
||||
aliases = append(aliases, "cyclonedx-json")
|
||||
case syft.GitHubID:
|
||||
aliases = append(aliases, "github", "github-json")
|
||||
default:
|
||||
aliases = append(aliases, string(id))
|
||||
}
|
||||
}
|
||||
return aliases
|
||||
}
|
||||
+12
-20
@@ -4,9 +4,10 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/internal/formats"
|
||||
"github.com/anchore/syft/internal/output"
|
||||
"github.com/anchore/syft/syft/format"
|
||||
"github.com/anchore/syft/internal/formats/table"
|
||||
|
||||
"github.com/anchore/syft/syft"
|
||||
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
)
|
||||
@@ -19,7 +20,7 @@ func makeWriter(outputs []string, defaultFile string) (sbom.Writer, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
writer, err := output.MakeWriter(outputOptions...)
|
||||
writer, err := sbom.NewWriter(outputOptions...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -28,10 +29,10 @@ func makeWriter(outputs []string, defaultFile string) (sbom.Writer, error) {
|
||||
}
|
||||
|
||||
// parseOptions utility to parse command-line option strings and retain the existing behavior of default format and file
|
||||
func parseOptions(outputs []string, defaultFile string) (out []output.WriterOption, errs error) {
|
||||
func parseOptions(outputs []string, defaultFile string) (out []sbom.WriterOption, errs error) {
|
||||
// always should have one option -- we generally get the default of "table", but just make sure
|
||||
if len(outputs) == 0 {
|
||||
outputs = append(outputs, string(format.TableOption))
|
||||
outputs = append(outputs, string(table.ID))
|
||||
}
|
||||
|
||||
for _, name := range outputs {
|
||||
@@ -40,33 +41,24 @@ func parseOptions(outputs []string, defaultFile string) (out []output.WriterOpti
|
||||
// split to at most two parts for <format>=<file>
|
||||
parts := strings.SplitN(name, "=", 2)
|
||||
|
||||
// the format option is the first part
|
||||
// the format name is the first part
|
||||
name = parts[0]
|
||||
|
||||
// default to the --file or empty string if not specified
|
||||
file := defaultFile
|
||||
|
||||
// If a file is specified as part of the output option, use that
|
||||
// If a file is specified as part of the output formatName, use that
|
||||
if len(parts) > 1 {
|
||||
file = parts[1]
|
||||
}
|
||||
|
||||
option := format.ParseOption(name)
|
||||
if option == format.UnknownFormatOption {
|
||||
format := syft.FormatByName(name)
|
||||
if format == nil {
|
||||
errs = multierror.Append(errs, fmt.Errorf("bad output format: '%s'", name))
|
||||
continue
|
||||
}
|
||||
|
||||
encoder := formats.ByOption(option)
|
||||
if encoder == nil {
|
||||
errs = multierror.Append(errs, fmt.Errorf("unknown format: %s", outputFormat))
|
||||
continue
|
||||
}
|
||||
|
||||
out = append(out, output.WriterOption{
|
||||
Format: *encoder,
|
||||
Path: file,
|
||||
})
|
||||
out = append(out, sbom.NewWriterOption(format, file))
|
||||
}
|
||||
return out, errs
|
||||
}
|
||||
|
||||
+93
-51
@@ -10,12 +10,13 @@ import (
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/anchore"
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/formats/table"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/ui"
|
||||
"github.com/anchore/syft/internal/version"
|
||||
"github.com/anchore/syft/syft"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/syft/format"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
@@ -27,26 +28,33 @@ import (
|
||||
)
|
||||
|
||||
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
|
||||
packagesExample = ` {{.appName}} {{.command}} alpine:latest a summary of discovered packages
|
||||
{{.appName}} {{.command}} alpine:latest -o json show all possible cataloging details
|
||||
{{.appName}} {{.command}} alpine:latest -o cyclonedx show a CycloneDX formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -o cyclonedx-json show a CycloneDX JSON formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -o spdx show a SPDX 2.2 Tag-Value formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -o spdx-json show a SPDX 2.2 JSON formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -vv show verbose debug information
|
||||
|
||||
Supports the following image sources:
|
||||
{{.appName}} {{.command}} yourrepo/yourimage:tag defaults to using images from a Docker daemon. If Docker is not present, the image is pulled directly from the registry.
|
||||
{{.appName}} {{.command}} path/to/a/file/or/dir a Docker tar, OCI tar, OCI directory, or generic filesystem directory
|
||||
`
|
||||
|
||||
You can also explicitly specify the scheme to use:
|
||||
{{.appName}} {{.command}} docker:yourrepo/yourimage:tag explicitly use the Docker daemon
|
||||
schemeHelpHeader = "You can also explicitly specify the scheme to use:"
|
||||
imageSchemeHelp = ` {{.appName}} {{.command}} docker:yourrepo/yourimage:tag explicitly use the Docker daemon
|
||||
{{.appName}} {{.command}} podman:yourrepo/yourimage:tag explicitly use the Podman daemon
|
||||
{{.appName}} {{.command}} registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required)
|
||||
{{.appName}} {{.command}} docker-archive:path/to/yourimage.tar use a tarball from disk for archives created from "docker save"
|
||||
{{.appName}} {{.command}} oci-archive:path/to/yourimage.tar use a tarball from disk for OCI archives (from Skopeo or otherwise)
|
||||
{{.appName}} {{.command}} oci-dir:path/to/yourimage read directly from a path on disk for OCI layout directories (from Skopeo or otherwise)
|
||||
{{.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)
|
||||
`
|
||||
nonImageSchemeHelp = ` {{.appName}} {{.command}} dir:path/to/yourproject read directly from a path on disk (any directory)
|
||||
{{.appName}} {{.command}} file:path/to/yourproject/file read directly from a path on disk (any single file)
|
||||
`
|
||||
packagesSchemeHelp = "\n" + indent + schemeHelpHeader + "\n" + imageSchemeHelp + nonImageSchemeHelp
|
||||
|
||||
packagesHelp = packagesExample + packagesSchemeHelp
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -54,7 +62,7 @@ var (
|
||||
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{}{
|
||||
Example: internal.Tprintf(packagesHelp, map[string]interface{}{
|
||||
"appName": internal.ApplicationName,
|
||||
"command": "packages",
|
||||
}),
|
||||
@@ -88,14 +96,13 @@ func init() {
|
||||
|
||||
func setPackageFlags(flags *pflag.FlagSet) {
|
||||
// Formatting & Input options //////////////////////////////////////////////
|
||||
|
||||
flags.StringP(
|
||||
"scope", "s", cataloger.DefaultSearchConfig().Scope.String(),
|
||||
fmt.Sprintf("selection of layers to catalog, options=%v", source.AllScopes))
|
||||
|
||||
flags.StringArrayP(
|
||||
"output", "o", []string{string(format.TableOption)},
|
||||
fmt.Sprintf("report output format, options=%v", format.AllOptions),
|
||||
"output", "o", formatAliases(table.ID),
|
||||
fmt.Sprintf("report output format, options=%v", formatAliases(syft.FormatIDs()...)),
|
||||
)
|
||||
|
||||
flags.StringP(
|
||||
@@ -103,6 +110,11 @@ func setPackageFlags(flags *pflag.FlagSet) {
|
||||
"file to write the default report output to (default is STDOUT)",
|
||||
)
|
||||
|
||||
flags.StringP(
|
||||
"platform", "", "",
|
||||
"an optional platform specifier for container image sources (e.g. 'linux/arm64', 'linux/arm64/v8', 'arm64', 'linux')",
|
||||
)
|
||||
|
||||
// Upload options //////////////////////////////////////////////////////////
|
||||
flags.StringP(
|
||||
"host", "H", "",
|
||||
@@ -141,13 +153,22 @@ func setPackageFlags(flags *pflag.FlagSet) {
|
||||
}
|
||||
|
||||
func bindPackagesConfigOptions(flags *pflag.FlagSet) error {
|
||||
// Formatting & Input options //////////////////////////////////////////////
|
||||
|
||||
if err := viper.BindPFlag("package.cataloger.scope", flags.Lookup("scope")); err != nil {
|
||||
if err := bindExclusivePackagesConfigOptions(flags); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := bindSharedConfigOption(flags); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := viper.BindPFlag("output", flags.Lookup("output")); err != nil {
|
||||
// NOTE(alex): Write a helper for the binding operation, which can be used to perform the binding but also double check that the intended effect was had or else return an error. Another thought is to somehow provide zero-valued defaults for all values in our config struct (maybe with reflection?). There may be a mechanism that already exists in viper that protects against this that I'm not aware of. ref: https://github.com/anchore/syft/pull/805#discussion_r801931192
|
||||
func bindExclusivePackagesConfigOptions(flags *pflag.FlagSet) error {
|
||||
// Formatting & Input options //////////////////////////////////////////////
|
||||
|
||||
// note: output is not included since this configuration option is shared between multiple subcommands
|
||||
|
||||
if err := viper.BindPFlag("package.cataloger.scope", flags.Lookup("scope")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -201,7 +222,7 @@ func validateInputArgs(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
func packagesExec(_ *cobra.Command, args []string) error {
|
||||
writer, err := makeWriter(appConfig.Output, appConfig.File)
|
||||
writer, err := makeWriter(appConfig.Outputs, appConfig.File)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -214,9 +235,13 @@ func packagesExec(_ *cobra.Command, args []string) error {
|
||||
|
||||
// could be an image or a directory, with or without a scheme
|
||||
userInput := args[0]
|
||||
si, err := source.ParseInput(userInput, appConfig.Platform, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not generate source input for packages command: %w", err)
|
||||
}
|
||||
|
||||
return eventLoop(
|
||||
packagesExecWorker(userInput, writer),
|
||||
packagesExecWorker(*si, writer),
|
||||
setupSignals(),
|
||||
eventSubscription,
|
||||
stereoscope.Cleanup,
|
||||
@@ -235,46 +260,63 @@ func isVerbose() (result bool) {
|
||||
return appConfig.CliOptions.Verbosity > 0 || isPipedInput
|
||||
}
|
||||
|
||||
func packagesExecWorker(userInput string, writer sbom.Writer) <-chan error {
|
||||
func generateSBOM(src *source.Source, errs chan error) (*sbom.SBOM, error) {
|
||||
tasks, err := tasks()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s := sbom.SBOM{
|
||||
Source: src.Metadata,
|
||||
Descriptor: sbom.Descriptor{
|
||||
Name: internal.ApplicationName,
|
||||
Version: version.FromBuild().Version,
|
||||
Configuration: appConfig,
|
||||
},
|
||||
}
|
||||
|
||||
buildRelationships(&s, src, tasks, errs)
|
||||
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
func buildRelationships(s *sbom.SBOM, src *source.Source, tasks []task, errs chan error) {
|
||||
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...)...)
|
||||
}
|
||||
|
||||
func packagesExecWorker(si source.Input, writer sbom.Writer) <-chan error {
|
||||
errs := make(chan error)
|
||||
go func() {
|
||||
defer close(errs)
|
||||
|
||||
tasks, err := tasks()
|
||||
src, cleanup, err := source.New(si, appConfig.Registry.ToOptions(), appConfig.Exclusions)
|
||||
if cleanup != nil {
|
||||
defer cleanup()
|
||||
}
|
||||
if err != nil {
|
||||
errs <- fmt.Errorf("failed to construct source from user input %q: %w", si.UserInput, err)
|
||||
return
|
||||
}
|
||||
|
||||
s, err := generateSBOM(src, errs)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
|
||||
src, cleanup, err := source.New(userInput, appConfig.Registry.ToOptions(), appConfig.Exclusions)
|
||||
if err != nil {
|
||||
errs <- fmt.Errorf("failed to construct source from user input %q: %w", userInput, err)
|
||||
return
|
||||
if s == nil {
|
||||
errs <- fmt.Errorf("no SBOM produced for %q", si.UserInput)
|
||||
}
|
||||
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 {
|
||||
if err := runPackageSbomUpload(src, *s); err != nil {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
@@ -282,7 +324,7 @@ func packagesExecWorker(userInput string, writer sbom.Writer) <-chan error {
|
||||
|
||||
bus.Publish(partybus.Event{
|
||||
Type: event.Exit,
|
||||
Value: func() error { return writer.Write(s) },
|
||||
Value: func() error { return writer.Write(*s) },
|
||||
})
|
||||
}()
|
||||
return errs
|
||||
|
||||
+14
-7
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/formats/syftjson"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/output"
|
||||
"github.com/anchore/syft/internal/ui"
|
||||
"github.com/anchore/syft/internal/version"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
@@ -26,7 +25,7 @@ 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.
|
||||
Only image sources are supported (e.g. docker: , podman: , docker-archive: , oci: , etc.), the directory source (dir:) is not supported.
|
||||
|
||||
All behavior is controlled via application configuration and environment variables (see https://github.com/anchore/syft#configuration)
|
||||
`
|
||||
@@ -74,10 +73,12 @@ func powerUserExec(_ *cobra.Command, args []string) error {
|
||||
// could be an image or a directory, with or without a scheme
|
||||
userInput := args[0]
|
||||
|
||||
writer, err := output.MakeWriter(output.WriterOption{
|
||||
Format: syftjson.Format(),
|
||||
Path: appConfig.File,
|
||||
})
|
||||
writer, err := sbom.NewWriter(
|
||||
sbom.NewWriterOption(
|
||||
syftjson.Format(),
|
||||
appConfig.File,
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -115,7 +116,13 @@ func powerUserExecWorker(userInput string, writer sbom.Writer) <-chan error {
|
||||
return
|
||||
}
|
||||
|
||||
src, cleanup, err := source.New(userInput, appConfig.Registry.ToOptions(), appConfig.Exclusions)
|
||||
si, err := source.ParseInput(userInput, appConfig.Platform, true)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
|
||||
src, cleanup, err := source.New(*si, appConfig.Registry.ToOptions(), appConfig.Exclusions)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return
|
||||
|
||||
@@ -27,6 +27,8 @@ var rootCmd = &cobra.Command{
|
||||
Version: version.FromBuild().Version,
|
||||
}
|
||||
|
||||
const indent = " "
|
||||
|
||||
func init() {
|
||||
// set universal flags
|
||||
rootCmd.PersistentFlags().StringVarP(&persistentOpts.ConfigPath, "config", "c", "", "application config file")
|
||||
|
||||
+13
-12
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var outputFormat string
|
||||
var versionCmdOutputFormat string
|
||||
|
||||
var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
@@ -20,23 +20,24 @@ var versionCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
func init() {
|
||||
versionCmd.Flags().StringVarP(&outputFormat, "output", "o", "text", "format to show version information (available=[text, json])")
|
||||
versionCmd.Flags().StringVarP(&versionCmdOutputFormat, "output", "o", "text", "format to show version information (available=[text, json])")
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
}
|
||||
|
||||
func printVersion(_ *cobra.Command, _ []string) {
|
||||
versionInfo := version.FromBuild()
|
||||
|
||||
switch outputFormat {
|
||||
switch versionCmdOutputFormat {
|
||||
case "text":
|
||||
fmt.Println("Application: ", internal.ApplicationName)
|
||||
fmt.Println("Version: ", versionInfo.Version)
|
||||
fmt.Println("BuildDate: ", versionInfo.BuildDate)
|
||||
fmt.Println("GitCommit: ", versionInfo.GitCommit)
|
||||
fmt.Println("GitDescription: ", versionInfo.GitDescription)
|
||||
fmt.Println("Platform: ", versionInfo.Platform)
|
||||
fmt.Println("GoVersion: ", versionInfo.GoVersion)
|
||||
fmt.Println("Compiler: ", versionInfo.Compiler)
|
||||
fmt.Println("Application: ", internal.ApplicationName)
|
||||
fmt.Println("Version: ", versionInfo.Version)
|
||||
fmt.Println("JsonSchemaVersion: ", internal.JSONSchemaVersion)
|
||||
fmt.Println("BuildDate: ", versionInfo.BuildDate)
|
||||
fmt.Println("GitCommit: ", versionInfo.GitCommit)
|
||||
fmt.Println("GitDescription: ", versionInfo.GitDescription)
|
||||
fmt.Println("Platform: ", versionInfo.Platform)
|
||||
fmt.Println("GoVersion: ", versionInfo.GoVersion)
|
||||
fmt.Println("Compiler: ", versionInfo.Compiler)
|
||||
|
||||
case "json":
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
@@ -54,7 +55,7 @@ func printVersion(_ *cobra.Command, _ []string) {
|
||||
os.Exit(1)
|
||||
}
|
||||
default:
|
||||
fmt.Printf("unsupported output format: %s\n", outputFormat)
|
||||
fmt.Printf("unsupported output format: %s\n", versionCmdOutputFormat)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +1,331 @@
|
||||
module github.com/anchore/syft
|
||||
|
||||
go 1.16
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/CycloneDX/cyclonedx-go v0.4.0
|
||||
github.com/CycloneDX/cyclonedx-go v0.5.0
|
||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
|
||||
github.com/acobaugh/osrelease v0.1.0
|
||||
github.com/adrg/xdg v0.2.1
|
||||
github.com/alecthomas/jsonschema v0.0.0-20210301060011-54c507b6f074
|
||||
github.com/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf
|
||||
github.com/anchore/go-logger v0.0.0
|
||||
github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb
|
||||
github.com/anchore/go-rpmdb v0.0.0-20210914181456-a9c52348da63
|
||||
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
|
||||
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
|
||||
github.com/anchore/packageurl-go v0.0.0-20210922164639-b3fa992ebd29
|
||||
github.com/anchore/stereoscope v0.0.0-20220201190559-f162f1e96f45
|
||||
// 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/anchore/packageurl-go v0.1.1-0.20220314153042-1bcd40e5206b
|
||||
github.com/anchore/stereoscope v0.0.0-20220406160859-c03a18a6b270
|
||||
github.com/antihax/optional v1.0.0
|
||||
github.com/bmatcuk/doublestar/v4 v4.0.2
|
||||
github.com/containerd/containerd v1.5.9 // indirect
|
||||
github.com/docker/docker v20.10.12+incompatible
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/facebookincubator/nvdtools v0.1.4
|
||||
github.com/go-test/deep v1.0.8
|
||||
github.com/google/go-cmp v0.5.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/google/go-cmp v0.5.7
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/gookit/color v1.4.2
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/in-toto/in-toto-golang v0.3.4-0.20211211042327-af1f9fb822bf
|
||||
github.com/jinzhu/copier v0.3.2
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/mholt/archiver/v3 v3.5.1
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2
|
||||
github.com/mitchellh/mapstructure v1.4.1
|
||||
github.com/olekukonko/tablewriter v0.0.4
|
||||
github.com/pelletier/go-toml v1.9.3
|
||||
github.com/mitchellh/mapstructure v1.4.3
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
github.com/pelletier/go-toml v1.9.4
|
||||
github.com/pkg/profile v1.5.0
|
||||
github.com/scylladb/go-set v1.0.2
|
||||
github.com/sergi/go-diff v1.1.0
|
||||
// pinned to pull in 386 arch fix: https://github.com/scylladb/go-set/commit/cc7b2070d91ebf40d233207b633e28f5bd8f03a5
|
||||
github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e
|
||||
github.com/sergi/go-diff v1.2.0
|
||||
github.com/sigstore/sigstore v1.1.1-0.20220217212907-e48ca03a5ba7
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
github.com/spdx/tools-golang v0.1.0
|
||||
github.com/spf13/afero v1.6.0
|
||||
github.com/spf13/cobra v1.2.1
|
||||
github.com/spdx/tools-golang v0.2.0
|
||||
github.com/spf13/afero v1.8.0
|
||||
github.com/spf13/cobra v1.3.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/spf13/viper v1.8.1
|
||||
github.com/spf13/viper v1.10.1
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/vifraa/gopom v0.1.0
|
||||
github.com/wagoodman/go-partybus v0.0.0-20210627031916-db1f5573bbc5
|
||||
github.com/wagoodman/go-progress v0.0.0-20200731105512-1020f39e6240
|
||||
github.com/wagoodman/jotframe v0.0.0-20211129225309-56b0d0a4aebb
|
||||
github.com/x-cray/logrus-prefixed-formatter v0.5.2
|
||||
github.com/xeipuuv/gojsonschema v1.2.0
|
||||
golang.org/x/mod v0.4.2
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2
|
||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b
|
||||
golang.org/x/mod v0.5.1
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
replace github.com/anchore/stereoscope => ../stereoscope
|
||||
require (
|
||||
github.com/docker/docker v20.10.12+incompatible
|
||||
github.com/sigstore/cosign v1.6.0
|
||||
)
|
||||
|
||||
replace github.com/anchore/go-logger => ../go-logger
|
||||
require (
|
||||
bitbucket.org/creachadair/shell v0.0.6 // indirect
|
||||
cloud.google.com/go v0.100.2 // indirect
|
||||
cloud.google.com/go/compute v1.3.0 // indirect
|
||||
cloud.google.com/go/iam v0.1.1 // indirect
|
||||
cloud.google.com/go/kms v1.3.0 // indirect
|
||||
cloud.google.com/go/storage v1.21.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go v61.5.0+incompatible // indirect
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
|
||||
github.com/Azure/go-autorest/autorest v0.11.24 // indirect
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.18 // indirect
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 // indirect
|
||||
github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 // indirect
|
||||
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
|
||||
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
|
||||
github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect
|
||||
github.com/Azure/go-autorest/logger v0.2.1 // indirect
|
||||
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
|
||||
github.com/PaesslerAG/gval v1.0.0 // indirect
|
||||
github.com/PaesslerAG/jsonpath v0.1.1 // indirect
|
||||
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||
github.com/ReneKroon/ttlcache/v2 v2.11.0 // indirect
|
||||
github.com/ThalesIgnite/crypto11 v1.2.5 // indirect
|
||||
github.com/armon/go-metrics v0.3.10 // indirect
|
||||
github.com/armon/go-radix v1.0.0 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
|
||||
github.com/aws/aws-sdk-go v1.43.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.13.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.13.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.8.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.10.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.2.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ecr v1.14.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.11.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.7.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.9.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.14.0 // indirect
|
||||
github.com/aws/smithy-go v1.10.0 // indirect
|
||||
github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20220216180153-3d7835abdf40 // indirect
|
||||
github.com/benbjohnson/clock v1.1.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bgentry/speakeasy v0.1.0 // indirect
|
||||
github.com/blang/semver v3.5.1+incompatible // indirect
|
||||
github.com/cenkalti/backoff/v3 v3.2.2 // indirect
|
||||
github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/chrismellard/docker-credential-acr-env v0.0.0-20220119192733-fe33c00cee21 // indirect
|
||||
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490 // indirect
|
||||
github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be // indirect
|
||||
github.com/coreos/go-oidc/v3 v3.1.0 // indirect
|
||||
github.com/coreos/go-semver v0.3.0 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
|
||||
github.com/cyberphone/json-canonicalization v0.0.0-20210823021906-dc406ceaf94b // indirect
|
||||
github.com/dimchansky/utfbom v1.1.1 // indirect
|
||||
github.com/envoyproxy/go-control-plane v0.10.1 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v0.6.2 // indirect
|
||||
github.com/fatih/color v1.13.0 // indirect
|
||||
github.com/form3tech-oss/jwt-go v3.2.5+incompatible // indirect
|
||||
github.com/fullstorydev/grpcurl v1.8.2 // indirect
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
github.com/go-chi/chi v4.1.2+incompatible // indirect
|
||||
github.com/go-logr/logr v1.2.2 // indirect
|
||||
github.com/go-openapi/analysis v0.21.2 // indirect
|
||||
github.com/go-openapi/errors v0.20.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.19.6 // indirect
|
||||
github.com/go-openapi/loads v0.21.1 // indirect
|
||||
github.com/go-openapi/runtime v0.23.1 // indirect
|
||||
github.com/go-openapi/spec v0.20.4 // indirect
|
||||
github.com/go-openapi/strfmt v0.21.2 // indirect
|
||||
github.com/go-openapi/swag v0.21.1 // indirect
|
||||
github.com/go-openapi/validate v0.21.0 // indirect
|
||||
github.com/go-piv/piv-go v1.9.0 // indirect
|
||||
github.com/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.10.0 // indirect
|
||||
github.com/go-stack/stack v1.8.1 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.2.0 // indirect
|
||||
github.com/golang/glog v1.0.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/mock v1.6.0 // indirect
|
||||
github.com/google/btree v1.0.1 // indirect
|
||||
github.com/google/certificate-transparency-go v1.1.2 // indirect
|
||||
github.com/google/go-github/v42 v42.0.0 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/trillian v1.4.0 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.1.1 // indirect
|
||||
github.com/googleapis/gnostic v0.5.5 // indirect
|
||||
github.com/gorilla/websocket v1.4.2 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-hclog v1.1.0 // indirect
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||
github.com/hashicorp/go-plugin v1.4.3 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.0 // indirect
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
|
||||
github.com/hashicorp/go-secure-stdlib/mlock v0.1.2 // indirect
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.2 // indirect
|
||||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
|
||||
github.com/hashicorp/go-sockaddr v1.0.2 // indirect
|
||||
github.com/hashicorp/go-uuid v1.0.2 // indirect
|
||||
github.com/hashicorp/go-version v1.4.0 // indirect
|
||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||
github.com/hashicorp/vault/api v1.4.1 // indirect
|
||||
github.com/hashicorp/vault/sdk v0.4.1 // indirect
|
||||
github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 // indirect
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
github.com/jedisct1/go-minisign v0.0.0-20210703085342-c1f07ee84431 // indirect
|
||||
github.com/jhump/protoreflect v1.9.0 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/jonboulle/clockwork v0.2.2 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
|
||||
github.com/miekg/pkcs11 v1.1.1 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/oklog/run v1.1.0 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||
github.com/pierrec/lz4 v2.6.1+incompatible // indirect
|
||||
github.com/prometheus/client_golang v1.11.0 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.32.1 // indirect
|
||||
github.com/prometheus/procfs v0.7.3 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/ryanuber/go-glob v1.0.0 // indirect
|
||||
github.com/sassoftware/relic v0.0.0-20210427151427-dfb082b79b74 // indirect
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.3.1 // indirect
|
||||
github.com/segmentio/ksuid v1.0.4 // indirect
|
||||
github.com/shibumi/go-pathspec v1.3.0 // indirect
|
||||
github.com/sigstore/fulcio v0.1.2-0.20220114150912-86a2036f9bc7 // indirect
|
||||
github.com/sigstore/rekor v0.4.1-0.20220114213500-23f583409af3 // indirect
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect
|
||||
github.com/soheilhy/cmux v0.1.5 // indirect
|
||||
github.com/spiffe/go-spiffe/v2 v2.0.0-beta.12 // indirect
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect
|
||||
github.com/tent/canonical-json-go v0.0.0-20130607151641-96e4ba3a7613 // indirect
|
||||
github.com/thales-e-security/pool v0.0.2 // indirect
|
||||
github.com/theupdateframework/go-tuf v0.0.0-20220211205608-f0c3294f63b9 // indirect
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect
|
||||
github.com/urfave/cli v1.22.5 // indirect
|
||||
github.com/xanzy/go-gitlab v0.56.0 // indirect
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
|
||||
github.com/zeebo/errs v1.2.2 // indirect
|
||||
go.etcd.io/bbolt v1.3.6 // indirect
|
||||
go.etcd.io/etcd/api/v3 v3.5.1 // indirect
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.1 // indirect
|
||||
go.etcd.io/etcd/client/v2 v2.305.1 // indirect
|
||||
go.etcd.io/etcd/client/v3 v3.5.0 // indirect
|
||||
go.etcd.io/etcd/etcdctl/v3 v3.5.0 // indirect
|
||||
go.etcd.io/etcd/etcdutl/v3 v3.5.0 // indirect
|
||||
go.etcd.io/etcd/pkg/v3 v3.5.0 // indirect
|
||||
go.etcd.io/etcd/raft/v3 v3.5.0 // indirect
|
||||
go.etcd.io/etcd/server/v3 v3.5.0 // indirect
|
||||
go.etcd.io/etcd/tests/v3 v3.5.0 // indirect
|
||||
go.etcd.io/etcd/v3 v3.5.0 // indirect
|
||||
go.mongodb.org/mongo-driver v1.8.3 // indirect
|
||||
go.opencensus.io v0.23.0 // indirect
|
||||
go.opentelemetry.io/contrib v1.3.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0 // indirect
|
||||
go.opentelemetry.io/otel v0.20.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp v0.20.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v0.20.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v0.20.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/export/metric v0.20.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v0.20.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v0.20.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v0.12.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.7.0 // indirect
|
||||
go.uber.org/zap v1.21.0 // indirect
|
||||
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect
|
||||
golang.org/x/tools v0.1.8 // indirect
|
||||
google.golang.org/api v0.70.0 // indirect
|
||||
google.golang.org/protobuf v1.27.1 // indirect
|
||||
gopkg.in/cheggaaa/pb.v1 v1.0.28 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||
k8s.io/api v0.23.3 // indirect
|
||||
k8s.io/apimachinery v0.23.3 // indirect
|
||||
k8s.io/client-go v0.23.3 // indirect
|
||||
k8s.io/klog/v2 v2.40.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20220124234850-424119656bbf // indirect
|
||||
k8s.io/utils v0.0.0-20220127004650-9b3446523e65 // indirect
|
||||
knative.dev/pkg v0.0.0-20220202132633-df430fa0dd96 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect
|
||||
sigs.k8s.io/release-utils v0.4.1-0.20220207182343-6dadf2228617 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect
|
||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.5.1 // indirect
|
||||
// we are hinting brotli to latest due to warning when installing archiver v3:
|
||||
// go: warning: github.com/andybalholm/brotli@v1.0.1: retracted by module author: occasional panics and data corruption
|
||||
github.com/andybalholm/brotli v1.0.4 // indirect
|
||||
github.com/containerd/containerd v1.5.10 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.10.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/docker/cli v20.10.12+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.0+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.6.4 // indirect
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/docker/go-units v0.4.0 // indirect
|
||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.0 // indirect
|
||||
github.com/go-restruct/restruct v1.2.0-alpha // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/go-containerregistry v0.8.1-0.20220209165246-a44adc326839 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/klauspost/compress v1.14.2 // indirect
|
||||
github.com/klauspost/pgzip v1.2.5 // indirect
|
||||
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 // indirect
|
||||
github.com/magiconair/properties v1.8.5 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/nwaples/rardecode v1.1.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.2 // indirect
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/spf13/cast v1.4.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/stretchr/objx v0.3.0 // indirect
|
||||
github.com/subosito/gotenv v1.2.0 // indirect
|
||||
github.com/ulikunitz/xz v0.5.10 // indirect
|
||||
github.com/vbatts/tar-split v0.11.2 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220213190939-1e6e3497d506 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
|
||||
golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c // indirect
|
||||
google.golang.org/grpc v1.44.0 // indirect
|
||||
gopkg.in/ini.v1 v1.66.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
)
|
||||
|
||||
+25
-7
@@ -6,6 +6,11 @@ PROJECT_NAME="syft"
|
||||
OWNER=anchore
|
||||
REPO="${PROJECT_NAME}"
|
||||
GITHUB_DOWNLOAD_PREFIX=https://github.com/${OWNER}/${REPO}/releases/download
|
||||
INSTALL_SH_BASE_URL=https://raw.githubusercontent.com/${OWNER}/${PROJECT_NAME}
|
||||
PROGRAM_ARGS=$@
|
||||
|
||||
# do not change the name of this parameter (this must always be backwards compatible)
|
||||
DOWNLOAD_TAG_INSTALL_SCRIPT=${DOWNLOAD_TAG_INSTALL_SCRIPT:-true}
|
||||
|
||||
#
|
||||
# usage [script-name]
|
||||
@@ -607,7 +612,10 @@ install_asset() (
|
||||
main() (
|
||||
# parse arguments
|
||||
|
||||
# note: never change default install directory (this must always be backwards compatible)
|
||||
install_dir=${install_dir:-./bin}
|
||||
|
||||
# note: never change the program flags or arguments (this must always be backwards compatible)
|
||||
while getopts "b:dh?x" arg; do
|
||||
case "$arg" in
|
||||
b) install_dir="$OPTARG" ;;
|
||||
@@ -629,10 +637,10 @@ main() (
|
||||
tag=$1
|
||||
|
||||
if [ -z "${tag}" ]; then
|
||||
log_info "checking github for the current release tag"
|
||||
log_debug "checking github for the current release tag"
|
||||
tag=""
|
||||
else
|
||||
log_info "checking github for release tag='${tag}'"
|
||||
log_debug "checking github for release tag='${tag}'"
|
||||
fi
|
||||
set -u
|
||||
|
||||
@@ -644,20 +652,30 @@ main() (
|
||||
return 1
|
||||
fi
|
||||
|
||||
version=$(tag_to_version "${tag}")
|
||||
|
||||
download_dir=$(mktemp -d)
|
||||
trap 'rm -rf -- "$download_dir"' EXIT
|
||||
|
||||
# run the application
|
||||
|
||||
version=$(tag_to_version "${tag}")
|
||||
os=$(uname_os)
|
||||
arch=$(uname_arch)
|
||||
format=$(get_format_name "${os}" "${arch}" "tar.gz")
|
||||
binary=$(get_binary_name "${os}" "${arch}" "${PROJECT_NAME}")
|
||||
download_url="${GITHUB_DOWNLOAD_PREFIX}/${tag}"
|
||||
|
||||
# we always use the install.sh script that is associated with the tagged release. Why? the latest install.sh is not
|
||||
# guaranteed to be able to install every version of the application. We use the DOWNLOAD_TAG_INSTALL_SCRIPT env var
|
||||
# to indicate if we should continue processing with the existing script or to download the script from the given tag.
|
||||
if [ "${DOWNLOAD_TAG_INSTALL_SCRIPT}" = "true" ]; then
|
||||
export DOWNLOAD_TAG_INSTALL_SCRIPT=false
|
||||
log_info "fetching release script for tag='${tag}'"
|
||||
http_copy "${INSTALL_SH_BASE_URL}/${tag}/install.sh" "" | sh -s -- ${PROGRAM_ARGS}
|
||||
exit $?
|
||||
fi
|
||||
|
||||
log_info "using release tag='${tag}' version='${version}' os='${os}' arch='${arch}'"
|
||||
|
||||
download_dir=$(mktemp -d)
|
||||
trap 'rm -rf -- "$download_dir"' EXIT
|
||||
|
||||
log_debug "downloading files into ${download_dir}"
|
||||
|
||||
download_and_install_asset "${download_url}" "${download_dir}" "${install_dir}" "${PROJECT_NAME}" "${os}" "${arch}" "${version}" "${format}" "${binary}"
|
||||
|
||||
@@ -56,7 +56,7 @@ func (c *Client) Import(ctx context.Context, cfg ImportConfig) error {
|
||||
authedCtx := c.newRequestContext(ctxWithTimeout)
|
||||
|
||||
stage.Current = "starting session"
|
||||
startOperation, _, err := c.client.ImportsApi.CreateOperation(authedCtx)
|
||||
startOperation, createResponse, err := c.client.ImportsApi.CreateOperation(authedCtx)
|
||||
if err != nil {
|
||||
var detail = "no details given"
|
||||
var openAPIErr external.GenericOpenAPIError
|
||||
@@ -65,6 +65,8 @@ func (c *Client) Import(ctx context.Context, cfg ImportConfig) error {
|
||||
}
|
||||
return fmt.Errorf("unable to start import session: %w: %s", err, detail)
|
||||
}
|
||||
defer createResponse.Body.Close()
|
||||
|
||||
prog.N++
|
||||
sessionID := startOperation.Uuid
|
||||
|
||||
@@ -98,7 +100,7 @@ func (c *Client) Import(ctx context.Context, cfg ImportConfig) error {
|
||||
Force: optional.NewBool(cfg.OverwriteExistingUpload),
|
||||
}
|
||||
|
||||
_, _, err = c.client.ImagesApi.AddImage(authedCtx, imageModel, &opts)
|
||||
_, addResponse, err := c.client.ImagesApi.AddImage(authedCtx, imageModel, &opts)
|
||||
if err != nil {
|
||||
var detail = "no details given"
|
||||
var openAPIErr external.GenericOpenAPIError
|
||||
@@ -107,6 +109,8 @@ func (c *Client) Import(ctx context.Context, cfg ImportConfig) error {
|
||||
}
|
||||
return fmt.Errorf("unable to complete import session=%q: %w: %s", sessionID, err, detail)
|
||||
}
|
||||
defer addResponse.Body.Close()
|
||||
|
||||
prog.N++
|
||||
|
||||
stage.Current = ""
|
||||
|
||||
@@ -56,14 +56,14 @@ func sbomFixture() sbom.SBOM {
|
||||
Name: "name",
|
||||
Version: "version",
|
||||
FoundBy: "foundBy",
|
||||
Locations: []source.Location{
|
||||
{
|
||||
Locations: source.NewLocationSet(
|
||||
source.Location{
|
||||
Coordinates: source.Coordinates{
|
||||
RealPath: "path",
|
||||
FileSystemID: "layerID",
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
Licenses: []string{"license"},
|
||||
Language: pkg.Python,
|
||||
Type: pkg.PythonPkg,
|
||||
|
||||
@@ -30,7 +30,7 @@ type parser interface {
|
||||
// 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 format to use for output
|
||||
Outputs []string `yaml:"output" json:"output" mapstructure:"output"` // -o, the format to use for output
|
||||
File string `yaml:"file" json:"file" mapstructure:"file"` // --file, the file to write report output to
|
||||
Quiet bool `yaml:"quiet" json:"quiet" mapstructure:"quiet"` // -q, indicates to not show any status output to stderr (ETUI or logging UI)
|
||||
CheckForAppUpdate bool `yaml:"check-for-app-update" json:"check-for-app-update" mapstructure:"check-for-app-update"` // whether to check for an application update on start up or not
|
||||
@@ -45,6 +45,8 @@ type Application struct {
|
||||
Secrets secrets `yaml:"secrets" json:"secrets" mapstructure:"secrets"`
|
||||
Registry registry `yaml:"registry" json:"registry" mapstructure:"registry"`
|
||||
Exclusions []string `yaml:"exclude" json:"exclude" mapstructure:"exclude"`
|
||||
Attest attest `yaml:"attest" json:"attest" mapstructure:"attest"`
|
||||
Platform string `yaml:"platform" json:"platform" mapstructure:"platform"`
|
||||
}
|
||||
|
||||
// PowerUserCatalogerEnabledDefault switches all catalogers to be enabled when running power-user command
|
||||
@@ -102,6 +104,7 @@ func (cfg *Application) parseConfigValues() error {
|
||||
for _, optionFn := range []func() error{
|
||||
cfg.parseUploadOptions,
|
||||
cfg.parseLogLevelOption,
|
||||
cfg.parseFile,
|
||||
} {
|
||||
if err := optionFn(); err != nil {
|
||||
return err
|
||||
@@ -124,6 +127,17 @@ func (cfg *Application) parseConfigValues() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg *Application) parseFile() error {
|
||||
if cfg.File != "" {
|
||||
expandedPath, err := homedir.Expand(cfg.File)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to expand file path=%q: %w", cfg.File, err)
|
||||
}
|
||||
cfg.File = expandedPath
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg *Application) parseUploadOptions() error {
|
||||
if cfg.Anchore.Host == "" && cfg.Anchore.Dockerfile != "" {
|
||||
return fmt.Errorf("cannot provide dockerfile option without enabling upload")
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/docker/docker/pkg/homedir"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestApplication_parseFile(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config Application
|
||||
expected string
|
||||
wantErr require.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "expand home dir",
|
||||
config: Application{
|
||||
File: "~/place.txt",
|
||||
},
|
||||
expected: filepath.Join(homedir.Get(), "place.txt"),
|
||||
},
|
||||
{
|
||||
name: "passthrough other paths",
|
||||
config: Application{
|
||||
File: "/other/place.txt",
|
||||
},
|
||||
expected: "/other/place.txt",
|
||||
},
|
||||
{
|
||||
name: "no path",
|
||||
config: Application{
|
||||
File: "",
|
||||
},
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := tt.config
|
||||
|
||||
if tt.wantErr == nil {
|
||||
tt.wantErr = require.NoError
|
||||
}
|
||||
|
||||
tt.wantErr(t, cfg.parseFile())
|
||||
assert.Equal(t, tt.expected, cfg.File)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type attest struct {
|
||||
Key string `yaml:"key" json:"key" mapstructure:"key"` // same as --key, file path to the private key
|
||||
// IMPORTANT: do not show the password in any YAML/JSON output (sensitive information)
|
||||
Password string `yaml:"-" json:"-" mapstructure:"password"` // password for the private key
|
||||
}
|
||||
|
||||
func (cfg *attest) parseConfigValues() error {
|
||||
if cfg.Key != "" {
|
||||
expandedPath, err := homedir.Expand(cfg.Key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to expand key path=%q: %w", cfg.Key, err)
|
||||
}
|
||||
cfg.Key = expandedPath
|
||||
}
|
||||
|
||||
if cfg.Password == "" {
|
||||
// we allow for configuration via syft config/env vars and additionally interop with known cosign config env vars
|
||||
if pw, ok := os.LookupEnv("COSIGN_PASSWORD"); ok {
|
||||
cfg.Password = pw
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg attest) loadDefaultValues(v *viper.Viper) {
|
||||
v.SetDefault("attest.password", "")
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
@@ -13,6 +16,18 @@ type logging struct {
|
||||
FileLocation string `yaml:"file" json:"file-location" mapstructure:"file"` // the file path to write logs to
|
||||
}
|
||||
|
||||
func (cfg *logging) parseConfigValues() error {
|
||||
if cfg.FileLocation != "" {
|
||||
expandedPath, err := homedir.Expand(cfg.FileLocation)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to expand log file path=%q: %w", cfg.FileLocation, err)
|
||||
}
|
||||
cfg.FileLocation = expandedPath
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg logging) loadDefaultValues(v *viper.Viper) {
|
||||
v.SetDefault("log.structured", false)
|
||||
v.SetDefault("log.file", "")
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@ const (
|
||||
|
||||
// JSONSchemaVersion is the current schema version output by the JSON encoder
|
||||
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
|
||||
JSONSchemaVersion = "3.0.1"
|
||||
JSONSchemaVersion = "3.2.2"
|
||||
)
|
||||
|
||||
@@ -16,12 +16,13 @@ func CloseAndLogError(closer io.Closer, location string) {
|
||||
}
|
||||
|
||||
type ErrPath struct {
|
||||
Path string
|
||||
Err error
|
||||
Context string
|
||||
Path string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e ErrPath) Error() string {
|
||||
return fmt.Sprintf("unable to observe contents of %+v: %v", e.Path, e.Err)
|
||||
return fmt.Sprintf("%s unable to observe contents of %+v: %v", e.Context, e.Path, e.Err)
|
||||
}
|
||||
|
||||
func IsErrPath(err error) bool {
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func Author(p pkg.Package) string {
|
||||
func encodeAuthor(p pkg.Package) string {
|
||||
if hasMetadata(p) {
|
||||
switch metadata := p.Metadata.(type) {
|
||||
case pkg.NpmPackageJSONMetadata:
|
||||
@@ -30,3 +30,18 @@ func Author(p pkg.Package) string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func decodeAuthor(author string, metadata interface{}) {
|
||||
switch meta := metadata.(type) {
|
||||
case *pkg.NpmPackageJSONMetadata:
|
||||
meta.Author = author
|
||||
case *pkg.PythonPackageMetadata:
|
||||
parts := strings.SplitN(author, " <", 2)
|
||||
meta.Author = parts[0]
|
||||
if len(parts) > 1 {
|
||||
meta.AuthorEmail = strings.TrimSuffix(parts[1], ">")
|
||||
}
|
||||
case *pkg.GemMetadata:
|
||||
meta.Authors = strings.Split(author, ",")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Author(t *testing.T) {
|
||||
func Test_encodeAuthor(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input pkg.Package
|
||||
@@ -81,7 +81,7 @@ func Test_Author(t *testing.T) {
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, Author(test.input))
|
||||
assert.Equal(t, test.expected, encodeAuthor(test.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,122 @@
|
||||
package cyclonedxhelpers
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
|
||||
"github.com/anchore/packageurl-go"
|
||||
"github.com/anchore/syft/internal/formats/common"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
func Component(p pkg.Package) cyclonedx.Component {
|
||||
func encodeComponent(p pkg.Package) cyclonedx.Component {
|
||||
props := encodeProperties(p, "syft:package")
|
||||
props = append(props, encodeCPEs(p)...)
|
||||
locations := p.Locations.ToSlice()
|
||||
if len(locations) > 0 {
|
||||
props = append(props, encodeProperties(locations, "syft:location")...)
|
||||
}
|
||||
if hasMetadata(p) {
|
||||
props = append(props, encodeProperties(p.Metadata, "syft:metadata")...)
|
||||
}
|
||||
|
||||
var properties *[]cyclonedx.Property
|
||||
if len(props) > 0 {
|
||||
properties = &props
|
||||
}
|
||||
|
||||
return cyclonedx.Component{
|
||||
Type: cyclonedx.ComponentTypeLibrary,
|
||||
Name: p.Name,
|
||||
Group: Group(p),
|
||||
Group: encodeGroup(p),
|
||||
Version: p.Version,
|
||||
PackageURL: p.PURL,
|
||||
Licenses: Licenses(p),
|
||||
CPE: CPE(p),
|
||||
Author: Author(p),
|
||||
Publisher: Publisher(p),
|
||||
Description: Description(p),
|
||||
ExternalReferences: ExternalReferences(p),
|
||||
Properties: Properties(p),
|
||||
Licenses: encodeLicenses(p),
|
||||
CPE: encodeSingleCPE(p),
|
||||
Author: encodeAuthor(p),
|
||||
Publisher: encodePublisher(p),
|
||||
Description: encodeDescription(p),
|
||||
ExternalReferences: encodeExternalReferences(p),
|
||||
Properties: properties,
|
||||
BOMRef: deriveBomRef(p),
|
||||
}
|
||||
}
|
||||
|
||||
func deriveBomRef(p pkg.Package) string {
|
||||
// try and parse the PURL if possible and append syft id to it, to make
|
||||
// the purl unique in the BOM.
|
||||
// TODO: In the future we may want to dedupe by PURL and combine components with
|
||||
// the same PURL while preserving their unique metadata.
|
||||
if parsedPURL, err := packageurl.FromString(p.PURL); err == nil {
|
||||
parsedPURL.Qualifiers = append(parsedPURL.Qualifiers, packageurl.Qualifier{Key: "syft-id", Value: string(p.ID())})
|
||||
return parsedPURL.ToString()
|
||||
}
|
||||
// fallback is to use strictly the ID if there is no valid pURL
|
||||
return string(p.ID())
|
||||
}
|
||||
|
||||
func hasMetadata(p pkg.Package) bool {
|
||||
return p.Metadata != nil
|
||||
}
|
||||
|
||||
func decodeComponent(c *cyclonedx.Component) *pkg.Package {
|
||||
values := map[string]string{}
|
||||
if c.Properties != nil {
|
||||
for _, p := range *c.Properties {
|
||||
values[p.Name] = p.Value
|
||||
}
|
||||
}
|
||||
|
||||
p := &pkg.Package{
|
||||
Name: c.Name,
|
||||
Version: c.Version,
|
||||
Locations: decodeLocations(values),
|
||||
Licenses: decodeLicenses(c),
|
||||
CPEs: decodeCPEs(c),
|
||||
PURL: c.PackageURL,
|
||||
}
|
||||
|
||||
common.DecodeInto(p, values, "syft:package", CycloneDXFields)
|
||||
|
||||
p.Metadata = decodePackageMetadata(values, c, p.MetadataType)
|
||||
|
||||
if p.Type == "" {
|
||||
p.Type = pkg.TypeFromPURL(p.PURL)
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func decodeLocations(vals map[string]string) source.LocationSet {
|
||||
v := common.Decode(reflect.TypeOf([]source.Location{}), vals, "syft:location", CycloneDXFields)
|
||||
out, ok := v.([]source.Location)
|
||||
if !ok {
|
||||
out = nil
|
||||
}
|
||||
return source.NewLocationSet(out...)
|
||||
}
|
||||
|
||||
func decodePackageMetadata(vals map[string]string, c *cyclonedx.Component, typ pkg.MetadataType) interface{} {
|
||||
if typ != "" && c.Properties != nil {
|
||||
metaTyp, ok := pkg.MetadataTypeByName[typ]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
metaPtrTyp := reflect.PtrTo(metaTyp)
|
||||
metaPtr := common.Decode(metaPtrTyp, vals, "syft:metadata", CycloneDXFields)
|
||||
|
||||
// Map all explicit metadata properties
|
||||
decodeAuthor(c.Author, metaPtr)
|
||||
decodeGroup(c.Group, metaPtr)
|
||||
decodePublisher(c.Publisher, metaPtr)
|
||||
decodeDescription(c.Description, metaPtr)
|
||||
decodeExternalReferences(c, metaPtr)
|
||||
|
||||
// return the actual interface{} -> struct ... not interface{} -> *struct
|
||||
return common.PtrToStruct(metaPtr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
package cyclonedxhelpers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
func Test_encodeComponentProperties(t *testing.T) {
|
||||
epoch := 2
|
||||
tests := []struct {
|
||||
name string
|
||||
input pkg.Package
|
||||
expected *[]cyclonedx.Property
|
||||
}{
|
||||
{
|
||||
name: "no metadata",
|
||||
input: pkg.Package{},
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "from apk",
|
||||
input: pkg.Package{
|
||||
FoundBy: "cataloger",
|
||||
Locations: source.NewLocationSet(
|
||||
source.Location{Coordinates: source.Coordinates{RealPath: "test"}},
|
||||
),
|
||||
Metadata: pkg.ApkMetadata{
|
||||
Package: "libc-utils",
|
||||
OriginPackage: "libc-dev",
|
||||
Maintainer: "Natanael Copa <ncopa@alpinelinux.org>",
|
||||
Version: "0.7.2-r0",
|
||||
License: "BSD",
|
||||
Architecture: "x86_64",
|
||||
URL: "http://alpinelinux.org",
|
||||
Description: "Meta package to pull in correct libc",
|
||||
Size: 0,
|
||||
InstalledSize: 4096,
|
||||
PullDependencies: "musl-utils",
|
||||
PullChecksum: "Q1p78yvTLG094tHE1+dToJGbmYzQE=",
|
||||
GitCommitOfAport: "97b1c2842faa3bfa30f5811ffbf16d5ff9f1a479",
|
||||
Files: []pkg.ApkFileRecord{},
|
||||
},
|
||||
},
|
||||
expected: &[]cyclonedx.Property{
|
||||
{Name: "syft:package:foundBy", Value: "cataloger"},
|
||||
{Name: "syft:location:0:path", Value: "test"},
|
||||
{Name: "syft:metadata:gitCommitOfApkPort", Value: "97b1c2842faa3bfa30f5811ffbf16d5ff9f1a479"},
|
||||
{Name: "syft:metadata:installedSize", Value: "4096"},
|
||||
{Name: "syft:metadata:originPackage", Value: "libc-dev"},
|
||||
{Name: "syft:metadata:pullChecksum", Value: "Q1p78yvTLG094tHE1+dToJGbmYzQE="},
|
||||
{Name: "syft:metadata:pullDependencies", Value: "musl-utils"},
|
||||
{Name: "syft:metadata:size", Value: "0"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "from dpkg",
|
||||
input: pkg.Package{
|
||||
MetadataType: pkg.DpkgMetadataType,
|
||||
Metadata: pkg.DpkgMetadata{
|
||||
Package: "tzdata",
|
||||
Version: "2020a-0+deb10u1",
|
||||
Source: "tzdata-dev",
|
||||
SourceVersion: "1.0",
|
||||
Architecture: "all",
|
||||
InstalledSize: 3036,
|
||||
Maintainer: "GNU Libc Maintainers <debian-glibc@lists.debian.org>",
|
||||
Files: []pkg.DpkgFileRecord{},
|
||||
},
|
||||
},
|
||||
expected: &[]cyclonedx.Property{
|
||||
{Name: "syft:package:metadataType", Value: "DpkgMetadata"},
|
||||
{Name: "syft:metadata:installedSize", Value: "3036"},
|
||||
{Name: "syft:metadata:source", Value: "tzdata-dev"},
|
||||
{Name: "syft:metadata:sourceVersion", Value: "1.0"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "from go bin",
|
||||
input: pkg.Package{
|
||||
Name: "golang.org/x/net",
|
||||
Version: "v0.0.0-20211006190231-62292e806868",
|
||||
Language: pkg.Go,
|
||||
Type: pkg.GoModulePkg,
|
||||
MetadataType: pkg.GolangBinMetadataType,
|
||||
Metadata: pkg.GolangBinMetadata{
|
||||
GoCompiledVersion: "1.17",
|
||||
Architecture: "amd64",
|
||||
H1Digest: "h1:KlOXYy8wQWTUJYFgkUI40Lzr06ofg5IRXUK5C7qZt1k=",
|
||||
},
|
||||
},
|
||||
expected: &[]cyclonedx.Property{
|
||||
{Name: "syft:package:language", Value: pkg.Go.String()},
|
||||
{Name: "syft:package:metadataType", Value: "GolangBinMetadata"},
|
||||
{Name: "syft:package:type", Value: "go-module"},
|
||||
{Name: "syft:metadata:architecture", Value: "amd64"},
|
||||
{Name: "syft:metadata:goCompiledVersion", Value: "1.17"},
|
||||
{Name: "syft:metadata:h1Digest", Value: "h1:KlOXYy8wQWTUJYFgkUI40Lzr06ofg5IRXUK5C7qZt1k="},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "from rpm",
|
||||
input: pkg.Package{
|
||||
Name: "dive",
|
||||
Version: "0.9.2-1",
|
||||
Type: pkg.RpmPkg,
|
||||
MetadataType: pkg.RpmdbMetadataType,
|
||||
Metadata: pkg.RpmdbMetadata{
|
||||
Name: "dive",
|
||||
Epoch: &epoch,
|
||||
Arch: "x86_64",
|
||||
Release: "1",
|
||||
Version: "0.9.2",
|
||||
SourceRpm: "dive-0.9.2-1.src.rpm",
|
||||
Size: 12406784,
|
||||
License: "MIT",
|
||||
Vendor: "",
|
||||
Files: []pkg.RpmdbFileRecord{},
|
||||
},
|
||||
},
|
||||
expected: &[]cyclonedx.Property{
|
||||
{Name: "syft:package:metadataType", Value: "RpmdbMetadata"},
|
||||
{Name: "syft:package:type", Value: "rpm"},
|
||||
{Name: "syft:metadata:epoch", Value: "2"},
|
||||
{Name: "syft:metadata:release", Value: "1"},
|
||||
{Name: "syft:metadata:size", Value: "12406784"},
|
||||
{Name: "syft:metadata:sourceRpm", Value: "dive-0.9.2-1.src.rpm"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
c := encodeComponent(test.input)
|
||||
assert.Equal(t, test.expected, c.Properties)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_deriveBomRef(t *testing.T) {
|
||||
pkgWithPurl := pkg.Package{
|
||||
Name: "django",
|
||||
Version: "1.11.1",
|
||||
PURL: "pkg:pypi/django@1.11.1",
|
||||
}
|
||||
pkgWithPurl.SetID()
|
||||
|
||||
pkgWithOutPurl := pkg.Package{
|
||||
Name: "django",
|
||||
Version: "1.11.1",
|
||||
PURL: "",
|
||||
}
|
||||
pkgWithOutPurl.SetID()
|
||||
|
||||
pkgWithBadPurl := pkg.Package{
|
||||
Name: "django",
|
||||
Version: "1.11.1",
|
||||
PURL: "pkg:pyjango@1.11.1",
|
||||
}
|
||||
pkgWithBadPurl.SetID()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
pkg pkg.Package
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "use pURL-id hybrid",
|
||||
pkg: pkgWithPurl,
|
||||
want: fmt.Sprintf("pkg:pypi/django@1.11.1?syft-id=%s", pkgWithPurl.ID()),
|
||||
},
|
||||
{
|
||||
name: "fallback to ID when pURL is invalid",
|
||||
pkg: pkgWithBadPurl,
|
||||
want: string(pkgWithBadPurl.ID()),
|
||||
},
|
||||
{
|
||||
name: "fallback to ID when pURL is missing",
|
||||
pkg: pkgWithOutPurl,
|
||||
want: string(pkgWithOutPurl.ID()),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.pkg.ID()
|
||||
assert.Equal(t, tt.want, deriveBomRef(tt.pkg))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,13 @@
|
||||
package cyclonedxhelpers
|
||||
|
||||
import "github.com/anchore/syft/syft/pkg"
|
||||
import (
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
|
||||
func CPE(p pkg.Package) string {
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func encodeSingleCPE(p pkg.Package) string {
|
||||
// Since the CPEs in a package are sorted by specificity
|
||||
// we can extract the first CPE as the one to output in cyclonedx
|
||||
if len(p.CPEs) > 0 {
|
||||
@@ -10,3 +15,43 @@ func CPE(p pkg.Package) string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func encodeCPEs(p pkg.Package) (out []cyclonedx.Property) {
|
||||
for i, c := range p.CPEs {
|
||||
// first CPE is "most specific" and already encoded as the component CPE
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
out = append(out, cyclonedx.Property{
|
||||
Name: "syft:cpe23",
|
||||
Value: pkg.CPEString(c),
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func decodeCPEs(c *cyclonedx.Component) (out []pkg.CPE) {
|
||||
if c.CPE != "" {
|
||||
cp, err := pkg.NewCPE(c.CPE)
|
||||
if err != nil {
|
||||
log.Warnf("invalid CPE: %s", c.CPE)
|
||||
} else {
|
||||
out = append(out, cp)
|
||||
}
|
||||
}
|
||||
|
||||
if c.Properties != nil {
|
||||
for _, p := range *c.Properties {
|
||||
if p.Name == "syft:cpe23" {
|
||||
cp, err := pkg.NewCPE(p.Value)
|
||||
if err != nil {
|
||||
log.Warnf("invalid CPE: %s", p.Value)
|
||||
} else {
|
||||
out = append(out, cp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -3,11 +3,12 @@ package cyclonedxhelpers
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func Test_CPE(t *testing.T) {
|
||||
func Test_encodeCPE(t *testing.T) {
|
||||
testCPE := pkg.MustCPE("cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*")
|
||||
testCPE2 := pkg.MustCPE("cpe:2.3:a:name:name2:3.2:*:*:*:*:*:*:*")
|
||||
tests := []struct {
|
||||
@@ -51,7 +52,7 @@ func Test_CPE(t *testing.T) {
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, CPE(test.input))
|
||||
assert.Equal(t, test.expected, encodeSingleCPE(test.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
package cyclonedxhelpers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
|
||||
"github.com/anchore/syft/internal/formats/common"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/linux"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
func GetValidator(format cyclonedx.BOMFileFormat) sbom.Validator {
|
||||
return func(reader io.Reader) error {
|
||||
bom := &cyclonedx.BOM{}
|
||||
err := cyclonedx.NewBOMDecoder(reader, format).Decode(bom)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// random JSON does not necessarily cause an error (e.g. SPDX)
|
||||
if (cyclonedx.BOM{} == *bom) {
|
||||
return fmt.Errorf("not a valid CycloneDX document")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetDecoder(format cyclonedx.BOMFileFormat) sbom.Decoder {
|
||||
return func(reader io.Reader) (*sbom.SBOM, error) {
|
||||
bom := &cyclonedx.BOM{}
|
||||
err := cyclonedx.NewBOMDecoder(reader, format).Decode(bom)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s, err := toSyftModel(bom)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
}
|
||||
|
||||
func toSyftModel(bom *cyclonedx.BOM) (*sbom.SBOM, error) {
|
||||
meta := source.Metadata{}
|
||||
if bom.Metadata != nil && bom.Metadata.Component != nil {
|
||||
meta = decodeMetadata(bom.Metadata.Component)
|
||||
}
|
||||
s := &sbom.SBOM{
|
||||
Artifacts: sbom.Artifacts{
|
||||
PackageCatalog: pkg.NewCatalog(),
|
||||
LinuxDistribution: linuxReleaseFromComponents(*bom.Components),
|
||||
},
|
||||
Source: meta,
|
||||
//Descriptor: sbom.Descriptor{},
|
||||
}
|
||||
|
||||
idMap := make(map[string]interface{})
|
||||
|
||||
if err := collectBomPackages(bom, s, idMap); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
collectRelationships(bom, s, idMap)
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func collectBomPackages(bom *cyclonedx.BOM, s *sbom.SBOM, idMap map[string]interface{}) error {
|
||||
if bom.Components == nil {
|
||||
return fmt.Errorf("no components are defined in the CycloneDX BOM")
|
||||
}
|
||||
for i := range *bom.Components {
|
||||
collectPackages(&(*bom.Components)[i], s, idMap)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func collectPackages(component *cyclonedx.Component, s *sbom.SBOM, idMap map[string]interface{}) {
|
||||
switch component.Type {
|
||||
case cyclonedx.ComponentTypeOS:
|
||||
case cyclonedx.ComponentTypeContainer:
|
||||
case cyclonedx.ComponentTypeApplication, cyclonedx.ComponentTypeFramework, cyclonedx.ComponentTypeLibrary:
|
||||
p := decodeComponent(component)
|
||||
idMap[component.BOMRef] = p
|
||||
// TODO there must be a better way than needing to call this manually:
|
||||
p.SetID()
|
||||
s.Artifacts.PackageCatalog.Add(*p)
|
||||
}
|
||||
|
||||
if component.Components != nil {
|
||||
for i := range *component.Components {
|
||||
collectPackages(&(*component.Components)[i], s, idMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func linuxReleaseFromComponents(components []cyclonedx.Component) *linux.Release {
|
||||
for i := range components {
|
||||
component := &components[i]
|
||||
if component.Type == cyclonedx.ComponentTypeOS {
|
||||
return linuxReleaseFromOSComponent(component)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func linuxReleaseFromOSComponent(component *cyclonedx.Component) *linux.Release {
|
||||
if component == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var name string
|
||||
var version string
|
||||
if component.SWID != nil {
|
||||
name = component.SWID.Name
|
||||
version = component.SWID.Version
|
||||
}
|
||||
if name == "" {
|
||||
name = component.Name
|
||||
}
|
||||
if name == "" {
|
||||
name = getPropertyValue(component, "id")
|
||||
}
|
||||
if version == "" {
|
||||
version = component.Version
|
||||
}
|
||||
if version == "" {
|
||||
version = getPropertyValue(component, "versionID")
|
||||
}
|
||||
|
||||
rel := &linux.Release{
|
||||
CPEName: component.CPE,
|
||||
PrettyName: name,
|
||||
Name: name,
|
||||
ID: name,
|
||||
IDLike: []string{name},
|
||||
Version: version,
|
||||
VersionID: version,
|
||||
}
|
||||
if component.ExternalReferences != nil {
|
||||
for _, ref := range *component.ExternalReferences {
|
||||
switch ref.Type {
|
||||
case cyclonedx.ERTypeIssueTracker:
|
||||
rel.BugReportURL = ref.URL
|
||||
case cyclonedx.ERTypeWebsite:
|
||||
rel.HomeURL = ref.URL
|
||||
case cyclonedx.ERTypeOther:
|
||||
switch ref.Comment {
|
||||
case "support":
|
||||
rel.SupportURL = ref.URL
|
||||
case "privacyPolicy":
|
||||
rel.PrivacyPolicyURL = ref.URL
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if component.Properties != nil {
|
||||
values := map[string]string{}
|
||||
for _, p := range *component.Properties {
|
||||
values[p.Name] = p.Value
|
||||
}
|
||||
common.DecodeInto(&rel, values, "syft:distro", CycloneDXFields)
|
||||
}
|
||||
|
||||
return rel
|
||||
}
|
||||
|
||||
func getPropertyValue(component *cyclonedx.Component, name string) string {
|
||||
if component.Properties != nil {
|
||||
for _, p := range *component.Properties {
|
||||
if p.Name == name {
|
||||
return p.Value
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func collectRelationships(bom *cyclonedx.BOM, s *sbom.SBOM, idMap map[string]interface{}) {
|
||||
if bom.Dependencies == nil {
|
||||
return
|
||||
}
|
||||
for _, d := range *bom.Dependencies {
|
||||
from, fromOk := idMap[d.Ref].(artifact.Identifiable)
|
||||
if fromOk {
|
||||
if d.Dependencies == nil {
|
||||
continue
|
||||
}
|
||||
for _, t := range *d.Dependencies {
|
||||
to, toOk := idMap[t.Ref].(artifact.Identifiable)
|
||||
if toOk {
|
||||
s.Relationships = append(s.Relationships, artifact.Relationship{
|
||||
From: from,
|
||||
To: to,
|
||||
Type: artifact.DependencyOfRelationship, // FIXME this information is lost
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func decodeMetadata(component *cyclonedx.Component) source.Metadata {
|
||||
switch component.Type {
|
||||
case cyclonedx.ComponentTypeContainer:
|
||||
return source.Metadata{
|
||||
Scheme: source.ImageScheme,
|
||||
ImageMetadata: source.ImageMetadata{
|
||||
UserInput: component.Name,
|
||||
ID: component.BOMRef,
|
||||
ManifestDigest: component.Version,
|
||||
},
|
||||
}
|
||||
case cyclonedx.ComponentTypeFile:
|
||||
return source.Metadata{
|
||||
Scheme: source.FileScheme, // or source.DirectoryScheme
|
||||
Path: component.Name,
|
||||
ImageMetadata: source.ImageMetadata{
|
||||
UserInput: component.Name,
|
||||
ID: component.BOMRef,
|
||||
ManifestDigest: component.Version,
|
||||
},
|
||||
}
|
||||
}
|
||||
return source.Metadata{}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
package cyclonedxhelpers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_decode(t *testing.T) {
|
||||
type expected struct {
|
||||
os string
|
||||
pkg string
|
||||
ver string
|
||||
relation string
|
||||
purl string
|
||||
cpe string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
input cyclonedx.BOM
|
||||
expected []expected
|
||||
}{
|
||||
{
|
||||
name: "basic mapping from cyclonedx",
|
||||
input: cyclonedx.BOM{
|
||||
Metadata: nil,
|
||||
Components: &[]cyclonedx.Component{
|
||||
{
|
||||
BOMRef: "p1",
|
||||
Type: cyclonedx.ComponentTypeLibrary,
|
||||
Name: "package-1",
|
||||
Version: "1.0.1",
|
||||
Description: "",
|
||||
Hashes: nil,
|
||||
Licenses: &cyclonedx.Licenses{
|
||||
{
|
||||
License: &cyclonedx.License{
|
||||
ID: "MIT",
|
||||
},
|
||||
},
|
||||
},
|
||||
CPE: "cpe:2.3:*:some:package:1:*:*:*:*:*:*:*",
|
||||
PackageURL: "pkg:some/package-1@1.0.1?arch=arm64&upstream=upstream1&distro=alpine-1",
|
||||
ExternalReferences: &[]cyclonedx.ExternalReference{
|
||||
{
|
||||
URL: "",
|
||||
Comment: "",
|
||||
Hashes: nil,
|
||||
Type: "",
|
||||
},
|
||||
},
|
||||
Properties: &[]cyclonedx.Property{
|
||||
{
|
||||
Name: "foundBy",
|
||||
Value: "the-cataloger-1",
|
||||
},
|
||||
{
|
||||
Name: "language",
|
||||
Value: "python",
|
||||
},
|
||||
{
|
||||
Name: "type",
|
||||
Value: "python",
|
||||
},
|
||||
{
|
||||
Name: "metadataType",
|
||||
Value: "PythonPackageMetadata",
|
||||
},
|
||||
{
|
||||
Name: "path",
|
||||
Value: "/some/path/pkg1",
|
||||
},
|
||||
},
|
||||
Components: nil,
|
||||
Evidence: nil,
|
||||
},
|
||||
{
|
||||
BOMRef: "p2",
|
||||
Type: cyclonedx.ComponentTypeLibrary,
|
||||
Name: "package-2",
|
||||
Version: "2.0.2",
|
||||
Hashes: nil,
|
||||
Licenses: &cyclonedx.Licenses{
|
||||
{
|
||||
License: &cyclonedx.License{
|
||||
ID: "MIT",
|
||||
},
|
||||
},
|
||||
},
|
||||
CPE: "cpe:2.3:*:another:package:2:*:*:*:*:*:*:*",
|
||||
PackageURL: "pkg:alpine/alpine-baselayout@3.2.0-r16?arch=x86_64&upstream=alpine-baselayout&distro=alpine-3.14.2",
|
||||
Properties: &[]cyclonedx.Property{
|
||||
|
||||
{
|
||||
Name: "foundBy",
|
||||
Value: "apkdb-cataloger",
|
||||
},
|
||||
{
|
||||
Name: "type",
|
||||
Value: "apk",
|
||||
},
|
||||
{
|
||||
Name: "metadataType",
|
||||
Value: "ApkMetadata",
|
||||
},
|
||||
{
|
||||
Name: "path",
|
||||
Value: "/lib/apk/db/installed",
|
||||
},
|
||||
{
|
||||
Name: "layerID",
|
||||
Value: "sha256:9fb3aa2f8b8023a4bebbf92aa567caf88e38e969ada9f0ac12643b2847391635",
|
||||
},
|
||||
{
|
||||
Name: "originPackage",
|
||||
Value: "zlib",
|
||||
},
|
||||
{
|
||||
Name: "size",
|
||||
Value: "51213",
|
||||
},
|
||||
{
|
||||
Name: "installedSize",
|
||||
Value: "110592",
|
||||
},
|
||||
{
|
||||
Name: "pullDependencies",
|
||||
Value: "so:libc.musl-x86_64.so.1",
|
||||
},
|
||||
{
|
||||
Name: "pullChecksum",
|
||||
Value: "Q1uss4DfpvL16Nw2YUTwmzGBABz3Y=",
|
||||
},
|
||||
{
|
||||
Name: "gitCommitOfApkPort",
|
||||
Value: "d2bfb22c8e8f67ad7d8d02704f35ec4d2a19f9b9",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: cyclonedx.ComponentTypeOS,
|
||||
Name: "debian",
|
||||
Version: "1.2.3",
|
||||
Hashes: nil,
|
||||
Licenses: &cyclonedx.Licenses{
|
||||
{
|
||||
License: &cyclonedx.License{
|
||||
ID: "MIT",
|
||||
},
|
||||
},
|
||||
},
|
||||
Properties: &[]cyclonedx.Property{
|
||||
{
|
||||
Name: "prettyName",
|
||||
Value: "debian",
|
||||
},
|
||||
{
|
||||
Name: "id",
|
||||
Value: "debian",
|
||||
},
|
||||
{
|
||||
Name: "versionID",
|
||||
Value: "1.2.3",
|
||||
},
|
||||
},
|
||||
Components: nil,
|
||||
Evidence: nil,
|
||||
},
|
||||
},
|
||||
Dependencies: &[]cyclonedx.Dependency{
|
||||
{
|
||||
Ref: "p1",
|
||||
Dependencies: &[]cyclonedx.Dependency{
|
||||
{
|
||||
Ref: "p2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []expected{
|
||||
{
|
||||
os: "debian",
|
||||
ver: "1.2.3",
|
||||
},
|
||||
{
|
||||
pkg: "package-1",
|
||||
ver: "1.0.1",
|
||||
cpe: "cpe:2.3:*:some:package:1:*:*:*:*:*:*:*",
|
||||
purl: "pkg:some/package-1@1.0.1?arch=arm64&upstream=upstream1&distro=alpine-1",
|
||||
relation: "package-2",
|
||||
},
|
||||
{
|
||||
pkg: "package-2",
|
||||
ver: "2.0.2",
|
||||
purl: "pkg:alpine/alpine-baselayout@3.2.0-r16?arch=x86_64&upstream=alpine-baselayout&distro=alpine-3.14.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
sbom, err := toSyftModel(&test.input)
|
||||
assert.NoError(t, err)
|
||||
|
||||
test:
|
||||
for _, e := range test.expected {
|
||||
if e.os != "" {
|
||||
assert.Equal(t, e.os, sbom.Artifacts.LinuxDistribution.ID)
|
||||
assert.Equal(t, e.ver, sbom.Artifacts.LinuxDistribution.VersionID)
|
||||
}
|
||||
if e.pkg != "" {
|
||||
for p := range sbom.Artifacts.PackageCatalog.Enumerate() {
|
||||
if e.pkg != p.Name {
|
||||
continue
|
||||
}
|
||||
|
||||
assert.Equal(t, e.ver, p.Version)
|
||||
|
||||
if e.cpe != "" {
|
||||
foundCPE := false
|
||||
for _, c := range p.CPEs {
|
||||
cstr := c.BindToFmtString()
|
||||
if e.cpe == cstr {
|
||||
foundCPE = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundCPE {
|
||||
assert.Fail(t, fmt.Sprintf("CPE not found in package: %s", e.cpe))
|
||||
}
|
||||
}
|
||||
|
||||
if e.purl != "" {
|
||||
assert.Equal(t, e.purl, p.PURL)
|
||||
}
|
||||
|
||||
if e.relation != "" {
|
||||
foundRelation := false
|
||||
for _, r := range sbom.Relationships {
|
||||
p := sbom.Artifacts.PackageCatalog.Package(r.To.ID())
|
||||
if e.relation == p.Name {
|
||||
foundRelation = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundRelation {
|
||||
assert.Fail(t, fmt.Sprintf("relation not found: %s", e.relation))
|
||||
}
|
||||
}
|
||||
continue test
|
||||
}
|
||||
assert.Fail(t, fmt.Sprintf("package should be present: %s", e.pkg))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_missingDataDecode(t *testing.T) {
|
||||
bom := &cyclonedx.BOM{
|
||||
Metadata: nil,
|
||||
Components: &[]cyclonedx.Component{},
|
||||
}
|
||||
|
||||
_, err := toSyftModel(bom)
|
||||
assert.NoError(t, err)
|
||||
|
||||
bom.Metadata = &cyclonedx.Metadata{}
|
||||
|
||||
_, err = toSyftModel(bom)
|
||||
assert.NoError(t, err)
|
||||
|
||||
pkg := decodeComponent(&cyclonedx.Component{
|
||||
Licenses: &cyclonedx.Licenses{
|
||||
{
|
||||
License: nil,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
assert.Len(t, pkg.Licenses, 0)
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package cyclonedxhelpers
|
||||
|
||||
import "github.com/anchore/syft/syft/pkg"
|
||||
|
||||
func Description(p pkg.Package) string {
|
||||
func encodeDescription(p pkg.Package) string {
|
||||
if hasMetadata(p) {
|
||||
switch metadata := p.Metadata.(type) {
|
||||
case pkg.ApkMetadata:
|
||||
@@ -13,3 +13,12 @@ func Description(p pkg.Package) string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func decodeDescription(description string, metadata interface{}) {
|
||||
switch meta := metadata.(type) {
|
||||
case *pkg.ApkMetadata:
|
||||
meta.Description = description
|
||||
case *pkg.NpmPackageJSONMetadata:
|
||||
meta.Description = description
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Description(t *testing.T) {
|
||||
func Test_encodeDescription(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input pkg.Package
|
||||
@@ -50,7 +50,7 @@ func Test_Description(t *testing.T) {
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, Description(test.input))
|
||||
assert.Equal(t, test.expected, encodeDescription(test.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,13 @@ package cyclonedxhelpers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func ExternalReferences(p pkg.Package) *[]cyclonedx.ExternalReference {
|
||||
func encodeExternalReferences(p pkg.Package) *[]cyclonedx.ExternalReference {
|
||||
refs := []cyclonedx.ExternalReference{}
|
||||
if hasMetadata(p) {
|
||||
switch metadata := p.Metadata.(type) {
|
||||
@@ -63,3 +64,51 @@ func ExternalReferences(p pkg.Package) *[]cyclonedx.ExternalReference {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeExternalReferences(c *cyclonedx.Component, metadata interface{}) {
|
||||
if c.ExternalReferences == nil {
|
||||
return
|
||||
}
|
||||
switch meta := metadata.(type) {
|
||||
case *pkg.ApkMetadata:
|
||||
meta.URL = refURL(c, cyclonedx.ERTypeDistribution)
|
||||
case *pkg.CargoPackageMetadata:
|
||||
meta.Source = refURL(c, cyclonedx.ERTypeDistribution)
|
||||
case *pkg.NpmPackageJSONMetadata:
|
||||
meta.URL = refURL(c, cyclonedx.ERTypeDistribution)
|
||||
meta.Homepage = refURL(c, cyclonedx.ERTypeWebsite)
|
||||
case *pkg.GemMetadata:
|
||||
meta.Homepage = refURL(c, cyclonedx.ERTypeWebsite)
|
||||
case *pkg.PythonPackageMetadata:
|
||||
if meta.DirectURLOrigin == nil {
|
||||
meta.DirectURLOrigin = &pkg.PythonDirectURLOriginInfo{}
|
||||
}
|
||||
meta.DirectURLOrigin.URL = refURL(c, cyclonedx.ERTypeVCS)
|
||||
meta.DirectURLOrigin.CommitID = strings.TrimPrefix(refComment(c, cyclonedx.ERTypeVCS), "commit: ")
|
||||
}
|
||||
}
|
||||
|
||||
func findExternalRef(c *cyclonedx.Component, typ cyclonedx.ExternalReferenceType) *cyclonedx.ExternalReference {
|
||||
if c.ExternalReferences != nil {
|
||||
for _, r := range *c.ExternalReferences {
|
||||
if r.Type == typ {
|
||||
return &r
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func refURL(c *cyclonedx.Component, typ cyclonedx.ExternalReferenceType) string {
|
||||
if r := findExternalRef(c, typ); r != nil {
|
||||
return r.URL
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func refComment(c *cyclonedx.Component, typ cyclonedx.ExternalReferenceType) string {
|
||||
if r := findExternalRef(c, typ); r != nil {
|
||||
return r.Comment
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_ExternalReferences(t *testing.T) {
|
||||
func Test_encodeExternalReferences(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input pkg.Package
|
||||
@@ -127,7 +127,7 @@ func Test_ExternalReferences(t *testing.T) {
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, ExternalReferences(test.input))
|
||||
assert.Equal(t, test.expected, encodeExternalReferences(test.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/version"
|
||||
@@ -11,7 +13,6 @@ import (
|
||||
"github.com/anchore/syft/syft/linux"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func ToFormatModel(s sbom.SBOM) *cyclonedx.BOM {
|
||||
@@ -27,7 +28,7 @@ func ToFormatModel(s sbom.SBOM) *cyclonedx.BOM {
|
||||
packages := s.Artifacts.PackageCatalog.Sorted()
|
||||
components := make([]cyclonedx.Component, len(packages))
|
||||
for i, p := range packages {
|
||||
components[i] = Component(p)
|
||||
components[i] = encodeComponent(p)
|
||||
}
|
||||
components = append(components, toOSComponent(s.Artifacts.LinuxDistribution)...)
|
||||
cdxBOM.Components = &components
|
||||
@@ -74,18 +75,27 @@ func toOSComponent(distro *linux.Release) []cyclonedx.Component {
|
||||
if len(*eRefs) == 0 {
|
||||
eRefs = nil
|
||||
}
|
||||
props := getCycloneDXProperties(*distro)
|
||||
if len(*props) == 0 {
|
||||
props = nil
|
||||
props := encodeProperties(distro, "syft:distro")
|
||||
var properties *[]cyclonedx.Property
|
||||
if len(props) > 0 {
|
||||
properties = &props
|
||||
}
|
||||
return []cyclonedx.Component{
|
||||
{
|
||||
Type: cyclonedx.ComponentTypeOS,
|
||||
Name: distro.Name,
|
||||
Version: distro.Version,
|
||||
Type: cyclonedx.ComponentTypeOS,
|
||||
// FIXME is it idiomatic to be using SWID here for specific name and version information?
|
||||
SWID: &cyclonedx.SWID{
|
||||
TagID: distro.ID,
|
||||
Name: distro.ID,
|
||||
Version: distro.VersionID,
|
||||
},
|
||||
Description: distro.PrettyName,
|
||||
Name: distro.ID,
|
||||
Version: distro.VersionID,
|
||||
// TODO should we add a PURL?
|
||||
CPE: distro.CPEName,
|
||||
ExternalReferences: eRefs,
|
||||
Properties: props,
|
||||
Properties: properties,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package cyclonedxhelpers
|
||||
|
||||
import "github.com/anchore/syft/syft/pkg"
|
||||
|
||||
func Group(p pkg.Package) string {
|
||||
func encodeGroup(p pkg.Package) string {
|
||||
if hasMetadata(p) {
|
||||
if metadata, ok := p.Metadata.(pkg.JavaMetadata); ok && metadata.PomProperties != nil {
|
||||
return metadata.PomProperties.GroupID
|
||||
@@ -10,3 +10,12 @@ func Group(p pkg.Package) string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func decodeGroup(group string, metadata interface{}) {
|
||||
if meta, ok := metadata.(*pkg.JavaMetadata); ok {
|
||||
if meta.PomProperties == nil {
|
||||
meta.PomProperties = &pkg.PomProperties{}
|
||||
}
|
||||
meta.PomProperties.GroupID = group
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGroup(t *testing.T) {
|
||||
func Test_encodeGroup(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input pkg.Package
|
||||
@@ -46,7 +46,7 @@ func TestGroup(t *testing.T) {
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, Group(test.input))
|
||||
assert.Equal(t, test.expected, encodeGroup(test.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func Licenses(p pkg.Package) *cyclonedx.Licenses {
|
||||
func encodeLicenses(p pkg.Package) *cyclonedx.Licenses {
|
||||
lc := cyclonedx.Licenses{}
|
||||
for _, licenseName := range p.Licenses {
|
||||
if value, exists := spdxlicense.ID(licenseName); exists {
|
||||
@@ -22,3 +22,14 @@ func Licenses(p pkg.Package) *cyclonedx.Licenses {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeLicenses(c *cyclonedx.Component) (out []string) {
|
||||
if c.Licenses != nil {
|
||||
for _, l := range *c.Licenses {
|
||||
if l.License != nil {
|
||||
out = append(out, l.License.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_License(t *testing.T) {
|
||||
func Test_encodeLicense(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input pkg.Package
|
||||
@@ -77,7 +77,7 @@ func Test_License(t *testing.T) {
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, Licenses(test.input))
|
||||
assert.Equal(t, test.expected, encodeLicenses(test.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,78 +1,21 @@
|
||||
package cyclonedxhelpers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
|
||||
"github.com/anchore/syft/internal/formats/common"
|
||||
)
|
||||
|
||||
func Properties(p pkg.Package) *[]cyclonedx.Property {
|
||||
props := []cyclonedx.Property{}
|
||||
props = append(props, *getCycloneDXProperties(p)...)
|
||||
if len(p.Locations) > 0 {
|
||||
for _, l := range p.Locations {
|
||||
props = append(props, *getCycloneDXProperties(l.Coordinates)...)
|
||||
}
|
||||
}
|
||||
if hasMetadata(p) {
|
||||
props = append(props, *getCycloneDXProperties(p.Metadata)...)
|
||||
}
|
||||
if len(props) > 0 {
|
||||
return &props
|
||||
}
|
||||
return nil
|
||||
}
|
||||
var (
|
||||
CycloneDXFields = common.RequiredTag("cyclonedx")
|
||||
)
|
||||
|
||||
func getCycloneDXProperties(m interface{}) *[]cyclonedx.Property {
|
||||
props := []cyclonedx.Property{}
|
||||
structValue := reflect.ValueOf(m)
|
||||
if structValue.Kind() != reflect.Struct {
|
||||
return &props
|
||||
func encodeProperties(obj interface{}, prefix string) (out []cyclonedx.Property) {
|
||||
for _, p := range common.Sorted(common.Encode(obj, prefix, CycloneDXFields)) {
|
||||
out = append(out, cyclonedx.Property{
|
||||
Name: p.Name,
|
||||
Value: p.Value,
|
||||
})
|
||||
}
|
||||
structType := structValue.Type()
|
||||
for i := 0; i < structValue.NumField(); i++ {
|
||||
if name, value := getCycloneDXPropertyName(structType.Field(i)), getCycloneDXPropertyValue(structValue.Field(i)); name != "" && value != "" {
|
||||
// In the case of the value is a struct and has cyclonedx tag with name "-"
|
||||
// call the getCycloneDXProperties recursively.
|
||||
if name == "-" && reflect.ValueOf(value).Kind() == reflect.Struct {
|
||||
props = append(props, *getCycloneDXProperties(value)...)
|
||||
} else if reflect.ValueOf(value).Kind() == reflect.String {
|
||||
props = append(props, cyclonedx.Property{
|
||||
Name: name,
|
||||
Value: fmt.Sprint(value),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return &props
|
||||
}
|
||||
|
||||
func getCycloneDXPropertyName(field reflect.StructField) string {
|
||||
if value, exists := field.Tag.Lookup("cyclonedx"); exists {
|
||||
return value
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getCycloneDXPropertyValue(field reflect.Value) interface{} {
|
||||
if field.IsZero() {
|
||||
return ""
|
||||
}
|
||||
switch field.Kind() {
|
||||
case reflect.String, reflect.Bool, reflect.Int, reflect.Float32, reflect.Float64, reflect.Complex128, reflect.Complex64:
|
||||
if field.CanInterface() {
|
||||
return fmt.Sprint(field.Interface())
|
||||
}
|
||||
return ""
|
||||
case reflect.Struct:
|
||||
if field.CanInterface() {
|
||||
return field.Interface()
|
||||
}
|
||||
return ""
|
||||
case reflect.Ptr:
|
||||
return getCycloneDXPropertyValue(reflect.Indirect(field))
|
||||
}
|
||||
return ""
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
package cyclonedxhelpers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Properties(t *testing.T) {
|
||||
epoch := 2
|
||||
tests := []struct {
|
||||
name string
|
||||
input pkg.Package
|
||||
expected *[]cyclonedx.Property
|
||||
}{
|
||||
{
|
||||
name: "no metadata",
|
||||
input: pkg.Package{},
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "from apk",
|
||||
input: pkg.Package{
|
||||
FoundBy: "cataloger",
|
||||
Locations: []source.Location{
|
||||
{Coordinates: source.Coordinates{RealPath: "test"}},
|
||||
},
|
||||
Metadata: pkg.ApkMetadata{
|
||||
Package: "libc-utils",
|
||||
OriginPackage: "libc-dev",
|
||||
Maintainer: "Natanael Copa <ncopa@alpinelinux.org>",
|
||||
Version: "0.7.2-r0",
|
||||
License: "BSD",
|
||||
Architecture: "x86_64",
|
||||
URL: "http://alpinelinux.org",
|
||||
Description: "Meta package to pull in correct libc",
|
||||
Size: 0,
|
||||
InstalledSize: 4096,
|
||||
PullDependencies: "musl-utils",
|
||||
PullChecksum: "Q1p78yvTLG094tHE1+dToJGbmYzQE=",
|
||||
GitCommitOfAport: "97b1c2842faa3bfa30f5811ffbf16d5ff9f1a479",
|
||||
Files: []pkg.ApkFileRecord{},
|
||||
},
|
||||
},
|
||||
expected: &[]cyclonedx.Property{
|
||||
{Name: "foundBy", Value: "cataloger"},
|
||||
{Name: "path", Value: "test"},
|
||||
{Name: "originPackage", Value: "libc-dev"},
|
||||
{Name: "installedSize", Value: "4096"},
|
||||
{Name: "pullDependencies", Value: "musl-utils"},
|
||||
{Name: "pullChecksum", Value: "Q1p78yvTLG094tHE1+dToJGbmYzQE="},
|
||||
{Name: "gitCommitOfApkPort", Value: "97b1c2842faa3bfa30f5811ffbf16d5ff9f1a479"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "from dpkg",
|
||||
input: pkg.Package{
|
||||
MetadataType: pkg.DpkgMetadataType,
|
||||
Metadata: pkg.DpkgMetadata{
|
||||
Package: "tzdata",
|
||||
Version: "2020a-0+deb10u1",
|
||||
Source: "tzdata-dev",
|
||||
SourceVersion: "1.0",
|
||||
Architecture: "all",
|
||||
InstalledSize: 3036,
|
||||
Maintainer: "GNU Libc Maintainers <debian-glibc@lists.debian.org>",
|
||||
Files: []pkg.DpkgFileRecord{},
|
||||
},
|
||||
},
|
||||
expected: &[]cyclonedx.Property{
|
||||
{Name: "metadataType", Value: "DpkgMetadata"},
|
||||
{Name: "source", Value: "tzdata-dev"},
|
||||
{Name: "sourceVersion", Value: "1.0"},
|
||||
{Name: "installedSize", Value: "3036"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "from go bin",
|
||||
input: pkg.Package{
|
||||
Name: "golang.org/x/net",
|
||||
Version: "v0.0.0-20211006190231-62292e806868",
|
||||
Language: pkg.Go,
|
||||
Type: pkg.GoModulePkg,
|
||||
MetadataType: pkg.GolangBinMetadataType,
|
||||
Metadata: pkg.GolangBinMetadata{
|
||||
GoCompiledVersion: "1.17",
|
||||
Architecture: "amd64",
|
||||
H1Digest: "h1:KlOXYy8wQWTUJYFgkUI40Lzr06ofg5IRXUK5C7qZt1k=",
|
||||
},
|
||||
},
|
||||
expected: &[]cyclonedx.Property{
|
||||
{Name: "language", Value: pkg.Go.String()},
|
||||
{Name: "type", Value: "go-module"},
|
||||
{Name: "metadataType", Value: "GolangBinMetadata"},
|
||||
{Name: "goCompiledVersion", Value: "1.17"},
|
||||
{Name: "architecture", Value: "amd64"},
|
||||
{Name: "h1Digest", Value: "h1:KlOXYy8wQWTUJYFgkUI40Lzr06ofg5IRXUK5C7qZt1k="},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "from rpm",
|
||||
input: pkg.Package{
|
||||
Name: "dive",
|
||||
Version: "0.9.2-1",
|
||||
Type: pkg.RpmPkg,
|
||||
MetadataType: pkg.RpmdbMetadataType,
|
||||
Metadata: pkg.RpmdbMetadata{
|
||||
Name: "dive",
|
||||
Epoch: &epoch,
|
||||
Arch: "x86_64",
|
||||
Release: "1",
|
||||
Version: "0.9.2",
|
||||
SourceRpm: "dive-0.9.2-1.src.rpm",
|
||||
Size: 12406784,
|
||||
License: "MIT",
|
||||
Vendor: "",
|
||||
Files: []pkg.RpmdbFileRecord{},
|
||||
},
|
||||
},
|
||||
expected: &[]cyclonedx.Property{
|
||||
{Name: "type", Value: "rpm"},
|
||||
{Name: "metadataType", Value: "RpmdbMetadata"},
|
||||
{Name: "epoch", Value: "2"},
|
||||
{Name: "release", Value: "1"},
|
||||
{Name: "sourceRpm", Value: "dive-0.9.2-1.src.rpm"},
|
||||
{Name: "size", Value: "12406784"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, Properties(test.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func Publisher(p pkg.Package) string {
|
||||
func encodePublisher(p pkg.Package) string {
|
||||
if hasMetadata(p) {
|
||||
switch metadata := p.Metadata.(type) {
|
||||
case pkg.ApkMetadata:
|
||||
@@ -17,3 +17,14 @@ func Publisher(p pkg.Package) string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func decodePublisher(publisher string, metadata interface{}) {
|
||||
switch meta := metadata.(type) {
|
||||
case *pkg.ApkMetadata:
|
||||
meta.Maintainer = publisher
|
||||
case *pkg.RpmdbMetadata:
|
||||
meta.Vendor = publisher
|
||||
case *pkg.DpkgMetadata:
|
||||
meta.Maintainer = publisher
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Publisher(t *testing.T) {
|
||||
func Test_encodePublisher(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input pkg.Package
|
||||
@@ -59,7 +59,7 @@ func Test_Publisher(t *testing.T) {
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, Publisher(test.input))
|
||||
assert.Equal(t, test.expected, encodePublisher(test.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
)
|
||||
|
||||
// FieldName return a flag to indicate this is a valid field and a name to use
|
||||
type FieldName func(field reflect.StructField) (string, bool)
|
||||
|
||||
// OptionalTag given a tag name, will return the defined tag or fall back to lower camel case field name
|
||||
func OptionalTag(tag string) FieldName {
|
||||
return func(f reflect.StructField) (string, bool) {
|
||||
if n, ok := f.Tag.Lookup(tag); ok {
|
||||
return n, true
|
||||
}
|
||||
return lowerFirst(f.Name), true
|
||||
}
|
||||
}
|
||||
|
||||
// TrimOmitempty trims `,omitempty` from the name
|
||||
func TrimOmitempty(fn FieldName) FieldName {
|
||||
return func(f reflect.StructField) (string, bool) {
|
||||
if v, ok := fn(f); ok {
|
||||
return strings.TrimSuffix(v, ",omitempty"), true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
// RequiredTag based on the given tag, only use a field if present
|
||||
func RequiredTag(tag string) FieldName {
|
||||
return func(f reflect.StructField) (string, bool) {
|
||||
if n, ok := f.Tag.Lookup(tag); ok {
|
||||
return n, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
// OptionalJSONTag uses field names defined in json tags, if available
|
||||
OptionalJSONTag = TrimOmitempty(OptionalTag("json"))
|
||||
)
|
||||
|
||||
// lowerFirst converts the first character of the string to lower case
|
||||
func lowerFirst(s string) string {
|
||||
return strings.ToLower(s[0:1]) + s[1:]
|
||||
}
|
||||
|
||||
// Encode recursively encodes the object's properties as an ordered set of NameValue pairs
|
||||
func Encode(obj interface{}, prefix string, fn FieldName) map[string]string {
|
||||
if obj == nil {
|
||||
return nil
|
||||
}
|
||||
props := map[string]string{}
|
||||
encode(props, reflect.ValueOf(obj), prefix, fn)
|
||||
return props
|
||||
}
|
||||
|
||||
// NameValue a simple type to store stringified name/value pairs
|
||||
type NameValue struct {
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
|
||||
// Sorted returns a sorted set of NameValue pairs
|
||||
func Sorted(values map[string]string) (out []NameValue) {
|
||||
var keys []string
|
||||
for k := range values {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
out = append(out, NameValue{
|
||||
Name: k,
|
||||
Value: values[k],
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func encode(out map[string]string, value reflect.Value, prefix string, fn FieldName) {
|
||||
if !value.IsValid() || value.Type() == nil {
|
||||
return
|
||||
}
|
||||
|
||||
typ := value.Type()
|
||||
|
||||
switch typ.Kind() {
|
||||
case reflect.Ptr:
|
||||
if value.IsNil() {
|
||||
return
|
||||
}
|
||||
value = value.Elem()
|
||||
encode(out, value, prefix, fn)
|
||||
case reflect.String:
|
||||
v := value.String()
|
||||
if v != "" {
|
||||
out[prefix] = v
|
||||
}
|
||||
case reflect.Bool:
|
||||
v := value.Bool()
|
||||
out[prefix] = strconv.FormatBool(v)
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
v := value.Int()
|
||||
out[prefix] = strconv.FormatInt(v, 10)
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
v := value.Uint()
|
||||
out[prefix] = strconv.FormatUint(v, 10)
|
||||
case reflect.Float32, reflect.Float64:
|
||||
v := value.Float()
|
||||
out[prefix] = fmt.Sprintf("%f", v)
|
||||
case reflect.Array, reflect.Slice:
|
||||
for idx := 0; idx < value.Len(); idx++ {
|
||||
encode(out, value.Index(idx), fmt.Sprintf("%s:%d", prefix, idx), fn)
|
||||
}
|
||||
case reflect.Struct:
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
pv := value.Field(i)
|
||||
f := typ.Field(i)
|
||||
name, ok := fieldName(f, prefix, fn)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
encode(out, pv, name, fn)
|
||||
}
|
||||
default:
|
||||
log.Warnf("skipping encoding of unsupported property: %s", prefix)
|
||||
}
|
||||
}
|
||||
|
||||
// fieldName gets the name of the field using the provided FieldName function
|
||||
func fieldName(f reflect.StructField, prefix string, fn FieldName) (string, bool) {
|
||||
name, ok := fn(f)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
if name == "" {
|
||||
return prefix, true
|
||||
}
|
||||
if prefix != "" {
|
||||
name = fmt.Sprintf("%s:%s", prefix, name)
|
||||
}
|
||||
return name, true
|
||||
}
|
||||
|
||||
// Decode based on the given type, applies all values to hydrate a new instance
|
||||
func Decode(typ reflect.Type, values map[string]string, prefix string, fn FieldName) interface{} {
|
||||
isPtr := false
|
||||
for typ.Kind() == reflect.Ptr {
|
||||
typ = typ.Elem()
|
||||
isPtr = true
|
||||
}
|
||||
|
||||
isSlice := false
|
||||
if typ.Kind() == reflect.Slice {
|
||||
typ = reflect.PtrTo(typ)
|
||||
isSlice = true
|
||||
}
|
||||
|
||||
v := reflect.New(typ)
|
||||
|
||||
decode(values, v, prefix, fn)
|
||||
|
||||
switch {
|
||||
case isSlice && isPtr:
|
||||
return v.Elem().Interface()
|
||||
case isSlice:
|
||||
return PtrToStruct(v.Elem().Interface())
|
||||
case isPtr:
|
||||
return v.Interface()
|
||||
}
|
||||
return v.Elem().Interface()
|
||||
}
|
||||
|
||||
// DecodeInto decodes all values to hydrate the given object instance
|
||||
func DecodeInto(obj interface{}, values map[string]string, prefix string, fn FieldName) {
|
||||
value := reflect.ValueOf(obj)
|
||||
|
||||
for value.Type().Kind() == reflect.Ptr {
|
||||
value = value.Elem()
|
||||
}
|
||||
|
||||
decode(values, value, prefix, fn)
|
||||
}
|
||||
|
||||
// nolint: funlen, gocognit, gocyclo
|
||||
func decode(vals map[string]string, value reflect.Value, prefix string, fn FieldName) bool {
|
||||
if !value.IsValid() || value.Type() == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
typ := value.Type()
|
||||
|
||||
incoming, valid := vals[prefix]
|
||||
switch typ.Kind() {
|
||||
case reflect.Ptr:
|
||||
t := typ.Elem()
|
||||
v := value
|
||||
if v.IsNil() {
|
||||
v = reflect.New(t)
|
||||
}
|
||||
if decode(vals, v.Elem(), prefix, fn) && value.CanSet() {
|
||||
o := v.Interface()
|
||||
log.Infof("%v", o)
|
||||
value.Set(v)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case reflect.String:
|
||||
if valid {
|
||||
value.SetString(incoming)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case reflect.Bool:
|
||||
if !valid {
|
||||
return false
|
||||
}
|
||||
if b, err := strconv.ParseBool(incoming); err == nil {
|
||||
value.SetBool(b)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
if !valid {
|
||||
return false
|
||||
}
|
||||
if i, err := strconv.ParseInt(incoming, 10, 64); err == nil {
|
||||
value.SetInt(i)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
if !valid {
|
||||
return false
|
||||
}
|
||||
if i, err := strconv.ParseUint(incoming, 10, 64); err == nil {
|
||||
value.SetUint(i)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case reflect.Float32, reflect.Float64:
|
||||
if !valid {
|
||||
return false
|
||||
}
|
||||
if i, err := strconv.ParseFloat(incoming, 64); err == nil {
|
||||
value.SetFloat(i)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case reflect.Array, reflect.Slice:
|
||||
values := false
|
||||
t := typ.Elem()
|
||||
slice := reflect.MakeSlice(typ, 0, 0)
|
||||
for idx := 0; ; idx++ {
|
||||
// test for index
|
||||
str := fmt.Sprintf("%s:%d", prefix, idx)
|
||||
// create new placeholder and decode values
|
||||
newType := t
|
||||
if t.Kind() == reflect.Ptr {
|
||||
newType = t.Elem()
|
||||
}
|
||||
v := reflect.New(newType)
|
||||
if decode(vals, v.Elem(), str, fn) {
|
||||
// append to slice
|
||||
if t.Kind() != reflect.Ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
slice = reflect.Append(slice, v)
|
||||
values = true
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
if values {
|
||||
value.Set(slice)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case reflect.Struct:
|
||||
values := false
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
f := typ.Field(i)
|
||||
v := value.Field(i)
|
||||
|
||||
name, ok := fieldName(f, prefix, fn)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if decode(vals, v, name, fn) {
|
||||
values = true
|
||||
}
|
||||
}
|
||||
return values
|
||||
default:
|
||||
log.Warnf("unable to set field: %s", prefix)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func PtrToStruct(ptr interface{}) interface{} {
|
||||
v := reflect.ValueOf(ptr)
|
||||
if v.IsZero() {
|
||||
return nil
|
||||
}
|
||||
switch v.Type().Kind() {
|
||||
case reflect.Ptr:
|
||||
return PtrToStruct(v.Elem().Interface())
|
||||
case reflect.Interface:
|
||||
return PtrToStruct(v.Elem().Interface())
|
||||
}
|
||||
return v.Interface()
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type T1 struct {
|
||||
Name string
|
||||
Val int
|
||||
ValU uint
|
||||
Flag bool `json:"bool_flag"`
|
||||
Float float64
|
||||
T2 T2
|
||||
T2Ptr *T2
|
||||
T2Arr []T2
|
||||
T2PtrArr []*T2
|
||||
StrArr []string
|
||||
IntArr []int
|
||||
FloatArr []float64
|
||||
BoolArr []bool
|
||||
T3Arr []T3
|
||||
}
|
||||
|
||||
type T2 struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
type T3 struct {
|
||||
T4Arr []T4
|
||||
}
|
||||
|
||||
type T4 struct {
|
||||
Typ string
|
||||
IntPtr *int
|
||||
}
|
||||
|
||||
func Test_EncodeDecodeCycle(t *testing.T) {
|
||||
val := 99
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
value interface{}
|
||||
}{
|
||||
{
|
||||
name: "all values",
|
||||
value: T1{
|
||||
Name: "name",
|
||||
Val: 10,
|
||||
ValU: 16,
|
||||
Flag: true,
|
||||
Float: 1.2,
|
||||
T2: T2{
|
||||
Name: "embedded t2",
|
||||
},
|
||||
T2Ptr: &T2{
|
||||
"t2 ptr",
|
||||
},
|
||||
T2Arr: []T2{
|
||||
{"t2 elem 0"},
|
||||
{"t2 elem 1"},
|
||||
},
|
||||
T2PtrArr: []*T2{
|
||||
{"t2 ptr v1"},
|
||||
{"t2 ptr v2"},
|
||||
},
|
||||
StrArr: []string{"s 1", "s 2", "s 3"},
|
||||
IntArr: []int{9, 12, -1},
|
||||
FloatArr: []float64{-23.99, 15.234321, 39912342314},
|
||||
BoolArr: []bool{false, true, true, true, false},
|
||||
T3Arr: []T3{
|
||||
{
|
||||
T4Arr: []T4{
|
||||
{
|
||||
Typ: "t4 nested typ 1",
|
||||
},
|
||||
{
|
||||
Typ: "t4 nested typ 2",
|
||||
IntPtr: &val,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nil values",
|
||||
value: T1{
|
||||
Name: "t1 test",
|
||||
Val: 0,
|
||||
ValU: 0,
|
||||
Flag: false,
|
||||
Float: 0,
|
||||
T2: T2{},
|
||||
T2Ptr: nil,
|
||||
T2Arr: nil,
|
||||
T2PtrArr: nil,
|
||||
StrArr: nil,
|
||||
IntArr: nil,
|
||||
FloatArr: nil,
|
||||
BoolArr: nil,
|
||||
T3Arr: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "array values",
|
||||
value: []T2{
|
||||
{"t2 elem 0"},
|
||||
{"t2 elem 1"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "array ptr",
|
||||
value: &[]T2{
|
||||
{"t2 elem 0"},
|
||||
{"t2 elem 1"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
vals := Encode(test.value, "props", OptionalJSONTag)
|
||||
|
||||
typ := reflect.TypeOf(test.value)
|
||||
|
||||
if typ.Kind() != reflect.Slice && typ.Kind() != reflect.Ptr {
|
||||
assert.NotEmpty(t, vals["props:bool_flag"])
|
||||
|
||||
t2 := T1{}
|
||||
DecodeInto(&t2, vals, "props", OptionalJSONTag)
|
||||
|
||||
assert.EqualValues(t, test.value, t2)
|
||||
}
|
||||
|
||||
t3 := Decode(typ, vals, "props", OptionalJSONTag)
|
||||
|
||||
assert.EqualValues(t, test.value, t3)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,9 @@ package spdxhelpers
|
||||
|
||||
import "github.com/anchore/syft/syft/pkg"
|
||||
|
||||
const NONE = "NONE"
|
||||
const NOASSERTION = "NOASSERTION"
|
||||
|
||||
func DownloadLocation(p pkg.Package) string {
|
||||
// 3.7: Package Download Location
|
||||
// Cardinality: mandatory, one
|
||||
@@ -19,5 +22,5 @@ func DownloadLocation(p pkg.Package) string {
|
||||
return NoneIfEmpty(metadata.URL)
|
||||
}
|
||||
}
|
||||
return "NOASSERTION"
|
||||
return NOASSERTION
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ func Test_DownloadLocation(t *testing.T) {
|
||||
{
|
||||
name: "no metadata",
|
||||
input: pkg.Package{},
|
||||
expected: "NOASSERTION",
|
||||
expected: NOASSERTION,
|
||||
},
|
||||
{
|
||||
name: "from apk",
|
||||
@@ -43,7 +43,7 @@ func Test_DownloadLocation(t *testing.T) {
|
||||
URL: "",
|
||||
},
|
||||
},
|
||||
expected: "NONE",
|
||||
expected: NONE,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package model
|
||||
package spdxhelpers
|
||||
|
||||
type ReferenceCategory string
|
||||
|
||||
@@ -1,51 +1,26 @@
|
||||
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)
|
||||
func ExternalRefs(p pkg.Package) (externalRefs []ExternalRef) {
|
||||
externalRefs = make([]ExternalRef, 0)
|
||||
|
||||
for _, c := range p.CPEs {
|
||||
externalRefs = append(externalRefs, model.ExternalRef{
|
||||
ReferenceCategory: model.SecurityReferenceCategory,
|
||||
externalRefs = append(externalRefs, ExternalRef{
|
||||
ReferenceCategory: SecurityReferenceCategory,
|
||||
ReferenceLocator: pkg.CPEString(c),
|
||||
ReferenceType: model.Cpe23ExternalRefType,
|
||||
ReferenceType: Cpe23ExternalRefType,
|
||||
})
|
||||
}
|
||||
|
||||
if p.PURL != "" {
|
||||
externalRefs = append(externalRefs, model.ExternalRef{
|
||||
ReferenceCategory: model.PackageManagerReferenceCategory,
|
||||
externalRefs = append(externalRefs, ExternalRef{
|
||||
ReferenceCategory: PackageManagerReferenceCategory,
|
||||
ReferenceLocator: p.PURL,
|
||||
ReferenceType: model.PurlExternalRefType,
|
||||
ReferenceType: 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
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package spdxhelpers
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/syft/internal/formats/spdx22json/model"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -13,7 +12,7 @@ func Test_ExternalRefs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input pkg.Package
|
||||
expected []model.ExternalRef
|
||||
expected []ExternalRef
|
||||
}{
|
||||
{
|
||||
name: "cpe + purl",
|
||||
@@ -23,16 +22,16 @@ func Test_ExternalRefs(t *testing.T) {
|
||||
},
|
||||
PURL: "a-purl",
|
||||
},
|
||||
expected: []model.ExternalRef{
|
||||
expected: []ExternalRef{
|
||||
{
|
||||
ReferenceCategory: model.SecurityReferenceCategory,
|
||||
ReferenceCategory: SecurityReferenceCategory,
|
||||
ReferenceLocator: pkg.CPEString(testCPE),
|
||||
ReferenceType: model.Cpe23ExternalRefType,
|
||||
ReferenceType: Cpe23ExternalRefType,
|
||||
},
|
||||
{
|
||||
ReferenceCategory: model.PackageManagerReferenceCategory,
|
||||
ReferenceCategory: PackageManagerReferenceCategory,
|
||||
ReferenceLocator: "a-purl",
|
||||
ReferenceType: model.PurlExternalRefType,
|
||||
ReferenceType: PurlExternalRefType,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package spdxhelpers
|
||||
|
||||
type FileType string
|
||||
|
||||
const (
|
||||
DocumentationFileType FileType = "DOCUMENTATION" // if the file serves as documentation
|
||||
ImageFileType FileType = "IMAGE" // if the file is associated with a picture image file (MIME type of image/*, e.g., .jpg, .gif)
|
||||
VideoFileType FileType = "VIDEO" // if the file is associated with a video file type (MIME type of video/*)
|
||||
ArchiveFileType FileType = "ARCHIVE" // if the file represents an archive (.tar, .jar, etc.)
|
||||
SpdxFileType FileType = "SPDX" // if the file is an SPDX document
|
||||
ApplicationFileType FileType = "APPLICATION" // if the file is associated with a specific application type (MIME type of application/*)
|
||||
SourceFileType FileType = "SOURCE" // if the file is human readable source code (.c, .html, etc.)
|
||||
BinaryFileType FileType = "BINARY" // if the file is a compiled object, target image or binary executable (.o, .a, etc.)
|
||||
TextFileType FileType = "TEXT" // if the file is human readable text file (MIME type of text/*)
|
||||
AudioFileType FileType = "AUDIO" // if the file is associated with an audio file (MIME type of audio/* , e.g. .mp3)
|
||||
OtherFileType FileType = "OTHER" // if the file doesn't fit into the above categories (generated artifacts, data files, etc.)
|
||||
)
|
||||
@@ -18,7 +18,7 @@ func License(p pkg.Package) string {
|
||||
// (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"
|
||||
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/
|
||||
@@ -30,7 +30,7 @@ func License(p pkg.Package) string {
|
||||
}
|
||||
|
||||
if len(parsedLicenses) == 0 {
|
||||
return "NOASSERTION"
|
||||
return NOASSERTION
|
||||
}
|
||||
|
||||
return strings.Join(parsedLicenses, " AND ")
|
||||
|
||||
@@ -16,7 +16,7 @@ func Test_License(t *testing.T) {
|
||||
{
|
||||
name: "no licenses",
|
||||
input: pkg.Package{},
|
||||
expected: "NONE",
|
||||
expected: NONE,
|
||||
},
|
||||
{
|
||||
name: "no SPDX licenses",
|
||||
@@ -25,7 +25,7 @@ func Test_License(t *testing.T) {
|
||||
"made-up",
|
||||
},
|
||||
},
|
||||
expected: "NOASSERTION",
|
||||
expected: NOASSERTION,
|
||||
},
|
||||
{
|
||||
name: "with SPDX license",
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
func NoneIfEmpty(value string) string {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return "NONE"
|
||||
return NONE
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
@@ -20,17 +20,17 @@ func Test_noneIfEmpty(t *testing.T) {
|
||||
{
|
||||
name: "empty",
|
||||
value: "",
|
||||
expected: "NONE",
|
||||
expected: NONE,
|
||||
},
|
||||
{
|
||||
name: "space",
|
||||
value: " ",
|
||||
expected: "NONE",
|
||||
expected: NONE,
|
||||
},
|
||||
{
|
||||
name: "tab",
|
||||
value: "\t",
|
||||
expected: "NONE",
|
||||
expected: NONE,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
package spdxhelpers
|
||||
|
||||
// source: https://spdx.github.io/spdx-spec/7-relationships-between-SPDX-elements/
|
||||
type RelationshipType string
|
||||
|
||||
const (
|
||||
// DescribedByRelationship is to be used when SPDXRef-A is described by SPDXREF-Document.
|
||||
// Example: The package 'WildFly' is described by SPDX document WildFly.spdx.
|
||||
DescribedByRelationship RelationshipType = "DESCRIBED_BY"
|
||||
|
||||
// ContainsRelationship is to be used when SPDXRef-A contains SPDXRef-B.
|
||||
// Example: An ARCHIVE file bar.tgz contains a SOURCE file foo.c.
|
||||
ContainsRelationship RelationshipType = "CONTAINS"
|
||||
|
||||
// ContainedByRelationship is to be used when SPDXRef-A is contained by SPDXRef-B.
|
||||
// Example: A SOURCE file foo.c is contained by ARCHIVE file bar.tgz
|
||||
ContainedByRelationship RelationshipType = "CONTAINED_BY"
|
||||
|
||||
// DependsOnRelationship is to be used when SPDXRef-A depends on SPDXRef-B.
|
||||
// Example: Package A depends on the presence of package B in order to build and run
|
||||
DependsOnRelationship RelationshipType = "DEPENDS_ON"
|
||||
|
||||
// DependencyOfRelationship is to be used when SPDXRef-A is dependency of SPDXRef-B.
|
||||
// Example: A is explicitly stated as a dependency of B in a machine-readable file. Use when a package manager does not define scopes.
|
||||
DependencyOfRelationship RelationshipType = "DEPENDENCY_OF"
|
||||
|
||||
// DependencyManifestOfRelationship is to be used when SPDXRef-A is a manifest file that lists a set of dependencies for SPDXRef-B.
|
||||
// Example: A file package.json is the dependency manifest of a package foo. Note that only one manifest should be used to define the same dependency graph.
|
||||
DependencyManifestOfRelationship RelationshipType = "DEPENDENCY_MANIFEST_OF"
|
||||
|
||||
// BuildDependencyOfRelationship is to be used when SPDXRef-A is a build dependency of SPDXRef-B.
|
||||
// Example: A is in the compile scope of B in a Maven project.
|
||||
BuildDependencyOfRelationship RelationshipType = "BUILD_DEPENDENCY_OF"
|
||||
|
||||
// DevDependencyOfRelationship is to be used when SPDXRef-A is a development dependency of SPDXRef-B.
|
||||
// Example: A is in the devDependencies scope of B in a Maven project.
|
||||
DevDependencyOfRelationship RelationshipType = "DEV_DEPENDENCY_OF"
|
||||
|
||||
// OptionalDependencyOfRelationship is to be used when SPDXRef-A is an optional dependency of SPDXRef-B.
|
||||
// Example: Use when building the code will proceed even if a dependency cannot be found, fails to install, or is only installed on a specific platform. For example, A is in the optionalDependencies scope of npm project B.
|
||||
OptionalDependencyOfRelationship RelationshipType = "OPTIONAL_DEPENDENCY_OF"
|
||||
|
||||
// ProvidedDependencyOfRelationship is to be used when SPDXRef-A is a to be provided dependency of SPDXRef-B.
|
||||
// Example: A is in the provided scope of B in a Maven project, indicating that the project expects it to be provided, for instance, by the container or JDK.
|
||||
ProvidedDependencyOfRelationship RelationshipType = "PROVIDED_DEPENDENCY_OF"
|
||||
|
||||
// TestDependencyOfRelationship is to be used when SPDXRef-A is a test dependency of SPDXRef-B.
|
||||
// Example: A is in the test scope of B in a Maven project.
|
||||
TestDependencyOfRelationship RelationshipType = "TEST_DEPENDENCY_OF"
|
||||
|
||||
// RuntimeDependencyOfRelationship is to be used when SPDXRef-A is a dependency required for the execution of SPDXRef-B.
|
||||
// Example: A is in the runtime scope of B in a Maven project.
|
||||
RuntimeDependencyOfRelationship RelationshipType = "RUNTIME_DEPENDENCY_OF"
|
||||
|
||||
// ExampleOfRelationship is to be used when SPDXRef-A is an example of SPDXRef-B.
|
||||
// Example: The file or snippet that illustrates how to use an application or library.
|
||||
ExampleOfRelationship RelationshipType = "EXAMPLE_OF"
|
||||
|
||||
// GeneratesRelationship is to be used when SPDXRef-A generates SPDXRef-B.
|
||||
// Example: A SOURCE file makefile.mk generates a BINARY file a.out
|
||||
GeneratesRelationship RelationshipType = "GENERATES"
|
||||
|
||||
// GeneratedFromRelationship is to be used when SPDXRef-A was generated from SPDXRef-B.
|
||||
// Example: A BINARY file a.out has been generated from a SOURCE file makefile.mk. A BINARY file foolib.a is generated from a SOURCE file bar.c.
|
||||
GeneratedFromRelationship RelationshipType = "GENERATED_FROM"
|
||||
|
||||
// AncestorOfRelationship is to be used when SPDXRef-A is an ancestor (same lineage but pre-dates) SPDXRef-B.
|
||||
// Example: A SOURCE file makefile.mk is a version of the original ancestor SOURCE file 'makefile2.mk'
|
||||
AncestorOfRelationship RelationshipType = "ANCESTOR_OF"
|
||||
|
||||
// DescendantOfRelationship is to be used when SPDXRef-A is a descendant of (same lineage but postdates) SPDXRef-B.
|
||||
// Example: A SOURCE file makefile2.mk is a descendant of the original SOURCE file 'makefile.mk'
|
||||
DescendantOfRelationship RelationshipType = "DESCENDANT_OF"
|
||||
|
||||
// VariantOfRelationship is to be used when SPDXRef-A is a variant of (same lineage but not clear which came first) SPDXRef-B.
|
||||
// Example: A SOURCE file makefile2.mk is a variant of SOURCE file makefile.mk if they differ by some edit, but there is no way to tell which came first (no reliable date information).
|
||||
VariantOfRelationship RelationshipType = "VARIANT_OF"
|
||||
|
||||
// DistributionArtifactRelationship is to be used when distributing SPDXRef-A requires that SPDXRef-B also be distributed.
|
||||
// Example: A BINARY file foo.o requires that the ARCHIVE file bar-sources.tgz be made available on distribution.
|
||||
DistributionArtifactRelationship RelationshipType = "DISTRIBUTION_ARTIFACT"
|
||||
|
||||
// PatchForRelationship is to be used when SPDXRef-A is a patch file for (to be applied to) SPDXRef-B.
|
||||
// Example: A SOURCE file foo.diff is a patch file for SOURCE file foo.c.
|
||||
PatchForRelationship RelationshipType = "PATCH_FOR"
|
||||
|
||||
// PatchAppliedRelationship is to be used when SPDXRef-A is a patch file that has been applied to SPDXRef-B.
|
||||
// Example: A SOURCE file foo.diff is a patch file that has been applied to SOURCE file 'foo-patched.c'.
|
||||
PatchAppliedRelationship RelationshipType = "PATCH_APPLIED"
|
||||
|
||||
// CopyOfRelationship is to be used when SPDXRef-A is an exact copy of SPDXRef-B.
|
||||
// Example: A BINARY file alib.a is an exact copy of BINARY file a2lib.a.
|
||||
CopyOfRelationship RelationshipType = "COPY_OF"
|
||||
|
||||
// FileAddedRelationship is to be used when SPDXRef-A is a file that was added to SPDXRef-B.
|
||||
// Example: A SOURCE file foo.c has been added to package ARCHIVE bar.tgz.
|
||||
FileAddedRelationship RelationshipType = "FILE_ADDED"
|
||||
|
||||
// FileDeletedRelationship is to be used when SPDXRef-A is a file that was deleted from SPDXRef-B.
|
||||
// Example: A SOURCE file foo.diff has been deleted from package ARCHIVE bar.tgz.
|
||||
FileDeletedRelationship RelationshipType = "FILE_DELETED"
|
||||
|
||||
// FileModifiedRelationship is to be used when SPDXRef-A is a file that was modified from SPDXRef-B.
|
||||
// Example: A SOURCE file foo.c has been modified from SOURCE file foo.orig.c.
|
||||
FileModifiedRelationship RelationshipType = "FILE_MODIFIED"
|
||||
|
||||
// ExpandedFromArchiveRelationship is to be used when SPDXRef-A is expanded from the archive SPDXRef-B.
|
||||
// Example: A SOURCE file foo.c, has been expanded from the archive ARCHIVE file xyz.tgz.
|
||||
ExpandedFromArchiveRelationship RelationshipType = "EXPANDED_FROM_ARCHIVE"
|
||||
|
||||
// DynamicLinkRelationship is to be used when SPDXRef-A dynamically links to SPDXRef-B.
|
||||
// Example: An APPLICATION file 'myapp' dynamically links to BINARY file zlib.so.
|
||||
DynamicLinkRelationship RelationshipType = "DYNAMIC_LINK"
|
||||
|
||||
// StaticLinkRelationship is to be used when SPDXRef-A statically links to SPDXRef-B.
|
||||
// Example: An APPLICATION file 'myapp' statically links to BINARY zlib.a.
|
||||
StaticLinkRelationship RelationshipType = "STATIC_LINK"
|
||||
|
||||
// DataFileOfRelationship is to be used when SPDXRef-A is a data file used in SPDXRef-B.
|
||||
// Example: An IMAGE file 'kitty.jpg' is a data file of an APPLICATION 'hellokitty'.
|
||||
DataFileOfRelationship RelationshipType = "DATA_FILE_OF"
|
||||
|
||||
// TestCaseOfRelationship is to be used when SPDXRef-A is a test case used in testing SPDXRef-B.
|
||||
// Example: A SOURCE file testMyCode.java is a unit test file used to test an APPLICATION MyPackage.
|
||||
TestCaseOfRelationship RelationshipType = "TEST_CASE_OF"
|
||||
|
||||
// BuildToolOfRelationship is to be used when SPDXRef-A is used to build SPDXRef-B.
|
||||
// Example: A SOURCE file makefile.mk is used to build an APPLICATION 'zlib'.
|
||||
BuildToolOfRelationship RelationshipType = "BUILD_TOOL_OF"
|
||||
|
||||
// DevToolOfRelationship is to be used when SPDXRef-A is used as a development tool for SPDXRef-B.
|
||||
// Example: Any tool used for development such as a code debugger.
|
||||
DevToolOfRelationship RelationshipType = "DEV_TOOL_OF"
|
||||
|
||||
// TestOfRelationship is to be used when SPDXRef-A is used for testing SPDXRef-B.
|
||||
// Example: Generic relationship for cases where it's clear that something is used for testing but unclear whether it's TEST_CASE_OF or TEST_TOOL_OF.
|
||||
TestOfRelationship RelationshipType = "TEST_OF"
|
||||
|
||||
// TestToolOfRelationship is to be used when SPDXRef-A is used as a test tool for SPDXRef-B.
|
||||
// Example: Any tool used to test the code such as ESlint.
|
||||
TestToolOfRelationship RelationshipType = "TEST_TOOL_OF"
|
||||
|
||||
// DocumentationOfRelationship is to be used when SPDXRef-A provides documentation of SPDXRef-B.
|
||||
// Example: A DOCUMENTATION file readme.txt documents the APPLICATION 'zlib'.
|
||||
DocumentationOfRelationship RelationshipType = "DOCUMENTATION_OF"
|
||||
|
||||
// OptionalComponentOfRelationship is to be used when SPDXRef-A is an optional component of SPDXRef-B.
|
||||
// Example: A SOURCE file fool.c (which is in the contributors directory) may or may not be included in the build of APPLICATION 'atthebar'.
|
||||
OptionalComponentOfRelationship RelationshipType = "OPTIONAL_COMPONENT_OF"
|
||||
|
||||
// MetafileOfRelationship is to be used when SPDXRef-A is a metafile of SPDXRef-B.
|
||||
// Example: A SOURCE file pom.xml is a metafile of the APPLICATION 'Apache Xerces'.
|
||||
MetafileOfRelationship RelationshipType = "METAFILE_OF"
|
||||
|
||||
// PackageOfRelationship is to be used when SPDXRef-A is used as a package as part of SPDXRef-B.
|
||||
// Example: A Linux distribution contains an APPLICATION package gawk as part of the distribution MyLinuxDistro.
|
||||
PackageOfRelationship RelationshipType = "PACKAGE_OF"
|
||||
|
||||
// AmendsRelationship is to be used when (current) SPDXRef-DOCUMENT amends the SPDX information in SPDXRef-B.
|
||||
// Example: (Current) SPDX document A version 2 contains a correction to a previous version of the SPDX document A version 1. Note the reserved identifier SPDXRef-DOCUMENT for the current document is required.
|
||||
AmendsRelationship RelationshipType = "AMENDS"
|
||||
|
||||
// PrerequisiteForRelationship is to be used when SPDXRef-A is a prerequisite for SPDXRef-B.
|
||||
// Example: A library bar.dll is a prerequisite or dependency for APPLICATION foo.exe
|
||||
PrerequisiteForRelationship RelationshipType = "PREREQUISITE_FOR"
|
||||
|
||||
// HasPrerequisiteRelationship is to be used when SPDXRef-A has as a prerequisite SPDXRef-B.
|
||||
// Example: An APPLICATION foo.exe has prerequisite or dependency on bar.dll
|
||||
HasPrerequisiteRelationship RelationshipType = "HAS_PREREQUISITE"
|
||||
|
||||
// OtherRelationship is to be used for a relationship which has not been defined in the formal SPDX specification. A description of the relationship should be included in the Relationship comments field.
|
||||
OtherRelationship RelationshipType = "OTHER"
|
||||
)
|
||||
@@ -13,6 +13,8 @@ func SourceInfo(p pkg.Package) string {
|
||||
answer = "acquired package info from RPM DB"
|
||||
case pkg.ApkPkg:
|
||||
answer = "acquired package info from APK DB"
|
||||
case pkg.DartPubPkg:
|
||||
answer = "acquired package info from pubspec manifest"
|
||||
case pkg.DebPkg:
|
||||
answer = "acquired package info from DPKG DB"
|
||||
case pkg.NpmPkg:
|
||||
@@ -33,7 +35,7 @@ func SourceInfo(p pkg.Package) string {
|
||||
answer = "acquired package info from the following paths"
|
||||
}
|
||||
var paths []string
|
||||
for _, l := range p.Locations {
|
||||
for _, l := range p.Locations.ToSlice() {
|
||||
paths = append(paths, l.RealPath)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,10 +18,10 @@ func Test_SourceInfo(t *testing.T) {
|
||||
name: "locations are captured",
|
||||
input: pkg.Package{
|
||||
// note: no type given
|
||||
Locations: []source.Location{
|
||||
Locations: source.NewLocationSet(
|
||||
source.NewVirtualLocation("/a-place", "/b-place"),
|
||||
source.NewVirtualLocation("/c-place", "/d-place"),
|
||||
},
|
||||
),
|
||||
},
|
||||
expected: []string{
|
||||
"from the following paths",
|
||||
@@ -126,6 +126,14 @@ func Test_SourceInfo(t *testing.T) {
|
||||
"from PHP composer manifest",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: pkg.Package{
|
||||
Type: pkg.DartPubPkg,
|
||||
},
|
||||
expected: []string{
|
||||
"from pubspec manifest",
|
||||
},
|
||||
},
|
||||
}
|
||||
var pkgTypes []pkg.Type
|
||||
for _, test := range tests {
|
||||
|
||||
@@ -0,0 +1,344 @@
|
||||
package spdxhelpers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/spdx/tools-golang/spdx"
|
||||
|
||||
"github.com/anchore/packageurl-go"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/linux"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
func ToSyftModel(doc *spdx.Document2_2) (*sbom.SBOM, error) {
|
||||
if doc == nil {
|
||||
return nil, errors.New("cannot convert SPDX document to Syft model because document is nil")
|
||||
}
|
||||
|
||||
spdxIDMap := make(map[string]interface{})
|
||||
|
||||
s := &sbom.SBOM{
|
||||
Artifacts: sbom.Artifacts{
|
||||
PackageCatalog: pkg.NewCatalog(),
|
||||
FileMetadata: map[source.Coordinates]source.FileMetadata{},
|
||||
FileDigests: map[source.Coordinates][]file.Digest{},
|
||||
LinuxDistribution: findLinuxReleaseByPURL(doc),
|
||||
},
|
||||
}
|
||||
|
||||
collectSyftPackages(s, spdxIDMap, doc)
|
||||
|
||||
collectSyftFiles(s, spdxIDMap, doc)
|
||||
|
||||
s.Relationships = toSyftRelationships(spdxIDMap, doc)
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func findLinuxReleaseByPURL(doc *spdx.Document2_2) *linux.Release {
|
||||
for _, p := range doc.Packages {
|
||||
purlValue := findPURLValue(p)
|
||||
if purlValue == "" {
|
||||
continue
|
||||
}
|
||||
purl, err := packageurl.FromString(purlValue)
|
||||
if err != nil {
|
||||
log.Warnf("unable to parse purl: %s", purlValue)
|
||||
continue
|
||||
}
|
||||
distro := findQualifierValue(purl, pkg.PURLQualifierDistro)
|
||||
if distro != "" {
|
||||
parts := strings.Split(distro, "-")
|
||||
name := parts[0]
|
||||
version := ""
|
||||
if len(parts) > 1 {
|
||||
version = parts[1]
|
||||
}
|
||||
return &linux.Release{
|
||||
PrettyName: name,
|
||||
Name: name,
|
||||
ID: name,
|
||||
IDLike: []string{name},
|
||||
Version: version,
|
||||
VersionID: version,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func collectSyftPackages(s *sbom.SBOM, spdxIDMap map[string]interface{}, doc *spdx.Document2_2) {
|
||||
for _, p := range doc.Packages {
|
||||
syftPkg := toSyftPackage(p)
|
||||
spdxIDMap[string(p.PackageSPDXIdentifier)] = syftPkg
|
||||
s.Artifacts.PackageCatalog.Add(*syftPkg)
|
||||
}
|
||||
}
|
||||
|
||||
func collectSyftFiles(s *sbom.SBOM, spdxIDMap map[string]interface{}, doc *spdx.Document2_2) {
|
||||
for _, f := range doc.UnpackagedFiles {
|
||||
l := toSyftLocation(f)
|
||||
spdxIDMap[string(f.FileSPDXIdentifier)] = l
|
||||
|
||||
s.Artifacts.FileMetadata[l.Coordinates] = toFileMetadata(f)
|
||||
s.Artifacts.FileDigests[l.Coordinates] = toFileDigests(f)
|
||||
}
|
||||
}
|
||||
|
||||
func toFileDigests(f *spdx.File2_2) (digests []file.Digest) {
|
||||
for _, digest := range f.FileChecksums {
|
||||
digests = append(digests, file.Digest{
|
||||
Algorithm: string(digest.Algorithm),
|
||||
Value: digest.Value,
|
||||
})
|
||||
}
|
||||
return digests
|
||||
}
|
||||
|
||||
func toFileMetadata(f *spdx.File2_2) (meta source.FileMetadata) {
|
||||
// FIXME Syft is currently lossy due to the SPDX 2.2.1 spec not supporting arbitrary mimetypes
|
||||
for _, typ := range f.FileType {
|
||||
switch FileType(typ) {
|
||||
case ImageFileType:
|
||||
meta.MIMEType = "image/"
|
||||
case VideoFileType:
|
||||
meta.MIMEType = "video/"
|
||||
case ApplicationFileType:
|
||||
meta.MIMEType = "application/"
|
||||
case TextFileType:
|
||||
meta.MIMEType = "text/"
|
||||
case AudioFileType:
|
||||
meta.MIMEType = "audio/"
|
||||
case BinaryFileType:
|
||||
case ArchiveFileType:
|
||||
case OtherFileType:
|
||||
}
|
||||
}
|
||||
return meta
|
||||
}
|
||||
|
||||
func toSyftRelationships(spdxIDMap map[string]interface{}, doc *spdx.Document2_2) []artifact.Relationship {
|
||||
var out []artifact.Relationship
|
||||
for _, r := range doc.Relationships {
|
||||
// FIXME what to do with r.RefA.DocumentRefID and r.RefA.SpecialID
|
||||
if r.RefA.DocumentRefID != "" && requireAndTrimPrefix(r.RefA.DocumentRefID, "DocumentRef-") != string(doc.CreationInfo.SPDXIdentifier) {
|
||||
log.Debugf("ignoring relationship to external document: %+v", r)
|
||||
continue
|
||||
}
|
||||
a := spdxIDMap[string(r.RefA.ElementRefID)]
|
||||
b := spdxIDMap[string(r.RefB.ElementRefID)]
|
||||
from, fromOk := a.(*pkg.Package)
|
||||
toPackage, toPackageOk := b.(*pkg.Package)
|
||||
toLocation, toLocationOk := b.(*source.Location)
|
||||
if !fromOk || !(toPackageOk || toLocationOk) {
|
||||
log.Debugf("unable to find valid relationship mapping from SPDX 2.2 JSON, ignoring: (from: %+v) (to: %+v)", a, b)
|
||||
continue
|
||||
}
|
||||
var to artifact.Identifiable
|
||||
var typ artifact.RelationshipType
|
||||
if toLocationOk {
|
||||
if r.Relationship == string(ContainsRelationship) {
|
||||
typ = artifact.ContainsRelationship
|
||||
to = toLocation
|
||||
}
|
||||
} else {
|
||||
switch RelationshipType(r.Relationship) {
|
||||
case ContainsRelationship:
|
||||
typ = artifact.ContainsRelationship
|
||||
to = toPackage
|
||||
case BuildDependencyOfRelationship:
|
||||
typ = artifact.BuildDependencyOfRelationship
|
||||
to = toPackage
|
||||
case RuntimeDependencyOfRelationship:
|
||||
typ = artifact.RuntimeDependencyOfRelationship
|
||||
to = toPackage
|
||||
case OtherRelationship:
|
||||
// Encoding uses a specifically formatted comment...
|
||||
if strings.Index(r.RelationshipComment, string(artifact.OwnershipByFileOverlapRelationship)) == 0 {
|
||||
typ = artifact.RuntimeDependencyOfRelationship
|
||||
to = toPackage
|
||||
}
|
||||
}
|
||||
}
|
||||
if typ != "" && to != nil {
|
||||
out = append(out, artifact.Relationship{
|
||||
From: from,
|
||||
To: to,
|
||||
Type: typ,
|
||||
})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func toSyftCoordinates(f *spdx.File2_2) source.Coordinates {
|
||||
const layerIDPrefix = "layerID: "
|
||||
var fileSystemID string
|
||||
if strings.Index(f.FileComment, layerIDPrefix) == 0 {
|
||||
fileSystemID = strings.TrimPrefix(f.FileComment, layerIDPrefix)
|
||||
}
|
||||
if strings.Index(string(f.FileSPDXIdentifier), layerIDPrefix) == 0 {
|
||||
fileSystemID = strings.TrimPrefix(string(f.FileSPDXIdentifier), layerIDPrefix)
|
||||
}
|
||||
return source.Coordinates{
|
||||
RealPath: f.FileName,
|
||||
FileSystemID: fileSystemID,
|
||||
}
|
||||
}
|
||||
|
||||
func toSyftLocation(f *spdx.File2_2) *source.Location {
|
||||
return &source.Location{
|
||||
Coordinates: toSyftCoordinates(f),
|
||||
VirtualPath: f.FileName,
|
||||
}
|
||||
}
|
||||
|
||||
func requireAndTrimPrefix(val interface{}, prefix string) string {
|
||||
if v, ok := val.(string); ok {
|
||||
if i := strings.Index(v, prefix); i == 0 {
|
||||
return strings.Replace(v, prefix, "", 1)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type pkgInfo struct {
|
||||
purl packageurl.PackageURL
|
||||
typ pkg.Type
|
||||
lang pkg.Language
|
||||
}
|
||||
|
||||
func (p *pkgInfo) qualifierValue(name string) string {
|
||||
return findQualifierValue(p.purl, name)
|
||||
}
|
||||
|
||||
func findQualifierValue(purl packageurl.PackageURL, qualifier string) string {
|
||||
for _, q := range purl.Qualifiers {
|
||||
if q.Key == qualifier {
|
||||
return q.Value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func extractPkgInfo(p *spdx.Package2_2) pkgInfo {
|
||||
pu := findPURLValue(p)
|
||||
purl, err := packageurl.FromString(pu)
|
||||
if err != nil {
|
||||
return pkgInfo{}
|
||||
}
|
||||
return pkgInfo{
|
||||
purl,
|
||||
pkg.TypeByName(purl.Type),
|
||||
pkg.LanguageByName(purl.Type),
|
||||
}
|
||||
}
|
||||
|
||||
func toSyftPackage(p *spdx.Package2_2) *pkg.Package {
|
||||
info := extractPkgInfo(p)
|
||||
metadataType, metadata := extractMetadata(p, info)
|
||||
sP := pkg.Package{
|
||||
Type: info.typ,
|
||||
Name: p.PackageName,
|
||||
Version: p.PackageVersion,
|
||||
Licenses: parseLicense(p.PackageLicenseDeclared),
|
||||
CPEs: extractCPEs(p),
|
||||
PURL: info.purl.String(),
|
||||
Language: info.lang,
|
||||
MetadataType: metadataType,
|
||||
Metadata: metadata,
|
||||
}
|
||||
|
||||
sP.SetID()
|
||||
|
||||
return &sP
|
||||
}
|
||||
|
||||
func extractMetadata(p *spdx.Package2_2, info pkgInfo) (pkg.MetadataType, interface{}) {
|
||||
arch := info.qualifierValue(pkg.PURLQualifierArch)
|
||||
upstreamValue := info.qualifierValue(pkg.PURLQualifierUpstream)
|
||||
upstream := strings.SplitN(upstreamValue, "@", 2)
|
||||
upstreamName := upstream[0]
|
||||
upstreamVersion := ""
|
||||
if len(upstream) > 1 {
|
||||
upstreamVersion = upstream[1]
|
||||
}
|
||||
switch info.typ {
|
||||
case pkg.ApkPkg:
|
||||
return pkg.ApkMetadataType, pkg.ApkMetadata{
|
||||
Package: p.PackageName,
|
||||
OriginPackage: upstreamName,
|
||||
Maintainer: p.PackageSupplierPerson,
|
||||
Version: p.PackageVersion,
|
||||
License: p.PackageLicenseDeclared,
|
||||
Architecture: arch,
|
||||
URL: p.PackageHomePage,
|
||||
Description: p.PackageDescription,
|
||||
}
|
||||
case pkg.RpmPkg:
|
||||
converted, err := strconv.Atoi(info.qualifierValue(pkg.PURLQualifierEpoch))
|
||||
var epoch *int
|
||||
if err != nil {
|
||||
epoch = nil
|
||||
} else {
|
||||
epoch = &converted
|
||||
}
|
||||
return pkg.RpmdbMetadataType, pkg.RpmdbMetadata{
|
||||
Name: p.PackageName,
|
||||
Version: p.PackageVersion,
|
||||
Epoch: epoch,
|
||||
Arch: arch,
|
||||
SourceRpm: upstreamValue,
|
||||
License: p.PackageLicenseConcluded,
|
||||
Vendor: p.PackageOriginatorOrganization,
|
||||
}
|
||||
case pkg.DebPkg:
|
||||
return pkg.DpkgMetadataType, pkg.DpkgMetadata{
|
||||
Package: p.PackageName,
|
||||
Source: upstreamName,
|
||||
Version: p.PackageVersion,
|
||||
SourceVersion: upstreamVersion,
|
||||
Architecture: arch,
|
||||
Maintainer: p.PackageOriginatorPerson,
|
||||
}
|
||||
}
|
||||
return pkg.UnknownMetadataType, nil
|
||||
}
|
||||
|
||||
func findPURLValue(p *spdx.Package2_2) string {
|
||||
for _, r := range p.PackageExternalReferences {
|
||||
if r.RefType == string(PurlExternalRefType) {
|
||||
return r.Locator
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func extractCPEs(p *spdx.Package2_2) (cpes []pkg.CPE) {
|
||||
for _, r := range p.PackageExternalReferences {
|
||||
if r.RefType == string(Cpe23ExternalRefType) {
|
||||
cpe, err := pkg.NewCPE(r.Locator)
|
||||
if err != nil {
|
||||
log.Warnf("unable to extract SPDX CPE=%q: %+v", r.Locator, err)
|
||||
continue
|
||||
}
|
||||
cpes = append(cpes, cpe)
|
||||
}
|
||||
}
|
||||
return cpes
|
||||
}
|
||||
|
||||
func parseLicense(l string) []string {
|
||||
if l == NOASSERTION || l == NONE {
|
||||
return nil
|
||||
}
|
||||
return strings.Split(l, " AND ")
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
package spdxhelpers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/spdx/tools-golang/spdx"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestToSyftModel(t *testing.T) {
|
||||
sbom, err := ToSyftModel(&spdx.Document2_2{
|
||||
CreationInfo: &spdx.CreationInfo2_2{
|
||||
SPDXVersion: "1",
|
||||
DataLicense: "GPL",
|
||||
SPDXIdentifier: "id-doc-1",
|
||||
DocumentName: "docName",
|
||||
DocumentNamespace: "docNamespace",
|
||||
ExternalDocumentReferences: nil,
|
||||
LicenseListVersion: "",
|
||||
CreatorPersons: nil,
|
||||
CreatorOrganizations: nil,
|
||||
CreatorTools: nil,
|
||||
Created: "",
|
||||
CreatorComment: "",
|
||||
DocumentComment: "",
|
||||
},
|
||||
Packages: map[spdx.ElementID]*spdx.Package2_2{
|
||||
"id-pkg-1": {
|
||||
PackageName: "pkg-1",
|
||||
PackageSPDXIdentifier: "id-pkg-1",
|
||||
PackageVersion: "5.4.3",
|
||||
PackageSupplierPerson: "",
|
||||
PackageSupplierOrganization: "",
|
||||
PackageLicenseDeclared: "",
|
||||
PackageDescription: "",
|
||||
PackageExternalReferences: []*spdx.PackageExternalReference2_2{
|
||||
{
|
||||
Category: "SECURITY",
|
||||
Locator: "cpe:2.3:a:pkg-1:pkg-1:5.4.3:*:*:*:*:*:*:*",
|
||||
RefType: "cpe23Type",
|
||||
},
|
||||
{
|
||||
Category: "SECURITY",
|
||||
Locator: "cpe:2.3:a:pkg_1:pkg_1:5.4.3:*:*:*:*:*:*:*",
|
||||
RefType: "cpe23Type",
|
||||
},
|
||||
{
|
||||
Category: "PACKAGE_MANAGER",
|
||||
Locator: "pkg:alpine/pkg-1@5.4.3?arch=x86_64&upstream=p1-origin&distro=alpine-3.10.9",
|
||||
RefType: "purl",
|
||||
},
|
||||
},
|
||||
Files: nil,
|
||||
},
|
||||
"id-pkg-2": {
|
||||
PackageName: "pkg-2",
|
||||
PackageSPDXIdentifier: "id-pkg-2",
|
||||
PackageVersion: "7.3.1",
|
||||
PackageSupplierPerson: "",
|
||||
PackageSupplierOrganization: "",
|
||||
PackageLicenseDeclared: "",
|
||||
PackageDescription: "",
|
||||
PackageExternalReferences: []*spdx.PackageExternalReference2_2{
|
||||
{
|
||||
Category: "SECURITY",
|
||||
Locator: "cpe:2.3:a:pkg-2:pkg-2:7.3.1:*:*:*:*:*:*:*",
|
||||
RefType: "cpe23Type",
|
||||
},
|
||||
{
|
||||
Category: "SECURITY",
|
||||
Locator: "cpe:2.3:a:pkg_2:pkg_2:7.3.1:*:*:*:*:*:*:*",
|
||||
RefType: "cpe23Type",
|
||||
},
|
||||
{
|
||||
Category: "SECURITY",
|
||||
Locator: "cpe:2.3:a:pkg-2:pkg_2:7.3.1:*:*:*:*:*:*:*",
|
||||
RefType: "cpe23Type",
|
||||
},
|
||||
{
|
||||
Category: "PACKAGE_MANAGER",
|
||||
Locator: "pkg:deb/pkg-2@7.3.1?arch=x86_64&upstream=p2-origin@9.1.3&distro=debian-3.10.9",
|
||||
RefType: "purl",
|
||||
},
|
||||
},
|
||||
Files: nil,
|
||||
},
|
||||
},
|
||||
UnpackagedFiles: map[spdx.ElementID]*spdx.File2_2{},
|
||||
Relationships: []*spdx.Relationship2_2{},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NotNil(t, sbom)
|
||||
|
||||
pkgs := sbom.Artifacts.PackageCatalog.Sorted()
|
||||
|
||||
assert.Len(t, pkgs, 2)
|
||||
|
||||
p1 := pkgs[0]
|
||||
assert.Equal(t, p1.Name, "pkg-1")
|
||||
assert.Equal(t, p1.MetadataType, pkg.ApkMetadataType)
|
||||
p1meta := p1.Metadata.(pkg.ApkMetadata)
|
||||
assert.Equal(t, p1meta.OriginPackage, "p1-origin")
|
||||
assert.Len(t, p1.CPEs, 2)
|
||||
|
||||
p2 := pkgs[1]
|
||||
assert.Equal(t, p2.Name, "pkg-2")
|
||||
assert.Equal(t, p2.MetadataType, pkg.DpkgMetadataType)
|
||||
p2meta := p2.Metadata.(pkg.DpkgMetadata)
|
||||
assert.Equal(t, p2meta.Source, "p2-origin")
|
||||
assert.Equal(t, p2meta.SourceVersion, "9.1.3")
|
||||
assert.Len(t, p2.CPEs, 3)
|
||||
}
|
||||
|
||||
func Test_extractMetadata(t *testing.T) {
|
||||
oneTwoThreeFour := 1234
|
||||
tests := []struct {
|
||||
pkg spdx.Package2_2
|
||||
metaType pkg.MetadataType
|
||||
meta interface{}
|
||||
}{
|
||||
{
|
||||
pkg: spdx.Package2_2{
|
||||
PackageName: "SomeDebPkg",
|
||||
PackageVersion: "43.1.235",
|
||||
PackageExternalReferences: []*spdx.PackageExternalReference2_2{
|
||||
{
|
||||
Category: "PACKAGE_MANAGER",
|
||||
Locator: "pkg:deb/pkg-2@7.3.1?arch=x86_64&upstream=somedebpkg-origin@9.1.3&distro=debian-3.10.9",
|
||||
RefType: "purl",
|
||||
},
|
||||
},
|
||||
},
|
||||
metaType: pkg.DpkgMetadataType,
|
||||
meta: pkg.DpkgMetadata{
|
||||
Package: "SomeDebPkg",
|
||||
Source: "somedebpkg-origin",
|
||||
Version: "43.1.235",
|
||||
SourceVersion: "9.1.3",
|
||||
Architecture: "x86_64",
|
||||
},
|
||||
},
|
||||
{
|
||||
pkg: spdx.Package2_2{
|
||||
PackageName: "SomeApkPkg",
|
||||
PackageVersion: "3.2.9",
|
||||
PackageExternalReferences: []*spdx.PackageExternalReference2_2{
|
||||
{
|
||||
Category: "PACKAGE_MANAGER",
|
||||
Locator: "pkg:alpine/pkg-2@7.3.1?arch=x86_64&upstream=apk-origin@9.1.3&distro=alpine-3.10.9",
|
||||
RefType: "purl",
|
||||
},
|
||||
},
|
||||
},
|
||||
metaType: pkg.ApkMetadataType,
|
||||
meta: pkg.ApkMetadata{
|
||||
Package: "SomeApkPkg",
|
||||
OriginPackage: "apk-origin",
|
||||
Version: "3.2.9",
|
||||
Architecture: "x86_64",
|
||||
},
|
||||
},
|
||||
{
|
||||
pkg: spdx.Package2_2{
|
||||
PackageName: "SomeRpmPkg",
|
||||
PackageVersion: "13.2.79",
|
||||
PackageExternalReferences: []*spdx.PackageExternalReference2_2{
|
||||
{
|
||||
Category: "PACKAGE_MANAGER",
|
||||
Locator: "pkg:rpm/pkg-2@7.3.1?arch=x86_64&epoch=1234&upstream=some-rpm-origin-1.16.3&distro=alpine-3.10.9",
|
||||
RefType: "purl",
|
||||
},
|
||||
},
|
||||
},
|
||||
metaType: pkg.RpmdbMetadataType,
|
||||
meta: pkg.RpmdbMetadata{
|
||||
Name: "SomeRpmPkg",
|
||||
Version: "13.2.79",
|
||||
Epoch: &oneTwoThreeFour,
|
||||
Arch: "x86_64",
|
||||
Release: "",
|
||||
SourceRpm: "some-rpm-origin-1.16.3",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.pkg.PackageName, func(t *testing.T) {
|
||||
info := extractPkgInfo(&test.pkg)
|
||||
metaType, meta := extractMetadata(&test.pkg, info)
|
||||
assert.Equal(t, test.metaType, metaType)
|
||||
assert.EqualValues(t, test.meta, meta)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/anchore/stereoscope/pkg/filetree"
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
"github.com/anchore/syft/syft/format"
|
||||
"github.com/anchore/syft/syft/linux"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
@@ -32,7 +31,7 @@ func FromSnapshot() ImageOption {
|
||||
}
|
||||
}
|
||||
|
||||
func AssertEncoderAgainstGoldenImageSnapshot(t *testing.T, format format.Format, sbom sbom.SBOM, testImage string, updateSnapshot bool, redactors ...redactor) {
|
||||
func AssertEncoderAgainstGoldenImageSnapshot(t *testing.T, format sbom.Format, sbom sbom.SBOM, testImage string, updateSnapshot bool, redactors ...redactor) {
|
||||
var buffer bytes.Buffer
|
||||
|
||||
// grab the latest image contents and persist
|
||||
@@ -66,7 +65,7 @@ func AssertEncoderAgainstGoldenImageSnapshot(t *testing.T, format format.Format,
|
||||
}
|
||||
}
|
||||
|
||||
func AssertEncoderAgainstGoldenSnapshot(t *testing.T, format format.Format, sbom sbom.SBOM, updateSnapshot bool, redactors ...redactor) {
|
||||
func AssertEncoderAgainstGoldenSnapshot(t *testing.T, format sbom.Format, sbom sbom.SBOM, updateSnapshot bool, redactors ...redactor) {
|
||||
var buffer bytes.Buffer
|
||||
|
||||
err := format.Encode(&buffer, sbom)
|
||||
@@ -158,9 +157,9 @@ func populateImageCatalog(catalog *pkg.Catalog, img *image.Image) {
|
||||
catalog.Add(pkg.Package{
|
||||
Name: "package-1",
|
||||
Version: "1.0.1",
|
||||
Locations: []source.Location{
|
||||
Locations: source.NewLocationSet(
|
||||
source.NewLocationFromImage(string(ref1.RealPath), *ref1, img),
|
||||
},
|
||||
),
|
||||
Type: pkg.PythonPkg,
|
||||
FoundBy: "the-cataloger-1",
|
||||
Language: pkg.Python,
|
||||
@@ -170,7 +169,7 @@ func populateImageCatalog(catalog *pkg.Catalog, img *image.Image) {
|
||||
Name: "package-1",
|
||||
Version: "1.0.1",
|
||||
},
|
||||
PURL: "a-purl-1",
|
||||
PURL: "a-purl-1", // intentionally a bad pURL for test fixtures
|
||||
CPEs: []pkg.CPE{
|
||||
pkg.MustCPE("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*"),
|
||||
},
|
||||
@@ -178,9 +177,9 @@ func populateImageCatalog(catalog *pkg.Catalog, img *image.Image) {
|
||||
catalog.Add(pkg.Package{
|
||||
Name: "package-2",
|
||||
Version: "2.0.1",
|
||||
Locations: []source.Location{
|
||||
Locations: source.NewLocationSet(
|
||||
source.NewLocationFromImage(string(ref2.RealPath), *ref2, img),
|
||||
},
|
||||
),
|
||||
Type: pkg.DebPkg,
|
||||
FoundBy: "the-cataloger-2",
|
||||
MetadataType: pkg.DpkgMetadataType,
|
||||
@@ -188,7 +187,7 @@ func populateImageCatalog(catalog *pkg.Catalog, img *image.Image) {
|
||||
Package: "package-2",
|
||||
Version: "2.0.1",
|
||||
},
|
||||
PURL: "a-purl-2",
|
||||
PURL: "pkg:deb/debian/package-2@2.0.1",
|
||||
CPEs: []pkg.CPE{
|
||||
pkg.MustCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"),
|
||||
},
|
||||
@@ -235,9 +234,9 @@ func newDirectoryCatalog() *pkg.Catalog {
|
||||
Version: "1.0.1",
|
||||
Type: pkg.PythonPkg,
|
||||
FoundBy: "the-cataloger-1",
|
||||
Locations: []source.Location{
|
||||
Locations: source.NewLocationSet(
|
||||
source.NewLocation("/some/path/pkg1"),
|
||||
},
|
||||
),
|
||||
Language: pkg.Python,
|
||||
MetadataType: pkg.PythonPackageMetadataType,
|
||||
Licenses: []string{"MIT"},
|
||||
@@ -250,7 +249,7 @@ func newDirectoryCatalog() *pkg.Catalog {
|
||||
},
|
||||
},
|
||||
},
|
||||
PURL: "a-purl-2",
|
||||
PURL: "a-purl-2", // intentionally a bad pURL for test fixtures
|
||||
CPEs: []pkg.CPE{
|
||||
pkg.MustCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"),
|
||||
},
|
||||
@@ -260,15 +259,15 @@ func newDirectoryCatalog() *pkg.Catalog {
|
||||
Version: "2.0.1",
|
||||
Type: pkg.DebPkg,
|
||||
FoundBy: "the-cataloger-2",
|
||||
Locations: []source.Location{
|
||||
Locations: source.NewLocationSet(
|
||||
source.NewLocation("/some/path/pkg1"),
|
||||
},
|
||||
),
|
||||
MetadataType: pkg.DpkgMetadataType,
|
||||
Metadata: pkg.DpkgMetadata{
|
||||
Package: "package-2",
|
||||
Version: "2.0.1",
|
||||
},
|
||||
PURL: "a-purl-2",
|
||||
PURL: "pkg:deb/debian/package-2@2.0.1",
|
||||
CPEs: []pkg.CPE{
|
||||
pkg.MustCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"),
|
||||
},
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
package cyclonedx13json
|
||||
|
||||
import "github.com/anchore/syft/syft/format"
|
||||
|
||||
func Format() format.Format {
|
||||
return format.NewFormat(
|
||||
format.CycloneDxJSONOption,
|
||||
encoder,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package cyclonedx13xml
|
||||
|
||||
import "github.com/anchore/syft/syft/format"
|
||||
|
||||
func Format() format.Format {
|
||||
return format.NewFormat(
|
||||
format.CycloneDxXMLOption,
|
||||
encoder,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
}
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bom xmlns="http://cyclonedx.org/schema/bom/1.3" serialNumber="urn:uuid:16a426e7-fcc7-4b94-abd2-66c67569cc44" version="1">
|
||||
<metadata>
|
||||
<timestamp>2022-01-14T22:46:49Z</timestamp>
|
||||
<tools>
|
||||
<tool>
|
||||
<vendor>anchore</vendor>
|
||||
<name>syft</name>
|
||||
<version>[not provided]</version>
|
||||
</tool>
|
||||
</tools>
|
||||
<component type="file">
|
||||
<name>/some/path</name>
|
||||
<version></version>
|
||||
</component>
|
||||
</metadata>
|
||||
<components>
|
||||
<component type="library">
|
||||
<name>package-1</name>
|
||||
<version>1.0.1</version>
|
||||
<licenses>
|
||||
<license>
|
||||
<id>MIT</id>
|
||||
</license>
|
||||
</licenses>
|
||||
<cpe>cpe:2.3:*:some:package:2:*:*:*:*:*:*:*</cpe>
|
||||
<purl>a-purl-2</purl>
|
||||
<properties>
|
||||
<property name="foundBy">the-cataloger-1</property>
|
||||
<property name="language">python</property>
|
||||
<property name="type">python</property>
|
||||
<property name="metadataType">PythonPackageMetadata</property>
|
||||
<property name="path">/some/path/pkg1</property>
|
||||
</properties>
|
||||
</component>
|
||||
<component type="library">
|
||||
<name>package-2</name>
|
||||
<version>2.0.1</version>
|
||||
<cpe>cpe:2.3:*:some:package:2:*:*:*:*:*:*:*</cpe>
|
||||
<purl>a-purl-2</purl>
|
||||
<properties>
|
||||
<property name="foundBy">the-cataloger-2</property>
|
||||
<property name="type">deb</property>
|
||||
<property name="metadataType">DpkgMetadata</property>
|
||||
<property name="path">/some/path/pkg1</property>
|
||||
</properties>
|
||||
</component>
|
||||
<component type="operating-system">
|
||||
<name>debian</name>
|
||||
<version>1.2.3</version>
|
||||
<properties>
|
||||
<property name="prettyName">debian</property>
|
||||
<property name="id">debian</property>
|
||||
<property name="versionID">1.2.3</property>
|
||||
</properties>
|
||||
</component>
|
||||
</components>
|
||||
</bom>
|
||||
-60
@@ -1,60 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bom xmlns="http://cyclonedx.org/schema/bom/1.3" serialNumber="urn:uuid:a3d776c6-a2ca-4116-8b99-b3538dd1a460" version="1">
|
||||
<metadata>
|
||||
<timestamp>2022-01-14T22:46:49Z</timestamp>
|
||||
<tools>
|
||||
<tool>
|
||||
<vendor>anchore</vendor>
|
||||
<name>syft</name>
|
||||
<version>[not provided]</version>
|
||||
</tool>
|
||||
</tools>
|
||||
<component type="container">
|
||||
<name>user-image-input</name>
|
||||
<version>sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368</version>
|
||||
</component>
|
||||
</metadata>
|
||||
<components>
|
||||
<component type="library">
|
||||
<name>package-1</name>
|
||||
<version>1.0.1</version>
|
||||
<licenses>
|
||||
<license>
|
||||
<id>MIT</id>
|
||||
</license>
|
||||
</licenses>
|
||||
<cpe>cpe:2.3:*:some:package:1:*:*:*:*:*:*:*</cpe>
|
||||
<purl>a-purl-1</purl>
|
||||
<properties>
|
||||
<property name="foundBy">the-cataloger-1</property>
|
||||
<property name="language">python</property>
|
||||
<property name="type">python</property>
|
||||
<property name="metadataType">PythonPackageMetadata</property>
|
||||
<property name="path">/somefile-1.txt</property>
|
||||
<property name="layerID">sha256:16e64541f2ddf59a90391ce7bb8af90313f7d373f2105d88f3d3267b72e0ebab</property>
|
||||
</properties>
|
||||
</component>
|
||||
<component type="library">
|
||||
<name>package-2</name>
|
||||
<version>2.0.1</version>
|
||||
<cpe>cpe:2.3:*:some:package:2:*:*:*:*:*:*:*</cpe>
|
||||
<purl>a-purl-2</purl>
|
||||
<properties>
|
||||
<property name="foundBy">the-cataloger-2</property>
|
||||
<property name="type">deb</property>
|
||||
<property name="metadataType">DpkgMetadata</property>
|
||||
<property name="path">/somefile-2.txt</property>
|
||||
<property name="layerID">sha256:de6c235f76ea24c8503ec08891445b5d6a8bdf8249117ed8d8b0b6fb3ebe4f67</property>
|
||||
</properties>
|
||||
</component>
|
||||
<component type="operating-system">
|
||||
<name>debian</name>
|
||||
<version>1.2.3</version>
|
||||
<properties>
|
||||
<property name="prettyName">debian</property>
|
||||
<property name="id">debian</property>
|
||||
<property name="versionID">1.2.3</property>
|
||||
</properties>
|
||||
</component>
|
||||
</components>
|
||||
</bom>
|
||||
@@ -0,0 +1,70 @@
|
||||
package cyclonedxjson
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_decodeJSON(t *testing.T) {
|
||||
tests := []struct {
|
||||
file string
|
||||
err bool
|
||||
distro string
|
||||
packages []string
|
||||
}{
|
||||
{
|
||||
file: "snapshot/TestCycloneDxDirectoryEncoder.golden",
|
||||
distro: "debian:1.2.3",
|
||||
packages: []string{"package-1:1.0.1", "package-2:2.0.1"},
|
||||
},
|
||||
{
|
||||
file: "snapshot/TestCycloneDxImageEncoder.golden",
|
||||
distro: "debian:1.2.3",
|
||||
packages: []string{"package-1:1.0.1", "package-2:2.0.1"},
|
||||
},
|
||||
{
|
||||
file: "image-simple/Dockerfile",
|
||||
err: true,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.file, func(t *testing.T) {
|
||||
reader, err := os.Open("test-fixtures/" + test.file)
|
||||
assert.NoError(t, err)
|
||||
|
||||
if test.err {
|
||||
err = Format().Validate(reader)
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
bom, err := Format().Decode(reader)
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
split := strings.SplitN(test.distro, ":", 2)
|
||||
name := split[0]
|
||||
version := split[1]
|
||||
assert.Equal(t, bom.Artifacts.LinuxDistribution.ID, name)
|
||||
assert.Equal(t, bom.Artifacts.LinuxDistribution.Version, version)
|
||||
|
||||
pkgs:
|
||||
for _, pkg := range test.packages {
|
||||
split = strings.SplitN(pkg, ":", 2)
|
||||
name = split[0]
|
||||
version = split[1]
|
||||
for p := range bom.Artifacts.PackageCatalog.Enumerate() {
|
||||
if p.Name == name {
|
||||
assert.Equal(t, version, p.Version)
|
||||
continue pkgs
|
||||
}
|
||||
}
|
||||
assert.Fail(t, fmt.Sprintf("package should be present: %s", pkg))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package cyclonedx13json
|
||||
package cyclonedxjson
|
||||
|
||||
import (
|
||||
"io"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package cyclonedx13json
|
||||
package cyclonedxjson
|
||||
|
||||
import (
|
||||
"flag"
|
||||
@@ -0,0 +1,18 @@
|
||||
package cyclonedxjson
|
||||
|
||||
import (
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
"github.com/anchore/syft/internal/formats/common/cyclonedxhelpers"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
const ID sbom.FormatID = "cyclonedx-1-json"
|
||||
|
||||
func Format() sbom.Format {
|
||||
return sbom.NewFormat(
|
||||
ID,
|
||||
encoder,
|
||||
cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatJSON),
|
||||
cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatJSON),
|
||||
)
|
||||
}
|
||||
+40
-24
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.3",
|
||||
"serialNumber": "urn:uuid:258d2616-5b1f-48cd-82a3-d6c95e262950",
|
||||
"specVersion": "1.4",
|
||||
"serialNumber": "urn:uuid:dec3f6b4-8458-48bb-b60d-dfd312f6ec4e",
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"timestamp": "2022-01-14T22:47:00Z",
|
||||
"timestamp": "2022-04-01T11:48:04-04:00",
|
||||
"tools": [
|
||||
{
|
||||
"vendor": "anchore",
|
||||
@@ -13,13 +13,14 @@
|
||||
}
|
||||
],
|
||||
"component": {
|
||||
"bom-ref": "163686ac6e30c752",
|
||||
"type": "file",
|
||||
"name": "/some/path",
|
||||
"version": ""
|
||||
"name": "/some/path"
|
||||
}
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
"bom-ref": "b85dbb4e6ece5082",
|
||||
"type": "library",
|
||||
"name": "package-1",
|
||||
"version": "1.0.1",
|
||||
@@ -34,49 +35,54 @@
|
||||
"purl": "a-purl-2",
|
||||
"properties": [
|
||||
{
|
||||
"name": "foundBy",
|
||||
"name": "syft:package:foundBy",
|
||||
"value": "the-cataloger-1"
|
||||
},
|
||||
{
|
||||
"name": "language",
|
||||
"name": "syft:package:language",
|
||||
"value": "python"
|
||||
},
|
||||
{
|
||||
"name": "type",
|
||||
"value": "python"
|
||||
},
|
||||
{
|
||||
"name": "metadataType",
|
||||
"name": "syft:package:metadataType",
|
||||
"value": "PythonPackageMetadata"
|
||||
},
|
||||
{
|
||||
"name": "path",
|
||||
"name": "syft:package:type",
|
||||
"value": "python"
|
||||
},
|
||||
{
|
||||
"name": "syft:location:0:path",
|
||||
"value": "/some/path/pkg1"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"bom-ref": "pkg:deb/debian/package-2@2.0.1?syft-id=ceda99598967ae8d",
|
||||
"type": "library",
|
||||
"name": "package-2",
|
||||
"version": "2.0.1",
|
||||
"cpe": "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*",
|
||||
"purl": "a-purl-2",
|
||||
"purl": "pkg:deb/debian/package-2@2.0.1",
|
||||
"properties": [
|
||||
{
|
||||
"name": "foundBy",
|
||||
"name": "syft:package:foundBy",
|
||||
"value": "the-cataloger-2"
|
||||
},
|
||||
{
|
||||
"name": "type",
|
||||
"value": "deb"
|
||||
},
|
||||
{
|
||||
"name": "metadataType",
|
||||
"name": "syft:package:metadataType",
|
||||
"value": "DpkgMetadata"
|
||||
},
|
||||
{
|
||||
"name": "path",
|
||||
"name": "syft:package:type",
|
||||
"value": "deb"
|
||||
},
|
||||
{
|
||||
"name": "syft:location:0:path",
|
||||
"value": "/some/path/pkg1"
|
||||
},
|
||||
{
|
||||
"name": "syft:metadata:installedSize",
|
||||
"value": "0"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -84,17 +90,27 @@
|
||||
"type": "operating-system",
|
||||
"name": "debian",
|
||||
"version": "1.2.3",
|
||||
"description": "debian",
|
||||
"swid": {
|
||||
"tagId": "debian",
|
||||
"name": "debian",
|
||||
"version": "1.2.3"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"name": "prettyName",
|
||||
"name": "syft:distro:id",
|
||||
"value": "debian"
|
||||
},
|
||||
{
|
||||
"name": "id",
|
||||
"name": "syft:distro:idLike:0",
|
||||
"value": "like!"
|
||||
},
|
||||
{
|
||||
"name": "syft:distro:prettyName",
|
||||
"value": "debian"
|
||||
},
|
||||
{
|
||||
"name": "versionID",
|
||||
"name": "syft:distro:versionID",
|
||||
"value": "1.2.3"
|
||||
}
|
||||
]
|
||||
+44
-27
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.3",
|
||||
"serialNumber": "urn:uuid:8a84b1cf-e918-4842-a6a8-c7fdafc55bc0",
|
||||
"specVersion": "1.4",
|
||||
"serialNumber": "urn:uuid:054d973e-fe99-4762-92e4-eaf01997ae41",
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"timestamp": "2022-01-14T22:47:00Z",
|
||||
"timestamp": "2022-04-01T11:48:04-04:00",
|
||||
"tools": [
|
||||
{
|
||||
"vendor": "anchore",
|
||||
@@ -13,6 +13,7 @@
|
||||
}
|
||||
],
|
||||
"component": {
|
||||
"bom-ref": "e777314b02b362e4",
|
||||
"type": "container",
|
||||
"name": "user-image-input",
|
||||
"version": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368"
|
||||
@@ -20,6 +21,7 @@
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
"bom-ref": "2a46171f91c8d4bc",
|
||||
"type": "library",
|
||||
"name": "package-1",
|
||||
"version": "1.0.1",
|
||||
@@ -34,57 +36,62 @@
|
||||
"purl": "a-purl-1",
|
||||
"properties": [
|
||||
{
|
||||
"name": "foundBy",
|
||||
"name": "syft:package:foundBy",
|
||||
"value": "the-cataloger-1"
|
||||
},
|
||||
{
|
||||
"name": "language",
|
||||
"name": "syft:package:language",
|
||||
"value": "python"
|
||||
},
|
||||
{
|
||||
"name": "type",
|
||||
"value": "python"
|
||||
},
|
||||
{
|
||||
"name": "metadataType",
|
||||
"name": "syft:package:metadataType",
|
||||
"value": "PythonPackageMetadata"
|
||||
},
|
||||
{
|
||||
"name": "path",
|
||||
"value": "/somefile-1.txt"
|
||||
"name": "syft:package:type",
|
||||
"value": "python"
|
||||
},
|
||||
{
|
||||
"name": "layerID",
|
||||
"value": "sha256:16e64541f2ddf59a90391ce7bb8af90313f7d373f2105d88f3d3267b72e0ebab"
|
||||
"name": "syft:location:0:layerID",
|
||||
"value": "sha256:fb6beecb75b39f4bb813dbf177e501edd5ddb3e69bb45cedeb78c676ee1b7a59"
|
||||
},
|
||||
{
|
||||
"name": "syft:location:0:path",
|
||||
"value": "/somefile-1.txt"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"bom-ref": "pkg:deb/debian/package-2@2.0.1?syft-id=ae77680e9b1d087e",
|
||||
"type": "library",
|
||||
"name": "package-2",
|
||||
"version": "2.0.1",
|
||||
"cpe": "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*",
|
||||
"purl": "a-purl-2",
|
||||
"purl": "pkg:deb/debian/package-2@2.0.1",
|
||||
"properties": [
|
||||
{
|
||||
"name": "foundBy",
|
||||
"name": "syft:package:foundBy",
|
||||
"value": "the-cataloger-2"
|
||||
},
|
||||
{
|
||||
"name": "type",
|
||||
"value": "deb"
|
||||
},
|
||||
{
|
||||
"name": "metadataType",
|
||||
"name": "syft:package:metadataType",
|
||||
"value": "DpkgMetadata"
|
||||
},
|
||||
{
|
||||
"name": "path",
|
||||
"name": "syft:package:type",
|
||||
"value": "deb"
|
||||
},
|
||||
{
|
||||
"name": "syft:location:0:layerID",
|
||||
"value": "sha256:319b588ce64253a87b533c8ed01cf0025e0eac98e7b516e12532957e1244fdec"
|
||||
},
|
||||
{
|
||||
"name": "syft:location:0:path",
|
||||
"value": "/somefile-2.txt"
|
||||
},
|
||||
{
|
||||
"name": "layerID",
|
||||
"value": "sha256:de6c235f76ea24c8503ec08891445b5d6a8bdf8249117ed8d8b0b6fb3ebe4f67"
|
||||
"name": "syft:metadata:installedSize",
|
||||
"value": "0"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -92,17 +99,27 @@
|
||||
"type": "operating-system",
|
||||
"name": "debian",
|
||||
"version": "1.2.3",
|
||||
"description": "debian",
|
||||
"swid": {
|
||||
"tagId": "debian",
|
||||
"name": "debian",
|
||||
"version": "1.2.3"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"name": "prettyName",
|
||||
"name": "syft:distro:id",
|
||||
"value": "debian"
|
||||
},
|
||||
{
|
||||
"name": "id",
|
||||
"name": "syft:distro:idLike:0",
|
||||
"value": "like!"
|
||||
},
|
||||
{
|
||||
"name": "syft:distro:prettyName",
|
||||
"value": "debian"
|
||||
},
|
||||
{
|
||||
"name": "versionID",
|
||||
"name": "syft:distro:versionID",
|
||||
"value": "1.2.3"
|
||||
}
|
||||
]
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,70 @@
|
||||
package cyclonedxxml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_decodeXML(t *testing.T) {
|
||||
tests := []struct {
|
||||
file string
|
||||
err bool
|
||||
distro string
|
||||
packages []string
|
||||
}{
|
||||
{
|
||||
file: "snapshot/TestCycloneDxDirectoryEncoder.golden",
|
||||
distro: "debian:1.2.3",
|
||||
packages: []string{"package-1:1.0.1", "package-2:2.0.1"},
|
||||
},
|
||||
{
|
||||
file: "snapshot/TestCycloneDxImageEncoder.golden",
|
||||
distro: "debian:1.2.3",
|
||||
packages: []string{"package-1:1.0.1", "package-2:2.0.1"},
|
||||
},
|
||||
{
|
||||
file: "image-simple/Dockerfile",
|
||||
err: true,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.file, func(t *testing.T) {
|
||||
reader, err := os.Open("test-fixtures/" + test.file)
|
||||
assert.NoError(t, err)
|
||||
|
||||
if test.err {
|
||||
err = Format().Validate(reader)
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
bom, err := Format().Decode(reader)
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
split := strings.SplitN(test.distro, ":", 2)
|
||||
name := split[0]
|
||||
version := split[1]
|
||||
assert.Equal(t, bom.Artifacts.LinuxDistribution.ID, name)
|
||||
assert.Equal(t, bom.Artifacts.LinuxDistribution.Version, version)
|
||||
|
||||
pkgs:
|
||||
for _, pkg := range test.packages {
|
||||
split = strings.SplitN(pkg, ":", 2)
|
||||
name = split[0]
|
||||
version = split[1]
|
||||
for p := range bom.Artifacts.PackageCatalog.Enumerate() {
|
||||
if p.Name == name {
|
||||
assert.Equal(t, version, p.Version)
|
||||
continue pkgs
|
||||
}
|
||||
}
|
||||
assert.Fail(t, fmt.Sprintf("package should be present: %s", pkg))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package cyclonedx13xml
|
||||
package cyclonedxxml
|
||||
|
||||
import (
|
||||
"io"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package cyclonedx13xml
|
||||
package cyclonedxxml
|
||||
|
||||
import (
|
||||
"flag"
|
||||
@@ -0,0 +1,18 @@
|
||||
package cyclonedxxml
|
||||
|
||||
import (
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
"github.com/anchore/syft/internal/formats/common/cyclonedxhelpers"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
const ID sbom.FormatID = "cyclonedx-1-xml"
|
||||
|
||||
func Format() sbom.Format {
|
||||
return sbom.NewFormat(
|
||||
ID,
|
||||
encoder,
|
||||
cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatXML),
|
||||
cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatXML),
|
||||
)
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bom xmlns="http://cyclonedx.org/schema/bom/1.4" serialNumber="urn:uuid:554fd820-210b-40c8-8c0b-75690274e21c" version="1">
|
||||
<metadata>
|
||||
<timestamp>2022-04-01T11:57:46-04:00</timestamp>
|
||||
<tools>
|
||||
<tool>
|
||||
<vendor>anchore</vendor>
|
||||
<name>syft</name>
|
||||
<version>[not provided]</version>
|
||||
</tool>
|
||||
</tools>
|
||||
<component bom-ref="163686ac6e30c752" type="file">
|
||||
<name>/some/path</name>
|
||||
</component>
|
||||
</metadata>
|
||||
<components>
|
||||
<component bom-ref="b85dbb4e6ece5082" type="library">
|
||||
<name>package-1</name>
|
||||
<version>1.0.1</version>
|
||||
<licenses>
|
||||
<license>
|
||||
<id>MIT</id>
|
||||
</license>
|
||||
</licenses>
|
||||
<cpe>cpe:2.3:*:some:package:2:*:*:*:*:*:*:*</cpe>
|
||||
<purl>a-purl-2</purl>
|
||||
<properties>
|
||||
<property name="syft:package:foundBy">the-cataloger-1</property>
|
||||
<property name="syft:package:language">python</property>
|
||||
<property name="syft:package:metadataType">PythonPackageMetadata</property>
|
||||
<property name="syft:package:type">python</property>
|
||||
<property name="syft:location:0:path">/some/path/pkg1</property>
|
||||
</properties>
|
||||
</component>
|
||||
<component bom-ref="pkg:deb/debian/package-2@2.0.1?syft-id=ceda99598967ae8d" type="library">
|
||||
<name>package-2</name>
|
||||
<version>2.0.1</version>
|
||||
<cpe>cpe:2.3:*:some:package:2:*:*:*:*:*:*:*</cpe>
|
||||
<purl>pkg:deb/debian/package-2@2.0.1</purl>
|
||||
<properties>
|
||||
<property name="syft:package:foundBy">the-cataloger-2</property>
|
||||
<property name="syft:package:metadataType">DpkgMetadata</property>
|
||||
<property name="syft:package:type">deb</property>
|
||||
<property name="syft:location:0:path">/some/path/pkg1</property>
|
||||
<property name="syft:metadata:installedSize">0</property>
|
||||
</properties>
|
||||
</component>
|
||||
<component type="operating-system">
|
||||
<name>debian</name>
|
||||
<version>1.2.3</version>
|
||||
<description>debian</description>
|
||||
<swid tagId="debian" name="debian" version="1.2.3"></swid>
|
||||
<properties>
|
||||
<property name="syft:distro:id">debian</property>
|
||||
<property name="syft:distro:idLike:0">like!</property>
|
||||
<property name="syft:distro:prettyName">debian</property>
|
||||
<property name="syft:distro:versionID">1.2.3</property>
|
||||
</properties>
|
||||
</component>
|
||||
</components>
|
||||
</bom>
|
||||
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bom xmlns="http://cyclonedx.org/schema/bom/1.4" serialNumber="urn:uuid:1535f940-172f-4d97-8280-d5a5764d1557" version="1">
|
||||
<metadata>
|
||||
<timestamp>2022-04-01T11:57:46-04:00</timestamp>
|
||||
<tools>
|
||||
<tool>
|
||||
<vendor>anchore</vendor>
|
||||
<name>syft</name>
|
||||
<version>[not provided]</version>
|
||||
</tool>
|
||||
</tools>
|
||||
<component bom-ref="e777314b02b362e4" type="container">
|
||||
<name>user-image-input</name>
|
||||
<version>sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368</version>
|
||||
</component>
|
||||
</metadata>
|
||||
<components>
|
||||
<component bom-ref="2a46171f91c8d4bc" type="library">
|
||||
<name>package-1</name>
|
||||
<version>1.0.1</version>
|
||||
<licenses>
|
||||
<license>
|
||||
<id>MIT</id>
|
||||
</license>
|
||||
</licenses>
|
||||
<cpe>cpe:2.3:*:some:package:1:*:*:*:*:*:*:*</cpe>
|
||||
<purl>a-purl-1</purl>
|
||||
<properties>
|
||||
<property name="syft:package:foundBy">the-cataloger-1</property>
|
||||
<property name="syft:package:language">python</property>
|
||||
<property name="syft:package:metadataType">PythonPackageMetadata</property>
|
||||
<property name="syft:package:type">python</property>
|
||||
<property name="syft:location:0:layerID">sha256:fb6beecb75b39f4bb813dbf177e501edd5ddb3e69bb45cedeb78c676ee1b7a59</property>
|
||||
<property name="syft:location:0:path">/somefile-1.txt</property>
|
||||
</properties>
|
||||
</component>
|
||||
<component bom-ref="pkg:deb/debian/package-2@2.0.1?syft-id=ae77680e9b1d087e" type="library">
|
||||
<name>package-2</name>
|
||||
<version>2.0.1</version>
|
||||
<cpe>cpe:2.3:*:some:package:2:*:*:*:*:*:*:*</cpe>
|
||||
<purl>pkg:deb/debian/package-2@2.0.1</purl>
|
||||
<properties>
|
||||
<property name="syft:package:foundBy">the-cataloger-2</property>
|
||||
<property name="syft:package:metadataType">DpkgMetadata</property>
|
||||
<property name="syft:package:type">deb</property>
|
||||
<property name="syft:location:0:layerID">sha256:319b588ce64253a87b533c8ed01cf0025e0eac98e7b516e12532957e1244fdec</property>
|
||||
<property name="syft:location:0:path">/somefile-2.txt</property>
|
||||
<property name="syft:metadata:installedSize">0</property>
|
||||
</properties>
|
||||
</component>
|
||||
<component type="operating-system">
|
||||
<name>debian</name>
|
||||
<version>1.2.3</version>
|
||||
<description>debian</description>
|
||||
<swid tagId="debian" name="debian" version="1.2.3"></swid>
|
||||
<properties>
|
||||
<property name="syft:distro:id">debian</property>
|
||||
<property name="syft:distro:idLike:0">like!</property>
|
||||
<property name="syft:distro:prettyName">debian</property>
|
||||
<property name="syft:distro:versionID">1.2.3</property>
|
||||
</properties>
|
||||
</component>
|
||||
</components>
|
||||
</bom>
|
||||
BIN
Binary file not shown.
@@ -1,46 +0,0 @@
|
||||
package formats
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/anchore/syft/internal/formats/cyclonedx13json"
|
||||
"github.com/anchore/syft/internal/formats/cyclonedx13xml"
|
||||
"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(),
|
||||
cyclonedx13xml.Format(),
|
||||
cyclonedx13json.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
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user