From 5aa85338d68ceff8c24f713731e94fc1bfe7dd81 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Fri, 11 Feb 2022 14:24:25 -0500 Subject: [PATCH] Normalize release assets and refactor install.sh (#630) * refactor release to keep snapshot assets in parity with release assets Signed-off-by: Alex Goodman * refactor install.sh and put under test Signed-off-by: Alex Goodman * tidy go.sum Signed-off-by: Alex Goodman * add mac acceptance test to github actions workflow Signed-off-by: Alex Goodman * rm use of goreleaser in cli tests Signed-off-by: Alex Goodman * go mod tidy with go 1.17 Signed-off-by: Alex Goodman --- .github/scripts/apple-signing/.gitignore | 3 + .github/scripts/apple-signing/cleanup.sh | 10 + .github/scripts/apple-signing/notarize.sh | 53 ++ .github/scripts/apple-signing/setup-dev.sh | 171 ++++ .github/scripts/apple-signing/setup-prod.sh | 56 ++ .github/scripts/apple-signing/setup.sh | 46 ++ .github/scripts/apple-signing/sign.sh | 94 +++ .github/scripts/apple-signing/utils.sh | 78 ++ .github/scripts/goreleaser-install.sh | 2 +- .github/scripts/mac-prepare-for-signing.sh | 31 - .github/scripts/mac-sign-and-notarize.sh | 19 - .github/scripts/verify-signature.sh | 14 - .github/workflows/acceptance-test.yaml | 113 --- .github/workflows/release.yaml | 77 +- .github/workflows/validations.yaml | 39 +- .goreleaser.yaml | 116 ++- Makefile | 188 +++-- cmd/version.go | 2 +- go.sum | 6 +- gon.hcl | 15 - install.sh | 777 ++++++++++++------ internal/version/build.go | 45 +- internal/version/update.go | 12 +- test/inline-compare/.gitignore | 3 - test/inline-compare/Makefile | 50 -- test/inline-compare/compare-all.sh | 29 - test/inline-compare/compare.py | 336 -------- .../images/alpine-vuln/Dockerfile | 5 - .../images/java-vuln/Dockerfile | 2 - .../images/python-vuln/Dockerfile | 2 - test/install/.dockerignore | 1 + test/install/.gitignore | 1 + test/install/0_search_for_asset_test.sh | 40 + .../install/1_download_snapshot_asset_test.sh | 86 ++ test/install/2_download_release_asset_test.sh | 41 + test/install/3_install_asset_test.sh | 90 ++ test/install/Makefile | 103 +++ .../environments/Dockerfile-alpine-3.6 | 2 + .../environments/Dockerfile-ubuntu-20.04 | 2 + test/install/github_test.sh | 68 ++ .../github-api-grype-v0.32.0-release.json | 1 + ...rype_0.32.0-SNAPSHOT-d461f63_checksums.txt | 9 + .../test-fixtures/grype_0.32.0_checksums.txt | 9 + test/install/test_harness.sh | 163 ++++ 44 files changed, 1928 insertions(+), 1082 deletions(-) create mode 100644 .github/scripts/apple-signing/.gitignore create mode 100755 .github/scripts/apple-signing/cleanup.sh create mode 100755 .github/scripts/apple-signing/notarize.sh create mode 100755 .github/scripts/apple-signing/setup-dev.sh create mode 100755 .github/scripts/apple-signing/setup-prod.sh create mode 100755 .github/scripts/apple-signing/setup.sh create mode 100755 .github/scripts/apple-signing/sign.sh create mode 100644 .github/scripts/apple-signing/utils.sh delete mode 100755 .github/scripts/mac-prepare-for-signing.sh delete mode 100755 .github/scripts/mac-sign-and-notarize.sh delete mode 100755 .github/scripts/verify-signature.sh delete mode 100644 .github/workflows/acceptance-test.yaml delete mode 100644 gon.hcl delete mode 100644 test/inline-compare/.gitignore delete mode 100644 test/inline-compare/Makefile delete mode 100755 test/inline-compare/compare-all.sh delete mode 100755 test/inline-compare/compare.py delete mode 100644 test/inline-compare/images/alpine-vuln/Dockerfile delete mode 100644 test/inline-compare/images/java-vuln/Dockerfile delete mode 100644 test/inline-compare/images/python-vuln/Dockerfile create mode 100644 test/install/.dockerignore create mode 100644 test/install/.gitignore create mode 100755 test/install/0_search_for_asset_test.sh create mode 100755 test/install/1_download_snapshot_asset_test.sh create mode 100755 test/install/2_download_release_asset_test.sh create mode 100755 test/install/3_install_asset_test.sh create mode 100644 test/install/Makefile create mode 100644 test/install/environments/Dockerfile-alpine-3.6 create mode 100644 test/install/environments/Dockerfile-ubuntu-20.04 create mode 100755 test/install/github_test.sh create mode 100644 test/install/test-fixtures/github-api-grype-v0.32.0-release.json create mode 100644 test/install/test-fixtures/grype_0.32.0-SNAPSHOT-d461f63_checksums.txt create mode 100644 test/install/test-fixtures/grype_0.32.0_checksums.txt create mode 100644 test/install/test_harness.sh diff --git a/.github/scripts/apple-signing/.gitignore b/.github/scripts/apple-signing/.gitignore new file mode 100644 index 00000000000..301ab6fe60e --- /dev/null +++ b/.github/scripts/apple-signing/.gitignore @@ -0,0 +1,3 @@ +dev-pki +log +signing-identity.txt diff --git a/.github/scripts/apple-signing/cleanup.sh b/.github/scripts/apple-signing/cleanup.sh new file mode 100755 index 00000000000..318029af7e8 --- /dev/null +++ b/.github/scripts/apple-signing/cleanup.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -eu + +# grab utilities +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +. "$SCRIPT_DIR"/utils.sh + +# cleanup any dev certs left behind +. "$SCRIPT_DIR"/setup-dev.sh +cleanup_signing diff --git a/.github/scripts/apple-signing/notarize.sh b/.github/scripts/apple-signing/notarize.sh new file mode 100755 index 00000000000..219bbb9a6ea --- /dev/null +++ b/.github/scripts/apple-signing/notarize.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash + +set +xu +if [ -z "$AC_USERNAME" ]; then + exit_with_error "AC_USERNAME not set" +fi + +if [ -z "$AC_PASSWORD" ]; then + exit_with_error "AC_PASSWORD not set" +fi +set -u + + +# notarize [archive-path] +# +notarize() { + binary_path=$1 + archive_path=${binary_path}-archive-for-notarization.zip + + title "archiving release binary into ${archive_path}" + + parent=$(dirname "$binary_path") + ( + cd "${parent}" && zip "${archive_path}" "$(basename ${binary_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) + + # create config (note: json via stdin with gon is broken, can only use HCL from file) + hcl_file=$(mktemp).hcl + + cat < "$hcl_file" +notarize { + path = "$archive_path" + bundle_id = "com.anchore.toolbox.grype" +} + +apple_id { + username = "$AC_USERNAME" + password = "@env:AC_PASSWORD" +} +EOF + + gon -log-level info "$hcl_file" + + rm "${hcl_file}" "${archive_path}" +} + diff --git a/.github/scripts/apple-signing/setup-dev.sh b/.github/scripts/apple-signing/setup-dev.sh new file mode 100755 index 00000000000..7e2e8d87416 --- /dev/null +++ b/.github/scripts/apple-signing/setup-dev.sh @@ -0,0 +1,171 @@ +#!/usr/bin/env bash +set -eu + +NAME=grype-dev +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +DIR=$SCRIPT_DIR/dev-pki +FILE_PREFIX=$DIR/$NAME +IDENTITY=${NAME}-id-415d8c69793 + +## OpenSSL material + +KEY_PASSWORD="letthedevin" +P12_PASSWORD="popeofnope" + +KEY_FILE=$FILE_PREFIX-key.pem +CSR_FILE=$FILE_PREFIX-csr.pem +CERT_FILE=$FILE_PREFIX-cert.pem +EXT_FILE=$FILE_PREFIX-ext.cnf +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 +# +function setup_signing() { + # check to see if this has already been done... if so, bail! + set +ue + if security find-identity -p codesigning "$KEYCHAIN_PATH" | grep $IDENTITY ; then + export MAC_SIGNING_IDENTITY=$IDENTITY + commentary "skipping creating dev certificate material (already exists)" + commentary "setting MAC_SIGNING_IDENTITY=${IDENTITY}" + return 0 + fi + set -ue + + title "setting up developer certificate material" + + mkdir -p "${DIR}" + + # configure the openssl extensions + cat << EOF > $EXT_FILE + [ req ] + default_bits = 2048 # RSA key size + encrypt_key = yes # Protect private key + default_md = sha256 # MD to use + utf8 = yes # Input is UTF-8 + string_mask = utf8only # Emit UTF-8 strings + prompt = yes # Prompt for DN + distinguished_name = codesign_dn # DN template + req_extensions = $EXT_SECTION # Desired extensions + + [ codesign_dn ] + commonName = $IDENTITY + commonName_max = 64 + + [ $EXT_SECTION ] + keyUsage = critical,digitalSignature + extendedKeyUsage = critical,codeSigning + subjectKeyIdentifier = hash +EOF + + title "create the private key" + openssl genrsa \ + -des3 \ + -out "$KEY_FILE" \ + -passout "pass:$KEY_PASSWORD" \ + 2048 + + title "create the csr" + openssl req \ + -new \ + -key "$KEY_FILE" \ + -out "$CSR_FILE" \ + -passin "pass:$KEY_PASSWORD" \ + -config "$EXT_FILE" \ + -subj "/CN=$IDENTITY" + + commentary "verify the csr: we should see X509 v3 extensions for codesigning in the CSR" + openssl req -in "$CSR_FILE" -noout -text | grep -A1 "X509v3" || exit_with_error "could not find x509 extensions in CSR" + + title "create the certificate" + # note: Extensions in certificates are not transferred to certificate requests and vice versa. This means that + # just because the CSR has x509 v3 extensions doesn't mean that you'll see these extensions in the cert output. + # To prove this do: + # openssl x509 -text -noout -in server.crt | grep -A10 "X509v3 extensions:" + # ... and you will see no output (if -extensions is not used). (see https://www.openssl.org/docs/man1.1.0/man1/x509.html#BUGS) + # To get the extensions, use "-extensions codesign_reqext" when creating the cert. The codesign_reqext value matches + # the section name in the ext file used in CSR / cert creation (-extfile and -config). + openssl x509 \ + -req \ + -days 10000 \ + -in "$CSR_FILE" \ + -signkey "$KEY_FILE" \ + -out "$CERT_FILE" \ + -extfile "$EXT_FILE" \ + -passin "pass:$KEY_PASSWORD" \ + -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" + + 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 + openssl pkcs12 \ + -export \ + -out "$P12_FILE" \ + -inkey "$KEY_FILE" \ + -in "$CERT_FILE" \ + -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 + 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}" + +} + +function cleanup_signing() { + title "delete the dev keychain and all certificate material" + set -xue + security delete-keychain "$KEYCHAIN_NAME" + rm -f "$KEYCHAIN_PATH" + rm -rf "${DIR}" +} diff --git a/.github/scripts/apple-signing/setup-prod.sh b/.github/scripts/apple-signing/setup-prod.sh new file mode 100755 index 00000000000..5f7bc73b3d8 --- /dev/null +++ b/.github/scripts/apple-signing/setup-prod.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +set -eu + +assert_in_ci + +set +xu +if [ -z "$APPLE_DEVELOPER_ID_CERT" ]; then + exit_with_error "APPLE_DEVELOPER_ID_CERT not set" +fi + +if [ -z "$APPLE_DEVELOPER_ID_CERT_PASS" ]; then + exit_with_error "APPLE_DEVELOPER_ID_CERT_PASS not set" +fi + +if [ -z "$DOCKER_USERNAME" ]; then + exit_with_error "DOCKER_USERNAME not set" +fi + +if [ -z "$DOCKER_PASSWORD" ]; then + exit_with_error "DOCKER_PASSWORD not set" +fi +set -u + +# setup_signing +# +# preps the MAC_SIGNING_IDENTITY env var for use in the signing process, using production certificate material +# +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" + + # 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}" + + # 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}" + + 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 +} diff --git a/.github/scripts/apple-signing/setup.sh b/.github/scripts/apple-signing/setup.sh new file mode 100755 index 00000000000..6fa7e44ac4a --- /dev/null +++ b/.github/scripts/apple-signing/setup.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +set -eu + +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() { + + case "$IS_SNAPSHOT" in + + "1" | "true" | "yes") + commentary "assuming development setup..." + . "$SCRIPT_DIR"/setup-dev.sh + ;; + + "0" | "false" | "no") + commentary "assuming production setup..." + . "$SCRIPT_DIR"/setup-prod.sh + ;; + + *) + exit_with_error "could not determine if this was a production build (isSnapshot='$IS_SNAPSHOT')" + ;; + esac + + # load up all signing material into a keychain (note: this should set the MAC_SIGNING_IDENTITY env var) + setup_signing + + # write out identity to a file + echo -n "$MAC_SIGNING_IDENTITY" > "$SCRIPT_DIR/$SIGNING_IDENTITY_FILENAME" +} + +# 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 + main + fi +) 2>&1) | tee "$SCRIPT_DIR/log/setup.txt" \ No newline at end of file diff --git a/.github/scripts/apple-signing/sign.sh b/.github/scripts/apple-signing/sign.sh new file mode 100755 index 00000000000..9231c58a55d --- /dev/null +++ b/.github/scripts/apple-signing/sign.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +set -eu -o pipefail + +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] +# +# signs a single binary with cosign +# +sign_binary() { + exe_path=$1 + identity=$2 + + if [ -x "$exe_path" ] && file -b "$exe_path" | grep -q "Mach-O" + then + echo "signing $exe_path ..." + else + echo "skip signing $exe_path ..." + return 0 + fi + + codesign \ + -s "$identity" \ + -f \ + --verbose=4 \ + --timestamp \ + --options runtime \ + $exe_path + + if [ $? -ne 0 ]; then + exit_with_error "signing failed" + fi + + codesign --verify "$exe_path" --verbose=4 +} + + +main() { + binary_abs_path=$(realpath "$BINARY_PATH") + + if [ ! -f "$binary_abs_path" ]; then + echo "archive does not exist: $binary_abs_path" + fi + + case "$IS_SNAPSHOT" in + + "1" | "true" | "yes") + commentary "disabling notarization..." + perform_notarization=false + ;; + + "0" | "false" | "no") + commentary "enabling notarization..." + . "$SCRIPT_DIR"/notarize.sh + perform_notarization=true + ;; + + *) + exit_with_error "could not determine if this was a production build (isSnapshot='$IS_SNAPSHOT')" + ;; + esac + + # grab the signing identity from the local temp file (setup by setup.sh) + 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_binary "$binary_abs_path" "$MAC_SIGNING_IDENTITY" + + # send all of the binaries off to apple to bless + if $perform_notarization ; then + notarize "$binary_abs_path" + else + commentary "skipping notarization..." + fi +} + +# 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 + main + fi +) 2>&1) | tee "$SCRIPT_DIR/log/signing-$(basename $BINARY_PATH)-$TARGET_NAME.txt" \ No newline at end of file diff --git a/.github/scripts/apple-signing/utils.sh b/.github/scripts/apple-signing/utils.sh new file mode 100644 index 00000000000..8e798df0d01 --- /dev/null +++ b/.github/scripts/apple-signing/utils.sh @@ -0,0 +1,78 @@ +SIGNING_IDENTITY_FILENAME=signing-identity.txt + +## terminal goodies +PURPLE='\033[0;35m' +GREEN='\033[0;32m' +RED='\033[0;31m' +BOLD=$(tput -T linux bold) +RESET='\033[0m' + +function success() { + echo -e "\n${GREEN}${BOLD}$@${RESET}" +} + +function title() { + success "Task: $@" +} + +function commentary() { + echo -e "\n${PURPLE}# $@${RESET}" +} + +function error() { + echo -e "${RED}${BOLD}error: $@${RESET}" +} + +function exit_with_error() { + error $@ + exit 1 +} + +function exit_with_message() { + success $@ + exit 0 +} + +function realpath { + echo "$(cd $(dirname $1); pwd)/$(basename $1)"; +} + + +# this function adds all of the existing keychains plus the new one which is the same as going to Keychain Access +# and selecting "Add Keychain" to make the keychain visible under "Custom Keychains". This is done with +# "security list-keychains -s" for some reason. The downside is that this sets the search path, not appends +# to it, so you will loose existing keychains in the search path... which is truly terrible. +function add_keychain() { + keychains=$(security list-keychains -d user) + keychainNames=(); + for keychain in $keychains + do + basename=$(basename "$keychain") + keychainName=${basename::${#basename}-4} + keychainNames+=("$keychainName") + done + + echo "existing user keychains: ${keychainNames[@]}" + + security -v list-keychains -s "${keychainNames[@]}" "$1" +} + +function exit_not_ci() { + printf "WARNING! It looks like this isn't the CI environment. This script modifies the macOS Keychain setup in ways you probably wouldn't want for your own machine. It also requires an Apple Developer ID Certificate that you shouldn't have outside of the CI environment.\n\nExiting early to make sure nothing bad happens.\n" + exit 1 +} + +CI_HOME="/Users/runner" + +function assert_in_ci() { + + if [[ "${HOME}" != "${CI_HOME}" ]]; then + exit_not_ci + fi + + set +u + if [ -z "${GITHUB_ACTIONS}" ]; then + exit_not_ci + fi + set -u +} \ No newline at end of file diff --git a/.github/scripts/goreleaser-install.sh b/.github/scripts/goreleaser-install.sh index d4933957437..ac990bf8200 100755 --- a/.github/scripts/goreleaser-install.sh +++ b/.github/scripts/goreleaser-install.sh @@ -338,7 +338,7 @@ hash_sha256_verify() { return 1 fi BASENAME=${TARGET##*/} - want=$(grep "${BASENAME}" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1) + want=$(grep "${BASENAME}$" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1) if [ -z "$want" ]; then log_err "hash_sha256_verify unable to find checksum for '${TARGET}' in '${checksums}'" return 1 diff --git a/.github/scripts/mac-prepare-for-signing.sh b/.github/scripts/mac-prepare-for-signing.sh deleted file mode 100755 index 7d0045d4d28..00000000000 --- a/.github/scripts/mac-prepare-for-signing.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env bash -set -eu - -CI_HOME="/Users/runner" -if [[ "${HOME}" != "${CI_HOME}" ]]; then - printf "WARNING! It looks like this isn't the CI environment. This script modifies the macOS Keychain setup in ways you probably wouldn't want for your own machine. It also requires an Apple Developer ID Certificate that you shouldn't have outside of the CI environment.\n\nExiting early to make sure nothing bad happens.\n" - exit 1 -fi - -# Install gon (see https://github.com/mitchellh/gon for details). -brew tap mitchellh/gon -brew install mitchellh/gon/gon - -# 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" - -# 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}" - -# 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}" diff --git a/.github/scripts/mac-sign-and-notarize.sh b/.github/scripts/mac-sign-and-notarize.sh deleted file mode 100755 index 344debcda37..00000000000 --- a/.github/scripts/mac-sign-and-notarize.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -set -eu - -IS_SNAPSHOT="$1" # e.g. "true", "false" - -if [[ "${IS_SNAPSHOT}" == "true" ]]; then - # This is a snapshot build —— skipping signing and notarization... - exit 0 -fi - -GON_CONFIG="$2" # e.g. "gon.hcl" -NEW_NAME_WITHOUT_EXTENSION="$3" # e.g. "./dist/syft-0.1.0" -ORIGINAL_NAME_WITHOUT_EXTENSION="./dist/output" # This should match dmg and zip output_path in the gon config file, without the extension. - -gon "${GON_CONFIG}" - -# Rename outputs with specified desired name -mv -v "${ORIGINAL_NAME_WITHOUT_EXTENSION}.dmg" "${NEW_NAME_WITHOUT_EXTENSION}.dmg" -mv -v "${ORIGINAL_NAME_WITHOUT_EXTENSION}.zip" "${NEW_NAME_WITHOUT_EXTENSION}.zip" diff --git a/.github/scripts/verify-signature.sh b/.github/scripts/verify-signature.sh deleted file mode 100755 index c022afc5d44..00000000000 --- a/.github/scripts/verify-signature.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash -set -ue - -DISTDIR=$1 - -export FINGERPRINT=$(gpg --verify ${DISTDIR}/*checksums.txt.sig ${DISTDIR}/*checksums.txt 2>&1 | grep 'using RSA key' | awk '{ print $NF }') - -if [[ "${FINGERPRINT}" == "${SIGNING_FINGERPRINT}" ]]; then - echo 'verified signature' -else - echo "signed with unknown fingerprint: ${FINGERPRINT}" - echo " expected fingerprint: ${SIGNING_FINGERPRINT}" - exit 1 -fi diff --git a/.github/workflows/acceptance-test.yaml b/.github/workflows/acceptance-test.yaml deleted file mode 100644 index 0875ff08f40..00000000000 --- a/.github/workflows/acceptance-test.yaml +++ /dev/null @@ -1,113 +0,0 @@ -name: 'Acceptance' -on: - push: - workflow_dispatch: - # ... only act on pushes to main - branches: - - main - # ... do not act on release tags - tags-ignore: - - v* - -env: - GO_VERSION: "1.17.x" - -jobs: - # Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline - Build-Snapshot-Artifacts: - runs-on: ubuntu-20.04 - steps: - - - uses: actions/setup-go@v2 - with: - go-version: ${{ env.GO_VERSION }} - - - uses: actions/checkout@v2 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v1 - - - name: Restore bootstrap cache - id: cache - uses: actions/cache@v2 - with: - path: | - ~/go/pkg/mod - ${{ github.workspace }}/.tmp - key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}-${{ hashFiles('Makefile') }} - - - name: Bootstrap project dependencies - if: steps.bootstrap-cache.outputs.cache-hit != 'true' - run: make bootstrap - - - name: Import GPG key - id: import_gpg - uses: crazy-max/ghaction-import-gpg@v2 - env: - GPG_PRIVATE_KEY: ${{ secrets.SIGNING_GPG_PRIVATE_KEY }} - PASSPHRASE: ${{ secrets.SIGNING_GPG_PASSPHRASE }} - - - name: GPG signing info - run: | - echo "fingerprint: ${{ steps.import_gpg.outputs.fingerprint }}" - echo "keyid: ${{ steps.import_gpg.outputs.keyid }}" - echo "name: ${{ steps.import_gpg.outputs.name }}" - echo "email: ${{ steps.import_gpg.outputs.email }}" - - - name: Build snapshot artifacts - run: make snapshot - env: - GPG_PRIVATE_KEY: ${{ secrets.SIGNING_GPG_PRIVATE_KEY }} - PASSPHRASE: ${{ secrets.SIGNING_GPG_PASSPHRASE }} - - - uses: actions/upload-artifact@v2 - with: - name: artifacts - path: snapshot/**/* - - - uses: 8398a7/action-slack@v3 - with: - status: ${{ job.status }} - fields: repo,workflow,job,commit,message,author - text: The grype acceptance tests have failed tragically! - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }} - if: ${{ failure() }} - - - # TODO: add basic acceptance tests against snapshot artifacts - - # Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline - Inline-Compare: - needs: [ Build-Snapshot-Artifacts ] - runs-on: ubuntu-latest - steps: - - - uses: actions/checkout@v2 - - - name: Fingerprint inline-compare sources - run: make compare-fingerprint - - - name: Restore inline reports cache - id: cache - uses: actions/cache@v2 - with: - path: ${{ github.workspace }}/test/inline-compare/inline-reports - key: inline-reports-${{ hashFiles('**/inline-compare.fingerprint') }} - - - uses: actions/download-artifact@v2 - with: - name: artifacts - path: snapshot - - - name: Compare Anchore inline-scan results against snapshot build output - run: make compare-snapshot - - - uses: 8398a7/action-slack@v3 - with: - status: ${{ job.status }} - fields: repo,workflow,job,commit,message,author - text: The grype acceptance tests have failed tragically! - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }} - if: ${{ failure() }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 4e47f16353c..98ede851065 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -14,7 +14,7 @@ env: jobs: quality-gate: environment: release - runs-on: ubuntu-latest # This OS choice is arbitrary. None of the steps in this job are specific to either Linux or macOS. + runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -61,6 +61,15 @@ jobs: checkName: "Acceptance tests (Linux)" ref: ${{ github.event.pull_request.head.sha || github.sha }} + - name: Check acceptance test results (mac) + uses: fountainhead/action-wait-for-check@v1.0.0 + id: acceptance-mac + with: + token: ${{ secrets.GITHUB_TOKEN }} + # This check name is defined as the github action job name (in .github/workflows/testing.yaml) + checkName: "Acceptance tests (Mac)" + ref: ${{ github.event.pull_request.head.sha || github.sha }} + - name: Check cli test results (linux) uses: fountainhead/action-wait-for-check@v1.0.0 id: cli-linux @@ -71,19 +80,20 @@ jobs: ref: ${{ github.event.pull_request.head.sha || github.sha }} - name: Quality gate - if: steps.static-analysis.outputs.conclusion != 'success' || steps.unit.outputs.conclusion != 'success' || steps.integration.outputs.conclusion != 'success' || steps.cli-linux.outputs.conclusion != 'success' || steps.acceptance-linux.outputs.conclusion != 'success' + if: steps.static-analysis.outputs.conclusion != 'success' || steps.unit.outputs.conclusion != 'success' || steps.integration.outputs.conclusion != 'success' || steps.cli-linux.outputs.conclusion != 'success' || steps.acceptance-linux.outputs.conclusion != 'success' || steps.acceptance-mac.outputs.conclusion != 'success' run: | echo "Static Analysis Status: ${{ steps.static-analysis.conclusion }}" echo "Unit Test Status: ${{ steps.unit.outputs.conclusion }}" echo "Integration Test Status: ${{ steps.integration.outputs.conclusion }}" echo "Acceptance Test (Linux) Status: ${{ steps.acceptance-linux.outputs.conclusion }}" + echo "Acceptance Test (Mac) Status: ${{ steps.acceptance-mac.outputs.conclusion }}" echo "CLI Test (Linux) Status: ${{ steps.cli-linux.outputs.conclusion }}" false - release: - needs: [ quality-gate ] - runs-on: macos-latest # Due to our code signing process, it's vital that we run our release steps on macOS. + needs: [quality-gate] + # due to our code signing process, it's vital that we run our release steps on macOS + runs-on: macos-latest steps: - uses: docker-practice/actions-setup-docker@v1 @@ -95,52 +105,41 @@ jobs: with: fetch-depth: 0 - - name: Set up QEMU - uses: docker/setup-qemu-action@v1 + - name: Restore tool cache + id: tool-cache + uses: actions/cache@v2.1.3 + with: + path: ${{ github.workspace }}/.tmp + key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }} - # We are expecting this cache to have been created during the "Build-Snapshot-Artifacts" job in the "Acceptance" workflow. - - name: Restore bootstrap cache - id: bootstrap-cache - uses: actions/cache@v2 + - name: Restore go cache + id: go-cache + uses: actions/cache@v2.1.3 with: - path: | - ~/go/pkg/mod - ${{ github.workspace }}/.tmp - key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('Makefile') }}-${{ hashFiles('**/go.sum') }} + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go-${{ env.GO_VERSION }}- - - name: Bootstrap project dependencies - if: steps.bootstrap-cache.outputs.cache-hit != 'true' + - name: (cache-miss) Bootstrap all project dependencies + if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true' run: make bootstrap - - name: Import GPG key - id: import_gpg - uses: crazy-max/ghaction-import-gpg@v2 - env: - GPG_PRIVATE_KEY: ${{ secrets.SIGNING_GPG_PRIVATE_KEY }} - PASSPHRASE: ${{ secrets.SIGNING_GPG_PASSPHRASE }} - - - name: GPG signing info - run: | - echo "fingerprint: ${{ steps.import_gpg.outputs.fingerprint }}" - echo "keyid: ${{ steps.import_gpg.outputs.keyid }}" - echo "name: ${{ steps.import_gpg.outputs.name }}" - echo "email: ${{ steps.import_gpg.outputs.email }}" - - - name: Build release artifacts + - name: Build & publish release artifacts run: make release env: 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 GITHUB_TOKEN: ${{ secrets.ANCHORE_GIT_READ_TOKEN }} - GPG_PRIVATE_KEY: ${{ secrets.SIGNING_GPG_PRIVATE_KEY }} - PASSPHRASE: ${{ secrets.SIGNING_GPG_PASSPHRASE }} - SIGNING_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} AWS_ACCESS_KEY_ID: ${{ secrets.TOOLBOX_AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.TOOLBOX_AWS_SECRET_ACCESS_KEY }} - APPLE_DEVELOPER_ID_CERT: ${{ secrets.APPLE_DEVELOPER_ID_CERT }} # Used during macOS code signing. - APPLE_DEVELOPER_ID_CERT_PASS: ${{ secrets.APPLE_DEVELOPER_ID_CERT_PASS }} # Used during macOS code signing. - AC_USERNAME: ${{ secrets.ENG_CI_APPLE_ID }} # Used during macOS notarization. - AC_PASSWORD: ${{ secrets.ENG_CI_APPLE_ID_PASS }} # Used during macOS notarization. + # used during macOS code signing + APPLE_DEVELOPER_ID_CERT: ${{ secrets.APPLE_DEVELOPER_ID_CERT }} + APPLE_DEVELOPER_ID_CERT_PASS: ${{ secrets.APPLE_DEVELOPER_ID_CERT_PASS }} + # used during macOS notarization + AC_USERNAME: ${{ secrets.ENG_CI_APPLE_ID }} + AC_PASSWORD: ${{ secrets.ENG_CI_APPLE_ID_PASS }} - uses: anchore/sbom-action@v0 with: diff --git a/.github/workflows/validations.yaml b/.github/workflows/validations.yaml index b70c769fff7..463e645a8c4 100644 --- a/.github/workflows/validations.yaml +++ b/.github/workflows/validations.yaml @@ -199,8 +199,43 @@ jobs: name: artifacts path: snapshot - - name: Run Acceptance Tests (Linux) - run: make acceptance-linux + - name: Build key for image cache + run: make install-fingerprint + + - name: Restore install.sh test image cache + id: install-test-image-cache + uses: actions/cache@v2.1.3 + with: + path: ${{ github.workspace }}/test/install/cache + key: ${{ runner.os }}-install-test-image-cache-${{ hashFiles('test/install/cache.fingerprint') }} + + - name: Load test image cache + if: steps.install-test-image-cache.outputs.cache-hit == 'true' + run: make install-test-cache-load + + - name: Run install.sh tests (Linux) + run: make install-test + + - name: (cache-miss) Create test image cache + if: steps.install-test-image-cache.outputs.cache-hit != 'true' + run: make install-test-cache-save + + Acceptance-Mac: + # Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline + name: "Acceptance tests (Mac)" + needs: [Build-Snapshot-Artifacts] + runs-on: macos-latest + steps: + - uses: actions/checkout@v2 + + - uses: actions/download-artifact@v2 + with: + name: artifacts + path: snapshot + + - name: Run install.sh tests (Mac) + run: make install-test-ci-mac + Cli-Linux: # Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline diff --git a/.goreleaser.yaml b/.goreleaser.yaml index b9ec8aab25d..8ea75086b23 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -1,26 +1,28 @@ release: - # If set to auto, will mark the release as not ready for production - # in case there is an indicator for this in the tag e.g. v1.0.0-rc1 - # If set to true, will mark the release as not ready for production. prerelease: auto - - # If set to true, will not auto-publish the release. This is done to allow us to review the changelog before publishing. draft: true +env: + # required to support multi architecture docker builds + - DOCKER_CLI_EXPERIMENTAL=enabled + +before: + hooks: + - ./.github/scripts/apple-signing/setup.sh {{ .IsSnapshot }} + builds: - - binary: grype - id: grype - env: - - CGO_ENABLED=0 + - id: linux-build + binary: grype goos: - linux - - windows goarch: - amd64 - arm64 - # Set the modified timestamp on the output binary to the git timestamp (to ensure a reproducible build) - mod_timestamp: '{{ .CommitTimestamp }}' - ldflags: | + # set the modified timestamp on the output binary to the git timestamp to ensure a reproducible build + mod_timestamp: &build-timestamp '{{ .CommitTimestamp }}' + env: &build-env + - CGO_ENABLED=0 + ldflags: &build-ldflags | -w -s -extldflags '-static' @@ -28,60 +30,47 @@ builds: -X github.com/anchore/grype/internal/version.syftVersion={{.Env.SYFT_VERSION}} -X github.com/anchore/grype/internal/version.gitCommit={{.Commit}} -X github.com/anchore/grype/internal/version.buildDate={{.Date}} - -X github.com/anchore/grype/internal/version.gitTreeState={{.Env.BUILD_GIT_TREE_STATE}} + -X github.com/anchore/grype/internal/version.gitDescription={{.Summary}} - # For more info on this macOS build, see: https://github.com/mitchellh/gon#usage-with-goreleaser - - binary: grype - id: grype-macos - env: - - CGO_ENABLED=0 + - id: darwin-build + binary: grype goos: - darwin goarch: - amd64 - arm64 - # Set the modified timestamp on the output binary to the git timestamp (to ensure a reproducible build) - mod_timestamp: '{{ .CommitTimestamp }}' - ldflags: | - -w - -s - -extldflags '-static' - -X github.com/anchore/grype/internal/version.version={{.Version}} - -X github.com/anchore/grype/internal/version.syftVersion={{.Env.SYFT_VERSION}} - -X github.com/anchore/grype/internal/version.gitCommit={{.Commit}} - -X github.com/anchore/grype/internal/version.buildDate={{.Date}} - -X github.com/anchore/grype/internal/version.gitTreeState={{.Env.BUILD_GIT_TREE_STATE}} + 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: grype + goos: + - windows + goarch: + - amd64 + mod_timestamp: *build-timestamp + env: *build-env + ldflags: *build-ldflags archives: - - name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' - id: grype - format_overrides: - - goos: windows - format: zip + - id: linux-archives + builds: + - linux-build - - format: zip # This is a hack for grype-macos! We don't actually intend to use _this_ ZIP file, we just need goreleaser to consider the ZIP file produced by gon (which will have the same file name) to be an artifact so we can use it downstream in publishing (e.g. to a homebrew tap) - id: grype-exception + - id: darwin-archives builds: - - grype-macos + - darwin-build -signs: - - artifacts: checksum - cmd: sh - args: - - '-c' - # we should not include the zip artifact, as the artifact is mutated throughout the next macOS notarization step - # note: sed -i is not portable - - 'sed "/.*_darwin_.*\.zip/d" ${artifact} > tmpfile && mv tmpfile ${artifact} && gpg --output ${signature} --detach-sign ${artifact}' - - id: grype-macos-signing - ids: - - grype-macos - cmd: ./.github/scripts/mac-sign-and-notarize.sh - signature: "grype_${VERSION}_darwin_amd64.dmg" # This is somewhat unintuitive. This gets the DMG file recognized as an artifact. In fact, both a DMG and a ZIP file are being produced by this signing step. - args: - - "{{ .IsSnapshot }}" - - "gon.hcl" - - "./dist/grype_{{ .Version }}_darwin_amd64" - artifacts: all + - id: windows-archives + format: zip + builds: + - windows-build nfpms: - license: "Apache 2.0" @@ -97,19 +86,11 @@ brews: owner: anchore name: homebrew-grype ids: - - grype - install: | - bin.install "grype" - - # Install bash completion - output = Utils.popen_read("#{bin}/grype completion bash") - (bash_completion/"grype").write output - - # Install zsh completion - output = Utils.popen_read("#{bin}/grype completion zsh") - (zsh_completion/"_grype").write output + - darwin-archives + - linux-archives homepage: *website description: *description + license: "Apache License 2.0" dockers: - image_templates: @@ -157,4 +138,3 @@ docker_manifests: - anchore/grype:{{ .Tag }}-arm64v8 - anchore/grype:v{{ .Major }}-arm64v8 - anchore/grype:v{{ .Major }}.{{ .Minor }}-arm64v8 - diff --git a/Makefile b/Makefile index 4a1ff75f3c2..b4fe7bba1e8 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,11 @@ COVER_REPORT = $(RESULTSDIR)/cover.report COVER_TOTAL = $(RESULTSDIR)/cover.total LICENSES_REPORT = $(RESULTSDIR)/licenses.json LINTCMD = $(TEMPDIR)/golangci-lint run --tests=false --timeout 5m --config .golangci.yaml +RELEASE_CMD=$(TEMPDIR)/goreleaser release --rm-dist +SNAPSHOT_CMD=$(RELEASE_CMD) --skip-publish --snapshot +VERSION=$(shell git describe --dirty --always --tags) + +# formatting variables BOLD := $(shell tput -T linux bold) PURPLE := $(shell tput -T linux setaf 5) GREEN := $(shell tput -T linux setaf 2) @@ -13,28 +18,21 @@ RED := $(shell tput -T linux setaf 1) RESET := $(shell tput -T linux sgr0) TITLE := $(BOLD)$(PURPLE) SUCCESS := $(BOLD)$(GREEN) + # the quality gate lower threshold for unit test total % coverage (by function statements) COVERAGE_THRESHOLD := 47 + +# CI cache busting values; change these if you want CI to not use previous stored cache BOOTSTRAP_CACHE="c7afb99ad" INTEGRATION_CACHE_BUSTER="894d8ca" - ## Build variables DISTDIR=./dist SNAPSHOTDIR=./snapshot -GITTREESTATE=$(if $(shell git status --porcelain),dirty,clean) -SYFTVERSION=$(shell go list -m all | grep github.com/anchore/syft | awk '{print $$2}') -OS := $(shell uname) - -ifeq ($(OS),Darwin) - SNAPSHOT_CMD=$(shell realpath $(shell pwd)/$(SNAPSHOTDIR)/$(BIN)-macos_darwin_amd64/$(BIN)) -else - SNAPSHOT_CMD=$(shell realpath $(shell pwd)/$(SNAPSHOTDIR)/$(BIN)_linux_amd64/$(BIN)) -endif +OS=$(shell uname | tr '[:upper:]' '[:lower:]') +SYFT_VERSION=$(shell go list -m all | grep github.com/anchore/syft | awk '{print $$2}') +SNAPSHOT_BIN=$(shell realpath $(shell pwd)/$(SNAPSHOTDIR)/$(OS)-build_$(OS)_amd64/$(BIN)) -ifeq "$(strip $(VERSION))" "" - override VERSION = $(shell git describe --always --tags --dirty) -endif ## Variable assertions @@ -54,16 +52,28 @@ ifndef SNAPSHOTDIR $(error SNAPSHOTDIR is not set) endif +ifndef VERSION + $(error VERSION is not set) +endif + define title @printf '$(TITLE)$(1)$(RESET)\n' endef +define safe_rm_rf + bash -c 'test -z "$(1)" && false || rm -rf $(1)' +endef + +define safe_rm_rf_children + bash -c 'test -z "$(1)" && false || rm -rf $(1)/*' +endef + .PHONY: all all: clean static-analysis test ## Run all checks (linting, license check, unit, integration, and linux acceptance tests tests) @printf '$(SUCCESS)All checks pass!$(RESET)\n' .PHONY: test -test: unit validate-cyclonedx-schema integration acceptance-linux cli ## Run all tests (unit, integration, linux acceptance, and CLI tests) +test: unit validate-cyclonedx-schema integration cli ## Run all tests (unit, integration, linux acceptance, and CLI tests) help: @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "$(BOLD)$(CYAN)%-25s$(RESET)%s\n", $$1, $$2}' @@ -72,10 +82,6 @@ help: ci-bootstrap: DEBIAN_FRONTEND=noninteractive sudo apt update && sudo -E apt install -y bc jq libxml2-utils -.PHONY: -ci-bootstrap-mac: - github_changelog_generator --version || sudo gem install github_changelog_generator - $(RESULTSDIR): mkdir -p $(RESULTSDIR) @@ -87,7 +93,7 @@ bootstrap-tools: $(TEMPDIR) curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(TEMPDIR)/ v1.42.1 curl -sSfL https://raw.githubusercontent.com/wagoodman/go-bouncer/master/bouncer.sh | sh -s -- -b $(TEMPDIR)/ v0.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 -b $(TEMPDIR)/ v0.177.0 + .github/scripts/goreleaser-install.sh -b $(TEMPDIR)/ v1.4.1 .PHONY: bootstrap-go bootstrap-go: @@ -114,6 +120,17 @@ lint: ## Run gofmt + golangci lint checks $(eval MALFORMED_FILENAMES := $(shell find . | grep -e ':')) @bash -c "[[ '$(MALFORMED_FILENAMES)' == '' ]] || (printf '\nfound unsupported filename characters:\n$(MALFORMED_FILENAMES)\n\n' && false)" +.PHONY: lint-fix +lint-fix: ## Auto-format all source code + run golangci lint fixers + $(call title,Running lint fixers) + gofmt -w -s . + $(LINTCMD) --fix + go mod tidy + +.PHONY: check-licenses +check-licenses: + $(TEMPDIR)/bouncer check + check-go-mod-tidy: @ .github/scripts/go-mod-tidy-check.sh && echo "go.mod and go.sum are tidy!" @@ -126,17 +143,6 @@ validate-grype-db-schema: # ensure the codebase is only referencing a single grype-db schema version, multiple is not allowed python test/validate-grype-db-schema.py -.PHONY: lint-fix -lint-fix: ## Auto-format all source code + run golangci lint fixers - $(call title,Running lint fixers) - gofmt -w -s . - $(LINTCMD) --fix - go mod tidy - -.PHONY: check-licenses -check-licenses: - $(TEMPDIR)/bouncer check - .PHONY: unit unit: ## Run unit tests (with coverage) $(call title,Running unit tests) @@ -146,6 +152,27 @@ unit: ## Run unit tests (with coverage) @echo "Coverage: $$(cat $(COVER_TOTAL))" @if [ $$(echo "$$(cat $(COVER_TOTAL)) >= $(COVERAGE_THRESHOLD)" | bc -l) -ne 1 ]; then echo "$(RED)$(BOLD)Failed coverage quality gate (> $(COVERAGE_THRESHOLD)%)$(RESET)" && false; fi +# note: this is used by CI to determine if the install test fixture cache (docker image tars) should be busted +install-fingerprint: + cd test/install && \ + make cache.fingerprint + +install-test: $(SNAPSHOTDIR) + cd test/install && \ + make + +install-test-cache-save: $(SNAPSHOTDIR) + cd test/install && \ + make save + +install-test-cache-load: $(SNAPSHOTDIR) + cd test/install && \ + make load + +install-test-ci-mac: $(SNAPSHOTDIR) + cd test/install && \ + make ci-test-mac + .PHONY: integration integration: ## Run integration tests $(call title,Running integration tests) @@ -163,48 +190,43 @@ cli-fingerprint: .PHONY: cli cli: $(SNAPSHOTDIR) ## Run CLI tests - chmod 755 "$(SNAPSHOT_CMD)" - $(SNAPSHOT_CMD) version - GRYPE_BINARY_LOCATION='$(SNAPSHOT_CMD)' \ + chmod 755 "$(SNAPSHOT_BIN)" + GRYPE_BINARY_LOCATION='$(SNAPSHOT_BIN)' \ go test -count=1 -v ./test/cli -.PHONY: clear-test-cache -clear-test-cache: ## Delete all test cache (built docker image tars) - find . -type f -wholename "**/test-fixtures/cache/*.tar" -delete - .PHONY: build build: $(SNAPSHOTDIR) ## Build release snapshot binaries and packages $(SNAPSHOTDIR): ## Build snapshot release binaries and packages $(call title,Building snapshot artifacts) + + # create a config with the dist dir overridden + echo "dist: $(SNAPSHOTDIR)" > $(TEMPDIR)/goreleaser.yaml + cat .goreleaser.yaml >> $(TEMPDIR)/goreleaser.yaml + + # build release snapshots + bash -c "\ + SKIP_SIGNING=true \ + SYFT_VERSION=$(SYFT_VERSION)\ + $(SNAPSHOT_CMD) --skip-sign --config $(TEMPDIR)/goreleaser.yaml" + +.PHONY: snapshot-with-signing +snapshot-with-signing: ## Build snapshot release binaries and packages (with dummy signing) + $(call title,Building snapshot artifacts (+ signing)) + # create a config with the dist dir overridden echo "dist: $(SNAPSHOTDIR)" > $(TEMPDIR)/goreleaser.yaml cat .goreleaser.yaml >> $(TEMPDIR)/goreleaser.yaml + rm -f .github/scripts/apple-signing/log/*.txt + # build release snapshots - # DOCKER_CLI_EXPERIMENTAL needed to support multi architecture builds for goreleaser - # the release command protects us from image build regressions if QEMU fails or docker is changed - BUILD_GIT_TREE_STATE=$(GITTREESTATE) \ - DOCKER_CLI_EXPERIMENTAL=enabled \ - SYFT_VERSION=$(SYFTVERSION) \ - $(TEMPDIR)/goreleaser release --skip-publish --skip-sign --rm-dist --snapshot --config $(TEMPDIR)/goreleaser.yaml - -.PHONY: acceptance-linux -acceptance-linux: $(SNAPSHOTDIR) ## Run acceptance tests on build snapshot binaries and packages (Linux) - -# note: this is used by CI to determine if the inline-scan report cache should be busted for the inline-compare tests -.PHONY: compare-fingerprint -compare-fingerprint: ## Compare a snapshot build run of grype against inline-scan - find test/inline-compare/* -type f -exec md5sum {} + | grep -v '\-reports' | grep -v 'fingerprint' | awk '{print $1}' | sort | md5sum | tee test/inline-compare/inline-compare.fingerprint - -.PHONY: compare-snapshot -compare-snapshot: $(SNAPSHOTDIR) ## Compare a main branch build run of grype against inline-scan - chmod 755 $(SNAPSHOT_CMD) - @cd test/inline-compare && GRYPE_CMD=$(SNAPSHOT_CMD) make - -.PHONY: compare -compare: - @cd test/inline-compare && make + bash -c "\ + SYFT_VERSION=$(SYFT_VERSION)\ + $(SNAPSHOT_CMD) --config $(TEMPDIR)/goreleaser.yaml || (cat .github/scripts/apple-signing/log/*.txt && false)" + + # remove the keychain with the trusted self-signed cert automatically + .github/scripts/apple-signing/cleanup.sh .PHONY: changelog changelog: clean-changelog CHANGELOG.md @@ -231,50 +253,48 @@ validate-syft-release-version: @./.github/scripts/syft-released-version-check.sh .PHONY: release -release: clean-dist validate-grype-test-config CHANGELOG.md ## Build and publish final binaries and packages. Intended to be run only on macOS. +release: clean-dist CHANGELOG.md ## Build and publish final binaries and packages. Intended to be run only on macOS. $(call title,Publishing release artifacts) - # Prepare for macOS-specific signing process - .github/scripts/mac-prepare-for-signing.sh - - # login to docker - # note: the previous step creates a new keychain, so it is important to reauth into docker.io - @echo $${DOCKER_PASSWORD} | docker login docker.io -u $${DOCKER_USERNAME} --password-stdin - # create a config with the dist dir overridden echo "dist: $(DISTDIR)" > $(TEMPDIR)/goreleaser.yaml cat .goreleaser.yaml >> $(TEMPDIR)/goreleaser.yaml - # release (note the version transformation from v0.7.0 --> 0.7.0) - # DOCKER_CLI_EXPERIMENTAL needed to support multi architecture builds for goreleaser + rm -f .github/scripts/apple-signing/log/*.txt + + # note: notarization cannot be done in parallel, thus --parallelism 1 bash -c "\ - BUILD_GIT_TREE_STATE=$(GITTREESTATE) \ - DOCKER_CLI_EXPERIMENTAL=enabled \ - SYFT_VERSION=$(SYFTVERSION) \ - VERSION=$(VERSION:v%=%) \ - $(TEMPDIR)/goreleaser \ - --rm-dist \ - --config $(TEMPDIR)/goreleaser.yaml \ - --release-notes <(cat CHANGELOG.md)" - - # verify checksum signatures - .github/scripts/verify-signature.sh "$(DISTDIR)" + SYFT_VERSION=$(SYFT_VERSION)\ + $(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 + + # 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)" .PHONY: clean clean: clean-dist clean-snapshot ## Remove previous builds and result reports - rm -rf $(RESULTSDIR)/* + $(call safe_rm_rf_children,$(RESULTSDIR)) .PHONY: clean-snapshot clean-snapshot: - rm -rf $(SNAPSHOTDIR) $(TEMPDIR)/goreleaser.yaml + $(call safe_rm_rf,$(SNAPSHOTDIR)) + rm -f $(TEMPDIR)/goreleaser.yaml .PHONY: clean-dist clean-dist: clean-changelog - rm -rf $(DISTDIR) $(TEMPDIR)/goreleaser.yaml + $(call safe_rm_rf,$(DISTDIR)) + rm -f $(TEMPDIR)/goreleaser.yaml .PHONY: clean-changelog clean-changelog: rm -f CHANGELOG.md + +.PHONY: clean-test-cache +clean-test-cache: ## Delete all test cache (built docker image tars) + find . -type f -wholename "**/test-fixtures/cache/*.tar" -delete diff --git a/cmd/version.go b/cmd/version.go index aeb3d98b14d..28ff2629c59 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -34,7 +34,7 @@ func printVersion(_ *cobra.Command, _ []string) error { fmt.Println("Syft Version: ", versionInfo.SyftVersion) fmt.Println("BuildDate: ", versionInfo.BuildDate) fmt.Println("GitCommit: ", versionInfo.GitCommit) - fmt.Println("GitTreeState: ", versionInfo.GitTreeState) + fmt.Println("GitDescription: ", versionInfo.GitDescription) fmt.Println("Platform: ", versionInfo.Platform) fmt.Println("GoVersion: ", versionInfo.GoVersion) fmt.Println("Compiler: ", versionInfo.Compiler) diff --git a/go.sum b/go.sum index 24366fcb70d..80a15e7f40f 100644 --- a/go.sum +++ b/go.sum @@ -128,7 +128,6 @@ github.com/anchore/go-version v1.2.2-0.20210903204242-51efa5b487c4/go.mod h1:Bkc github.com/anchore/packageurl-go v0.0.0-20210922164639-b3fa992ebd29 h1:K9LfnxwhqvihqU0+MF325FNy7fsKV9EGaUxdfR4gnWk= github.com/anchore/packageurl-go v0.0.0-20210922164639-b3fa992ebd29/go.mod h1:Oc1UkGaJwY6ND6vtAqPSlYrptKRJngHwkwB6W7l1uP0= github.com/anchore/stereoscope v0.0.0-20220209160132-2e595043fa19/go.mod h1:QpDHHV2h1NNfu7klzU75XC8RvSlaPK6HHgi0dy8A6sk= -github.com/anchore/stereoscope v0.0.0-20220209160132-2e595043fa19/go.mod h1:QpDHHV2h1NNfu7klzU75XC8RvSlaPK6HHgi0dy8A6sk= github.com/anchore/stereoscope v0.0.0-20220209180455-403dd709a3fb h1:yicFaC7dVBS4uYvU7sxsnEVi/2rndM0axZUgfhx+1qs= github.com/anchore/stereoscope v0.0.0-20220209180455-403dd709a3fb/go.mod h1:QpDHHV2h1NNfu7klzU75XC8RvSlaPK6HHgi0dy8A6sk= github.com/anchore/syft v0.37.11-0.20220210211800-220f3a24fdf5 h1:GLShI62a8Y5pW+SIWnwsoXC4szWIj98rzwzEurKem84= @@ -1046,11 +1045,16 @@ go.opentelemetry.io/otel/metric v0.26.0/go.mod h1:c6YL0fhRo4YVoNs6GoByzUgBp36hBL go.opentelemetry.io/otel/sdk v1.3.0/go.mod h1:rIo4suHNhQwBIPg9axF8V9CA72Wz2mKF1teNrup8yzs= go.opentelemetry.io/otel/trace v1.3.0/go.mod h1:c/VDhno8888bvQYmbYLqe41/Ldmr/KKunbvWM4/fEjk= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= diff --git a/gon.hcl b/gon.hcl deleted file mode 100644 index 8e688514dd4..00000000000 --- a/gon.hcl +++ /dev/null @@ -1,15 +0,0 @@ -source = ["./dist/grype-macos_darwin_amd64/grype"] # The 'dist' directory path should ideally reference an env var, where the source of truth is the Makefile. I wasn't able to figure out how to solve this. -bundle_id = "com.anchore.toolbox.grype" - -sign { - application_identity = "Developer ID Application: ANCHORE, INC. (9MJHKYX5AT)" -} - -dmg { - output_path = "./dist/output.dmg" - volume_name = "Grype" -} - -zip { - output_path = "./dist/output.zip" -} diff --git a/install.sh b/install.sh index 0772c2b1929..6c47882e2c6 100755 --- a/install.sh +++ b/install.sh @@ -1,198 +1,129 @@ #!/bin/sh -set -e +# note: we require errors to propagate (don't set -e) +set -u -usage() { +PROJECT_NAME="grype" +OWNER=anchore +REPO="${PROJECT_NAME}" +GITHUB_DOWNLOAD_PREFIX=https://github.com/${OWNER}/${REPO}/releases/download + +# +# usage [script-name] +# +usage() ( this=$1 cat </dev/null -} -echoerr() { +) + +echo_stderr() ( echo "$@" 1>&2 -} -log_prefix() { - echo "$0" -} -_logp=6 +) + +_logp=2 log_set_priority() { _logp="$1" } -log_priority() { + +log_priority() ( if test -z "$1"; then echo "$_logp" return fi [ "$1" -le "$_logp" ] +) + +init_colors() { + RED='' + BLUE='' + PURPLE='' + BOLD='' + RESET='' + # check if stdout is a terminal + if test -t 1 && is_command tput; then + # see if it supports colors + ncolors=$(tput colors) + if test -n "$ncolors" && test $ncolors -ge 8; then + RED='\033[0;31m' + BLUE='\033[0;34m' + PURPLE='\033[0;35m' + BOLD='\033[1m' + RESET='\033[0m' + fi + fi } -log_tag() { + +init_colors + +log_tag() ( case $1 in - 0) echo "emerg" ;; - 1) echo "alert" ;; - 2) echo "crit" ;; - 3) echo "err" ;; - 4) echo "warning" ;; - 5) echo "notice" ;; - 6) echo "info" ;; - 7) echo "debug" ;; - *) echo "$1" ;; - esac -} -log_debug() { - log_priority 7 || return 0 - echoerr "$(log_prefix)" "$(log_tag 7)" "$@" -} -log_info() { - log_priority 6 || return 0 - echoerr "$(log_prefix)" "$(log_tag 6)" "$@" -} -log_err() { - log_priority 3 || return 0 - echoerr "$(log_prefix)" "$(log_tag 3)" "$@" -} -log_crit() { - log_priority 2 || return 0 - echoerr "$(log_prefix)" "$(log_tag 2)" "$@" -} -uname_os() { - os=$(uname -s | tr '[:upper:]' '[:lower:]') - case "$os" in - cygwin_nt*) os="windows" ;; - mingw*) os="windows" ;; - msys_nt*) os="windows" ;; - esac - echo "$os" -} -uname_arch() { - arch=$(uname -m) - case $arch in - x86_64) arch="amd64" ;; - x86) arch="386" ;; - i686) arch="386" ;; - i386) arch="386" ;; - aarch64) arch="arm64" ;; - armv5*) arch="armv5" ;; - armv6*) arch="armv6" ;; - armv7*) arch="armv7" ;; + 0) echo "${RED}${BOLD}[error]${RESET}" ;; + 1) echo "${RED}[warn]${RESET}" ;; + 2) echo "[info]${RESET}" ;; + 3) echo "${BLUE}[debug]${RESET}" ;; + 4) echo "${PURPLE}[trace]${RESET}" ;; + *) echo "[$1]" ;; esac - echo ${arch} -} -uname_os_check() { - os=$(uname_os) +) + + +log_trace_priority=4 +log_trace() ( + priority=$log_trace_priority + log_priority "$priority" || return 0 + echo_stderr "$(log_tag $priority)" "${@}" "${RESET}" +) + +log_debug_priority=3 +log_debug() ( + priority=$log_debug_priority + log_priority "$priority" || return 0 + echo_stderr "$(log_tag $priority)" "${@}" "${RESET}" +) + +log_info_priority=2 +log_info() ( + priority=$log_info_priority + log_priority "$priority" || return 0 + echo_stderr "$(log_tag $priority)" "${@}" "${RESET}" +) + +log_warn_priority=1 +log_warn() ( + priority=$log_warn_priority + log_priority "$priority" || return 0 + echo_stderr "$(log_tag $priority)" "${@}" "${RESET}" +) + +log_err_priority=0 +log_err() ( + priority=$log_err_priority + log_priority "$priority" || return 0 + echo_stderr "$(log_tag $priority)" "${@}" "${RESET}" +) + +uname_os_check() ( + os=$1 case "$os" in darwin) return 0 ;; dragonfly) return 0 ;; @@ -206,11 +137,12 @@ uname_os_check() { solaris) return 0 ;; windows) return 0 ;; esac - log_crit "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib" + log_err "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib" return 1 -} -uname_arch_check() { - arch=$(uname_arch) +) + +uname_arch_check() ( + arch=$1 case "$arch" in 386) return 0 ;; amd64) return 0 ;; @@ -227,56 +159,72 @@ uname_arch_check() { s390x) return 0 ;; amd64p32) return 0 ;; esac - log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib" + log_err "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib" return 1 -} -unpack() { +) + +unpack() ( archive=$1 + + log_trace "unpack(archive=${archive})" + case "${archive}" in *.tar.gz | *.tgz) tar --no-same-owner -xzf "${archive}" ;; *.tar) tar --no-same-owner -xf "${archive}" ;; - *.zip) unzip "${archive}" ;; + *.zip) unzip -q "${archive}" ;; *.dmg) extract_from_dmg "${archive}" ;; *) log_err "unpack unknown archive format for ${archive}" return 1 ;; esac -} -extract_from_dmg() { +) + +extract_from_dmg() ( dmg_file=$1 + mount_point="/Volumes/tmp-dmg" hdiutil attach -quiet -nobrowse -mountpoint "${mount_point}" "${dmg_file}" cp -fR "${mount_point}/." ./ hdiutil detach -quiet -force "${mount_point}" -} -http_download_curl() { +) + +http_download_curl() ( local_file=$1 source_url=$2 header=$3 + + log_trace "http_download_curl(local_file=$local_file, source_url=$source_url, header=$header)" + if [ -z "$header" ]; then code=$(curl -w '%{http_code}' -sL -o "$local_file" "$source_url") else code=$(curl -w '%{http_code}' -sL -H "$header" -o "$local_file" "$source_url") fi + if [ "$code" != "200" ]; then - log_debug "http_download_curl received HTTP status $code" + log_err "received HTTP status=$code for url='$source_url'" return 1 fi return 0 -} -http_download_wget() { +) + +http_download_wget() ( local_file=$1 source_url=$2 header=$3 + + log_trace "http_download_wget(local_file=$local_file, source_url=$source_url, header=$header)" + if [ -z "$header" ]; then wget -q -O "$local_file" "$source_url" else wget -q --header "$header" -O "$local_file" "$source_url" fi -} -http_download() { - log_debug "http_download $2" +) + +http_download() ( + log_debug "http_download(url=$2)" if is_command curl; then http_download_curl "$@" return @@ -284,28 +232,19 @@ http_download() { http_download_wget "$@" return fi - log_crit "http_download unable to find wget or curl" + log_err "http_download unable to find wget or curl" return 1 -} -http_copy() { +) + +http_copy() ( tmp=$(mktemp) http_download "${tmp}" "$1" "$2" || return 1 body=$(cat "$tmp") rm -f "${tmp}" echo "$body" -} -github_release() { - owner_repo=$1 - version=$2 - test -z "$version" && version="latest" - giturl="https://github.com/${owner_repo}/releases/${version}" - json=$(http_copy "$giturl" "Accept:application/json") - test -z "$json" && return 1 - version=$(echo "$json" | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//') - test -z "$version" && return 1 - echo "$version" -} -hash_sha256() { +) + +hash_sha256() ( TARGET=${1:-/dev/stdin} if is_command gsha256sum; then hash=$(gsha256sum "$TARGET") || return 1 @@ -320,11 +259,12 @@ hash_sha256() { hash=$(openssl -dst openssl dgst -sha256 "$TARGET") || return 1 echo "$hash" | cut -d ' ' -f a else - log_crit "hash_sha256 unable to find command to compute sha-256 hash" + log_err "hash_sha256 unable to find command to compute sha-256 hash" return 1 fi -} -hash_sha256_verify() { +) + +hash_sha256_verify() ( TARGET=$1 checksums=$2 if [ -z "$checksums" ]; then @@ -342,51 +282,400 @@ hash_sha256_verify() { log_err "hash_sha256_verify checksum for '$TARGET' did not verify ${want} vs $got" return 1 fi -} -cat /dev/null < $(GRYPE_REPORT) - -.PHONY: clean -clean: - rm -f $(INLINE_DIR)/* - -.PHONY: clean-grype -clean-grype: - rm -f $(GRYPE_DIR)/* - diff --git a/test/inline-compare/compare-all.sh b/test/inline-compare/compare-all.sh deleted file mode 100755 index bad3739179b..00000000000 --- a/test/inline-compare/compare-all.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash -set -eu - -image_build_dir="./images" -builds=($(ls -d -1 images/*)) -images=("debian:10.5" "centos:8.2.2004" "alpine:3.12.0") - -# build images -for build_path in "${builds[@]}"; do - echo "Building $build_path" - pushd $build_path - new_image=$(basename $build_path):latest - docker build -q -t $new_image . - - images+=($new_image) - popd -done - -# gather all image analyses -for img in "${images[@]}"; do - echo "Gathering facts for $img" - COMPARE_IMAGE=${img} make gather-image -done - -# compare all results -for img in "${images[@]}"; do - echo "Comparing results for $img" - COMPARE_IMAGE=${img} make compare-image -done \ No newline at end of file diff --git a/test/inline-compare/compare.py b/test/inline-compare/compare.py deleted file mode 100755 index f41ba3d6445..00000000000 --- a/test/inline-compare/compare.py +++ /dev/null @@ -1,336 +0,0 @@ -#!/usr/bin/env python3 -import os -import re -import sys -import json -import collections - -INCLUDE_SEVERITY = False -NO_COMPARE_VALUE = "n/a" -QUALITY_GATE_THRESHOLD = 0.85 -INDENT = " " -IMAGE_QUALITY_GATE = collections.defaultdict(lambda: QUALITY_GATE_THRESHOLD, **{ - # not necessary if not comparing severity - # "debian:10.5": 0.86, # anchore is replacing "Negligible" severity with "Low" in some (all?) situations - "alpine:3.12.0": 1.0, # no known vulnerabilities - "alpine-vuln:latest": 1.0, - "python-vuln:latest": 1.0, - "java-vuln:latest": 1.0, -}) - -# We additionally fail if an image is above a particular threshold. Why? We expect the lower threshold to be 90%, -# however additional functionality in grype is still being implemented, so this threshold may not be able to be met. -# In these cases the IMAGE_QUALITY_GATE is set to a lower value to allow the test to pass for known issues. Once these -# issues/enhancements are done we want to ensure that the lower threshold is bumped up to catch regression. The only way -# to do this is to select an upper threshold for images with known threshold values, so we have a failure that -# loudly indicates the lower threshold should be bumped. -IMAGE_UPPER_THRESHOLD = collections.defaultdict(lambda: 1, **{ -}) - -Metadata = collections.namedtuple("Metadata", "version severity") -Package = collections.namedtuple("Package", "name type") -Vulnerability = collections.namedtuple("Vulnerability", "id package") - - -def clean(image: str) -> str: - return os.path.basename(image.replace(":", "_")) - - -class InlineScan: - - report_tmpl = "{image}-{report}.json" - - def __init__(self, image, report_dir="./"): - self.report_dir = report_dir - self.image = image - - def _report_path(self, report): - return os.path.join( - self.report_dir, - self.report_tmpl.format(image=clean(self.image), report=report), - ) - - def _enumerate_section(self, report, section): - report_path = self._report_path(report=report) - os_report_path = self._report_path(report="content-os") - - if os.path.exists(os_report_path) and not os.path.exists(report_path): - # if the OS report is there but the target report is not, that is engine's way of saying "no findings" - return - - with open(report_path) as json_file: - data = json.load(json_file) - for entry in data[section]: - yield entry - - def vulnerabilities(self): - vulnerabilities = set() - metadata = collections.defaultdict(dict) - for entry in self._enumerate_section(report="vuln", section="vulnerabilities"): - package = Package( - name=entry["package_name"], - type=entry["package_type"].lower(), - ) - vulnerability = Vulnerability( - id=entry["vuln"], - package=package, - ) - vulnerabilities.add(vulnerability) - - severity = entry["severity"] - if not INCLUDE_SEVERITY: - severity = NO_COMPARE_VALUE - - metadata[package.type][package] = Metadata(version=entry["package_version"], severity=severity) - return vulnerabilities, metadata - - def packages(self): - python_packages = self._python_packages() - os_packages = self._os_packages() - return python_packages | os_packages - - def _python_packages(self): - packages = set() - for entry in self._enumerate_section( - report="content-python", section="content" - ): - package = Package(name=entry["package"], type=entry["type"].lower(),) - packages.add(package) - - return packages - - def _os_packages(self): - packages = set() - for entry in self._enumerate_section(report="content-os", section="content"): - package = Package(name=entry["package"], type=entry["type"].lower()) - packages.add(package) - - return packages - - -class Grype: - - report_tmpl = "{image}.json" - - def __init__(self, image, report_dir="./"): - self.report_path = os.path.join( - report_dir, self.report_tmpl.format(image=clean(image)) - ) - - def _enumerate_section(self, section): - with open(self.report_path) as json_file: - data = json.load(json_file) - for entry in data[section]: - yield entry - - def vulnerabilities(self): - vulnerabilities = set() - metadata = collections.defaultdict(dict) - for entry in self._enumerate_section(section="matches"): - - # normalize to inline - pkg_type = entry["artifact"]["type"].lower() - if pkg_type in ("wheel", "egg"): - pkg_type = "python" - elif pkg_type in ("deb",): - pkg_type = "dpkg" - elif pkg_type in ("java-archive",): - pkg_type = "java" - elif pkg_type in ("apk",): - pkg_type = "apkg" - - package = Package(name=entry["artifact"]["name"], type=pkg_type,) - - vulnerability = Vulnerability( - id=entry["vulnerability"]["id"], - package=package, - ) - vulnerabilities.add(vulnerability) - - severity = entry["vulnerability"]["severity"] - if not INCLUDE_SEVERITY: - severity = NO_COMPARE_VALUE - - # engine doesn't capture epoch info, so we cannot use it during comparison - version = entry["artifact"]["version"] - if re.match(r'^\d+:', version): - version = ":".join(version.split(":")[1:]) - - metadata[package.type][package] = Metadata(version=version, severity=severity) - - return vulnerabilities, metadata - - -def print_rows(rows): - if not rows: - return - widths = [] - for col, _ in enumerate(rows[0]): - width = max(len(row[col]) for row in rows) + 2 # padding - widths.append(width) - for row in rows: - print("".join(word.ljust(widths[col_idx]) for col_idx, word in enumerate(row))) - - -def main(image): - print(colors.bold+"Image:", image, colors.reset) - - if not INCLUDE_SEVERITY: - print(colors.bold + colors.fg.orange + "Warning: not comparing severity", colors.reset) - - inline = InlineScan(image=image, report_dir="inline-reports") - inline_vulnerabilities, inline_metadata = inline.vulnerabilities() - - grype = Grype(image=image, report_dir="grype-reports") - grype_vulnerabilities, grype_metadata = grype.vulnerabilities() - - if len(inline.packages()) == 0: - # we don't want to accidentally pass the vulnerability check if there were no packages discovered. - # (we are purposefully selecting test images that are guaranteed to have packages, so this should never happen) - print(colors.bold + colors.fg.red + "inline found no packages!", colors.reset) - return 1 - - if len(inline_vulnerabilities) == 0: - if len(grype_vulnerabilities) == 0: - print(colors.bold+"nobody found any vulnerabilities", colors.reset) - return 0 - print(colors.bold+"inline does not have any vulnerabilities to compare to", colors.reset) - return 0 - - same_vulnerabilities = grype_vulnerabilities & inline_vulnerabilities - if len(inline_vulnerabilities) == 0: - percent_overlap_vulnerabilities = 0 - else: - percent_overlap_vulnerabilities = ( - float(len(same_vulnerabilities)) / float(len(inline_vulnerabilities)) - ) * 100.0 - - bonus_vulnerabilities = grype_vulnerabilities - inline_vulnerabilities - missing_vulnerabilities = inline_vulnerabilities - grype_vulnerabilities - - inline_metadata_set = set() - for vulnerability in inline_vulnerabilities: - metadata = inline_metadata[vulnerability.package.type][vulnerability.package] - inline_metadata_set.add((vulnerability.package, metadata)) - - grype_overlap_metadata_set = set() - for vulnerability in grype_vulnerabilities: - metadata = grype_metadata[vulnerability.package.type][vulnerability.package] - # we only want to really count mismatched metadata for packages that are at least found by inline - if vulnerability.package in inline_metadata[vulnerability.package.type]: - grype_overlap_metadata_set.add((vulnerability.package, metadata)) - - same_metadata = grype_overlap_metadata_set & inline_metadata_set - missing_metadata = inline_metadata_set - same_metadata - if len(inline_metadata_set) == 0: - percent_overlap_metadata = 0 - else: - percent_overlap_metadata = ( - float(len(same_metadata)) / float(len(inline_metadata_set)) - ) * 100.0 - - if len(bonus_vulnerabilities) > 0: - rows = [] - print(colors.bold + "Grype found extra vulnerabilities:", colors.reset) - for vulnerability in sorted(list(bonus_vulnerabilities)): - metadata = grype_metadata[vulnerability.package.type][vulnerability.package] - rows.append([INDENT, repr(vulnerability), repr(metadata)]) - print_rows(rows) - print() - - if len(missing_vulnerabilities) > 0: - rows = [] - print(colors.bold + "Grype missed vulnerabilities:", colors.reset) - for vulnerability in sorted(list(missing_vulnerabilities)): - metadata = inline_metadata[vulnerability.package.type][vulnerability.package] - rows.append([INDENT, repr(vulnerability), repr(metadata)]) - print_rows(rows) - print() - - if len(missing_metadata) > 0: - rows = [] - print(colors.bold + "Grype mismatched metadata:", colors.reset) - for inline_metadata_pair in sorted(list(missing_metadata)): - pkg, metadata = inline_metadata_pair - if pkg in grype_metadata[pkg.type]: - grype_metadata_item = grype_metadata[pkg.type][pkg] - else: - grype_metadata_item = "--- MISSING ---" - rows.append([INDENT, "for:", repr(pkg), ":", repr(grype_metadata_item), "!=", repr(metadata)]) - print_rows(rows) - print() - - print(colors.bold+"Summary:", colors.reset) - print(" Image: %s" % image) - print(" Inline Vulnerabilities : %d" % len(inline_vulnerabilities)) - print(" Grype Vulnerabilities : %d " % len(grype_vulnerabilities)) - print(" (extra) : %d (note: this is ignored in the analysis!)" % len(bonus_vulnerabilities)) - print(" (missing) : %d " % len(missing_vulnerabilities)) - print( - " Baseline Vulnerabilities Matched : %2.1f %% (%d/%d vulnerability)" - % (percent_overlap_vulnerabilities, len(same_vulnerabilities), len(inline_vulnerabilities)) - ) - print( - " Baseline Metadata Matched : %2.1f %% (%d/%d metadata)" - % (percent_overlap_metadata, len(same_metadata), len(inline_metadata_set)) - ) - - overall_score = (percent_overlap_vulnerabilities + percent_overlap_metadata) / 2.0 - - print(colors.bold + " Overall Score: %2.1f %%" % overall_score, colors.reset) - - upper_gate_value = IMAGE_UPPER_THRESHOLD[image] * 100 - lower_gate_value = IMAGE_QUALITY_GATE[image] * 100 - if overall_score < lower_gate_value: - print(colors.bold + " Quality Gate: " + colors.fg.red + "FAILED (is not >= %d %%)\n" % lower_gate_value, colors.reset) - return 1 - elif overall_score > upper_gate_value: - print(colors.bold + " Quality Gate: " + colors.fg.orange + "FAILED (lower threshold is artificially low and should be updated)\n", colors.reset) - return 1 - else: - print(colors.bold + " Quality Gate: " + colors.fg.green + "pass (>= %d %%)\n" % lower_gate_value, colors.reset) - - return 0 - - -class colors: - reset='\033[0m' - bold='\033[01m' - disable='\033[02m' - underline='\033[04m' - reverse='\033[07m' - strikethrough='\033[09m' - invisible='\033[08m' - class fg: - black='\033[30m' - red='\033[31m' - green='\033[32m' - orange='\033[33m' - blue='\033[34m' - purple='\033[35m' - cyan='\033[36m' - lightgrey='\033[37m' - darkgrey='\033[90m' - lightred='\033[91m' - lightgreen='\033[92m' - yellow='\033[93m' - lightblue='\033[94m' - pink='\033[95m' - lightcyan='\033[96m' - class bg: - black='\033[40m' - red='\033[41m' - green='\033[42m' - orange='\033[43m' - blue='\033[44m' - purple='\033[45m' - cyan='\033[46m' - lightgrey='\033[47m' - - -if __name__ == "__main__": - if len(sys.argv) != 2: - sys.exit("provide an image") - - rc = main(sys.argv[1]) - sys.exit(rc) diff --git a/test/inline-compare/images/alpine-vuln/Dockerfile b/test/inline-compare/images/alpine-vuln/Dockerfile deleted file mode 100644 index 6e5277ad2f5..00000000000 --- a/test/inline-compare/images/alpine-vuln/Dockerfile +++ /dev/null @@ -1,5 +0,0 @@ -FROM alpine:3.12.0 -RUN wget http://dl-cdn.alpinelinux.org/alpine/v3.9/main/x86_64/libvncserver-0.9.11-r3.apk -RUN apk add libvncserver-0.9.11-r3.apk -# I know this is cheating a bit... -RUN sed -i 's/V:0.9.11-r3/V:0.9.9-r0/' /lib/apk/db/installed \ No newline at end of file diff --git a/test/inline-compare/images/java-vuln/Dockerfile b/test/inline-compare/images/java-vuln/Dockerfile deleted file mode 100644 index c85cf256c33..00000000000 --- a/test/inline-compare/images/java-vuln/Dockerfile +++ /dev/null @@ -1,2 +0,0 @@ -FROM alpine:3.12.0 -RUN wget https://repo1.maven.org/maven2/org/quartz-scheduler/quartz/2.3.1/quartz-2.3.1.jar \ No newline at end of file diff --git a/test/inline-compare/images/python-vuln/Dockerfile b/test/inline-compare/images/python-vuln/Dockerfile deleted file mode 100644 index d04077aeffd..00000000000 --- a/test/inline-compare/images/python-vuln/Dockerfile +++ /dev/null @@ -1,2 +0,0 @@ -FROM python:3.8.5-alpine3.12 -RUN pip install requests==2.10.0 \ No newline at end of file diff --git a/test/install/.dockerignore b/test/install/.dockerignore new file mode 100644 index 00000000000..fa29cdfff91 --- /dev/null +++ b/test/install/.dockerignore @@ -0,0 +1 @@ +** \ No newline at end of file diff --git a/test/install/.gitignore b/test/install/.gitignore new file mode 100644 index 00000000000..6e25fa8f106 --- /dev/null +++ b/test/install/.gitignore @@ -0,0 +1 @@ +cache/ \ No newline at end of file diff --git a/test/install/0_search_for_asset_test.sh b/test/install/0_search_for_asset_test.sh new file mode 100755 index 00000000000..939678289d7 --- /dev/null +++ b/test/install/0_search_for_asset_test.sh @@ -0,0 +1,40 @@ +. test_harness.sh + +# search for an asset in a release checksums file +test_search_for_asset_release() { + fixture=./test-fixtures/grype_0.32.0_checksums.txt + + # search_for_asset [checksums-file-path] [name] [os] [arch] [format] + + # positive case + actual=$(search_for_asset "${fixture}" "grype" "linux" "amd64" "tar.gz") + assertEquals "grype_0.32.0_linux_amd64.tar.gz" "${actual}" "unable to find release asset" + + # negative cases + actual=$(search_for_asset "${fixture}" "grype" "Linux" "amd64" "tar.gz") + assertEquals "" "${actual}" "found a release asset but did not expect to (os)" + + actual=$(search_for_asset "${fixture}" "grype" "darwin" "amd64" "rpm") + assertEquals "" "${actual}" "found a release asset but did not expect to (format)" + +} + +run_test_case test_search_for_asset_release + + +# search for an asset in a snapshot checksums file +test_search_for_asset_snapshot() { + fixture=./test-fixtures/grype_0.32.0-SNAPSHOT-d461f63_checksums.txt + + # search_for_asset [checksums-file-path] [name] [os] [arch] [format] + + # positive case + actual=$(search_for_asset "${fixture}" "grype" "linux" "amd64" "rpm") + assertEquals "grype_0.32.0-SNAPSHOT-d461f63_linux_amd64.rpm" "${actual}" "unable to find snapshot asset" + + # negative case + actual=$(search_for_asset "${fixture}" "grype" "linux" "amd64" "zip") + assertEquals "" "${actual}" "found a snapshot asset but did not expect to (format)" +} + +run_test_case test_search_for_asset_snapshot diff --git a/test/install/1_download_snapshot_asset_test.sh b/test/install/1_download_snapshot_asset_test.sh new file mode 100755 index 00000000000..71623755b4c --- /dev/null +++ b/test/install/1_download_snapshot_asset_test.sh @@ -0,0 +1,86 @@ +. test_harness.sh + +DOWNLOAD_SNAPSHOT_POSITIVE_CASES=0 + +# helper for asserting test_positive_snapshot_download_asset positive cases +test_positive_snapshot_download_asset() { + os="$1" + arch="$2" + format="$3" + + # for troubleshooting + # log_set_priority 10 + + name=${PROJECT_NAME} + github_download=$(snapshot_download_url) + version=$(snapshot_version) + + tmpdir=$(mktemp -d) + + actual_filepath=$(download_asset "${github_download}" "${tmpdir}" "${name}" "${os}" "${arch}" "${version}" "${format}" ) + + assertFileExists "${actual_filepath}" "download_asset os=${os} arch=${arch} format=${format}" + + assertFilesEqual \ + "$(snapshot_dir)/${name}_${version}_${os}_${arch}.${format}" \ + "${actual_filepath}" \ + "unable to download os=${os} arch=${arch} format=${format}" + + ((DOWNLOAD_SNAPSHOT_POSITIVE_CASES++)) + + rm -rf -- "$tmpdir" +} + + +test_download_snapshot_asset_exercised_all_assets() { + expected=$(snapshot_assets_count) + + assertEquals "${expected}" "${DOWNLOAD_SNAPSHOT_POSITIVE_CASES}" "did not download all possible assets (missing an os/arch/format variant?)" +} + +# helper for asserting download_asset negative cases +test_negative_snapshot_download_asset() { + os="$1" + arch="$2" + format="$3" + + # for troubleshooting + # log_set_priority 10 + + name=${PROJECT_NAME} + github_download=$(snapshot_download_url) + version=$(snapshot_version) + + tmpdir=$(mktemp -d) + + actual_filepath=$(download_asset "${github_download}" "${tmpdir}" "${name}" "${os}" "${arch}" "${version}" "${format}") + + assertEquals "" "${actual_filepath}" "unable to download os=${os} arch=${arch} format=${format}" + + rm -rf -- "$tmpdir" +} + + +worker_pid=$(setup_snapshot_server) +trap 'teardown_snapshot_server ${worker_pid}' EXIT + +# exercise all possible assets +run_test_case test_positive_snapshot_download_asset "linux" "amd64" "tar.gz" +run_test_case test_positive_snapshot_download_asset "linux" "amd64" "rpm" +run_test_case test_positive_snapshot_download_asset "linux" "amd64" "deb" +run_test_case test_positive_snapshot_download_asset "linux" "arm64" "tar.gz" +run_test_case test_positive_snapshot_download_asset "linux" "arm64" "rpm" +run_test_case test_positive_snapshot_download_asset "linux" "arm64" "deb" +run_test_case test_positive_snapshot_download_asset "darwin" "amd64" "tar.gz" +run_test_case test_positive_snapshot_download_asset "darwin" "arm64" "tar.gz" +run_test_case test_positive_snapshot_download_asset "windows" "amd64" "zip" +# note: the mac signing process produces a dmg which is not part of the snapshot process (thus is not exercised here) + +# let's make certain we covered all assets that were expected +run_test_case test_download_snapshot_asset_exercised_all_assets + +# make certain we handle missing assets alright +run_test_case test_negative_snapshot_download_asset "bogus" "amd64" "zip" + +trap - EXIT +teardown_snapshot_server "${worker_pid}" diff --git a/test/install/2_download_release_asset_test.sh b/test/install/2_download_release_asset_test.sh new file mode 100755 index 00000000000..614a73f5455 --- /dev/null +++ b/test/install/2_download_release_asset_test.sh @@ -0,0 +1,41 @@ +. test_harness.sh + +test_download_release_asset() { + release="$1" + os="$2" + arch="$3" + format="$4" + expected_mime_type="$5" + + # for troubleshooting + # log_set_priority 10 + + name=${PROJECT_NAME} + version=$(tag_to_version ${release}) + github_download="https://github.com/${OWNER}/${REPO}/releases/download/${release}" + + tmpdir=$(mktemp -d) + + actual_filepath=$(download_asset "${github_download}" "${tmpdir}" "${name}" "${os}" "${arch}" "${version}" "${format}" ) + + assertFileExists "${actual_filepath}" "download_asset os=${os} arch=${arch} format=${format}" + + actual_mime_type=$(file -b --mime-type ${actual_filepath}) + + assertEquals "${expected_mime_type}" "${actual_mime_type}" "unexpected mimetype for os=${os} arch=${arch} format=${format}" + + rm -rf -- "$tmpdir" +} + +# always test against the latest release +release=$(get_release_tag "${OWNER}" "${REPO}" "latest" ) + +# exercise all possible assets against a real github release (based on asset listing from https://github.com/anchore/grype/releases/tag/v0.32.0) +run_test_case test_download_release_asset "${release}" "darwin" "amd64" "tar.gz" "application/gzip" +run_test_case test_download_release_asset "${release}" "darwin" "arm64" "tar.gz" "application/gzip" +run_test_case test_download_release_asset "${release}" "linux" "amd64" "tar.gz" "application/gzip" +run_test_case test_download_release_asset "${release}" "linux" "amd64" "rpm" "application/x-rpm" +run_test_case test_download_release_asset "${release}" "linux" "amd64" "deb" "application/vnd.debian.binary-package" +run_test_case test_download_release_asset "${release}" "linux" "arm64" "tar.gz" "application/gzip" +run_test_case test_download_release_asset "${release}" "linux" "arm64" "rpm" "application/x-rpm" +run_test_case test_download_release_asset "${release}" "linux" "arm64" "deb" "application/vnd.debian.binary-package" diff --git a/test/install/3_install_asset_test.sh b/test/install/3_install_asset_test.sh new file mode 100755 index 00000000000..1d699ae42bb --- /dev/null +++ b/test/install/3_install_asset_test.sh @@ -0,0 +1,90 @@ +. test_harness.sh + +INSTALL_ARCHIVE_POSITIVE_CASES=0 + +# helper for asserting install_asset positive cases +test_positive_snapshot_install_asset() { + os="$1" + arch="$2" + format="$3" + + # for troubleshooting + # log_set_priority 10 + + name=${PROJECT_NAME} + binary=$(get_binary_name "${os}" "${arch}" "${PROJECT_NAME}") + github_download=$(snapshot_download_url) + version=$(snapshot_version) + + download_dir=$(mktemp -d) + install_dir=$(mktemp -d) + + download_and_install_asset "${github_download}" "${download_dir}" "${install_dir}" "${name}" "${os}" "${arch}" "${version}" "${format}" "${binary}" + + assertEquals "0" "$?" "download/install did not succeed" + + expected_path="${install_dir}/${binary}" + assertFileExists "${expected_path}" "install_asset os=${os} arch=${arch} format=${format}" + + assertFilesEqual \ + "$(snapshot_dir)/${os}-build_${os}_${arch}/${binary}" \ + "${expected_path}" \ + "unable to verify installation of os=${os} arch=${arch} format=${format}" + + ((INSTALL_ARCHIVE_POSITIVE_CASES++)) + + rm -rf -- "$download_dir" + rm -rf -- "$install_dir" +} + +# helper for asserting install_asset negative cases +test_negative_snapshot_install_asset() { + os="$1" + arch="$2" + format="$3" + + # for troubleshooting + # log_set_priority 10 + + name=${PROJECT_NAME} + binary=$(get_binary_name "${os}" "${arch}" "${PROJECT_NAME}") + github_download=$(snapshot_download_url) + version=$(snapshot_version) + + download_dir=$(mktemp -d) + install_dir=$(mktemp -d) + + download_and_install_asset "${github_download}" "${download_dir}" "${install_dir}" "${name}" "${os}" "${arch}" "${version}" "${format}" "${binary}" + + assertNotEquals "0" "$?" "download/install should have failed but did not" + + rm -rf -- "$download_dir" + rm -rf -- "$install_dir" +} + + +test_install_asset_exercised_all_archive_assets() { + expected=$(snapshot_assets_archive_count) + + assertEquals "${expected}" "${INSTALL_ARCHIVE_POSITIVE_CASES}" "did not download all possible archive assets (missing an os/arch/format variant?)" +} + + +worker_pid=$(setup_snapshot_server) +trap 'teardown_snapshot_server ${worker_pid}' EXIT + +# exercise all possible archive assets (not rpm/deb/dmg) against a snapshot build +run_test_case test_positive_snapshot_install_asset "linux" "amd64" "tar.gz" +run_test_case test_positive_snapshot_install_asset "linux" "arm64" "tar.gz" +run_test_case test_positive_snapshot_install_asset "darwin" "amd64" "tar.gz" +run_test_case test_positive_snapshot_install_asset "darwin" "arm64" "tar.gz" +run_test_case test_positive_snapshot_install_asset "windows" "amd64" "zip" + +# let's make certain we covered all assets that were expected +run_test_case test_install_asset_exercised_all_archive_assets + +# make certain we handle missing assets alright +run_test_case test_negative_snapshot_install_asset "bogus" "amd64" "zip" + +trap - EXIT +teardown_snapshot_server "${worker_pid}" diff --git a/test/install/Makefile b/test/install/Makefile new file mode 100644 index 00000000000..25a71b2c532 --- /dev/null +++ b/test/install/Makefile @@ -0,0 +1,103 @@ +NAME=grype + +IMAGE_NAME=$(NAME)-install.sh-env +UBUNTU_IMAGE=$(IMAGE_NAME):ubuntu-20.04 +ALPINE_IMAGE=$(IMAGE_NAME):alpine-3.6 +BUSYBOX_IMAGE=busybox:1.35 + +ENVS=./environments +DOCKER_RUN=docker run --rm -t -w /project/test/install -v $(shell pwd)/../../:/project +UNIT=make unit-local + +# acceptance testing is running the current install.sh against the latest release. Note: this could be a problem down +# the line if there are breaking changes made that don't align with the latest release (but will be OK with the next +# release) +ACCEPTANCE_CMD=sh -c '../../install.sh -b /usr/local/bin && grype version' + +# CI cache busting values; change these if you want CI to not use previous stored cache +INSTALL_TEST_CACHE_BUSTER=894d8ca + +define title + @printf '\n≡≡≡[ $(1) ]≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡\n' +endef + +.PHONY: test +test: unit acceptance + +.PHONY: ci-test-mac +ci-test-mac: unit-local acceptance-local + +# note: do not add acceptance-local to this list +acceptance: acceptance-ubuntu-20.04 acceptance-alpine-3.6 acceptance-busybox-1.35 + +unit: unit-ubuntu-20.04 + +unit-local: + $(call title,unit tests) + @for f in $(shell ls *_test.sh); do echo "Running unit test suite '$${f}'"; bash $${f} || exit 1; done + +acceptance-local: + $(acceptance) + +save: ubuntu-20.04 alpine-3.6 busybox-1.35 + @mkdir cache || true + docker image save -o cache/ubuntu-env.tar $(UBUNTU_IMAGE) + docker image save -o cache/alpine-env.tar $(ALPINE_IMAGE) + docker image save -o cache/busybox-env.tar $(BUSYBOX_IMAGE) + +load: + docker image load -i cache/ubuntu-env.tar + docker image load -i cache/alpine-env.tar + docker image load -i cache/busybox-env.tar + +## UBUNTU ####################################################### + +acceptance-ubuntu-20.04: ubuntu-20.04 + $(call title,ubuntu:20.04 - acceptance) + $(DOCKER_RUN) $(UBUNTU_IMAGE) \ + $(ACCEPTANCE_CMD) + +unit-ubuntu-20.04: ubuntu-20.04 + $(call title,ubuntu:20.04 - unit) + $(DOCKER_RUN) $(UBUNTU_IMAGE) \ + $(UNIT) + +ubuntu-20.04: + $(call title,ubuntu:20.04 - build environment) + docker build -t $(UBUNTU_IMAGE) -f $(ENVS)/Dockerfile-ubuntu-20.04 . + +## ALPINE ####################################################### + +# note: unit tests cannot be run with sh (alpine dosn't have bash by default) + +acceptance-alpine-3.6: alpine-3.6 + $(call title,alpine:3.6 - acceptance) + $(DOCKER_RUN) $(ALPINE_IMAGE) \ + $(ACCEPTANCE_CMD) + +alpine-3.6: + $(call title,alpine:3.6 - build environment) + docker build -t $(ALPINE_IMAGE) -f $(ENVS)/Dockerfile-alpine-3.6 . + +## BUSYBOX ####################################################### + +# note: unit tests cannot be run with sh (busybox dosn't have bash by default) + +# note: busybox by default will not have cacerts, so you will get TLS warnings (we want to test under these conditions) + +acceptance-busybox-1.35: busybox-1.35 + $(call title,busybox-1.35 - acceptance) + $(DOCKER_RUN) $(BUSYBOX_IMAGE) \ + $(ACCEPTANCE_CMD) + @echo "\n*** test note: you should see grype spit out a 'x509: certificate signed by unknown authority' error --this is expected ***" + +busybox-1.35: + $(call title,busybox-1.35 - build environment) + docker pull $(BUSYBOX_IMAGE) + +## For CI ######################################################## + +.PHONY: cache.fingerprint +cache.fingerprint: + $(call title,Install test fixture fingerprint) + @find ./environments/* -type f -exec md5sum {} + | awk '{print $1}' | sort | tee /dev/stderr | md5sum | tee cache.fingerprint && echo "$(INSTALL_TEST_CACHE_BUSTER)" >> cache.fingerprint diff --git a/test/install/environments/Dockerfile-alpine-3.6 b/test/install/environments/Dockerfile-alpine-3.6 new file mode 100644 index 00000000000..982e5402996 --- /dev/null +++ b/test/install/environments/Dockerfile-alpine-3.6 @@ -0,0 +1,2 @@ +FROM alpine:3.6 +RUN apk update && apk add python3 wget unzip make ca-certificates \ No newline at end of file diff --git a/test/install/environments/Dockerfile-ubuntu-20.04 b/test/install/environments/Dockerfile-ubuntu-20.04 new file mode 100644 index 00000000000..dafb64ed73d --- /dev/null +++ b/test/install/environments/Dockerfile-ubuntu-20.04 @@ -0,0 +1,2 @@ +FROM ubuntu:20.04 +RUN apt update -y && apt install make python3 curl unzip -y \ No newline at end of file diff --git a/test/install/github_test.sh b/test/install/github_test.sh new file mode 100755 index 00000000000..6128e5fe297 --- /dev/null +++ b/test/install/github_test.sh @@ -0,0 +1,68 @@ +. test_harness.sh + +# check that we can extract single json values +test_extract_json_value() { + fixture=./test-fixtures/github-api-grype-v0.32.0-release.json + content=$(cat ${fixture}) + + actual=$(extract_json_value "${content}" "tag_name") + assertEquals "v0.32.0" "${actual}" "unable to find tag_name" + + actual=$(extract_json_value "${content}" "id") + assertEquals "57501596" "${actual}" "unable to find tag_name" +} + +run_test_case test_extract_json_value + + +# check that we can extract github release tag from github api json +test_github_release_tag() { + fixture=./test-fixtures/github-api-grype-v0.32.0-release.json + content=$(cat ${fixture}) + + actual=$(github_release_tag "${content}") + assertEquals "v0.32.0" "${actual}" "unable to find release tag" +} + +run_test_case test_github_release_tag + + +# download a known good github release checksums and compare against a test-fixture +test_download_github_release_checksums() { + tmpdir=$(mktemp -d) + + tag=v0.32.0 + github_download="https://github.com/anchore/grype/releases/download/${tag}" + name=${PROJECT_NAME} + version=$(tag_to_version "${tag}") + + actual_filepath=$(download_github_release_checksums "${github_download}" "${name}" "${version}" "${tmpdir}") + assertFilesEqual \ + "./test-fixtures/grype_0.32.0_checksums.txt" \ + "${actual_filepath}" \ + "unable to find release tag" + + rm -rf -- "$tmpdir" +} + +run_test_case test_download_github_release_checksums + + +# download a checksums file from a locally served-up snapshot directory and compare against the file in the snapshot dir +test_download_github_release_checksums_snapshot() { + tmpdir=$(mktemp -d) + + github_download=$(snapshot_download_url) + name=${PROJECT_NAME} + version=$(snapshot_version) + + actual_filepath=$(download_github_release_checksums "${github_download}" "${name}" "${version}" "${tmpdir}") + assertFilesEqual \ + "$(snapshot_checksums_path)" \ + "${actual_filepath}" \ + "unable to find release tag" + + rm -rf -- "$tmpdir" +} + +run_test_case_with_snapshot_release test_download_github_release_checksums_snapshot \ No newline at end of file diff --git a/test/install/test-fixtures/github-api-grype-v0.32.0-release.json b/test/install/test-fixtures/github-api-grype-v0.32.0-release.json new file mode 100644 index 00000000000..5218f8fa878 --- /dev/null +++ b/test/install/test-fixtures/github-api-grype-v0.32.0-release.json @@ -0,0 +1 @@ +{"id":57501596,"tag_name":"v0.32.0","update_url":"/anchore/grype/releases/tag/v0.32.0","update_authenticity_token":"7XbNZgRHpbHegdv-xRlbe84Y983YgyXa3YKWwv_e0ocqTHagsHq5dxCTQUQnuX3vbsgdWQU3A3__hkVNhKGHSg","delete_url":"/anchore/grype/releases/tag/v0.32.0","delete_authenticity_token":"6tLaRtXKUc-zz4tHIwCbbD7CksxIHK5imZE1gnA39oVCe6fYux5a8cPD9J52kGUzM1Hs9JPBjceG7yyszBk_2A","edit_url":"/anchore/grype/releases/edit/v0.32.0"} diff --git a/test/install/test-fixtures/grype_0.32.0-SNAPSHOT-d461f63_checksums.txt b/test/install/test-fixtures/grype_0.32.0-SNAPSHOT-d461f63_checksums.txt new file mode 100644 index 00000000000..792d90ec919 --- /dev/null +++ b/test/install/test-fixtures/grype_0.32.0-SNAPSHOT-d461f63_checksums.txt @@ -0,0 +1,9 @@ +250dddf3338d34012b55b4167b72f8bc87944e61aee35879342206a115a0f64b grype_0.32.0-SNAPSHOT-d461f63_darwin_amd64.tar.gz +4b2973604085c14bc4c452f5354110384d371f0d5c3f93c0e3a44498f54283d7 grype_0.32.0-SNAPSHOT-d461f63_linux_amd64.rpm +569b040bde6d369b9e3b96fb3d9d7ee5aa11267f3aa91fad3d8f4095f1cee150 grype_0.32.0-SNAPSHOT-d461f63_darwin_arm64.tar.gz +5c666286bca9d8c84f7355d5afe720186b0a06bed23ac0518a35a79ff905de28 grype_0.32.0-SNAPSHOT-d461f63_linux_arm64.tar.gz +dd1d7492e7a7db9a765a02927b0d019d8f9facb1173ae7c245cd06fefedddfd0 grype_0.32.0-SNAPSHOT-d461f63_windows_amd64.zip +dd4e5857856b4655511a75911fd7b53a3ebb9d2f584ae3c7ff7f52ad0dd93745 grype_0.32.0-SNAPSHOT-d461f63_linux_amd64.tar.gz +dfe9d8212def2eb3685bacf3c77f664830680a475eb6356e67c96abe4af00e74 grype_0.32.0-SNAPSHOT-d461f63_linux_arm64.rpm +e1efed13fa93c207b773cbc2a9252b87049e1a826bacb77b756a20a13a29e465 grype_0.32.0-SNAPSHOT-d461f63_linux_arm64.deb +ef2725de0e154059fb59c6268e68fd0ba3a7ce5b23e604166140f284b54ef9b4 grype_0.32.0-SNAPSHOT-d461f63_linux_amd64.deb diff --git a/test/install/test-fixtures/grype_0.32.0_checksums.txt b/test/install/test-fixtures/grype_0.32.0_checksums.txt new file mode 100644 index 00000000000..372ab6d0600 --- /dev/null +++ b/test/install/test-fixtures/grype_0.32.0_checksums.txt @@ -0,0 +1,9 @@ +250dddf3338d34012b55b4167b72f8bc87944e61aee35879342206a115a0f64b grype_0.32.0_darwin_amd64.tar.gz +4b2973604085c14bc4c452f5354110384d371f0d5c3f93c0e3a44498f54283d7 grype_0.32.0_linux_amd64.rpm +569b040bde6d369b9e3b96fb3d9d7ee5aa11267f3aa91fad3d8f4095f1cee150 grype_0.32.0_darwin_arm64.tar.gz +5c666286bca9d8c84f7355d5afe720186b0a06bed23ac0518a35a79ff905de28 grype_0.32.0_linux_arm64.tar.gz +dd1d7492e7a7db9a765a02927b0d019d8f9facb1173ae7c245cd06fefedddfd0 grype_0.32.0_windows_amd64.zip +dd4e5857856b4655511a75911fd7b53a3ebb9d2f584ae3c7ff7f52ad0dd93745 grype_0.32.0_linux_amd64.tar.gz +dfe9d8212def2eb3685bacf3c77f664830680a475eb6356e67c96abe4af00e74 grype_0.32.0_linux_arm64.rpm +e1efed13fa93c207b773cbc2a9252b87049e1a826bacb77b756a20a13a29e465 grype_0.32.0_linux_arm64.deb +ef2725de0e154059fb59c6268e68fd0ba3a7ce5b23e604166140f284b54ef9b4 grype_0.32.0_linux_amd64.deb diff --git a/test/install/test_harness.sh b/test/install/test_harness.sh new file mode 100644 index 00000000000..e6f435ebfe5 --- /dev/null +++ b/test/install/test_harness.sh @@ -0,0 +1,163 @@ +# disable using the install.sh entrypoint such that we can unit test +# script functions without invoking main() +TEST_INSTALL_SH=true + +. ../../install.sh +set -u + +assertTrue() { + if eval "$1"; then + echo "assertTrue failed: $2" + exit 2 + fi +} + +assertFalse() { + if eval "$1"; then + echo "assertFalse failed: $2" + exit 2 + fi +} + +assertEquals() { + want=$1 + got=$2 + msg=$3 + if [ "$want" != "$got" ]; then + echo "assertEquals failed: want='$want' got='$got' $msg" + exit 2 + fi +} + +assertFilesDoesNotExist() { + path="$1" + msg=$2 + if [ -f "${path}" ]; then + echo "assertFilesDoesNotExist failed: path exists '$path': $msg" + exit 2 + fi +} + +assertFileExists() { + path="$1" + msg=$2 + if [ ! -f "${path}" ]; then + echo "assertFileExists failed: path does not exist '$path': $msg" + exit 2 + fi +} + +assertFilesEqual() { + want=$1 + got=$2 + msg=$3 + + diff "$1" "$2" + if [ $? -ne 0 ]; then + echo "assertFilesEqual failed: $msg" + exit 2 + fi +} + +assertNotEquals() { + want=$1 + got=$2 + msg=$3 + if [ "$want" = "$got" ]; then + echo "assertNotEquals failed: want='$want' got='$got' $msg" + exit 2 + fi +} + +log_test_case() { + echo " running $@" +} + +run_test_case_with_snapshot_release() { + log_test_case ${@:1} + + worker_pid=$(setup_snapshot_server) + trap "teardown_snapshot_server $worker_pid" EXIT + + # run test function with all arguments + ${@:1} + + trap - EXIT + teardown_snapshot_server "${worker_pid}" +} + +serve_port=8000 + +setup_snapshot_server() { + # if you want to see proof in the logs, feel free to adjust the redirection + python3 -m http.server --directory "$(snapshot_dir)" $serve_port &> /dev/null & + worker_pid=$! + + # it takes some time for the server to be ready... + sleep 3 + + echo "$worker_pid" +} + +teardown_snapshot_server() { + worker_pid="$1" + + kill $worker_pid +} + +snapshot_version() { + partial=$(ls ../../snapshot/*_checksums.txt | grep -o "_.*_checksums.txt") + partial="${partial%_checksums.txt}" + echo "${partial#_}" +} + +snapshot_download_url() { + echo "localhost:${serve_port}" +} + +snapshot_dir() { + echo "../../snapshot" +} + +snapshot_checksums_path() { + echo "$(ls $(snapshot_dir)/*_checksums.txt)" +} + +snapshot_assets_count() { + # example output before wc -l: + + # ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_linux_arm64.deb + # ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_linux_arm64.tar.gz + # ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_linux_amd64.rpm + # ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_darwin_arm64.tar.gz + # ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_linux_amd64.deb + # ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_linux_arm64.rpm + # ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_darwin_amd64.zip + # ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_windows_amd64.zip + # ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_darwin_arm64.zip + # ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_linux_amd64.tar.gz + # ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_darwin_amd64.tar.gz + + echo "$(find ../../snapshot -maxdepth 1 -type f | grep 'grype_' | grep -v checksums | wc -l | tr -d '[:space:]')" +} + + +snapshot_assets_archive_count() { + # example output before wc -l: + + # ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_linux_arm64.tar.gz + # ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_darwin_arm64.tar.gz + # ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_darwin_amd64.zip + # ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_windows_amd64.zip + # ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_darwin_arm64.zip + # ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_linux_amd64.tar.gz + # ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_darwin_amd64.tar.gz + + echo "$(find ../../snapshot -maxdepth 1 -type f | grep 'grype_' | grep 'tar\|zip' | wc -l | tr -d '[:space:]')" +} + + +run_test_case() { + log_test_case ${@:1} + ${@:1} +}