Compare commits

..
Author SHA1 Message Date
Alex Goodman 206fd8b7d3 working
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-09-08 08:37:40 -04:00
Alex Goodman c348d97fe0 not working
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-09-05 11:16:46 -04:00
Alex Goodman 30bb7cf992 pin stereoscope to dev branch
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-09-03 18:34:23 -04:00
Alex Goodman 54e0ee4c47 finish repository + make syft client
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-09-03 18:32:20 -04:00
Alex Goodman c231a9d7cb add collection
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-09-03 09:28:45 -04:00
Alex Goodman 3e27b6e93f rough draft for plugin system
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-09-02 23:34:00 -04:00
Alex Goodman 63ee5ba098 refactor catalogers to consume io.Readers instead of strings
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-08-31 17:41:05 -04:00
1182 changed files with 20306 additions and 325021 deletions
+1 -53
View File
@@ -1,61 +1,9 @@
permit:
- BSD.*
- CC0.*
- MIT.*
- Apache.*
- MPL.*
- ISC
- WTFPL
ignore-packages:
# packageurl-go is released under the MIT license located in the root of the repo at /mit.LICENSE
- github.com/anchore/packageurl-go
# both of these dependencies are specified as Apache-2.0 in their respective GitHub READMEs
- github.com/alibabacloud-go/cr-20160607/client
- github.com/alibabacloud-go/tea-xml/service
# crypto/internal/boring is released under the openSSL license as a part of the Golang Standard Libary
- crypto/internal/boring
# from: https://github.com/spdx/tools-golang/blob/main/LICENSE.code
# The tools-golang source code is provided and may be used, at your option,
# under either:
# * Apache License, version 2.0 (Apache-2.0), OR
# * GNU General Public License, version 2.0 or later (GPL-2.0-or-later).
# (we choose Apache-2.0)
- github.com/spdx/tools-golang
# from: https://github.com/xi2/xz/blob/master/LICENSE
# All these files have been put into the public domain.
# You can do whatever you want with these files.
- github.com/xi2/xz
# from: https://gitlab.com/cznic/sqlite/-/blob/v1.15.4/LICENSE
# This is a BSD-3-Clause license
- modernc.org/libc
- modernc.org/libc/errno
- modernc.org/libc/fcntl
- modernc.org/libc/fts
- modernc.org/libc/grp
- modernc.org/libc/langinfo
- modernc.org/libc/limits
- modernc.org/libc/netdb
- modernc.org/libc/netinet/in
- modernc.org/libc/poll
- modernc.org/libc/pthread
- modernc.org/libc/pwd
- modernc.org/libc/signal
- modernc.org/libc/stdio
- modernc.org/libc/stdlib
- modernc.org/libc/sys/socket
- modernc.org/libc/sys/stat
- modernc.org/libc/sys/types
- modernc.org/libc/termios
- modernc.org/libc/time
- modernc.org/libc/unistd
- modernc.org/libc/utime
- modernc.org/libc/uuid/uuid
- modernc.org/libc/wctype
- modernc.org/mathutil
- modernc.org/memory
- github.com/package-url/packageurl-go
+129
View File
@@ -0,0 +1,129 @@
version: 2.1
jobs:
run-static-analysis:
parameters:
version:
type: string
docker:
- image: circleci/golang:<< parameters.version >>
environment:
GO111MODULE: "on"
# work around for recent circle CI breaking change
# Error: "Error response from daemon: client version 1.39 is too new. Maximum supported API version is 1.38"
DOCKER_API_VERSION: "1.38"
# 2CPU / 4GB RAM
resource_class: medium
steps:
- checkout
- restore_cache:
keys:
- go-<< parameters.version >>-{{ checksum "go.sum" }}-{{ checksum "Makefile" }}
- run: make ci-bootstrap
- save_cache:
key: go-<< parameters.version >>-{{ checksum "go.sum" }}-{{ checksum "Makefile" }}
paths:
- "/go/pkg/mod"
- ".tmp"
- run:
name: run static analysis
command: make static-analysis
run-tests:
parameters:
version:
type: string
docker:
- image: circleci/golang:<< parameters.version >>
environment:
GO111MODULE: "on"
# work around for recent circle CI breaking change
# Error: "Error response from daemon: client version 1.39 is too new. Maximum supported API version is 1.38"
DOCKER_API_VERSION: "1.38"
# 2CPU / 4GB RAM
resource_class: medium
steps:
- checkout
- restore_cache:
keys:
- go-<< parameters.version >>-{{ checksum "go.sum" }}-{{ checksum "Makefile" }}
- run: make ci-bootstrap
- save_cache:
key: go-<< parameters.version >>-{{ checksum "go.sum" }}-{{ checksum "Makefile" }}
paths:
- "/go/pkg/mod"
- ".tmp"
- setup_remote_docker:
version: 18.06.0-ce
- run:
name: enable docker client
command: |
# all of this to enable "circleci local execute ..." cli commands for /var/run/docker.sock
mkdir -p ${HOME}/.local/bin
cat \<< EOF > ${HOME}/.local/bin/docker
#!/bin/bash
set -xue
sudo -E ${HOME}/.local/bin/docker.bin \$@
EOF
sudo mv /usr/bin/docker ${HOME}/.local/bin/docker.bin
chmod 755 ${HOME}/.local/bin/docker
- run:
name: build cache key for java test-fixture blobs
command: make java-packages-fingerprint
- restore_cache:
keys:
- syft-unit-test-java-cache-{{ checksum "syft/cataloger/java/test-fixtures/java-builds/packages.fingerprint" }}
- run:
name: run unit tests
command: make unit
- save_cache:
key: syft-unit-test-java-cache-{{ checksum "syft/cataloger/java/test-fixtures/java-builds/packages.fingerprint" }}
paths:
- "syft/cataloger/java/test-fixtures/java-builds/packages"
- run:
name: validate syft output against the CycloneDX schema
command: make validate-cyclonedx-schema
- run:
name: build hash key for integration test-fixtures blobs
command: make integration-fingerprint
- restore_cache:
keys:
- syft-integration-test-tar-cache-{{ checksum "test/integration/test-fixtures/tar-cache.fingerprint" }}
- run:
name: run integration tests
command: make integration
- save_cache:
key: syft-integration-test-tar-cache-{{ checksum "test/integration/test-fixtures/tar-cache.fingerprint" }}
paths:
- "test/integration/test-fixtures/tar-cache"
workflows:
# Note: changing this workflow name requires making the same update in the .github/workflows/release.yaml pipeline
"Static Analysis + Unit + Integration":
jobs:
- run-static-analysis:
name: "Static Analysis"
version: "1.14"
- run-tests:
name: "Unit & Integration Tests (go-1.13)"
version: "1.13"
- run-tests:
name: "Unit & Integration Tests (go-latest)"
version: "latest"
+3 -3
View File
@@ -7,14 +7,14 @@ assignees: ''
---
**Please provide a set of steps on how to reproduce the issue**
**What happened**:
**What you expected to happen**:
**How to reproduce it (as minimally and precisely as possible)**:
**Anything else we need to know?**:
**Environment**:
- Output of `syft version`:
- syft version (use `syft version -v`):
- OS (e.g: `cat /etc/os-release` or similar):
+9 -4
View File
@@ -1,6 +1,11 @@
contact_links:
- name: Join the Slack community 💬
# link to our community Slack registration page
url: https://anchore.com/slack
about: 'Come chat with us! Ask for help, join our software development efforts, or just give us feedback!'
- name: Help & Support Slack Channel ⛑️
# link to toolbox-help channel
url: https://anchorecommunity.slack.com/archives/C019BUXV7R6
about: Ask a questions and get answers
- name: Developer Slack Channel 💬
# link to toolbox-dev channel
url: https://anchorecommunity.slack.com/archives/C0190NF9TSM
about: Participate in design discussions and feature development
-31
View File
@@ -1,31 +0,0 @@
#!/usr/bin/env bash
set -eu
ORIGINAL_STATE_DIR=$(mktemp -d "TEMP-original-state-XXXXXXXXX")
TIDY_STATE_DIR=$(mktemp -d "TEMP-tidy-state-XXXXXXXXX")
trap "cp -v ${ORIGINAL_STATE_DIR}/* ./ && rm -fR ${ORIGINAL_STATE_DIR} ${TIDY_STATE_DIR}" EXIT
echo "Capturing original state of files..."
cp -v go.mod go.sum "${ORIGINAL_STATE_DIR}"
echo "Capturing state of go.mod and go.sum after running go mod tidy..."
go mod tidy
cp -v go.mod go.sum "${TIDY_STATE_DIR}"
echo ""
set +e
# Detect difference between the git HEAD state and the go mod tidy state
DIFF_MOD=$(diff -u "${ORIGINAL_STATE_DIR}/go.mod" "${TIDY_STATE_DIR}/go.mod")
DIFF_SUM=$(diff -u "${ORIGINAL_STATE_DIR}/go.sum" "${TIDY_STATE_DIR}/go.sum")
if [[ -n "${DIFF_MOD}" || -n "${DIFF_SUM}" ]]; then
echo "go.mod diff:"
echo "${DIFF_MOD}"
echo "go.sum diff:"
echo "${DIFF_SUM}"
echo ""
printf "FAILED! go.mod and/or go.sum are NOT tidy; please run 'go mod tidy'.\n\n"
exit 1
fi
-398
View File
@@ -1,398 +0,0 @@
#!/bin/sh
set -e
# Code generated by godownloader on 2019-12-25T12:47:14Z. DO NOT EDIT.
#
usage() {
this=$1
cat <<EOF
$this: download go binaries for goreleaser/goreleaser
Usage: $this [-b] bindir [-d] [tag]
-b sets bindir or installation directory, Defaults to ./bin
-d turns on debug logging
[tag] is a tag from
https://github.com/goreleaser/goreleaser/releases
If tag is missing, then the latest will be used.
Generated by godownloader
https://github.com/goreleaser/godownloader
EOF
exit 2
}
parse_args() {
#BINDIR is ./bin unless set be ENV
# over-ridden by flag below
BINDIR=${BINDIR:-./bin}
while getopts "b:dh?x" arg; do
case "$arg" in
b) BINDIR="$OPTARG" ;;
d) log_set_priority 10 ;;
h | \?) usage "$0" ;;
x) set -x ;;
esac
done
shift $((OPTIND - 1))
TAG=$1
}
# this function wraps all the destructive operations
# if a curl|bash cuts off the end of the script due to
# network, either nothing will happen or will syntax error
# out preventing half-done work
execute() {
tmpdir=$(mktemp -d)
log_debug "downloading files into ${tmpdir}"
http_download "${tmpdir}/${TARBALL}" "${TARBALL_URL}"
http_download "${tmpdir}/${CHECKSUM}" "${CHECKSUM_URL}"
hash_sha256_verify "${tmpdir}/${TARBALL}" "${tmpdir}/${CHECKSUM}"
srcdir="${tmpdir}"
(cd "${tmpdir}" && untar "${TARBALL}")
test ! -d "${BINDIR}" && install -d "${BINDIR}"
for binexe in $BINARIES; do
if [ "$OS" = "windows" ]; then
binexe="${binexe}.exe"
fi
install "${srcdir}/${binexe}" "${BINDIR}/"
log_info "installed ${BINDIR}/${binexe}"
done
rm -rf "${tmpdir}"
}
get_binaries() {
case "$PLATFORM" in
darwin/386) BINARIES="goreleaser" ;;
darwin/amd64) BINARIES="goreleaser" ;;
darwin/arm64) BINARIES="goreleaser" ;;
darwin/armv6) BINARIES="goreleaser" ;;
linux/386) BINARIES="goreleaser" ;;
linux/amd64) BINARIES="goreleaser" ;;
linux/arm64) BINARIES="goreleaser" ;;
linux/armv6) BINARIES="goreleaser" ;;
windows/386) BINARIES="goreleaser" ;;
windows/amd64) BINARIES="goreleaser" ;;
windows/arm64) BINARIES="goreleaser" ;;
windows/armv6) BINARIES="goreleaser" ;;
*)
log_crit "platform $PLATFORM is not supported. Make sure this script is up-to-date and file request at https://github.com/${PREFIX}/issues/new"
exit 1
;;
esac
}
tag_to_version() {
if [ -z "${TAG}" ]; then
log_info "checking GitHub for latest tag"
else
log_info "checking GitHub for tag '${TAG}'"
fi
REALTAG=$(github_release "$OWNER/$REPO" "${TAG}") && true
if test -z "$REALTAG"; then
log_crit "unable to find '${TAG}' - use 'latest' or see https://github.com/${PREFIX}/releases for details"
exit 1
fi
# if version starts with 'v', remove it
TAG="$REALTAG"
VERSION=${TAG#v}
}
adjust_format() {
# change format (tar.gz or zip) based on OS
case ${OS} in
windows) FORMAT=zip ;;
esac
true
}
adjust_os() {
# adjust archive name based on OS
case ${OS} in
386) OS=i386 ;;
amd64) OS=x86_64 ;;
darwin) OS=Darwin ;;
linux) OS=Linux ;;
windows) OS=Windows ;;
esac
true
}
adjust_arch() {
# adjust archive name based on ARCH
case ${ARCH} in
386) ARCH=i386 ;;
amd64) ARCH=x86_64 ;;
darwin) ARCH=Darwin ;;
linux) ARCH=Linux ;;
windows) ARCH=Windows ;;
esac
true
}
cat /dev/null <<EOF
------------------------------------------------------------------------
https://github.com/client9/shlib - portable posix shell functions
Public domain - http://unlicense.org
https://github.com/client9/shlib/blob/master/LICENSE.md
but credit (and pull requests) appreciated.
------------------------------------------------------------------------
EOF
is_command() {
command -v "$1" >/dev/null
}
echoerr() {
echo "$@" 1>&2
}
log_prefix() {
echo "$0"
}
_logp=6
log_set_priority() {
_logp="$1"
}
log_priority() {
if test -z "$1"; then
echo "$_logp"
return
fi
[ "$1" -le "$_logp" ]
}
log_tag() {
case $1 in
0) echo "emerg" ;;
1) echo "alert" ;;
2) echo "crit" ;;
3) echo "err" ;;
4) echo "warning" ;;
5) echo "notice" ;;
6) echo "info" ;;
7) echo "debug" ;;
*) echo "$1" ;;
esac
}
log_debug() {
log_priority 7 || return 0
echoerr "$(log_prefix)" "$(log_tag 7)" "$@"
}
log_info() {
log_priority 6 || return 0
echoerr "$(log_prefix)" "$(log_tag 6)" "$@"
}
log_err() {
log_priority 3 || return 0
echoerr "$(log_prefix)" "$(log_tag 3)" "$@"
}
log_crit() {
log_priority 2 || return 0
echoerr "$(log_prefix)" "$(log_tag 2)" "$@"
}
uname_os() {
os=$(uname -s | tr '[:upper:]' '[:lower:]')
case "$os" in
cygwin_nt*) os="windows" ;;
mingw*) os="windows" ;;
msys_nt*) os="windows" ;;
esac
echo "$os"
}
uname_arch() {
arch=$(uname -m)
case $arch in
x86_64) arch="amd64" ;;
x86) arch="386" ;;
i686) arch="386" ;;
i386) arch="386" ;;
aarch64) arch="arm64" ;;
armv5*) arch="armv5" ;;
armv6*) arch="armv6" ;;
armv7*) arch="armv7" ;;
esac
echo ${arch}
}
uname_os_check() {
os=$(uname_os)
case "$os" in
darwin) return 0 ;;
dragonfly) return 0 ;;
freebsd) return 0 ;;
linux) return 0 ;;
android) return 0 ;;
nacl) return 0 ;;
netbsd) return 0 ;;
openbsd) return 0 ;;
plan9) return 0 ;;
solaris) return 0 ;;
windows) return 0 ;;
esac
log_crit "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib"
return 1
}
uname_arch_check() {
arch=$(uname_arch)
case "$arch" in
386) return 0 ;;
amd64) return 0 ;;
arm64) return 0 ;;
armv5) return 0 ;;
armv6) return 0 ;;
armv7) return 0 ;;
ppc64) return 0 ;;
ppc64le) return 0 ;;
mips) return 0 ;;
mipsle) return 0 ;;
mips64) return 0 ;;
mips64le) return 0 ;;
s390x) return 0 ;;
amd64p32) return 0 ;;
esac
log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib"
return 1
}
untar() {
tarball=$1
case "${tarball}" in
*.tar.gz | *.tgz) tar --no-same-owner -xzf "${tarball}" ;;
*.tar) tar --no-same-owner -xf "${tarball}" ;;
*.zip) unzip "${tarball}" ;;
*)
log_err "untar unknown archive format for ${tarball}"
return 1
;;
esac
}
http_download_curl() {
local_file=$1
source_url=$2
header=$3
if [ -z "$header" ]; then
code=$(curl -w '%{http_code}' -sL -o "$local_file" "$source_url")
else
code=$(curl -w '%{http_code}' -sL -H "$header" -o "$local_file" "$source_url")
fi
if [ "$code" != "200" ]; then
log_debug "http_download_curl received HTTP status $code"
return 1
fi
return 0
}
http_download_wget() {
local_file=$1
source_url=$2
header=$3
if [ -z "$header" ]; then
wget -q -O "$local_file" "$source_url"
else
wget -q --header "$header" -O "$local_file" "$source_url"
fi
}
http_download() {
log_debug "http_download $2"
if is_command curl; then
http_download_curl "$@"
return
elif is_command wget; then
http_download_wget "$@"
return
fi
log_crit "http_download unable to find wget or curl"
return 1
}
http_copy() {
tmp=$(mktemp)
http_download "${tmp}" "$1" "$2" || return 1
body=$(cat "$tmp")
rm -f "${tmp}"
echo "$body"
}
github_release() {
owner_repo=$1
version=$2
test -z "$version" && version="latest"
giturl="https://github.com/${owner_repo}/releases/${version}"
json=$(http_copy "$giturl" "Accept:application/json")
test -z "$json" && return 1
version=$(echo "$json" | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//')
test -z "$version" && return 1
echo "$version"
}
hash_sha256() {
TARGET=${1:-/dev/stdin}
if is_command gsha256sum; then
hash=$(gsha256sum "$TARGET") || return 1
echo "$hash" | cut -d ' ' -f 1
elif is_command sha256sum; then
hash=$(sha256sum "$TARGET") || return 1
echo "$hash" | cut -d ' ' -f 1
elif is_command shasum; then
hash=$(shasum -a 256 "$TARGET" 2>/dev/null) || return 1
echo "$hash" | cut -d ' ' -f 1
elif is_command openssl; then
hash=$(openssl -dst openssl dgst -sha256 "$TARGET") || return 1
echo "$hash" | cut -d ' ' -f a
else
log_crit "hash_sha256 unable to find command to compute sha-256 hash"
return 1
fi
}
hash_sha256_verify() {
TARGET=$1
checksums=$2
if [ -z "$checksums" ]; then
log_err "hash_sha256_verify checksum file not specified in arg2"
return 1
fi
BASENAME=${TARGET##*/}
want=$(grep "${BASENAME}$" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1)
if [ -z "$want" ]; then
log_err "hash_sha256_verify unable to find checksum for '${TARGET}' in '${checksums}'"
return 1
fi
got=$(hash_sha256 "$TARGET")
if [ "$want" != "$got" ]; then
log_err "hash_sha256_verify checksum for '$TARGET' did not verify ${want} vs $got"
return 1
fi
}
cat /dev/null <<EOF
------------------------------------------------------------------------
End of functions from https://github.com/client9/shlib
------------------------------------------------------------------------
EOF
PROJECT_NAME="goreleaser"
OWNER=goreleaser
REPO="goreleaser"
BINARY=goreleaser
FORMAT=tar.gz
OS=$(uname_os)
ARCH=$(uname_arch)
PREFIX="$OWNER/$REPO"
# use in logging routines
log_prefix() {
echo "$PREFIX"
}
PLATFORM="${OS}/${ARCH}"
GITHUB_DOWNLOAD=https://github.com/${OWNER}/${REPO}/releases/download
uname_os_check "$OS"
uname_arch_check "$ARCH"
parse_args "$@"
get_binaries
tag_to_version
adjust_format
adjust_os
adjust_arch
log_info "found version: ${VERSION} for ${TAG}/${OS}/${ARCH}"
NAME=${PROJECT_NAME}_${OS}_${ARCH}
TARBALL=${NAME}.${FORMAT}
TARBALL_URL=${GITHUB_DOWNLOAD}/${TAG}/${TARBALL}
CHECKSUM=checksums.txt
CHECKSUM_URL=${GITHUB_DOWNLOAD}/${TAG}/${CHECKSUM}
execute
log_err "this script is deprecated, please do not use it anymore. check https://github.com/goreleaser/godownloader/issues/207"
+9 -4
View File
@@ -15,9 +15,14 @@ fi
echo "creating and publishing version file"
# create a version file for version-update checks
VERSION_FILE="${DISTDIR}/VERSION"
echo "${VERSION}" | tee "${VERSION_FILE}"
echo "${VERSION}" | tee ${DISTDIR}/VERSION
# upload the version file that supports the application version update check
export AWS_DEFAULT_REGION=us-west-2
aws s3 cp "${VERSION_FILE}" s3://toolbox-data.anchore.io/${BIN}/releases/latest/VERSION
docker run --rm \
-i \
-e AWS_DEFAULT_REGION=us-west-2 \
-e AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} \
-e AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} \
-v $(pwd)/${DISTDIR}/:/distmount \
amazon/aws-cli \
s3 cp /distmount/VERSION s3://toolbox-data.anchore.io/${BIN}/releases/latest/VERSION
+14
View File
@@ -0,0 +1,14 @@
#!/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
+123
View File
@@ -0,0 +1,123 @@
name: 'Acceptance'
on:
workflow_dispatch:
push:
# ... only act on pushes to main
branches:
- main
# ... do not act on release tags
tags-ignore:
- v*
env:
GO_VERSION: "1.14.x"
jobs:
Build-Snapshot-Artifacts:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v2
with:
go-version: ${{ env.GO_VERSION }}
- uses: actions/checkout@v2
- name: Restore bootstrap cache
id: cache
uses: actions/cache@v2
with:
path: |
~/go/pkg/mod
${{ github.workspace }}/.tmp
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}-${{ hashFiles('Makefile') }}
restore-keys: |
${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}-
${{ runner.os }}-go-${{ env.GO_VERSION }}-
- name: Bootstrap dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: make ci-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/**/*
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
Acceptance-Linux:
needs: [ Build-Snapshot-Artifacts ]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/download-artifact@v2
with:
name: artifacts
path: snapshot
- name: Run Acceptance Tests (Linux)
run: make acceptance-linux
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
Acceptance-Mac:
needs: [ Build-Snapshot-Artifacts ]
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- uses: actions/download-artifact@v2
with:
name: artifacts
path: snapshot
- name: Run Acceptance Tests (Mac)
run: make acceptance-mac
# 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
-62
View File
@@ -1,62 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
name: "CodeQL Security Scan"
on:
push:
branches:
# only run when there are pushes to the main branch (not on PRs)
- main
schedule:
- cron: '0 0 * * 3'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# Override automatic language detection by changing the below list
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
language: ['go', 'python']
# Learn more...
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
+53 -101
View File
@@ -1,21 +1,21 @@
name: "Release"
name: 'Release'
on:
push:
# take no actions on push to any branch...
branches-ignore:
- "**"
- '**'
# ... only act on release tags
tags:
- "v*"
- 'v*'
env:
GO_VERSION: "1.18.x"
GO_VERSION: "1.14.x"
jobs:
quality-gate:
environment: release
runs-on: ubuntu-20.04
wait-for-checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
# we don't want to release commits that have been pushed and tagged, but not necessarily merged onto main
@@ -25,31 +25,13 @@ jobs:
git fetch origin main
git merge-base --is-ancestor ${GITHUB_REF##*/} origin/main && echo "${GITHUB_REF##*/} is a commit on main!"
- name: Check static analysis results
- name: Check static anaylysis, unit, and integration test results
uses: fountainhead/action-wait-for-check@v1.0.0
id: static-analysis
id: sa-unit-int
with:
token: ${{ secrets.GITHUB_TOKEN }}
# This check name is defined as the github action job name (in .github/workflows/testing.yaml)
checkName: "Static analysis"
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Check unit test results
uses: fountainhead/action-wait-for-check@v1.0.0
id: unit
with:
token: ${{ secrets.GITHUB_TOKEN }}
# This check name is defined as the github action job name (in .github/workflows/testing.yaml)
checkName: "Unit tests"
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Check integration test results
uses: fountainhead/action-wait-for-check@v1.0.0
id: integration
with:
token: ${{ secrets.GITHUB_TOKEN }}
# This check name is defined as the github action job name (in .github/workflows/testing.yaml)
checkName: "Integration tests"
# This check name is defined as the circle-ci workflow name (in .circleci/config.yaml)
checkName: "Static Analysis + Unit + Integration"
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Check acceptance test results (linux)
@@ -57,8 +39,8 @@ jobs:
id: acceptance-linux
with:
token: ${{ secrets.GITHUB_TOKEN }}
# This check name is defined as the github action job name (in .github/workflows/testing.yaml)
checkName: "Acceptance tests (Linux)"
# This check name is defined as the github action job name (in .github/workflows/acceptance-test.yaml)
checkName: "Acceptance-Linux"
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Check acceptance test results (mac)
@@ -66,37 +48,33 @@ jobs:
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)"
# This check name is defined as the github action job name (in .github/workflows/acceptance-test.yaml)
checkName: "Acceptance-Mac"
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Check cli test results (linux)
- name: Check inline comparison test results
uses: fountainhead/action-wait-for-check@v1.0.0
id: cli-linux
id: inline-compare
with:
token: ${{ secrets.GITHUB_TOKEN }}
# This check name is defined as the github action job name (in .github/workflows/testing.yaml)
checkName: "CLI tests (Linux)"
# This check name is defined as the github action job name (in .github/workflows/acceptance-test.yaml)
checkName: "Inline-Compare"
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' || steps.acceptance-mac.outputs.conclusion != 'success'
if: steps.sa-unit-int.outputs.conclusion != 'success' || steps.inline-compare.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 "Static/Unit/Integration Status: ${{ steps.sa-unit-int.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 }}"
echo "Inline Compare Status: ${{ steps.inline-compare.outputs.conclusion }}"
false
release:
needs: [quality-gate]
runs-on: ubuntu-20.04
permissions:
contents: write
packages: write
needs: [ wait-for-checks ]
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v2
with:
go-version: ${{ env.GO_VERSION }}
@@ -105,73 +83,47 @@ jobs:
with:
fetch-depth: 0
- name: Restore tool cache
id: tool-cache
uses: actions/cache@v2.1.3
- name: Restore bootstrap cache
id: cache
uses: actions/cache@v2
with:
path: ${{ github.workspace }}/.tmp
key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }}
- name: Restore go cache
id: go-cache
uses: actions/cache@v2.1.3
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}
path: |
~/go/pkg/mod
${{ github.workspace }}/.tmp
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}-${{ hashFiles('Makefile') }}
restore-keys: |
${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}-
${{ runner.os }}-go-${{ env.GO_VERSION }}-
- name: (cache-miss) Bootstrap all project dependencies
if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true'
run: make bootstrap
- name: Bootstrap dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: make ci-bootstrap
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.TOOLBOX_DOCKER_USER }}
password: ${{ secrets.TOOLBOX_DOCKER_PASS }}
- 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: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- 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 & publish release artifacts
run: make release
env:
# for mac signing and notarization...
QUILL_SIGN_P12: ${{ secrets.ANCHORE_APPLE_DEVELOPER_ID_CERT_CHAIN }}
QUILL_SIGN_PASSWORD: ${{ secrets.ANCHORE_APPLE_DEVELOPER_ID_CERT_PASS }}
QUILL_NOTARY_ISSUER: ${{ secrets.APPLE_NOTARY_ISSUER }}
QUILL_NOTARY_KEY_ID: ${{ secrets.APPLE_NOTARY_KEY_ID }}
QUILL_NOTARY_KEY: ${{ secrets.APPLE_NOTARY_KEY }}
# for creating the release (requires write access to packages and content)
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# for updating brew formula in anchore/homebrew-syft
GITHUB_BREW_TOKEN: ${{ secrets.ANCHORE_GIT_READ_TOKEN }}
# for updating the VERSION file in S3...
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 }}
- uses: anchore/sbom-action@v0
continue-on-error: true
with:
artifact-name: sbom.spdx.json
- uses: 8398a7/action-slack@v3
continue-on-error: true
with:
status: ${{ job.status }}
fields: repo,workflow,action,eventName
text: "A new Syft release has been published: https://github.com/anchore/syft/releases/tag/${{ github.ref_name }}"
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }}
if: ${{ success() }}
- uses: actions/upload-artifact@v2
with:
name: artifacts
path: dist/**/*
path: dist/**/*
@@ -1,79 +0,0 @@
name: PR for latest versions of bootstrap tools
on:
schedule:
- cron: "0 8 * * *" # 3 AM EST
workflow_dispatch:
env:
GO_VERSION: "1.18.x"
GO_STABLE_VERSION: true
jobs:
update-bootstrap-tools:
runs-on: ubuntu-latest
if: github.repository == 'anchore/syft' # only run for main repo
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v2
with:
go-version: ${{ env.GO_VERSION }}
stable: ${{ env.GO_STABLE_VERSION }}
- run: |
GOLANGCILINT_LATEST_VERSION=$(go list -m -json github.com/golangci/golangci-lint@latest 2>/dev/null | jq -r '.Version')
BOUNCER_LATEST_VERSION=$(go list -m -json github.com/wagoodman/go-bouncer@latest 2>/dev/null | jq -r '.Version')
CHRONICLE_LATEST_VERSION=$(go list -m -json github.com/anchore/chronicle@latest 2>/dev/null | jq -r '.Version')
GORELEASER_LATEST_VERSION=$(go list -m -json github.com/goreleaser/goreleaser@latest 2>/dev/null | jq -r '.Version')
GOSIMPORTS_LATEST_VERSION=$(go list -m -json github.com/rinchsan/gosimports@latest 2>/dev/null | jq -r '.Version')
YAJSV_LATEST_VERSION=$(go list -m -json github.com/neilpa/yajsv@latest 2>/dev/null | jq -r '.Version')
COSIGN_LATEST_VERSION=$(go list -m -json github.com/sigstore/cosign@latest 2>/dev/null | jq -r '.Version')
# update version variables in the Makefile
sed -r -i -e 's/^(GOLANGCILINT_VERSION = ).*/\1'${GOLANGCILINT_LATEST_VERSION}'/' Makefile
sed -r -i -e 's/^(BOUNCER_VERSION = ).*/\1'${BOUNCER_LATEST_VERSION}'/' Makefile
sed -r -i -e 's/^(CHRONICLE_VERSION = ).*/\1'${CHRONICLE_LATEST_VERSION}'/' Makefile
sed -r -i -e 's/^(GORELEASER_VERSION = ).*/\1'${GORELEASER_LATEST_VERSION}'/' Makefile
sed -r -i -e 's/^(GOSIMPORTS_VERSION = ).*/\1'${GOSIMPORTS_LATEST_VERSION}'/' Makefile
sed -r -i -e 's/^(YAJSV_VERSION = ).*/\1'${YAJSV_LATEST_VERSION}'/' Makefile
sed -r -i -e 's/^(COSIGN_VERSION = ).*/\1'${COSIGN_LATEST_VERSION}'/' Makefile
# update cosign in go.mod as well
go get github.com/sigstore/cosign@$COSIGN_LATEST_VERSION
go mod tidy
# export the versions for use with create-pull-request
echo "::set-output name=GOLANGCILINT::$GOLANGCILINT_LATEST_VERSION"
echo "::set-output name=BOUNCER::$BOUNCER_LATEST_VERSION"
echo "::set-output name=CHRONICLE::$CHRONICLE_LATEST_VERSION"
echo "::set-output name=GORELEASER::$GORELEASER_LATEST_VERSION"
echo "::set-output name=GOSIMPORTS::$GOSIMPORTS_LATEST_VERSION"
echo "::set-output name=YAJSV::$YAJSV_LATEST_VERSION"
echo "::set-output name=COSIGN::$COSIGN_LATEST_VERSION"
id: latest-versions
- uses: tibdex/github-app-token@v1
id: generate-token
with:
app_id: ${{ secrets.TOKEN_APP_ID }}
private_key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }}
- uses: peter-evans/create-pull-request@v4
with:
signoff: true
delete-branch: true
branch: auto/latest-bootstrap-tools
labels: dependencies
commit-message: 'Update syft bootstrap tools to latest versions.'
title: 'Update syft bootstrap tools to latest versions.'
body: |
- [golangci-lint ${{ steps.latest-versions.outputs.GOLANGCILINT }}](https://github.com/golangci/golangci-lint/releases/tag/${{ steps.latest-versions.outputs.GOLANGCILINT }})
- [bouncer ${{ steps.latest-versions.outputs.BOUNCER }}](https://github.com/wagoodman/go-bouncer/releases/tag/${{ steps.latest-versions.outputs.BOUNCER }})
- [chronicle ${{ steps.latest-versions.outputs.CHRONICLE }}](https://github.com/anchore/chronicle/releases/tag/${{ steps.latest-versions.outputs.CHRONICLE }})
- [goreleaser ${{ steps.latest-versions.outputs.GORELEASER }}](https://github.com/goreleaser/goreleaser/releases/tag/${{ steps.latest-versions.outputs.GORELEASER }})
- [gosimports ${{ steps.latest-versions.outputs.GOSIMPORTS }}](https://github.com/rinchsan/gosimports/releases/tag/${{ steps.latest-versions.outputs.GOSIMPORTS }})
- [yajsv ${{ steps.latest-versions.outputs.YAJSV }}](https://github.com/neilpa/yajsv/releases/tag/${{ steps.latest-versions.outputs.YAJSV }})
- [cosign ${{ steps.latest-versions.outputs.COSIGN }}](https://github.com/sigstore/cosign/releases/tag/${{ steps.latest-versions.outputs.COSIGN }})
This is an auto-generated pull request to update all of the bootstrap tools to the latest versions.
token: ${{ steps.generate-token.outputs.token }}
@@ -1,51 +0,0 @@
name: PR for latest Stereoscope release
on:
schedule:
- cron: "0 8 * * *" # 3 AM EST
workflow_dispatch:
env:
GO_VERSION: "1.18.x"
GO_STABLE_VERSION: true
jobs:
upgrade-stereoscope:
runs-on: ubuntu-latest
if: github.repository == 'anchore/syft' # only run for main repo
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v2
with:
go-version: ${{ env.GO_VERSION }}
stable: ${{ env.GO_STABLE_VERSION }}
- run: |
LATEST_VERSION=$(git ls-remote https://github.com/anchore/stereoscope main | head -n1 | awk '{print $1;}')
# update go.mod
go get github.com/anchore/stereoscope@$LATEST_VERSION
go mod tidy
# export the version for use with create-pull-request
echo "::set-output name=LATEST_VERSION::$LATEST_VERSION"
id: latest-version
- uses: tibdex/github-app-token@v1
id: generate-token
with:
app_id: ${{ secrets.TOKEN_APP_ID }}
private_key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }}
- uses: peter-evans/create-pull-request@v4
with:
signoff: true
delete-branch: true
branch: auto/latest
labels: dependencies
commit-message: "Update Stereoscope to ${{ steps.latest-version.outputs.LATEST_VERSION }}"
title: "Update Stereoscope to ${{ steps.latest-version.outputs.LATEST_VERSION }}"
body: |
Update Stereoscope to ${{ steps.latest-version.outputs.LATEST_VERSION }}
token: ${{ steps.generate-token.outputs.token }}
-406
View File
@@ -1,406 +0,0 @@
name: "Validations"
on:
workflow_dispatch:
push:
branches:
- main
pull_request:
env:
GO_VERSION: "1.18.x"
GO_STABLE_VERSION: true
jobs:
Static-Analysis:
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
name: "Static analysis"
runs-on: ubuntu-20.04
steps:
- uses: actions/setup-go@v2
with:
go-version: ${{ env.GO_VERSION }}
stable: ${{ env.GO_STABLE_VERSION }}
- uses: actions/checkout@v2
- name: Restore tool cache
id: tool-cache
uses: actions/cache@v2.1.3
with:
path: ${{ github.workspace }}/.tmp
key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }}
- name: Restore go cache
id: go-cache
uses: actions/cache@v2.1.3
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-${{ env.GO_VERSION }}-
- name: (cache-miss) Bootstrap all project dependencies
if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true'
run: make bootstrap
- name: Bootstrap CI environment dependencies
run: make ci-bootstrap
- name: Run static analysis
run: make static-analysis
Unit-Test:
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
name: "Unit tests"
runs-on: ubuntu-20.04
steps:
- uses: actions/setup-go@v2
with:
go-version: ${{ env.GO_VERSION }}
stable: ${{ env.GO_STABLE_VERSION }}
- uses: actions/checkout@v2
- name: Restore tool cache
id: tool-cache
uses: actions/cache@v2.1.3
with:
path: ${{ github.workspace }}/.tmp
key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }}
- name: Restore go cache
id: go-cache
uses: actions/cache@v2.1.3
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-${{ env.GO_VERSION }}-
- name: (cache-miss) Bootstrap all project dependencies
if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true'
run: make bootstrap
- name: Bootstrap CI environment dependencies
run: make ci-bootstrap
- name: Build cache key for java test-fixture blobs (for unit tests)
run: make java-packages-fingerprint
- name: Restore Java test-fixture cache
id: unit-java-cache
uses: actions/cache@v2.1.3
with:
path: syft/pkg/cataloger/java/test-fixtures/java-builds/packages
key: ${{ runner.os }}-unit-java-cache-${{ hashFiles( 'syft/pkg/cataloger/java/test-fixtures/java-builds/packages.fingerprint' ) }}
- name: Build cache key for rpm test-fixture blobs (for unit tests)
run: make rpm-binaries-fingerprint
- name: Restore RPM test-fixture cache
id: unit-rpm-cache
uses: actions/cache@v2.1.3
with:
path: syft/pkg/cataloger/rpm/test-fixtures/rpms
key: ${{ runner.os }}-unit-rpm-cache-${{ hashFiles( 'syft/pkg/cataloger/rpm/test-fixtures/rpms.fingerprint' ) }}
- name: Build cache key for go binary test-fixture blobs (for unit tests)
run: make go-binaries-fingerprint
- name: Restore Go binary test-fixture cache
id: unit-go-binary-cache
uses: actions/cache@v2.1.3
with:
path: syft/pkg/cataloger/golang/test-fixtures/archs/binaries
key: ${{ runner.os }}-unit-go-binaries-cache-${{ hashFiles( 'syft/pkg/cataloger/golang/test-fixtures/archs/binaries.fingerprint' ) }}
- name: Run unit tests
run: make unit
- uses: actions/upload-artifact@v2
with:
name: unit-test-results
path: test/results/**/*
Integration-Test:
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
name: "Integration tests"
runs-on: ubuntu-20.04
steps:
- uses: actions/setup-go@v2
with:
go-version: ${{ env.GO_VERSION }}
stable: ${{ env.GO_STABLE_VERSION }}
- uses: actions/checkout@v2
- name: Restore tool cache
id: tool-cache
uses: actions/cache@v2.1.3
with:
path: ${{ github.workspace }}/.tmp
key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }}
- name: Restore go cache
id: go-cache
uses: actions/cache@v2.1.3
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-${{ env.GO_VERSION }}-
- name: (cache-miss) Bootstrap all project dependencies
if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true'
run: make bootstrap
- name: Bootstrap CI environment dependencies
run: make ci-bootstrap
- name: Validate syft output against the CycloneDX schema
run: make validate-cyclonedx-schema
- name: Build key for tar cache
run: make integration-fingerprint
- name: Restore integration test cache
uses: actions/cache@v2.1.3
with:
path: ${{ github.workspace }}/test/integration/test-fixtures/cache
key: ${{ runner.os }}-integration-test-cache-${{ hashFiles('test/integration/test-fixtures/cache.fingerprint') }}
- name: Run integration tests
run: make integration
Benchmark-Test:
name: "Benchmark tests"
runs-on: ubuntu-20.04
# note: we want benchmarks to run on pull_request events in order to publish results to a sticky comment, and
# we also want to run on push such that merges to main are recorded to the cache. For this reason we don't filter
# the job by event.
steps:
- uses: actions/setup-go@v2
with:
go-version: ${{ env.GO_VERSION }}
stable: ${{ env.GO_STABLE_VERSION }}
- uses: actions/checkout@v2
- name: Restore tool cache
id: tool-cache
uses: actions/cache@v2.1.3
with:
path: ${{ github.workspace }}/.tmp
key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }}
- name: Restore go cache
id: go-cache
uses: actions/cache@v2.1.3
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-${{ env.GO_VERSION }}-
- name: (cache-miss) Bootstrap all project dependencies
if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true'
run: make bootstrap
- name: Bootstrap CI environment dependencies
run: make ci-bootstrap
- name: Restore base benchmark result
uses: actions/cache@v2
with:
path: test/results/benchmark-main.txt
# use base sha for PR or new commit hash for main push in benchmark result key
key: ${{ runner.os }}-bench-${{ (github.event.pull_request.base.sha != github.event.after) && github.event.pull_request.base.sha || github.event.after }}
- name: Run benchmark tests
id: benchmark
run: |
REF_NAME=${GITHUB_REF##*/} make benchmark
OUTPUT=$(make show-benchstat)
OUTPUT="${OUTPUT//'%'/'%25'}" # URL encode all '%' characters
OUTPUT="${OUTPUT//$'\n'/'%0A'}" # URL encode all '\n' characters
OUTPUT="${OUTPUT//$'\r'/'%0D'}" # URL encode all '\r' characters
echo "::set-output name=result::$OUTPUT"
- uses: actions/upload-artifact@v2
with:
name: benchmark-test-results
path: test/results/**/*
- name: Update PR benchmark results comment
uses: marocchino/sticky-pull-request-comment@v2
continue-on-error: true
with:
header: benchmark
message: |
### Benchmark Test Results
<details>
<summary>Benchmark results from the latest changes vs base branch</summary>
```
${{ steps.benchmark.outputs.result }}
```
</details>
Build-Snapshot-Artifacts:
name: "Build snapshot artifacts"
runs-on: ubuntu-20.04
steps:
- uses: actions/setup-go@v2
with:
go-version: ${{ env.GO_VERSION }}
stable: ${{ env.GO_STABLE_VERSION }}
- uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Restore tool cache
id: tool-cache
uses: actions/cache@v2.1.3
with:
path: ${{ github.workspace }}/.tmp
key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }}
- name: Restore go cache
id: go-cache
uses: actions/cache@v2.1.3
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-${{ env.GO_VERSION }}-
- name: (cache-miss) Bootstrap all project dependencies
if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true'
run: make bootstrap
- name: Build snapshot artifacts
run: make snapshot
- uses: actions/upload-artifact@v2
with:
name: artifacts
path: snapshot/**/*
Acceptance-Linux:
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
name: "Acceptance tests (Linux)"
needs: [Build-Snapshot-Artifacts]
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: actions/download-artifact@v2
with:
name: artifacts
path: snapshot
- name: Run comparison tests (Linux)
run: make compare-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: Restore docker image cache for compare testing
id: mac-compare-testing-cache
uses: actions/cache@v2.1.3
with:
path: image.tar
key: ${{ runner.os }}-${{ hashFiles('test/compare/mac.sh') }}
- name: Run comparison tests (Mac)
run: make compare-mac
- 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
name: "CLI tests (Linux)"
needs: [Build-Snapshot-Artifacts]
runs-on: ubuntu-20.04
steps:
- uses: actions/setup-go@v2
with:
go-version: ${{ env.GO_VERSION }}
stable: ${{ env.GO_STABLE_VERSION }}
- uses: actions/checkout@v2
- name: Restore go cache
id: go-cache
uses: actions/cache@v2.1.3
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-${{ env.GO_VERSION }}-
- name: Restore tool cache
id: tool-cache
uses: actions/cache@v2.1.3
with:
path: ${{ github.workspace }}/.tmp
key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }}
- name: (cache-miss) Bootstrap all project dependencies
if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true'
run: make bootstrap
- name: Build key for tar cache
run: make cli-fingerprint
- name: Restore CLI test cache
uses: actions/cache@v2.1.3
with:
path: ${{ github.workspace }}/test/cli/test-fixtures/cache
key: ${{ runner.os }}-cli-test-cache-${{ hashFiles('test/cli/test-fixtures/cache.fingerprint') }}
- uses: actions/download-artifact@v2
with:
name: artifacts
path: snapshot
- name: Run CLI Tests (Linux)
run: make cli
-12
View File
@@ -1,10 +1,8 @@
CHANGELOG.md
/test/results
/dist
/snapshot
.server/
.vscode/
.history/
*.fingerprint
*.tar
*.jar
@@ -18,7 +16,6 @@ CHANGELOG.md
.images
.tmp/
coverage.txt
bin/
# Binaries for programs and plugins
*.exe
@@ -32,12 +29,3 @@ bin/
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# macOS Finder metadata
.DS_STORE
*.profile
# attestation
cosign.key
cosign.pub
+12 -28
View File
@@ -10,66 +10,50 @@ linters:
enable:
- asciicheck
- bodyclose
- deadcode
- depguard
- dogsled
- dupl
- errcheck
- errorlint
- exportloopref
- funlen
- gocognit
- goconst
- gocritic
- gocyclo
- gofmt
- tparallel
- importas
- goimports
- golint
- goprintffuncname
- gosec
- gosimple
- govet
- ineffassign
- misspell
- nakedret
- nolintlint
- revive
- rowserrcheck
- scopelint
- staticcheck
- structcheck
- stylecheck
- typecheck
- unconvert
- unparam
- unused
- varcheck
- whitespace
linters-settings:
funlen:
# Checks the number of lines in a function.
# If lower than 0, disable the check.
# Default: 60
lines: 70
# Checks the number of statements in a function.
# If lower than 0, disable the check.
# Default: 40
statements: 50
output:
uniq-by-line: false
run:
timeout: 10m
# do not enable...
# - dogsled # found to be to niche and ineffective
# - goprintffuncname # does not catch all cases and there are exceptions
# - nakedret # does not catch all cases and should not fail a build
# - gochecknoglobals
# - gochecknoinits # this is too aggressive
# - rowserrcheck disabled per generics https://github.com/golangci/golangci-lint/issues/2649
# - godot
# - godox
# - goerr113
# - goimports # we're using gosimports now instead to account for extra whitespaces (see https://github.com/golang/go/issues/20818)
# - golint # deprecated
# - gomnd # this is too aggressive
# - interfacer # this is a good idea, but is no longer supported and is prone to false positives
# - lll # without a way to specify per-line exception cases, this is not usable
# - maligned # this is an excellent linter, but tricky to optimize and we are not sensitive to memory layout optimizations
# - nestif
# - prealloc # following this rule isn't consistently a good idea, as it sometimes forces unnecessary allocations that result in less idiomatic code
# - scopelint # deprecated
# - prealloc # this unnecessarily requires preallocations for slices
# - testpackage
# - wsl # this doens't have an auto-fixer yet and is pretty noisy (https://github.com/bombsimon/wsl/issues/90)
# - wsl
+24 -220
View File
@@ -1,77 +1,36 @@
release:
prerelease: auto
draft: false
# 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: false
env:
# required to support multi architecture docker builds
- DOCKER_CLI_EXPERIMENTAL=enabled
# If set to true, will not auto-publish the release. This is done to allow us to review the changelog before publishing.
draft: true
signs:
- artifacts: checksum
args: ["--output", "${signature}", "--detach-sign", "${artifact}"]
builds:
- id: linux-build
dir: ./cmd/syft
binary: syft
- binary: syft
env:
- CGO_ENABLED=0
goos:
# windows not supported yet (due to jotframe)
# - windows
- linux
- darwin
goarch:
- amd64
- arm64
- ppc64le
- s390x
# 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 |
# Set the modified timestamp on the output binary to the git timestamp (to ensure a reproducible build)
mod_timestamp: '{{ .CommitTimestamp }}'
ldflags: |
-w
-s
-extldflags '-static'
-X github.com/anchore/syft/internal/version.version={{.Version}}
-X github.com/anchore/syft/internal/version.gitCommit={{.Commit}}
-X github.com/anchore/syft/internal/version.buildDate={{.Date}}
-X github.com/anchore/syft/internal/version.gitDescription={{.Summary}}
- id: darwin-build
dir: ./cmd/syft
binary: syft
goos:
- darwin
goarch:
- amd64
- arm64
mod_timestamp: *build-timestamp
env: *build-env
ldflags: *build-ldflags
hooks:
post:
- cmd: .tmp/quill sign-and-notarize "{{ .Path }}" --dry-run={{ .IsSnapshot }} --ad-hoc={{ .IsSnapshot }} -vv
env:
- QUILL_LOG_FILE=/tmp/quill-{{ .Target }}.log
- id: windows-build
dir: ./cmd/syft
binary: syft
goos:
- windows
goarch:
- amd64
mod_timestamp: *build-timestamp
env: *build-env
ldflags: *build-ldflags
archives:
- id: linux-archives
builds:
- linux-build
# note: the signing process is depending on tar.gz archives. If this format changes then .github/scripts/apple-signing/*.sh will need to be adjusted
- id: darwin-archives
builds:
- darwin-build
- id: windows-archives
format: zip
builds:
- windows-build
-X github.com/anchore/syft/internal/version.gitTreeState={{.Env.BUILD_GIT_TREE_STATE}}
nfpms:
- license: "Apache 2.0"
@@ -86,167 +45,12 @@ brews:
- tap:
owner: anchore
name: homebrew-syft
token: "{{.Env.GITHUB_BREW_TOKEN}}"
ids:
- darwin-archives
- linux-archives
homepage: *website
description: *description
license: "Apache License 2.0"
dockers:
- image_templates:
- anchore/syft:debug
- anchore/syft:{{.Tag}}-debug
- ghcr.io/anchore/syft:debug
- ghcr.io/anchore/syft:{{.Tag}}-debug
goarch: amd64
dockerfile: Dockerfile.debug
use: buildx
build_flag_templates:
- "--platform=linux/amd64"
- "--build-arg=BUILD_DATE={{.Date}}"
- "--build-arg=BUILD_VERSION={{.Version}}"
- "--build-arg=VCS_REF={{.FullCommit}}"
- "--build-arg=VCS_URL={{.GitURL}}"
archives:
- format: tar.gz
format_overrides:
- goos: windows
format: zip
- image_templates:
- anchore/syft:debug-arm64v8
- anchore/syft:{{.Tag}}-debug-arm64v8
- ghcr.io/anchore/syft:debug-arm64v8
- ghcr.io/anchore/syft:{{.Tag}}-debug-arm64v8
goarch: arm64
dockerfile: Dockerfile.debug
use: buildx
build_flag_templates:
- "--platform=linux/arm64/v8"
- "--build-arg=BUILD_DATE={{.Date}}"
- "--build-arg=BUILD_VERSION={{.Version}}"
- "--build-arg=VCS_REF={{.FullCommit}}"
- "--build-arg=VCS_URL={{.GitURL}}"
- image_templates:
- anchore/syft:debug-ppc64le
- anchore/syft:{{.Tag}}-debug-ppc64le
- ghcr.io/anchore/syft:debug-ppc64le
- ghcr.io/anchore/syft:{{.Tag}}-debug-ppc64le
goarch: ppc64le
dockerfile: Dockerfile.debug
use: buildx
build_flag_templates:
- "--platform=linux/ppc64le"
- "--build-arg=BUILD_DATE={{.Date}}"
- "--build-arg=BUILD_VERSION={{.Version}}"
- "--build-arg=VCS_REF={{.FullCommit}}"
- "--build-arg=VCS_URL={{.GitURL}}"
- image_templates:
- anchore/syft:debug-s390x
- anchore/syft:{{.Tag}}-debug-s390x
- ghcr.io/anchore/syft:debug-s390x
- ghcr.io/anchore/syft:{{.Tag}}-debug-s390x
goarch: s390x
dockerfile: Dockerfile.debug
use: buildx
build_flag_templates:
- "--platform=linux/s390x"
- "--build-arg=BUILD_DATE={{.Date}}"
- "--build-arg=BUILD_VERSION={{.Version}}"
- "--build-arg=VCS_REF={{.FullCommit}}"
- "--build-arg=VCS_URL={{.GitURL}}"
- image_templates:
- anchore/syft:latest
- anchore/syft:{{.Tag}}
- ghcr.io/anchore/syft:latest
- ghcr.io/anchore/syft:{{.Tag}}
goarch: amd64
dockerfile: Dockerfile
use: buildx
build_flag_templates:
- "--platform=linux/amd64"
- "--build-arg=BUILD_DATE={{.Date}}"
- "--build-arg=BUILD_VERSION={{.Version}}"
- "--build-arg=VCS_REF={{.FullCommit}}"
- "--build-arg=VCS_URL={{.GitURL}}"
- image_templates:
- anchore/syft:{{.Tag}}-arm64v8
- ghcr.io/anchore/syft:{{.Tag}}-arm64v8
goarch: arm64
dockerfile: Dockerfile
use: buildx
build_flag_templates:
- "--platform=linux/arm64/v8"
- "--build-arg=BUILD_DATE={{.Date}}"
- "--build-arg=BUILD_VERSION={{.Version}}"
- "--build-arg=VCS_REF={{.FullCommit}}"
- "--build-arg=VCS_URL={{.GitURL}}"
- image_templates:
- anchore/syft:{{.Tag}}-ppc64le
- ghcr.io/anchore/syft:{{.Tag}}-ppc64le
goarch: ppc64le
dockerfile: Dockerfile
use: buildx
build_flag_templates:
- "--platform=linux/ppc64le"
- "--build-arg=BUILD_DATE={{.Date}}"
- "--build-arg=BUILD_VERSION={{.Version}}"
- "--build-arg=VCS_REF={{.FullCommit}}"
- "--build-arg=VCS_URL={{.GitURL}}"
- image_templates:
- anchore/syft:{{.Tag}}-s390x
- ghcr.io/anchore/syft:{{.Tag}}-s390x
goarch: s390x
dockerfile: Dockerfile
use: buildx
build_flag_templates:
- "--platform=linux/s390x"
- "--build-arg=BUILD_DATE={{.Date}}"
- "--build-arg=BUILD_VERSION={{.Version}}"
- "--build-arg=VCS_REF={{.FullCommit}}"
- "--build-arg=VCS_URL={{.GitURL}}"
docker_manifests:
- name_template: anchore/syft:latest
image_templates:
- anchore/syft:{{.Tag}}
- anchore/syft:{{.Tag}}-arm64v8
- anchore/syft:{{.Tag}}-ppc64le
- anchore/syft:{{.Tag}}-s390x
- name_template: anchore/syft:debug
- anchore/syft:{{.Tag}}-debug
- anchore/syft:{{.Tag}}-debug-arm64v8
- anchore/syft:{{.Tag}}-debug-ppc64le
- anchore/syft:{{.Tag}}-debug-s390x
- name_template: anchore/syft:{{.Tag}}
image_templates:
- anchore/syft:{{.Tag}}
- anchore/syft:{{.Tag}}-arm64v8
- anchore/syft:{{.Tag}}-ppc64le
- anchore/syft:{{.Tag}}-s390x
- name_template: ghcr.io/anchore/syft:latest
image_templates:
- ghcr.io/anchore/syft:{{.Tag}}
- ghcr.io/anchore/syft:{{.Tag}}-arm64v8
- ghcr.io/anchore/syft:{{.Tag}}-ppc64le
- ghcr.io/anchore/syft:{{.Tag}}-s390x
- name_template: ghcr.io/anchore/syft:debug
image_templates:
- ghcr.io/anchore/syft:{{.Tag}}-debug
- ghcr.io/anchore/syft:{{.Tag}}-debug-arm64v8
- ghcr.io/anchore/syft:{{.Tag}}-debug-ppc64le
- ghcr.io/anchore/syft:{{.Tag}}-debug-s390x
- name_template: ghcr.io/anchore/syft:{{.Tag}}
image_templates:
- ghcr.io/anchore/syft:{{.Tag}}
- ghcr.io/anchore/syft:{{.Tag}}-arm64v8
- ghcr.io/anchore/syft:{{.Tag}}-ppc64le
- ghcr.io/anchore/syft:{{.Tag}}-s390x
+28 -79
View File
@@ -1,63 +1,12 @@
# Contributing to Syft
# Contributing to `syft`
If you are looking to contribute to this project and want to open a GitHub pull request ("PR"), there are a few guidelines of what we are looking for in patches. Make sure you go through this document and ensure that your code proposal is aligned.
If you are looking to contribute to this project and want to open a Github pull request ("PR"), there are a few guidelines of what we are looking for in patches. Make sure you go through this document and ensure that your code proposal is aligned.
## Setting up your environment
Before you can contribute to Syft, you need to configure your development environment.
### Debian setup
You will need to install Go. The version on https://go.dev works best, using the system golang doesn't always work the way you might expect.
At the time of writing, Go 1.19 does not work correctly with Syft. Please use Go 1.18 for now.
You will also need Docker. There's no reason the system packages shouldn't work, but we used the official Docker package. You can find instructions for installing Docker in Debian [here](https://docs.docker.com/engine/install/debian/).
You also need to install some Debian packages
```sh
sudo apt-get install build-essential zip bc libxml2-utils git
```
## Configuring Git
You will need to configure your git client with your name and email address. This is easily done from the command line.
```text
$ git config --global user.name "John Doe"
$ git config --global user.email "john.doe@example.com"
```
This username and email address will matter later in this guide.
## Fork the repo
You should fork the Syft repo using the "Fork" button at the top right of the Syft GitHub [site](https://github.com/anchore/syft/). You will be doing your development in your fork, then submit a pull request to Syft. There are many resources how to use GitHub effectively, we will not cover those here.
## Adding a feature or fix
If you look at the Syft [Issue](https://github.com/anchore/syft/issues) there are plenty of bugs and feature requests. Maybe look at the [good first issue](https://github.com/anchore/syft/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) list if you're not sure where to start.
## Commit guidelines
In the Syft project we like commits and pull requests (PR) to be easy to understand and review. Open source thrives best when everything happening is over documented and small enough to be understood.
### Granular commits
Please try to make every commit as simple as possible, but no simpler. The idea is that each commit should be a logical unit of code. Try not to commit too many tiny changes, for example every line changed in a file as a separate commit. And also try not to make a commit enormous, for example committing all your work at the end of the day.
Rather than try to follow a strict guide on what is or is not best, we try to be flexible and simple in this space. Do what makes the most sense for the changes you are trying to include.
### Commit title and description
Remember that the message you leave for a commit is for the reviewer in the present, and for someone (maybe you) changing something in the future. Please make sure the title and description used is easy to understand and explains what was done. Jokes and clever comments generally don't age well in commit messages. Just the facts please.
## Sign off your work
## Sign your work
The `sign-off` is an added line at the end of the explanation for the commit, certifying that you wrote it or otherwise have the right to submit it as an open-source patch. By submitting a contribution, you agree to be bound by the terms of the DCO Version 1.1 and Apache License Version 2.0.
Signing off a commit certifies the below Developer's Certificate of Origin (DCO):
Signing a commit certifies the below Developer's Certificate of Origin (DCO):
```text
Developer's Certificate of Origin 1.1
@@ -95,13 +44,20 @@ When committing your change, you can add the required line manually so that it l
Signed-off-by: John Doe <john.doe@example.com>
```
Creating a signed-off commit is then possible with `-s` or `--signoff`:
Alternatively, configure your Git client with your name and email to use the `-s` flag when creating a commit:
```text
$ git config --global user.name "John Doe"
$ git config --global user.email "john.doe@example.com"
```
Creating a signed commit is then possible with `-s`:
```text
$ git commit -s -m "this is a commit message"
```
To double-check that the commit was signed-off, look at the log output:
To double check if the commit was signed, look at the log output:
```text
$ git log -1
@@ -114,9 +70,22 @@ Date: Mon Aug 1 11:27:13 2020 -0400
Signed-off-by: John Doe <john.doe@example.com>
```
[//]: # (TODO: Commit guidelines, granular commits)
[//]: # (TODO: Commit guidelines, descriptive messages)
[//]: # (TODO: Commit guidelines, commit title, extra body description)
[//]: # (TODO: PR title and description)
## Test your changes
This project has a `Makefile` which includes many helpers running both unit and integration tests. You can run `make help` to see all the options. Although PRs will have automatic checks for these, it is useful to run them locally, ensuring they pass before submitting changes. Ensure you've bootstrapped once before running tests:
This project has a `Makefile` which includes many helpers running both unit and integration tests. Although PRs will have automatic checks for these, it is useful to run them locally, ensuring they pass before submitting changes. Ensure you've bootstrapped once before running tests:
```text
$ make bootstrap
@@ -129,26 +98,6 @@ $ make unit
$ make integration
```
You can also run `make all` to run a more extensive test suite, but there is additional configuration that will be needed for those tests to run correctly. We will not cover the extra steps here.
## Pull Request
If you made it this far and all the tests are passing, it's time to submit a Pull Request (PR) for Syft. Submitting a PR is always a scary moment as what happens next can be an unknown. The Syft project strives to be easy to work with, we appreciate all contributions. Nobody is going to yell at you or try to make you feel bad. We love contributions and know how scary that first PR can be.
### PR Title and Description
Just like the commit title and description mentioned above, the PR title and description is very important for letting others know what's happening. Please include any details you think a reviewer will need to more properly review your PR.
A PR that is very large or poorly described has a higher likelihood of being pushed to the end of the list. Reviewers like PRs they can understand and quickly review.
### What to expect next
Please be patient with the project. We try to review PRs in a timely manner, but this is highly dependent on all the other tasks we have going on. It's OK to ask for a status update every week or two, it's not OK to ask for a status update every day.
It's very likely the reviewer will have questions and suggestions for changes to your PR. If your changes don't match the current style and flow of the other code, expect a request to change what you've done.
## Document your changes
And lastly, when proposed changes are modifying user-facing functionality or output, it is expected the PR will include updates to the documentation as well. Syft is not a project that is heavy on documentation. This will mostly be updating the README and help for the tool.
If nobody knows new features exist, they can't use them!
When proposed changes are modifying user-facing functionality or output, it is expected the PR will include updates to the documentation as well.
-290
View File
@@ -1,290 +0,0 @@
# Developing
## Getting started
In order to test and develop in this repo you will need the following dependencies installed:
- Golang
- docker
- make
After cloning the following step can help you get setup:
1. run `make bootstrap` to download go mod dependencies, create the `/.tmp` dir, and download helper utilities.
2. run `make` to view the selection of developer commands in the Makefile
3. run `make build` to build the release snapshot binaries and packages
4. for an even quicker start you can run `go run cmd/syft/main.go` to print the syft help.
- this command `go run cmd/syft/main.go alpine:latest` will compile and run syft against `alpine:latest`
5. view the README or syft help output for more output options
#### Make output
The main make tasks for common static analysis and testing are `lint`, `lint-fix`, `unit`, `integration`, and `cli`.
```
all Run all linux-based checks (linting, license check, unit, integration, and linux compare tests)
benchmark Run benchmark tests and compare against the baseline (if available)
bootstrap Download and install all tooling dependencies (+ prep tooling in the ./tmp dir)
build Build release snapshot binaries and packages
check-licenses Ensure transitive dependencies are compliant with the current license policy
clean-test-image-cache Clean test image cache
clean Remove previous builds, result reports, and test cache
cli Run CLI tests
compare-linux Run compare tests on build snapshot binaries and packages (Linux)
compare-mac Run compare tests on build snapshot binaries and packages (Mac)
generate-json-schema Generate a new json schema
generate-license-list Generate an updated spdx license list
help Display this help
integration Run integration tests
lint-fix Auto-format all source code + run golangci lint fixers
lint Run gofmt + golangci lint checks
show-test-image-cache Show all docker and image tar cache
show-test-snapshots Show all test snapshots
snapshot-with-signing Build snapshot release binaries and packages (with dummy signing)
test Run all tests (currently unit, integration, linux compare, and cli tests)
unit Run unit tests (with coverage)
```
## Architecture
Syft is used to generate a Software Bill of Materials (SBOM) from different kinds of input.
### Code organization for the cmd package
Syft's entrypoint can be found in the `cmd` package at `cmd/syft/main.go`. `main.go` builds a new syft `cli` via `cli.New()`
and then executes the `cli` via `cli.Execute()`. The `cli` package is responsible for parsing command line arguments,
setting up the application context and configuration, and executing the application. Each of syft's commands
(e.g. `packages`, `attest`, `version`) are implemented as a `cobra.Command` in their respective `<command>.go` files.
They are registered in `syft/cli/commands/go`.
```
.
└── syft/
├── cli/
│   ├── attest/
│   ├── attest.go
│   ├── commands.go
│   ├── completion.go
│   ├── convert/
│   ├── convert.go
│   ├── eventloop/
│   ├── options/
│   ├── packages/
│   ├── packages.go
│   ├── poweruser/
│   ├── poweruser.go
│   └── version.go
└── main.go
```
#### Execution flow
```mermaid
sequenceDiagram
participant main as cmd/syft/main
participant cli as cli.New()
participant root as root.Execute()
participant cmd as <command>.Execute()
main->>+cli:
Note right of cli: wire ALL CLI commands
Note right of cli: add flags for ALL commands
cli-->>-main: root command
main->>+root:
root->>+cmd:
cmd-->>-root: (error)
root-->>-main: (error)
Note right of cmd: Execute SINGLE command from USER
```
### Code organization for syft library
Syft's core library (see, exported) functionality is implemented in the `syft` package. The `syft` package is responsible for organizing the core
SBOM data model, it's translated output formats, and the core SBOM generation logic.
#### Organization and design notes for the syft library
- analysis creates a static SBOM which can be encoded and decoded
- format objects, should strive to not add or enrich data in encoding that could otherwise be done during analysis
- package catalogers and their organization can be viewed/added to the `syft/pkg/cataloger` package
- file catalogers and their organization can be viewed/added to the `syft/file` package
- The source package provides an abstraction to allow a user to loosely define a data source that can be cataloged
- Logging Abstraction ...
#### Code example of syft as a library
Here is a gist of using syft as a library to generate a SBOM from a docker image: [link](https://gist.github.com/wagoodman/57ed59a6d57600c23913071b8470175b).
The execution flow for the example is detailed below.
#### Execution flow examples for the syft library
```mermaid
sequenceDiagram
participant source as source.New(ubuntu:latest)
participant sbom as sbom.SBOM
participant catalog as syft.CatalogPackages(src)
participant encoder as syft.Encode(sbom, format)
Note right of source: use "ubuntu:latest" as SBOM input
source-->>+sbom: add source to SBOM struct
source-->>+catalog: pass src to generate catalog
catalog-->-sbom: add cataloging results onto SBOM
sbom-->>encoder: pass SBOM and format desiered to syft encoder
encoder-->>source: return bytes that are the SBOM of the original input
Note right of catalog: cataloger configuration is done based on src
```
## Testing
### Levels of testing
- `unit`: The default level of test which is distributed throughout the repo are unit tests. Any `_test.go` file that
does not reside somewhere within the `/test` directory is a unit test. Other forms of testing should be organized in
the `/test` directory. These tests should focus on correctness of functionality in depth. % test coverage metrics
only considers unit tests and no other forms of testing.
- `integration`: located within `test/integration`, these tests focus on the behavior surfaced by the common library
entrypoints from the `syft` package and make light assertions about the results surfaced. Additionally, these tests
tend to make diversity assertions for enum-like objects, ensuring that as enum values are added to a definition
that integration tests will automatically fail if no test attempts to use that enum value. For more details see
the "Data diversity and freshness assertions" section below.
- `cli`: located with in `test/cli`, these are tests that test the correctness of application behavior from a
snapshot build. This should be used in cases where a unit or integration test will not do or if you are looking
for in-depth testing of code in the `cmd/` package (such as testing the proper behavior of application configuration,
CLI switches, and glue code before syft library calls).
- `acceptance`: located within `test/compare` and `test/install`, these are smoke-like tests that ensure that application
packaging and installation works as expected. For example, during release we provide RPM packages as a download
artifact. We also have an accompanying RPM acceptance test that installs the RPM from a snapshot build and ensures the
output of a syft invocation matches canned expected output. New acceptance tests should be added for each release artifact
and architecture supported (when possible).
### Data diversity and freshness assertions
It is important that tests against the codebase are flexible enough to begin failing when they do not cover "enough"
of the objects under test. "Cover" in this case does not mean that some percentage of the code has been executed
during testing, but instead that there is enough diversity of data input reflected in testing relative to the
definitions available.
For instance, consider an enum-like value like so:
```go
type Language string
const (
Java Language = "java"
JavaScript Language = "javascript"
Python Language = "python"
Ruby Language = "ruby"
Go Language = "go"
)
```
Say we have a test that exercises all the languages defined today:
```go
func TestCatalogPackages(t *testing.T) {
testTable := []struct {
// ... the set of test cases that test all languages
}
for _, test := range cases {
t.Run(test.name, func (t *testing.T) {
// use inputFixturePath and assert that syft.CatalogPackages() returns the set of expected Package objects
// ...
})
}
}
```
Where each test case has a `inputFixturePath` that would result with packages from each language. This test is
brittle since it does not assert that all languages were exercised directly and future modifications (such as
adding a new language) won't be covered by any test cases.
To address this the enum-like object should have a definition of all objects that can be used in testing:
```go
type Language string
// const( Java Language = ..., ... )
var AllLanguages = []Language{
Java,
JavaScript,
Python,
Ruby,
Go,
Rust,
}
```
Allowing testing to automatically fail when adding a new language:
```go
func TestCatalogPackages(t *testing.T) {
testTable := []struct {
// ... the set of test cases that (hopefully) covers all languages
}
// new stuff...
observedLanguages := strset.New()
for _, test := range cases {
t.Run(test.name, func (t *testing.T) {
// use inputFixturePath and assert that syft.CatalogPackages() returns the set of expected Package objects
// ...
// new stuff...
for _, actualPkg := range actual {
observedLanguages.Add(string(actualPkg.Language))
}
})
}
// new stuff...
for _, expectedLanguage := range pkg.AllLanguages {
if !observedLanguages.Contains(expectedLanguage) {
t.Errorf("failed to test language=%q", expectedLanguage)
}
}
}
```
This is a better test since it will fail when someone adds a new language but fails to write a test case that should
exercise that new language. This method is ideal for integration-level testing, where testing correctness in depth
is not needed (that is what unit tests are for) but instead testing in breadth to ensure that units are well integrated.
A similar case can be made for data freshness; if the quality of the results will be diminished if the input data
is not kept up to date then a test should be written (when possible) to assert any input data is not stale.
An example of this is the static list of licenses that is stored in `internal/spdxlicense` for use by the SPDX
presenters. This list is updated and published periodically by an external group and syft can grab and update this
list by running `go generate ./...` from the root of the repo.
An integration test has been written to grabs the latest license list version externally and compares that version
with the version generated in the codebase. If they differ, the test fails, indicating to someone that there is an
action needed to update it.
**_The key takeaway is to try and write tests that fail when data assumptions change and not just when code changes.**_
### Snapshot tests
The format objects make a lot of use of "snapshot" testing, where you save the expected output bytes from a call into the
git repository and during testing make a comparison of the actual bytes from the subject under test with the golden
copy saved in the repo. The "golden" files are stored in the `test-fixtures/snapshot` directory relative to the go
package under test and should always be updated by invoking `go test` on the specific test file with a specific CLI
update flag provided.
Many of the `Format` tests make use of this approach, where the raw SBOM report is saved in the repo and the test
compares that SBOM with what is generated from the latest presenter code. For instance, at the time of this writing
the CycloneDX presenter snapshots can be updated by running:
```bash
go test ./internal/formats -update-cyclonedx
```
These flags are defined at the top of the test files that have tests that use the snapshot files.
Snapshot testing is only as good as the manual verification of the golden snapshot file saved to the repo! Be careful
and diligent when updating these files.
-29
View File
@@ -1,29 +0,0 @@
FROM gcr.io/distroless/static-debian11:debug AS build
FROM scratch
# needed for version check HTTPS request
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
# create the /tmp dir, which is needed for image content cache
WORKDIR /tmp
COPY syft /
ARG BUILD_DATE
ARG BUILD_VERSION
ARG VCS_REF
ARG VCS_URL
LABEL org.opencontainers.image.created=$BUILD_DATE
LABEL org.opencontainers.image.title="syft"
LABEL org.opencontainers.image.description="CLI tool and library for generating a Software Bill of Materials from container images and filesystems"
LABEL org.opencontainers.image.source=$VCS_URL
LABEL org.opencontainers.image.revision=$VCS_REF
LABEL org.opencontainers.image.vendor="Anchore, Inc."
LABEL org.opencontainers.image.version=$BUILD_VERSION
LABEL org.opencontainers.image.licenses="Apache-2.0"
LABEL io.artifacthub.package.readme-url="https://raw.githubusercontent.com/anchore/syft/main/README.md"
LABEL io.artifacthub.package.logo-url="https://user-images.githubusercontent.com/5199289/136844524-1527b09f-c5cb-4aa9-be54-5aa92a6086c1.png"
LABEL io.artifacthub.package.license="Apache-2.0"
ENTRYPOINT ["/syft"]
-25
View File
@@ -1,25 +0,0 @@
FROM gcr.io/distroless/static-debian11:debug
# create the /tmp dir, which is needed for image content cache
WORKDIR /tmp
COPY syft /
ARG BUILD_DATE
ARG BUILD_VERSION
ARG VCS_REF
ARG VCS_URL
LABEL org.opencontainers.image.created=$BUILD_DATE
LABEL org.opencontainers.image.title="syft"
LABEL org.opencontainers.image.description="CLI tool and library for generating a Software Bill of Materials from container images and filesystems"
LABEL org.opencontainers.image.source=$VCS_URL
LABEL org.opencontainers.image.revision=$VCS_REF
LABEL org.opencontainers.image.vendor="Anchore, Inc."
LABEL org.opencontainers.image.version=$BUILD_VERSION
LABEL org.opencontainers.image.licenses="Apache-2.0"
LABEL io.artifacthub.package.readme-url="https://raw.githubusercontent.com/anchore/syft/main/README.md"
LABEL io.artifacthub.package.logo-url="https://user-images.githubusercontent.com/5199289/136844524-1527b09f-c5cb-4aa9-be54-5aa92a6086c1.png"
LABEL io.artifacthub.package.license="Apache-2.0"
ENTRYPOINT ["/syft"]
+137 -266
View File
@@ -1,24 +1,11 @@
BIN = syft
VERSION=$(shell git describe --dirty --always --tags)
TEMPDIR = ./.tmp
# commands and versions
LINTCMD = $(TEMPDIR)/golangci-lint run --tests=false
GOIMPORTS_CMD = $(TEMPDIR)/gosimports -local github.com/anchore
RELEASE_CMD=$(TEMPDIR)/goreleaser release --rm-dist
SNAPSHOT_CMD=$(RELEASE_CMD) --skip-publish --snapshot
# tool versions
GOLANGCILINT_VERSION = v1.50.1
GOSIMPORTS_VERSION = v0.3.4
BOUNCER_VERSION = v0.4.0
CHRONICLE_VERSION = v0.4.2
GORELEASER_VERSION = v1.13.1
YAJSV_VERSION = v1.4.1
COSIGN_VERSION = v1.13.1
QUILL_VERSION = v0.2.0
# formatting variables
RESULTSDIR = $(TEMPDIR)/results
COVER_REPORT = $(RESULTSDIR)/cover.report
COVER_TOTAL = $(RESULTSDIR)/cover.total
LINTCMD = $(TEMPDIR)/golangci-lint run --tests=false --config .golangci.yaml
ACC_TEST_IMAGE = centos:8.2.2004
ACC_DIR = ./test/acceptance
BOLD := $(shell tput -T linux bold)
PURPLE := $(shell tput -T linux setaf 5)
GREEN := $(shell tput -T linux setaf 2)
@@ -27,28 +14,21 @@ RED := $(shell tput -T linux setaf 1)
RESET := $(shell tput -T linux sgr0)
TITLE := $(BOLD)$(PURPLE)
SUCCESS := $(BOLD)$(GREEN)
# test variables
RESULTSDIR = test/results
COMPARE_DIR = ./test/compare
COMPARE_TEST_IMAGE = centos:8.2.2004
COVER_REPORT = $(RESULTSDIR)/unit-coverage-details.txt
COVER_TOTAL = $(RESULTSDIR)/unit-coverage-summary.txt
# the quality gate lower threshold for unit test total % coverage (by function statements)
COVERAGE_THRESHOLD := 62
# CI cache busting values; change these if you want CI to not use previous stored cache
INTEGRATION_CACHE_BUSTER="894d8ca"
CLI_CACHE_BUSTER="e5cdfd8"
BOOTSTRAP_CACHE="c7afb99ad"
COVERAGE_THRESHOLD := 72
## Build variables
DISTDIR=./dist
SNAPSHOTDIR=./snapshot
OS=$(shell uname | tr '[:upper:]' '[:lower:]')
SNAPSHOT_BIN=$(realpath $(shell pwd)/$(SNAPSHOTDIR)/$(OS)-build_$(OS)_amd64_v1/$(BIN))
GITTREESTATE=$(if $(shell git status --porcelain),dirty,clean)
SNAPSHOT_CMD=$(shell realpath $(shell pwd)/$(SNAPSHOTDIR)/syft_linux_amd64/syft)
ifeq "$(strip $(VERSION))" ""
override VERSION = $(shell git describe --always --tags --dirty)
endif
## Variable assertions
ifndef TEMPDIR
$(error TEMPDIR is not set)
endif
@@ -57,8 +37,8 @@ ifndef RESULTSDIR
$(error RESULTSDIR is not set)
endif
ifndef COMPARE_DIR
$(error COMPARE_DIR is not set)
ifndef ACC_DIR
$(error ACC_DIR is not set)
endif
ifndef DISTDIR
@@ -69,75 +49,43 @@ ifndef SNAPSHOTDIR
$(error SNAPSHOTDIR is not set)
endif
ifndef VERSION
$(error VERSION is not set)
endif
ifndef REF_NAME
REF_NAME = $(VERSION)
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
## Default Task
.DEFAULT_GOAL:=help
## Tasks
.PHONY: all
all: clean static-analysis test ## Run all linux-based checks (linting, license check, unit, integration, and linux compare tests)
all: clean static-analysis test ## Run all linux-based checks (linting, license check, unit, integration, and linux acceptance tests)
@printf '$(SUCCESS)All checks pass!$(RESET)\n'
.PHONY: test
test: unit validate-cyclonedx-schema integration benchmark compare-linux cli ## Run all tests (currently unit, integration, linux compare, and cli tests)
test: unit validate-cyclonedx-schema integration acceptance-linux ## Run all tests (currently unit, integration, and linux acceptance tests)
.PHONY: help
help:
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "$(BOLD)$(CYAN)%-25s$(RESET)%s\n", $$1, $$2}'
.PHONY: ci-bootstrap
ci-bootstrap:
ci-bootstrap: 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)
$(TEMPDIR):
mkdir -p $(TEMPDIR)
.PHONY: bootstrap-tools
bootstrap-tools: $(TEMPDIR)
curl -sSfL https://raw.githubusercontent.com/anchore/quill/main/install.sh | sh -s -- -b $(TEMPDIR)/ $(QUILL_VERSION)
GO111MODULE=off GOBIN=$(realpath $(TEMPDIR)) go get -u golang.org/x/perf/cmd/benchstat
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(TEMPDIR)/ $(GOLANGCILINT_VERSION)
curl -sSfL https://raw.githubusercontent.com/wagoodman/go-bouncer/master/bouncer.sh | sh -s -- -b $(TEMPDIR)/ $(BOUNCER_VERSION)
curl -sSfL https://raw.githubusercontent.com/anchore/chronicle/main/install.sh | sh -s -- -b $(TEMPDIR)/ $(CHRONICLE_VERSION)
.github/scripts/goreleaser-install.sh -d -b $(TEMPDIR)/ $(GORELEASER_VERSION)
# the only difference between goimports and gosimports is that gosimports removes extra whitespace between import blocks (see https://github.com/golang/go/issues/20818)
GOBIN="$(realpath $(TEMPDIR))" go install github.com/rinchsan/gosimports/cmd/gosimports@$(GOSIMPORTS_VERSION)
GOBIN="$(realpath $(TEMPDIR))" go install github.com/neilpa/yajsv@$(YAJSV_VERSION)
GOBIN="$(realpath $(TEMPDIR))" go install github.com/sigstore/cosign/cmd/cosign@$(COSIGN_VERSION)
.PHONY: bootstrap-go
bootstrap-go:
go mod download
.PHONY: bootstrap
bootstrap: $(RESULTSDIR) bootstrap-go bootstrap-tools ## Download and install all tooling dependencies (+ prep tooling in the ./tmp dir)
bootstrap: ## Download and install all go dependencies (+ prep tooling in the ./tmp dir)
$(call title,Bootstrapping dependencies)
@pwd
# prep temp dirs
mkdir -p $(TEMPDIR)
mkdir -p $(RESULTSDIR)
# install go dependencies
go mod download
# install utilities
[ -f "$(TEMPDIR)/golangci" ] || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(TEMPDIR)/ v1.26.0
[ -f "$(TEMPDIR)/bouncer" ] || curl -sSfL https://raw.githubusercontent.com/wagoodman/go-bouncer/master/bouncer.sh | sh -s -- -b $(TEMPDIR)/ v0.2.0
[ -f "$(TEMPDIR)/goreleaser" ] || curl -sfL https://install.goreleaser.com/github.com/goreleaser/goreleaser.sh | sh -s -- -b $(TEMPDIR)/ v0.140.0
.PHONY: static-analysis
static-analysis: check-go-mod-tidy check-licenses lint
static-analysis: lint check-licenses
.PHONY: lint
lint: ## Run gofmt + golangci lint checks
@@ -148,7 +96,6 @@ lint: ## Run gofmt + golangci lint checks
# run all golangci-lint rules
$(LINTCMD)
@[ -z "$(shell $(GOIMPORTS_CMD) -d .)" ] || (echo "goimports needs to be fixed" && false)
# go tooling does not play well with certain filename characters, ensure the common cases don't result in future "go get" failures
$(eval MALFORMED_FILENAMES := $(shell find . | grep -e ':'))
@@ -158,251 +105,175 @@ lint: ## Run gofmt + golangci lint checks
lint-fix: ## Auto-format all source code + run golangci lint fixers
$(call title,Running lint fixers)
gofmt -w -s .
$(GOIMPORTS_CMD) -w .
$(LINTCMD) --fix
go mod tidy
.PHONY: check-licenses
check-licenses: ## Ensure transitive dependencies are compliant with the current license policy
$(TEMPDIR)/bouncer check ./...
check-go-mod-tidy:
@ .github/scripts/go-mod-tidy-check.sh && echo "go.mod and go.sum are tidy!"
check-licenses:
$(TEMPDIR)/bouncer check
.PHONY: validate-cyclonedx-schema
validate-cyclonedx-schema:
cd schema/cyclonedx && make
.PHONY: unit
unit: $(RESULTSDIR) fixtures ## Run unit tests (with coverage)
unit: fixtures ## Run unit tests (with coverage)
$(call title,Running unit tests)
go test -coverprofile $(COVER_REPORT) $(shell go list ./... | grep -v anchore/syft/test)
go test -coverprofile $(COVER_REPORT) ./...
@go tool cover -func $(COVER_REPORT) | grep total | awk '{print substr($$3, 1, length($$3)-1)}' > $(COVER_TOTAL)
@echo "Coverage: $$(cat $(COVER_TOTAL))"
@if [ $$(echo "$$(cat $(COVER_TOTAL)) >= $(COVERAGE_THRESHOLD)" | bc -l) -ne 1 ]; then echo "$(RED)$(BOLD)Failed coverage quality gate (> $(COVERAGE_THRESHOLD)%)$(RESET)" && false; fi
.PHONY: benchmark
benchmark: $(RESULTSDIR) ## Run benchmark tests and compare against the baseline (if available)
$(call title,Running benchmark tests)
go test -p 1 -run=^Benchmark -bench=. -count=5 -benchmem ./... | tee $(RESULTSDIR)/benchmark-$(REF_NAME).txt
(test -s $(RESULTSDIR)/benchmark-main.txt && \
$(TEMPDIR)/benchstat $(RESULTSDIR)/benchmark-main.txt $(RESULTSDIR)/benchmark-$(REF_NAME).txt || \
$(TEMPDIR)/benchstat $(RESULTSDIR)/benchmark-$(REF_NAME).txt) \
| tee $(RESULTSDIR)/benchstat.txt
.PHONY: show-benchstat
show-benchstat:
@cat $(RESULTSDIR)/benchstat.txt
# 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)
go test -v ./test/integration
go test -v -tags=integration ./test/integration
# note: this is used by CI to determine if the integration test fixture cache (docker image tars) should be busted
integration-fingerprint:
$(call title,Integration test fixture fingerprint)
find test/integration/test-fixtures/image-* -type f -exec md5sum {} + | awk '{print $1}' | sort | tee /dev/stderr | md5sum | tee test/integration/test-fixtures/cache.fingerprint && echo "$(INTEGRATION_CACHE_BUSTER)" >> test/integration/test-fixtures/cache.fingerprint
find test/integration/test-fixtures/image-* -type f -exec md5sum {} + | awk '{print $1}' | sort | md5sum | tee test/integration/test-fixtures/tar-cache.fingerprint
.PHONY: java-packages-fingerprint
java-packages-fingerprint:
$(call title,Java test fixture fingerprint)
cd syft/pkg/cataloger/java/test-fixtures/java-builds && \
make packages.fingerprint
.PHONY: go-binaries-fingerprint
go-binaries-fingerprint:
$(call title,Go binaries test fixture fingerprint)
cd syft/pkg/cataloger/golang/test-fixtures/archs && \
make binaries.fingerprint
.PHONY: rpm-binaries-fingerprint
rpm-binaries-fingerprint:
$(call title,RPM binary test fixture fingerprint)
cd syft/pkg/cataloger/rpm/test-fixtures && \
make rpms.fingerprint
@cd syft/cataloger/java/test-fixtures/java-builds && \
make packages.fingerprint
.PHONY: fixtures
fixtures:
$(call title,Generating test fixtures)
cd syft/pkg/cataloger/java/test-fixtures/java-builds && make
cd syft/pkg/cataloger/rpm/test-fixtures && make
cd syft/cataloger/java/test-fixtures/java-builds && make
.PHONY: generate-json-schema
generate-json-schema: ## Generate a new json schema
cd schema/json && go run generate.go
generate-json-schema: clean-json-schema-examples integration ## Generate a new json schema for the json presenter, derived from integration test cases
docker run \
-i \
--rm \
-v $(shell pwd)/schema/json:/work \
-w /work \
python:3.8 \
bash -x -c "\
pip install -r requirements.txt && \
python generate.py \
"
.PHONY: generate-license-list
generate-license-list: ## Generate an updated spdx license list
go generate ./internal/spdxlicense/...
gofmt -s -w ./internal/spdxlicense
.PHONY: clear-test-cache
clear-test-cache: ## Delete all test cache (built docker image tars)
find . -type f -wholename "**/test-fixtures/tar-cache/*.tar" -delete
.PHONY: check-pipeline
check-pipeline: ## Run local CircleCI pipeline locally (sanity check)
$(call title,Check pipeline)
# note: this is meant for local development & testing of the pipeline, NOT to be run in CI
mkdir -p $(TEMPDIR)
circleci config process .circleci/config.yml > .tmp/circleci.yml
circleci local execute -c .tmp/circleci.yml --job "Static Analysis"
circleci local execute -c .tmp/circleci.yml --job "Unit & Integration Tests (go-latest)"
@printf '$(SUCCESS)Pipeline checks pass!$(RESET)\n'
proto:
protoc -I syft/plugin/proto/ syft/plugin/proto/cataloger.proto --go_out=plugins=grpc:syft/plugin/proto/
.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 $(SNAPSHOT_CMD) --skip-sign --config $(TEMPDIR)/goreleaser.yaml"
BUILD_GIT_TREE_STATE=$(GITTREESTATE) \
$(TEMPDIR)/goreleaser release --skip-publish --rm-dist --snapshot --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
# build release snapshots
bash -c "\
$(SNAPSHOT_CMD) \
--config $(TEMPDIR)/goreleaser.yaml \
|| (cat /tmp/quill-*.log && false)"
# note: we cannot clean the snapshot directory since the pipeline builds the snapshot separately
.PHONY: compare-mac
compare-mac: $(RESULTSDIR) $(SNAPSHOTDIR) ## Run compare tests on build snapshot binaries and packages (Mac)
$(call title,Running compare test: Run on Mac)
$(COMPARE_DIR)/mac.sh \
.PHONY: acceptance-mac
acceptance-mac: $(SNAPSHOTDIR) ## Run acceptance tests on build snapshot binaries and packages (Mac)
$(call title,Running acceptance test: Run on Mac)
$(ACC_DIR)/mac.sh \
$(SNAPSHOTDIR) \
$(COMPARE_DIR) \
$(COMPARE_TEST_IMAGE) \
$(ACC_DIR) \
$(ACC_TEST_IMAGE) \
$(RESULTSDIR)
# note: we cannot clean the snapshot directory since the pipeline builds the snapshot separately
.PHONY: compare-linux
compare-linux: compare-test-deb-package-install compare-test-rpm-package-install ## Run compare tests on build snapshot binaries and packages (Linux)
.PHONY: acceptance-linux
acceptance-linux: acceptance-test-deb-package-install acceptance-test-rpm-package-install ## Run acceptance tests on build snapshot binaries and packages (Linux)
.PHONY: compare-test-deb-package-install
compare-test-deb-package-install: $(RESULTSDIR) $(SNAPSHOTDIR)
$(call title,Running compare test: DEB install)
$(COMPARE_DIR)/deb.sh \
# note: this is used by CI to determine if the inline-scan report cache should be busted for the inline-compare tests
.PHONY: compare-fingerprint
compare-fingerprint:
find test/inline-compare/* -type f -exec md5sum {} + | grep -v '\-reports' | grep -v 'fingerprint' | awk '{print $1}' | sort | md5sum | tee test/inline-compare/inline-compare.fingerprint
.PHONY: compare-snapshot
compare-snapshot: $(SNAPSHOTDIR) ## Compare the reports of a run of a snapshot build of syft against inline-scan
chmod 755 $(SNAPSHOT_CMD)
@cd test/inline-compare && SYFT_CMD=$(SNAPSHOT_CMD) make
.PHONY: compare
compare: ## Compare the reports of a run of a main-branch build of syft against inline-scan
@cd test/inline-compare && make
.PHONY: acceptance-test-deb-package-install
acceptance-test-deb-package-install: $(SNAPSHOTDIR)
$(call title,Running acceptance test: DEB install)
$(ACC_DIR)/deb.sh \
$(SNAPSHOTDIR) \
$(COMPARE_DIR) \
$(COMPARE_TEST_IMAGE) \
$(ACC_DIR) \
$(ACC_TEST_IMAGE) \
$(RESULTSDIR)
.PHONY: compare-test-rpm-package-install
compare-test-rpm-package-install: $(RESULTSDIR) $(SNAPSHOTDIR)
$(call title,Running compare test: RPM install)
$(COMPARE_DIR)/rpm.sh \
.PHONY: acceptance-test-rpm-package-install
acceptance-test-rpm-package-install: $(SNAPSHOTDIR)
$(call title,Running acceptance test: RPM install)
$(ACC_DIR)/rpm.sh \
$(SNAPSHOTDIR) \
$(COMPARE_DIR) \
$(COMPARE_TEST_IMAGE) \
$(ACC_DIR) \
$(ACC_TEST_IMAGE) \
$(RESULTSDIR)
# note: this is used by CI to determine if the integration test fixture cache (docker image tars) should be busted
cli-fingerprint:
$(call title,CLI test fixture fingerprint)
find test/cli/test-fixtures/image-* -type f -exec md5sum {} + | awk '{print $1}' | sort | md5sum | tee test/cli/test-fixtures/cache.fingerprint && echo "$(CLI_CACHE_BUSTER)" >> test/cli/test-fixtures/cache.fingerprint
.PHONY: cli
cli: $(SNAPSHOTDIR) ## Run CLI tests
chmod 755 "$(SNAPSHOT_BIN)"
$(SNAPSHOT_BIN) version
SYFT_BINARY_LOCATION='$(SNAPSHOT_BIN)' \
go test -count=1 -timeout=15m -v ./test/cli
.PHONY: changelog
changelog: clean-changelog CHANGELOG.md
@docker run -it --rm \
-v $(shell pwd)/CHANGELOG.md:/CHANGELOG.md \
rawkode/mdv \
-t 748.5989 \
/CHANGELOG.md
CHANGELOG.md:
$(TEMPDIR)/chronicle -vv > CHANGELOG.md
.PHONY: changlog
changelog:
mkdir -p $(DISTDIR)
@docker run -it --rm \
-v "$(shell pwd)":/usr/local/src/your-app ferrarimarco/github-changelog-generator \
--user anchore \
--project $(BIN) \
-t ${GITHUB_TOKEN} \
--no-pr-wo-labels \
--no-issues-wo-labels \
--unreleased-only \
--future-release $(VERSION)
.PHONY: release
release: clean-dist CHANGELOG.md
release: clean-dist changelog ## Build and publish final binaries and packages
$(call title,Publishing release artifacts)
# create a config with the dist dir overridden
echo "dist: $(DISTDIR)" > $(TEMPDIR)/goreleaser.yaml
cat .goreleaser.yaml >> $(TEMPDIR)/goreleaser.yaml
# note: notarization cannot be done in parallel, thus --parallelism 1
bash -c "\
$(RELEASE_CMD) \
--config $(TEMPDIR)/goreleaser.yaml \
--parallelism 1 \
--release-notes <(cat CHANGELOG.md) \
|| (cat /tmp/quill-*.log && false)"
# release
BUILD_GIT_TREE_STATE=$(GITTREESTATE) \
$(TEMPDIR)/goreleaser \
--rm-dist \
--config $(TEMPDIR)/goreleaser.yaml \
--release-notes <(cat CHANGELOG.md)
# verify checksum signatures
.github/scripts/verify-signature.sh "$(DISTDIR)"
# 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 clean-test-image-cache ## Remove previous builds, result reports, and test cache
$(call safe_rm_rf_children,$(RESULTSDIR))
clean: clean-dist clean-snapshot clean-json-schema-examples ## Remove previous builds and result reports
rm -rf $(RESULTSDIR)/*
.PHONY: clean-snapshot
clean-snapshot:
$(call safe_rm_rf,$(SNAPSHOTDIR))
rm -f $(TEMPDIR)/goreleaser.yaml
rm -rf $(SNAPSHOTDIR) $(TEMPDIR)/goreleaser.yaml
.PHONY: clean-dist
clean-dist: clean-changelog
$(call safe_rm_rf,$(DISTDIR))
rm -f $(TEMPDIR)/goreleaser.yaml
clean-dist:
rm -rf $(DISTDIR) $(TEMPDIR)/goreleaser.yaml
.PHONY: clean-changelog
clean-changelog:
rm -f CHANGELOG.md
clean-test-image-cache: clean-test-image-tar-cache clean-test-image-docker-cache ## Clean test image cache
.PHONY: clear-test-image-tar-cache
clean-test-image-tar-cache:
## Delete all test cache (built docker image tars)
find . -type f -wholename "**/test-fixtures/cache/stereoscope-fixture-*.tar" -delete
.PHONY: clear-test-image-docker-cache
clean-test-image-docker-cache:
## Purge all test docker images
docker images --format '{{.ID}} {{.Repository}}' | grep stereoscope-fixture- | awk '{print $$1}' | uniq | xargs -r docker rmi --force
.PHONY: show-test-image-cache
show-test-image-cache: ## Show all docker and image tar cache
$(call title,Docker daemon cache)
@docker images --format '{{.ID}} {{.Repository}}:{{.Tag}}' | grep stereoscope-fixture- | sort
$(call title,Tar cache)
@find . -type f -wholename "**/test-fixtures/cache/stereoscope-fixture-*.tar" | sort
.PHONY: show-test-snapshots
show-test-snapshots: ## Show all test snapshots
$(call title,Test snapshots)
@find . -type f -wholename "**/test-fixtures/snapshot/*" | sort
.PHONY: help
help: ## Display this help
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "$(BOLD)$(CYAN)%-25s$(RESET)%s\n", $$1, $$2}'
.PHONY: clean-json-schema-examples
clean-json-schema-examples:
rm -f schema/json/examples/*
+43 -587
View File
@@ -1,405 +1,72 @@
<p align="center">
<img src="https://user-images.githubusercontent.com/5199289/136844524-1527b09f-c5cb-4aa9-be54-5aa92a6086c1.png" width="271" alt="Cute pink owl syft logo">
</p>
# syft
[![Validations](https://github.com/anchore/syft/actions/workflows/validations.yaml/badge.svg)](https://github.com/anchore/syft/actions/workflows/validations.yaml)
[![CircleCI](https://circleci.com/gh/anchore/syft.svg?style=svg)](https://circleci.com/gh/anchore/syft)
[![Go Report Card](https://goreportcard.com/badge/github.com/anchore/syft)](https://goreportcard.com/report/github.com/anchore/syft)
[![GitHub release](https://img.shields.io/github/release/anchore/syft.svg)](https://github.com/anchore/syft/releases/latest)
[![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/anchore/syft.svg)](https://github.com/anchore/syft)
[![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/anchore/syft/blob/main/LICENSE)
[![Slack Invite](https://img.shields.io/badge/Slack-Join-blue?logo=slack)](https://anchore.com/slack)
A CLI tool and Go library for generating a Software Bill of Materials (SBOM) from container images and filesystems. Exceptional for vulnerability detection when used with a scanner like [Grype](https://github.com/anchore/grype).
### Join our community meetings!
- Calendar: https://calendar.google.com/calendar/u/0/r?cid=Y182OTM4dGt0MjRtajI0NnNzOThiaGtnM29qNEBncm91cC5jYWxlbmRhci5nb29nbGUuY29t
- Agenda: https://docs.google.com/document/d/1ZtSAa6fj2a6KRWviTn3WoJm09edvrNUp4Iz_dOjjyY8/edit?usp=sharing (join [this group](https://groups.google.com/g/anchore-oss-community) for write access)
- All are welcome!
For commercial support options with Syft or Grype, please [contact Anchore](https://get.anchore.com/contact/)
A CLI tool and go library for generating a Software Bill of Materials (SBOM) from container images and filesystems.
![syft-demo](https://user-images.githubusercontent.com/590471/90277200-2a253000-de33-11ea-893f-32c219eea11a.gif)
## Features
- Generates SBOMs for container images, filesystems, archives, and more to discover packages and libraries
- Supports OCI, Docker and [Singularity](https://github.com/sylabs/singularity) image formats
- Linux distribution identification
- Works seamlessly with [Grype](https://github.com/anchore/grype) (a fast, modern vulnerability scanner)
- Able to create signed SBOM attestations using the [in-toto specification](https://github.com/in-toto/attestation/blob/main/spec/README.md)
- Convert between SBOM formats, such as CycloneDX, SPDX, and Syft's own format.
**Features**
- Catalog container images and filesystems to discover packages and libraries.
- Supports packages and libraries from various ecosystems (APK, DEB, RPM, Ruby Bundles, Python Wheel/Egg/requirements.txt, JavaScript NPM/Yarn, Java JAR/EAR/WAR, Jenkins plugins JPI/HPI, Go modules)
- Linux distribution identification (supports Alpine, BusyBox, CentOS/RedHat, Debian/Ubuntu flavored distributions)
### Supported Ecosystems
- Alpine (apk)
- C (conan)
- C++ (conan)
- Dart (pubs)
- Debian (dpkg)
- Dotnet (deps.json)
- Objective-C (cocoapods)
- Go (go.mod, Go binaries)
- Haskell (cabal, stack)
- Java (jar, ear, war, par, sar)
- JavaScript (npm, yarn)
- Jenkins Plugins (jpi, hpi)
- PHP (composer)
- Python (wheel, egg, poetry, requirements.txt)
- Red Hat (rpm)
- Ruby (gem)
- Rust (cargo.lock)
- Swift (cocoapods)
## Installation
**Note**: Currently, Syft is built only for Linux, macOS and Windows.
### Recommended
```bash
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
```
... or, you can specify a release version and destination directory for the installation:
```
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b <DESTINATION_DIR> <RELEASE_VERSION>
```
### Chocolatey
The chocolatey distribution of syft is community maintained and not distributed by the anchore team
```powershell
choco install syft -y
```
### Homebrew
```bash
brew tap anchore/syft
brew install syft
```
### Nix
**Note**: Nix packaging of Syft is [community maintained](https://github.com/NixOS/nixpkgs/blob/master/pkgs/tools/admin/syft/default.nix). Syft is currently only in the [unstable channel](https://nixos.wiki/wiki/Nix_channels#The_official_channels) awaiting the `22.05` release
```bash
nix-env -i syft
```
... or, just try it out in an ephemeral nix shell:
```bash
nix-shell -p syft
```
> :warning: **This is pre-release software** and it may not work as expected. If you encounter an issue, please [let us know using the issue tracker](https://github.com/anchore/syft/issues).
## Getting started
### SBOM
To generate an SBOM for a container image:
To generate an SBOM for an image:
```
syft <image>
```
The above output includes only software that is visible in the container (i.e., the squashed representation of the image). To include software from all image layers in the SBOM, regardless of its presence in the final image, provide `--scope all-layers`:
The above output includes only software that is visible in the container (i.e., the squashed representation of the image).
To include software from all image layers in the SBOM, regardless of its presence in the final image, provide `--scope all-layers`:
```
syft <image> --scope all-layers
```
## Supported sources
Syft can generate a SBOM from a variety of sources:
```
# catalog a container image archive (from the result of `docker image save ...`, `podman save ...`, or `skopeo copy` commands)
syft path/to/image.tar
# catalog a Singularity Image Format (SIF) container
syft path/to/image.sif
# catalog a docker image tar (from the result of "docker image save ... -o image.tar" command)
syft docker-archive://path/to/image.tar
# catalog a directory
syft path/to/dir
syft dir://path/to/dir
```
Sources can be explicitly provided with a scheme:
By default Syft shows a summary table, however, more detailed `text` and `json` formats are also available.
```
docker:yourrepo/yourimage:tag use images from the Docker daemon
podman:yourrepo/yourimage:tag use images from the Podman daemon
docker-archive:path/to/yourimage.tar use a tarball from disk for archives created from "docker save"
oci-archive:path/to/yourimage.tar use a tarball from disk for OCI archives (from Skopeo or otherwise)
oci-dir:path/to/yourimage read directly from a path on disk for OCI layout directories (from Skopeo or otherwise)
singularity:path/to/yourimage.sif read directly from a Singularity Image Format (SIF) container on disk
dir:path/to/yourproject read directly from a path on disk (any directory)
file:path/to/yourproject/file read directly from a path on disk (any single file)
registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required)
syft <image> -o json
syft <image> -o text
```
#### Default Cataloger Configuration by scan type
## Installation
##### Image Scanning:
- alpmdb
- rpmdb
- dpkgdb
- apkdb
- portage
- ruby-gemspec
- python-package
- php-composer-installed Cataloger
- javascript-package
- java
- go-module-binary
- dotnet-deps
**Recommended**
```bash
# install the latest version to /usr/local/bin
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
##### Directory Scanning:
- alpmdb
- apkdb
- dpkgdb
- portage
- rpmdb
- ruby-gemfile
- python-index
- python-package
- php-composer-lock
- javascript-lock
- java
- java-pom
- go-module-binary
- go-mod-file
- rust-cargo-lock
- dartlang-lock
- dotnet-deps
- cocoapods
- conan
- hackage
#### Non Default:
- cargo-auditable-binary
### Excluding file paths
Syft can exclude files and paths from being scanned within a source by using glob expressions
with one or more `--exclude` parameters:
```
syft <source> --exclude './out/**/*.json' --exclude /etc
```
**Note:** in the case of _image scanning_, since the entire filesystem is scanned it is
possible to use absolute paths like `/etc` or `/usr/**/*.txt` whereas _directory scans_
exclude files _relative to the specified directory_. For example: scanning `/usr/foo` with
`--exclude ./package.json` would exclude `/usr/foo/package.json` and `--exclude '**/package.json'`
would exclude all `package.json` files under `/usr/foo`. For _directory scans_,
it is required to begin path expressions with `./`, `*/`, or `**/`, all of which
will be resolved _relative to the specified scan directory_. Keep in mind, your shell
may attempt to expand wildcards, so put those parameters in single quotes, like:
`'**/*.json'`.
### Output formats
The output format for Syft is configurable as well using the
`-o` (or `--output`) option:
```
syft <image> -o <format>
# install a specific version into a specific dir
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s <RELEASE_VERSION> -b <SOME_BIN_PATH>
```
Where the `formats` available are:
- `json`: Use this to get as much information out of Syft as possible!
- `text`: A row-oriented, human-and-machine-friendly output.
- `cyclonedx-xml`: A XML report conforming to the [CycloneDX 1.4 specification](https://cyclonedx.org/specification/overview/).
- `cyclonedx-json`: A JSON report conforming to the [CycloneDX 1.4 specification](https://cyclonedx.org/specification/overview/).
- `spdx-tag-value`: A tag-value formatted report conforming to the [SPDX 2.2 specification](https://spdx.github.io/spdx-spec/).
- `spdx-json`: A JSON report conforming to the [SPDX 2.2 JSON Schema](https://github.com/spdx/spdx-spec/blob/v2.2/schemas/spdx-schema.json).
- `github`: A JSON report conforming to GitHub's dependency snapshot format.
- `table`: A columnar summary (default).
- `template`: Lets the user specify the output format. See ["Using templates"](#using-templates) below.
## Using templates
Syft lets you define custom output formats, using [Go templates](https://pkg.go.dev/text/template). Here's how it works:
- Define your format as a Go template, and save this template as a file.
- Set the output format to "template" (`-o template`).
- Specify the path to the template file (`-t ./path/to/custom.template`).
- Syft's template processing uses the same data models as the `json` output format — so if you're wondering what data is available as you author a template, you can use the output from `syft <image> -o json` as a reference.
**Example:** You could make Syft output data in CSV format by writing a Go template that renders CSV data and then running `syft <image> -o template -t ~/path/to/csv.tmpl`.
Here's what the `csv.tmpl` file might look like:
```gotemplate
"Package","Version Installed","Found by"
{{- range .Artifacts}}
"{{.Name}}","{{.Version}}","{{.FoundBy}}"
{{- end}}
**macOS**
```bash
brew tap anchore/syft
brew install syft
```
Which would produce output like:
```text
"Package","Version Installed","Found by"
"alpine-baselayout","3.2.0-r20","apkdb-cataloger"
"alpine-baselayout-data","3.2.0-r20","apkdb-cataloger"
"alpine-keys","2.4-r1","apkdb-cataloger"
...
You may experience a "macOS cannot verify app is free from malware" error upon running Syft because it is not yet signed and notarized. You can override this using `xattr`.
```bash
xattr -rd com.apple.quarantine syft
```
Syft also includes a vast array of utility templating functions from [sprig](http://masterminds.github.io/sprig/) apart from the default Golang [text/template](https://pkg.go.dev/text/template#hdr-Functions) to allow users to customize the output format.
## Multiple outputs
Syft can also output _multiple_ files in differing formats by appending
`=<file>` to the option, for example to output Syft JSON and SPDX JSON:
```shell
syft <image> -o json=sbom.syft.json -o spdx-json=sbom.spdx.json
```
## Private Registry Authentication
### Local Docker Credentials
When a container runtime is not present, Syft can still utilize credentials configured in common credential sources (such as `~/.docker/config.json`). It will pull images from private registries using these credentials. The config file is where your credentials are stored when authenticating with private registries via some command like `docker login`. For more information see the `go-containerregistry` [documentation](https://github.com/google/go-containerregistry/tree/main/pkg/authn).
An example `config.json` looks something like this:
```json
{
"auths": {
"registry.example.com": {
"username": "AzureDiamond",
"password": "hunter2"
}
}
}
```
You can run the following command as an example. It details the mount/environment configuration a container needs to access a private registry:
```
docker run -v ./config.json:/config/config.json -e "DOCKER_CONFIG=/config" anchore/syft:latest <private_image>
```
### Docker Credentials in Kubernetes
Here's a simple workflow to mount this config file as a secret into a container on Kubernetes.
1. Create a secret. The value of `config.json` is important. It refers to the specification detailed [here](https://github.com/google/go-containerregistry/tree/main/pkg/authn#the-config-file). Below this section is the `secret.yaml` file that the pod configuration will consume as a volume. The key `config.json` is important. It will end up being the name of the file when mounted into the pod.
```yaml
# secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: registry-config
namespace: syft
data:
config.json: <base64 encoded config.json>
```
`kubectl apply -f secret.yaml`
2. Create your pod running syft. The env `DOCKER_CONFIG` is important because it advertises where to look for the credential file. In the below example, setting `DOCKER_CONFIG=/config` informs syft that credentials can be found at `/config/config.json`. This is why we used `config.json` as the key for our secret. When mounted into containers the secrets' key is used as the filename. The `volumeMounts` section mounts our secret to `/config`. The `volumes` section names our volume and leverages the secret we created in step one.
```yaml
# pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: syft-k8s-usage
spec:
containers:
- image: anchore/syft:latest
name: syft-private-registry-demo
env:
- name: DOCKER_CONFIG
value: /config
volumeMounts:
- mountPath: /config
name: registry-config
readOnly: true
args:
- <private_image>
volumes:
- name: registry-config
secret:
secretName: registry-config
```
`kubectl apply -f pod.yaml`
3. The user can now run `kubectl logs syft-private-registry-demo`. The logs should show the Syft analysis for the `<private_image>` provided in the pod configuration.
Using the above information, users should be able to configure private registry access without having to do so in the `grype` or `syft` configuration files. They will also not be dependent on a Docker daemon, (or some other runtime software) for registry configuration and access.
## Format conversion (experimental)
The ability to convert existing SBOMs means you can create SBOMs in different formats quickly, without the need to regenerate the SBOM from scratch, which may take significantly more time.
```
syft convert <ORIGINAL-SBOM-FILE> -o <NEW-SBOM-FORMAT>[=<NEW-SBOM-FILE>]
```
This feature is experimental and data might be lost when converting formats. Packages are the main SBOM component easily transferable across formats, whereas files and relationships, as well as other information Syft doesn't support, are more likely to be lost.
We support formats with wide community usage AND good encode/decode support by Syft. The supported formats are:
- Syft JSON
- SPDX 2.2 JSON
- SPDX 2.2 tag-value
- CycloneDX 1.4 JSON
- CycloneDX 1.4 XML
Conversion example:
```sh
syft alpine:latest -o syft-json=sbom.syft.json # generate a syft SBOM
syft convert sbom.syft.json -o cyclonedx-json=sbom.cdx.json # convert it to CycloneDX
```
## Attestation (experimental)
### Keyless support
Syft supports generating attestations using cosign's [keyless](https://github.com/sigstore/cosign/blob/main/KEYLESS.md) signatures.
To use this feature with a format like CycloneDX json simply run:
```
syft attest --output cyclonedx-json <IMAGE WITH OCI WRITE ACCESS>
```
This command will open a web browser and allow the user to authenticate their OIDC identity as the root of trust for the attestation (Github, Google, Microsoft).
After authenticating, Syft will upload the attestation to the OCI registry specified by the image that the user has write access to.
You will need to make sure your credentials are configured for the OCI registry you are uploading to so that the attestation can write successfully.
Users can then verify the attestation(or any image with attestations) by running:
```
COSIGN_EXPERIMENTAL=1 cosign verify-attestation <IMAGE_WITH_ATTESTATIONS>
```
Users should see that the uploaded attestation claims are validated, the claims exist within the transparency log, and certificates on the attestations were verified against [fulcio](https://github.com/SigStore/fulcio).
There will also be a printout of the certificates subject `<user identity>` and the certificate issuer URL: `<provider of user identity (Github, Google, Microsoft)>`:
```
Certificate subject: test.email@testdomain.com
Certificate issuer URL: https://accounts.google.com
```
#### Local private key support
To generate an SBOM attestation for a container image using a local private key:
```
syft attest --output [FORMAT] --key [KEY] [SOURCE] [flags]
```
The above output is in the form of the [DSSE envelope](https://github.com/secure-systems-lab/dsse/blob/master/envelope.md#dsse-envelope).
The payload is a base64 encoded `in-toto` statement with the generated SBOM as the predicate. For details on workflows using this command see [here](#adding-an-sbom-to-an-image-as-an-attestation-using-syft).
## Configuration
Configuration search paths:
@@ -412,242 +79,31 @@ Configuration search paths:
Configuration options (example values are the default):
```yaml
# the output format(s) of the SBOM report (options: table, text, json, spdx, ...)
# same as -o, --output, and SYFT_OUTPUT env var
# to specify multiple output files in differing formats, use a list:
# output:
# - "json=<syft-json-output-file>"
# - "spdx-json=<spdx-json-output-file>"
# same as -o ; the output format of the SBOM report (options: table, text, json)
output: "table"
# suppress all output (except for the SBOM report)
# same as -q ; SYFT_QUIET env var
# same as -s ; the search space to look for packages (options: all-layers, squashed)
scope: "squashed"
# same as -q ; suppress all output (except for the SBOM report)
quiet: false
# same as --file; write output report to a file (default is to write to stdout)
file: ""
# enable/disable checking for application updates on startup
# same as SYFT_CHECK_FOR_APP_UPDATE env var
check-for-app-update: true
# a list of globs to exclude from scanning. same as --exclude ; for example:
# exclude:
# - "/etc/**"
# - "./out/**/*.json"
exclude: []
# os and/or architecture to use when referencing container images (e.g. "windows/armv6" or "arm64")
# same as --platform; SYFT_PLATFORM env var
platform: ""
# set the list of package catalogers to use when generating the SBOM
# default = empty (cataloger set determined automatically by the source type [image or file/directory])
# catalogers:
# - ruby-gemfile
# - ruby-gemspec
# - python-index
# - python-package
# - javascript-lock
# - javascript-package
# - php-composer-installed
# - php-composer-lock
# - alpmdb
# - dpkgdb
# - rpmdb
# - java
# - apkdb
# - go-module-binary
# - go-mod-file
# - dartlang-lock
# - rust
# - dotnet-deps
# rust-audit-binary scans Rust binaries built with https://github.com/Shnatsel/rust-audit
# - rust-audit-binary
catalogers:
# cataloging packages is exposed through the packages and power-user subcommands
package:
# search within archives that do contain a file index to search against (zip)
# note: for now this only applies to the java package cataloger
# SYFT_PACKAGE_SEARCH_INDEXED_ARCHIVES env var
search-indexed-archives: true
# search within archives that do not contain a file index to search against (tar, tar.gz, tar.bz2, etc)
# note: enabling this may result in a performance impact since all discovered compressed tars will be decompressed
# note: for now this only applies to the java package cataloger
# SYFT_PACKAGE_SEARCH_UNINDEXED_ARCHIVES env var
search-unindexed-archives: false
cataloger:
# enable/disable cataloging of packages
# SYFT_PACKAGE_CATALOGER_ENABLED env var
enabled: true
# the search space to look for packages (options: all-layers, squashed)
# same as -s ; SYFT_PACKAGE_CATALOGER_SCOPE env var
scope: "squashed"
# cataloging file classifications is exposed through the power-user subcommand
file-classification:
cataloger:
# enable/disable cataloging of file classifications
# SYFT_FILE_CLASSIFICATION_CATALOGER_ENABLED env var
enabled: true
# the search space to look for file classifications (options: all-layers, squashed)
# SYFT_FILE_CLASSIFICATION_CATALOGER_SCOPE env var
scope: "squashed"
# cataloging file contents is exposed through the power-user subcommand
file-contents:
cataloger:
# enable/disable cataloging of secrets
# SYFT_FILE_CONTENTS_CATALOGER_ENABLED env var
enabled: true
# the search space to look for secrets (options: all-layers, squashed)
# SYFT_FILE_CONTENTS_CATALOGER_SCOPE env var
scope: "squashed"
# skip searching a file entirely if it is above the given size (default = 1MB; unit = bytes)
# SYFT_FILE_CONTENTS_SKIP_FILES_ABOVE_SIZE env var
skip-files-above-size: 1048576
# file globs for the cataloger to match on
# SYFT_FILE_CONTENTS_GLOBS env var
globs: []
# cataloging file metadata is exposed through the power-user subcommand
file-metadata:
cataloger:
# enable/disable cataloging of file metadata
# SYFT_FILE_METADATA_CATALOGER_ENABLED env var
enabled: true
# the search space to look for file metadata (options: all-layers, squashed)
# SYFT_FILE_METADATA_CATALOGER_SCOPE env var
scope: "squashed"
# the file digest algorithms to use when cataloging files (options: "sha256", "md5", "sha1")
# SYFT_FILE_METADATA_DIGESTS env var
digests: ["sha256"]
# cataloging secrets is exposed through the power-user subcommand
secrets:
cataloger:
# enable/disable cataloging of secrets
# SYFT_SECRETS_CATALOGER_ENABLED env var
enabled: true
# the search space to look for secrets (options: all-layers, squashed)
# SYFT_SECRETS_CATALOGER_SCOPE env var
scope: "all-layers"
# show extracted secret values in the final JSON report
# SYFT_SECRETS_REVEAL_VALUES env var
reveal-values: false
# skip searching a file entirely if it is above the given size (default = 1MB; unit = bytes)
# SYFT_SECRETS_SKIP_FILES_ABOVE_SIZE env var
skip-files-above-size: 1048576
# name-regex pairs to consider when searching files for secrets. Note: the regex must match single line patterns
# but may also have OPTIONAL multiline capture groups. Regexes with a named capture group of "value" will
# use the entire regex to match, but the secret value will be assumed to be entirely contained within the
# "value" named capture group.
additional-patterns: {}
# names to exclude from the secrets search, valid values are: "aws-access-key", "aws-secret-key", "pem-private-key",
# "docker-config-auth", and "generic-api-key". Note: this does not consider any names introduced in the
# "secrets.additional-patterns" config option.
# SYFT_SECRETS_EXCLUDE_PATTERN_NAMES env var
exclude-pattern-names: []
# options when pulling directly from a registry via the "registry:" scheme
registry:
# skip TLS verification when communicating with the registry
# SYFT_REGISTRY_INSECURE_SKIP_TLS_VERIFY env var
insecure-skip-tls-verify: false
# use http instead of https when connecting to the registry
# SYFT_REGISTRY_INSECURE_USE_HTTP env var
insecure-use-http: false
# credentials for specific registries
auth:
# the URL to the registry (e.g. "docker.io", "localhost:5000", etc.)
# SYFT_REGISTRY_AUTH_AUTHORITY env var
- authority: ""
# SYFT_REGISTRY_AUTH_USERNAME env var
username: ""
# SYFT_REGISTRY_AUTH_PASSWORD env var
password: ""
# note: token and username/password are mutually exclusive
# SYFT_REGISTRY_AUTH_TOKEN env var
token: ""
# - ... # note, more credentials can be provided via config file only
# generate an attested SBOM
attest:
# path to the private key file to use for attestation
# SYFT_ATTEST_KEY env var
key: "cosign.key"
# password to decrypt to given private key
# SYFT_ATTEST_PASSWORD env var, additionally responds to COSIGN_PASSWORD
password: ""
log:
# use structured logging
# same as SYFT_LOG_STRUCTURED env var
structured: false
# the log level; note: detailed logging suppress the ETUI
# same as SYFT_LOG_LEVEL env var
level: "error"
# location to write the log file (default is not to have a log file)
# same as SYFT_LOG_FILE env var
file: ""
# enable/disable checking for application updates on startup
check-for-app-update: true
```
### Adding an SBOM to an image as an attestation using Syft
## Future plans
`syft attest --output [FORMAT] --key [KEY] [SOURCE] [flags]`
SBOMs themselves can serve as input to different analysis tools. [Grype](https://github.com/anchore/grype), a vulnerability scanner CLI tool from Anchore, is one such tool. Publishers of container images can use attestations to enable their consumers to trust Syft-generated SBOM descriptions of those container images. To create and provide these attestations, image publishers can run `syft attest` in conjunction with the [cosign](https://github.com/sigstore/cosign) tool to attach SBOM attestations to their images.
#### Example attestation
Note for the following example replace `docker.io/image:latest` with an image you own. You should also have push access to
its remote reference. Replace `$MY_PRIVATE_KEY` with a private key you own or have generated with cosign.
```bash
syft attest --key $MY_PRIVATE_KEY -o spdx-json docker.io/image:latest > image_latest_sbom_attestation.json
cosign attach attestation --attestation image_latest_sbom_attestation.json docker.io/image:latest
```
Verify the new attestation exists on your image.
```bash
cosign verify-attestation --key $MY_PUBLIC_KEY --type spdxjson docker.io/image:latest | jq '.payload | @base64d | .payload | fromjson | .predicate'
```
You should see this output along with the attached SBOM:
```
Verification for docker.io/image:latest --
The following checks were performed on each of these signatures:
- The cosign claims were validated
- The signatures were verified against the specified public key
- Any certificates were verified against the Fulcio roots.
```
Consumers of your image can now trust that the SBOM associated with your image is correct and from a trusted source.
The SBOM can be piped to Grype:
```bash
cosign verify-attestation --key $MY_PUBLIC_KEY --type spdxjson docker.io/image:latest | jq '.payload | @base64d | .payload | fromjson | .predicate' | grype
```
The following areas of potential development are currently being investigated:
- Add CycloneDX to list of output formats
- Establish a stable interchange format w/Grype
+6 -11
View File
@@ -87,18 +87,13 @@ This release process itself should be as automated as possible, and have only a
1. Tag the main branch with a full semantic-version, prefixed with a `v`. If there is a
milestone with a partial version, the full version should be used for the git tag (e.g.
with a Milestone of `v0.1` the tag should be `v0.1.0`). You can determine the changes going
into a release by running `make changelog-unreleased`. Use this change list to determine the
release increment. After determining the release increment (major, minor, patch), create the tag.
Given the above example the command to create the tag would be `git tag v0.1.0`.
with a Milestone of `v0.1` the tag should be `v0.1.0`).
1. Push the tag. Given the above example the command to push the tag would be `git push origin v0.1.0`.
1. A release admin must approve the release on the GitHub Actions release pipeline run page.
Once approved, the release pipeline will generate all assets and draft a GitHub Release.
1. Push the tag, the release pipeline will generate and publish all assets as well as a
draft GitHub Release.
1. Navigate to the GitHub Release draft page to review the final changelog and publish the
release. Once published, a release-follow-up pipeline will publish derivative artifacts
(docker image to DockerHub, brew formula to the external homebrew git repo, etc).
1. Promote the GitHub Release from draft to public. Note: since extra assets are made
available immediately from previous steps (i.e. the brew formula) the release should
only be in this state for a small amount of time (minutes).
1. If there is a release Milestone, close it.
-5
View File
@@ -1,5 +0,0 @@
# See documentation here: https://github.com/artifacthub/hub/blob/v1.6.0/docs/metadata/artifacthub-repo.yml
owners:
- name: wagoodman
email: wagoodman@gmail.com
+140
View File
@@ -0,0 +1,140 @@
package cmd
import (
"fmt"
"os"
"path"
"github.com/adrg/xdg"
"github.com/anchore/syft/internal"
"github.com/spf13/cobra"
"github.com/anchore/syft/syft/presenter"
"github.com/anchore/syft/syft/scope"
"github.com/anchore/stereoscope"
"github.com/anchore/syft/internal/config"
"github.com/anchore/syft/internal/format"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/logger"
"github.com/anchore/syft/syft"
"github.com/spf13/viper"
"github.com/wagoodman/go-partybus"
"gopkg.in/yaml.v2"
)
var appConfig *config.Application
var eventBus *partybus.Bus
var eventSubscription *partybus.Subscription
var cliOpts = config.CliOnlyOptions{}
func init() {
setGlobalCliOptions()
cobra.OnInitialize(
initAppConfig,
initLogging,
logAppConfig,
initEventBus,
)
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
}
func setGlobalCliOptions() {
rootCmd.PersistentFlags().StringVarP(&cliOpts.ConfigPath, "config", "c", "", "application config file")
// scan options
flag := "scope"
rootCmd.Flags().StringP(
"scope", "s", scope.SquashedScope.String(),
fmt.Sprintf("selection of layers to catalog, options=%v", scope.Options))
if err := viper.BindPFlag(flag, rootCmd.Flags().Lookup(flag)); err != nil {
fmt.Printf("unable to bind flag '%s': %+v", flag, err)
os.Exit(1)
}
// output & formatting options
flag = "output"
rootCmd.Flags().StringP(
flag, "o", string(presenter.TablePresenter),
fmt.Sprintf("report output formatter, options=%v", presenter.Options),
)
if err := viper.BindPFlag(flag, rootCmd.Flags().Lookup(flag)); err != nil {
fmt.Printf("unable to bind flag '%s': %+v", flag, err)
os.Exit(1)
}
flag = "quiet"
rootCmd.Flags().BoolP(
flag, "q", false,
"suppress all logging output",
)
if err := viper.BindPFlag(flag, rootCmd.Flags().Lookup(flag)); err != nil {
fmt.Printf("unable to bind flag '%s': %+v", flag, err)
os.Exit(1)
}
rootCmd.Flags().CountVarP(&cliOpts.Verbosity, "verbose", "v", "increase verbosity (-v = info, -vv = debug)")
// plugins
defaultPluginDir := path.Join(xdg.CacheHome, internal.ApplicationName, "plugins")
flag = "plugins.dir"
rootCmd.Flags().String(
flag, defaultPluginDir,
"plugin storage directory",
)
if err := viper.BindPFlag(flag, rootCmd.Flags().Lookup(flag)); err != nil {
fmt.Printf("unable to bind flag '%s': %+v", flag, err)
os.Exit(1)
}
}
func initAppConfig() {
cfg, err := config.LoadConfigFromFile(viper.GetViper(), &cliOpts)
if err != nil {
fmt.Printf("failed to load application config: \n\t%+v\n", err)
os.Exit(1)
}
appConfig = cfg
}
func initLogging() {
cfg := logger.LogrusConfig{
EnableConsole: (appConfig.Log.FileLocation == "" || appConfig.CliOptions.Verbosity > 0) && !appConfig.Quiet,
EnableFile: appConfig.Log.FileLocation != "",
Level: appConfig.Log.LevelOpt,
Structured: appConfig.Log.Structured,
FileLocation: appConfig.Log.FileLocation,
}
logWrapper := logger.NewLogrusLogger(cfg)
syft.SetLogger(logWrapper)
stereoscope.SetLogger(&logger.LogrusNestedLogger{
Logger: logWrapper.Logger.WithField("from-lib", "stereoscope"),
})
}
func logAppConfig() {
appCfgStr, err := yaml.Marshal(&appConfig)
if err != nil {
log.Debugf("Could not display application config: %+v", err)
} else {
log.Debugf("Application config:\n%+v", format.Magenta.Format(string(appCfgStr)))
}
}
func initEventBus() {
eventBus = partybus.NewBus()
eventSubscription = eventBus.Subscribe()
stereoscope.SetBus(eventBus)
syft.SetBus(eventBus)
}
+100
View File
@@ -0,0 +1,100 @@
package cmd
import (
"fmt"
"os"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/ui"
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/presenter"
"github.com/spf13/cobra"
"github.com/wagoodman/go-partybus"
)
var rootCmd = &cobra.Command{
Use: fmt.Sprintf("%s [SOURCE]", internal.ApplicationName),
Short: "A tool for generating a Software Bill Of Materials (SBOM) from container images and filesystems",
Long: internal.Tprintf(`
Supports the following image sources:
{{.appName}} yourrepo/yourimage:tag defaults to using images from a docker daemon
{{.appName}} docker://yourrepo/yourimage:tag explicitly use the docker daemon
{{.appName}} tar://path/to/yourimage.tar use a tarball from disk
{{.appName}} dir://path/to/yourproject read directly from a path in disk
`, map[string]interface{}{
"appName": internal.ApplicationName,
}),
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
if len(args) == 0 {
err := cmd.Help()
if err != nil {
log.Errorf(err.Error())
os.Exit(1)
}
os.Exit(1)
}
err := doRunCmd(cmd, args)
if err != nil {
log.Errorf(err.Error())
os.Exit(1)
}
},
}
func startWorker(userInput string) <-chan error {
errs := make(chan error)
go func() {
defer close(errs)
if appConfig.CheckForAppUpdate {
isAvailable, newVersion, err := version.IsUpdateAvailable()
if err != nil {
log.Errorf(err.Error())
}
if isAvailable {
log.Infof("new version of %s is available: %s", internal.ApplicationName, newVersion)
bus.Publish(partybus.Event{
Type: event.AppUpdateAvailable,
Value: newVersion,
})
} else {
log.Debugf("no new %s update available", internal.ApplicationName)
}
}
config := syft.Config{
PluginDirectory: appConfig.Plugins.Directory,
}
client, err := syft.NewClient(config)
if err != nil {
errs <- fmt.Errorf("could not initialize syft: %+v", err)
return
}
catalog, scope, distro, err := client.Catalog(userInput, appConfig.ScopeOpt)
if err != nil {
errs <- fmt.Errorf("failed to catalog input: %+v", err)
return
}
bus.Publish(partybus.Event{
Type: event.CatalogerFinished,
Value: presenter.GetPresenter(appConfig.PresenterOpt, *scope, catalog, distro),
})
}()
return errs
}
func doRunCmd(_ *cobra.Command, args []string) error {
userInput := args[0]
errs := startWorker(userInput)
ux := ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet)
return ux(errs, eventSubscription)
}
-80
View File
@@ -1,80 +0,0 @@
package cli
import (
"fmt"
"log"
sigopts "github.com/sigstore/cosign/cmd/cosign/cli/options"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/anchore/syft/cmd/syft/cli/attest"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/config"
)
const (
attestExample = ` {{.appName}} {{.command}} --output [FORMAT] --key [KEY] alpine:latest
Supports the following image sources:
{{.appName}} {{.command}} --key [KEY] yourrepo/yourimage:tag defaults to using images from a Docker daemon. If Docker is not present, the image is pulled directly from the registry.
{{.appName}} {{.command}} --key [KEY] path/to/a/file/or/dir only for OCI tar or OCI directory
`
attestSchemeHelp = "\n" + indent + schemeHelpHeader + "\n" + imageSchemeHelp
attestHelp = attestExample + attestSchemeHelp
)
func Attest(v *viper.Viper, app *config.Application, ro *options.RootOptions) *cobra.Command {
ao := options.AttestOptions{}
cmd := &cobra.Command{
Use: "attest --output [FORMAT] --key [KEY] [SOURCE]",
Short: "Generate a package SBOM as an attestation for the given [SOURCE] container image",
Long: "Generate a packaged-based Software Bill Of Materials (SBOM) from a container image as the predicate of an in-toto attestation",
Example: internal.Tprintf(attestHelp, map[string]interface{}{
"appName": internal.ApplicationName,
"command": "attest",
}),
Args: func(cmd *cobra.Command, args []string) error {
// run to unmarshal viper object onto app config
// the viper object correctly
if err := app.LoadAllValues(v, ro.Config); err != nil {
return fmt.Errorf("invalid application config: %w", err)
}
// configure logging for command
newLogWrapper(app)
logApplicationConfig(app)
return validateArgs(cmd, args)
},
SilenceUsage: true,
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
// this MUST be called first to make sure app config decodes
// the viper object correctly
if app.CheckForAppUpdate {
checkForApplicationUpdate()
}
// build cosign key options for attestation
ko := sigopts.KeyOpts{
KeyRef: app.Attest.KeyRef,
FulcioURL: app.Attest.FulcioURL,
IDToken: app.Attest.FulcioIdentityToken,
InsecureSkipFulcioVerify: app.Attest.InsecureSkipFulcioVerify,
RekorURL: app.Attest.RekorURL,
OIDCIssuer: app.Attest.OIDCIssuer,
OIDCClientID: app.Attest.OIDCClientID,
OIDCRedirectURL: app.Attest.OIDCRedirectURL,
}
return attest.Run(cmd.Context(), app, ko, args)
},
}
err := ao.AddFlags(cmd, v)
if err != nil {
log.Fatal(err)
}
return cmd
}
-398
View File
@@ -1,398 +0,0 @@
package attest
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/in-toto/in-toto-golang/in_toto"
sigopts "github.com/sigstore/cosign/cmd/cosign/cli/options"
"github.com/sigstore/cosign/cmd/cosign/cli/rekor"
"github.com/sigstore/cosign/cmd/cosign/cli/sign"
"github.com/sigstore/cosign/pkg/cosign"
"github.com/sigstore/cosign/pkg/cosign/attestation"
cbundle "github.com/sigstore/cosign/pkg/cosign/bundle"
"github.com/sigstore/cosign/pkg/oci/mutate"
ociremote "github.com/sigstore/cosign/pkg/oci/remote"
"github.com/sigstore/cosign/pkg/oci/static"
sigs "github.com/sigstore/cosign/pkg/signature"
"github.com/sigstore/cosign/pkg/types"
"github.com/sigstore/rekor/pkg/generated/client"
"github.com/sigstore/rekor/pkg/generated/models"
"github.com/sigstore/sigstore/pkg/signature/dsse"
signatureoptions "github.com/sigstore/sigstore/pkg/signature/options"
"github.com/wagoodman/go-partybus"
"github.com/wagoodman/go-progress"
"github.com/anchore/stereoscope"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/syft/cmd/syft/cli/eventloop"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/cmd/syft/cli/packages"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/config"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/ui"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/formats/cyclonedxjson"
"github.com/anchore/syft/syft/formats/spdxjson"
"github.com/anchore/syft/syft/formats/syftjson"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
var (
allowedAttestFormats = []sbom.FormatID{
syftjson.ID,
spdxjson.ID,
cyclonedxjson.ID,
}
intotoJSONDsseType = `application/vnd.in-toto+json`
)
func Run(ctx context.Context, app *config.Application, ko sigopts.KeyOpts, args []string) error {
// We cannot generate an attestation for more than one output
if len(app.Outputs) > 1 {
return fmt.Errorf("unable to generate attestation for more than one output")
}
// can only be an image for attestation or OCI DIR
userInput := args[0]
si, err := parseImageSource(userInput, app)
if err != nil {
return err
}
output := parseAttestationOutput(app.Outputs)
format := syft.FormatByName(output)
// user typo or unknown outputs provided
if format == nil {
format = syft.FormatByID(syftjson.ID) // default attestation format
}
predicateType := formatPredicateType(format)
if predicateType == "" {
return fmt.Errorf(
"could not produce attestation predicate for given format: %q. Available formats: %+v",
options.FormatAliases(format.ID()),
options.FormatAliases(allowedAttestFormats...),
)
}
if app.Attest.KeyRef != "" {
passFunc, err := selectPassFunc(app.Attest.KeyRef, app.Attest.Password)
if err != nil {
return err
}
ko.PassFunc = passFunc
}
sv, err := sign.SignerFromKeyOpts(ctx, "", "", ko)
if err != nil {
return err
}
defer sv.Close()
eventBus := partybus.NewBus()
stereoscope.SetBus(eventBus)
syft.SetBus(eventBus)
subscription := eventBus.Subscribe()
return eventloop.EventLoop(
execWorker(app, *si, format, predicateType, sv),
eventloop.SetupSignals(),
subscription,
stereoscope.Cleanup,
ui.Select(options.IsVerbose(app), app.Quiet)...,
)
}
func parseAttestationOutput(outputs []string) (format string) {
if len(outputs) == 0 {
outputs = append(outputs, string(syftjson.ID))
}
return outputs[0]
}
func parseImageSource(userInput string, app *config.Application) (s *source.Input, err error) {
si, err := source.ParseInputWithName(userInput, app.Platform, false, app.Name)
if err != nil {
return nil, fmt.Errorf("could not generate source input for attest command: %w", err)
}
switch si.Scheme {
case source.ImageScheme, source.UnknownScheme:
// at this point we know that it cannot be dir: or file: schemes;
// we will assume that the unknown scheme could represent an image;
si.Scheme = source.ImageScheme
default:
return nil, fmt.Errorf("attest command can only be used with image sources but discovered %q when given %q", si.Scheme, userInput)
}
// if the original detection was from the local daemon we want to short circuit
// that and attempt to generate the image source from its current registry source instead
switch si.ImageSource {
case image.UnknownSource, image.OciRegistrySource:
si.ImageSource = image.OciRegistrySource
case image.SingularitySource:
default:
return nil, fmt.Errorf("attest command can only be used with image sources fetch directly from the registry, but discovered an image source of %q when given %q", si.ImageSource, userInput)
}
return si, nil
}
func execWorker(app *config.Application, sourceInput source.Input, format sbom.Format, predicateType string, sv *sign.SignerVerifier) <-chan error {
errs := make(chan error)
go func() {
defer close(errs)
src, cleanup, err := source.NewFromRegistry(sourceInput, app.Registry.ToOptions(), app.Exclusions)
if cleanup != nil {
defer cleanup()
}
if err != nil {
errs <- fmt.Errorf("failed to construct source from user input %q: %w", sourceInput.UserInput, err)
return
}
s, err := packages.GenerateSBOM(src, errs, app)
if err != nil {
errs <- err
return
}
sbomBytes, err := syft.Encode(*s, format)
if err != nil {
errs <- err
return
}
signedPayload, err := generateAttestation(sourceInput, sbomBytes, src, sv, predicateType)
if err != nil {
errs <- err
return
}
err = publishAttestation(app, signedPayload, src, sv)
if err != nil {
errs <- err
return
}
bus.Publish(partybus.Event{
Type: event.Exit,
Value: func() error {
return nil
},
})
}()
return errs
}
func generateAttestation(si source.Input, predicate []byte, src *source.Source, sv *sign.SignerVerifier, predicateType string) ([]byte, error) {
var h v1.Hash
switch si.ImageSource {
case image.OciRegistrySource:
switch len(src.Image.Metadata.RepoDigests) {
case 0:
return nil, fmt.Errorf("cannot generate attestation since no repo digests were found; make sure you're passing an OCI registry source for the attest command")
case 1:
d, err := name.NewDigest(src.Image.Metadata.RepoDigests[0])
if err != nil {
return nil, err
}
h, err = v1.NewHash(d.Identifier())
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("cannot generate attestation since multiple repo digests were found for the image: %+v", src.Image.Metadata.RepoDigests)
}
case image.SingularitySource:
var err error
h, err = v1.NewHash(src.Image.Metadata.ID)
if err != nil {
return nil, err
}
}
sh, err := attestation.GenerateStatement(attestation.GenerateOpts{
Predicate: bytes.NewBuffer(predicate),
Type: predicateType,
Digest: h.Hex,
})
if err != nil {
return nil, err
}
payload, err := json.Marshal(sh)
if err != nil {
return nil, err
}
wrapped := dsse.WrapSigner(sv, intotoJSONDsseType)
return wrapped.SignMessage(bytes.NewReader(payload), signatureoptions.WithContext(context.Background()))
}
// publishAttestation publishes signedPayload to the location specified by the user.
func publishAttestation(app *config.Application, signedPayload []byte, src *source.Source, sv *sign.SignerVerifier) error {
switch {
// We want to give the option to not upload the generated attestation
// if passed or if the user is using local PKI
case app.Attest.NoUpload || app.Attest.KeyRef != "":
if app.File != "" {
return os.WriteFile(app.File, signedPayload, 0600)
}
_, err := os.Stdout.Write(signedPayload)
return err
default:
ref, err := name.ParseReference(src.Metadata.ImageMetadata.UserInput)
if err != nil {
return err
}
digest, err := ociremote.ResolveDigest(ref)
if err != nil {
return err
}
return uploadAttestation(app, signedPayload, digest, sv)
}
}
func trackUploadAttestation() (*progress.Stage, *progress.Manual) {
stage := &progress.Stage{}
prog := &progress.Manual{}
bus.Publish(partybus.Event{
Type: event.UploadAttestation,
Value: progress.StagedProgressable(&struct {
progress.Stager
progress.Progressable
}{
Stager: stage,
Progressable: prog,
}),
})
return stage, prog
}
// uploads signed SBOM payload to Rekor transparency log along with key information;
// returns a bundle for attestation annotations
// rekor bundle includes a signed payload and rekor timestamp;
// the bundle is then wrapped onto an OCI signed entity and uploaded to
// the user's image's OCI registry repository as *.att
func uploadAttestation(app *config.Application, signedPayload []byte, digest name.Digest, sv *sign.SignerVerifier) error {
// add application/vnd.dsse.envelope.v1+json as media type for other applications to decode attestation
opts := []static.Option{static.WithLayerMediaType(types.DssePayloadType)}
if sv.Cert != nil {
opts = append(opts, static.WithCertChain(sv.Cert, sv.Chain))
}
stage, prog := trackUploadAttestation()
defer prog.SetCompleted() // just in case we return early
prog.Total = 2
stage.Current = "uploading signing information to transparency log"
// uploads payload to Rekor transparency log along with key information;
// returns bundle for attesation annotations
// rekor bundle includes a signed payload and rekor timestamp;
// the bundle is then wrapped onto an OCI signed entity and uploaded to
// the user's image's OCI registry repository as *.att
bundle, err := uploadToTlog(context.TODO(), sv, app.Attest.RekorURL, func(r *client.Rekor, b []byte) (*models.LogEntryAnon, error) {
return cosign.TLogUploadInTotoAttestation(context.TODO(), r, signedPayload, b)
})
if err != nil {
return err
}
prog.N = 1
stage.Current = "uploading attestation to OCI registry"
// add bundle OCI attestation that is uploaded to
opts = append(opts, static.WithBundle(bundle))
sig, err := static.NewAttestation(signedPayload, opts...)
if err != nil {
return err
}
se, err := ociremote.SignedEntity(digest)
if err != nil {
return err
}
newSE, err := mutate.AttachAttestationToEntity(se, sig)
if err != nil {
return err
}
// Publish the attestations associated with this entity
err = ociremote.WriteAttestations(digest.Repository, newSE)
if err != nil {
return err
}
prog.SetCompleted()
return nil
}
func formatPredicateType(format sbom.Format) string {
switch format.ID() {
case spdxjson.ID:
return in_toto.PredicateSPDX
case cyclonedxjson.ID:
return in_toto.PredicateCycloneDX
case syftjson.ID:
return "https://syft.dev/bom"
default:
return ""
}
}
type tlogUploadFn func(*client.Rekor, []byte) (*models.LogEntryAnon, error)
func uploadToTlog(ctx context.Context, sv *sign.SignerVerifier, rekorURL string, upload tlogUploadFn) (*cbundle.RekorBundle, error) {
var rekorBytes []byte
// Upload the cert or the public key, depending on what we have
if sv.Cert != nil {
rekorBytes = sv.Cert
} else {
pemBytes, err := sigs.PublicKeyPem(sv, signatureoptions.WithContext(ctx))
if err != nil {
return nil, err
}
rekorBytes = pemBytes
}
rekorClient, err := rekor.NewClient(rekorURL)
if err != nil {
return nil, err
}
entry, err := upload(rekorClient, rekorBytes)
if err != nil {
return nil, err
}
if entry.LogIndex != nil {
log.Debugf("transparency log entry created with index: %v", *entry.LogIndex)
}
return cbundle.EntryToBundle(entry), nil
}
-57
View File
@@ -1,57 +0,0 @@
package attest
import (
"errors"
"fmt"
"io"
"os"
"strings"
"github.com/sigstore/cosign/pkg/cosign"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
)
func selectPassFunc(keypath, password string) (cosign.PassFunc, error) {
keyContents, err := os.ReadFile(keypath)
if err != nil {
return nil, err
}
var fn cosign.PassFunc = func(bool) (b []byte, err error) { return nil, nil }
_, err = cosign.LoadPrivateKey(keyContents, nil)
if err != nil {
fn = func(bool) (b []byte, err error) {
return fetchPassword(password)
}
}
return fn, nil
}
func fetchPassword(password string) (b []byte, err error) {
potentiallyPipedInput, err := internal.IsPipedInput()
if err != nil {
log.Warnf("unable to determine if there is piped input: %+v", err)
}
switch {
case password != "":
return []byte(password), nil
case potentiallyPipedInput:
// handle piped in passwords
pwBytes, err := io.ReadAll(os.Stdin)
if err != nil {
return nil, fmt.Errorf("unable to get password from stdin: %w", err)
}
// be resilient to input that may have newline characters (in case someone is using echo without -n)
cleanPw := strings.TrimRight(string(pwBytes), "\n")
return []byte(cleanPw), nil
case internal.IsTerminal():
return cosign.GetPassFromTerm(false)
}
return nil, errors.New("no method available to fetch password")
}
-169
View File
@@ -1,169 +0,0 @@
package cli
import (
"fmt"
"strings"
cranecmd "github.com/google/go-containerregistry/cmd/crane/cmd"
"github.com/gookit/color"
logrusUpstream "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/wagoodman/go-partybus"
"github.com/anchore/go-logger/adapter/logrus"
"github.com/anchore/stereoscope"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/config"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/event"
)
const indent = " "
// New constructs the `syft packages` command, aliases the root command to `syft packages`,
// and constructs the `syft power-user` and `syft attest` commands. It is also responsible for
// organizing flag usage and injecting the application config for each command.
// Because of how the `cobra` library behaves, the application's configuration is initialized
// at this level. Values from the config should only be used after `app.LoadAllValues` has been called.
// Cobra does not have knowledge of the user provided flags until the `RunE` block of each command.
// `RunE` is the earliest that the complete application configuration can be loaded.
func New() (*cobra.Command, error) {
app := &config.Application{}
// allow for nested options to be specified via environment variables
// e.g. pod.context = APPNAME_POD_CONTEXT
v := viper.NewWithOptions(viper.EnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")))
// since root is aliased as the packages cmd we need to construct this command first
// we also need the command to have information about the `root` options because of this alias
ro := &options.RootOptions{}
po := &options.PackagesOptions{}
packagesCmd := Packages(v, app, ro, po)
// root options are also passed to the attestCmd so that a user provided config location can be discovered
attestCmd := Attest(v, app, ro)
poweruserCmd := PowerUser(v, app, ro)
convertCmd := Convert(v, app, ro, po)
// rootCmd is currently an alias for the packages command
rootCmd := &cobra.Command{
Short: packagesCmd.Short,
Long: packagesCmd.Long,
Args: packagesCmd.Args,
Example: packagesCmd.Example,
SilenceUsage: true,
SilenceErrors: true,
RunE: packagesCmd.RunE,
Version: version.FromBuild().Version,
}
rootCmd.SetVersionTemplate(fmt.Sprintf("%s {{.Version}}\n", internal.ApplicationName))
// start adding flags to all the commands
err := ro.AddFlags(rootCmd, v)
if err != nil {
return nil, err
}
// package flags need to be decorated onto the rootCmd so that rootCmd can function as a packages alias
err = po.AddFlags(rootCmd, v)
if err != nil {
return nil, err
}
// attest also uses flags from the packagesCmd since it generates an sbom
err = po.AddFlags(attestCmd, v)
if err != nil {
return nil, err
}
// poweruser also uses the packagesCmd flags since it is a specialized version of the command
err = po.AddFlags(poweruserCmd, v)
if err != nil {
return nil, err
}
// commands to add to root
cmds := []*cobra.Command{
packagesCmd,
attestCmd,
convertCmd,
poweruserCmd,
poweruserCmd,
Completion(),
Version(v, app),
cranecmd.NewCmdAuthLogin("syft"),
}
// Add sub-commands.
for _, cmd := range cmds {
rootCmd.AddCommand(cmd)
}
return rootCmd, err
}
func validateArgs(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
// in the case that no arguments are given we want to show the help text and return with a non-0 return code.
if err := cmd.Help(); err != nil {
return fmt.Errorf("unable to display help: %w", err)
}
return fmt.Errorf("an image/directory argument is required")
}
return cobra.MaximumNArgs(1)(cmd, args)
}
func checkForApplicationUpdate() {
log.Debugf("checking if a new version of %s is available", internal.ApplicationName)
isAvailable, newVersion, err := version.IsUpdateAvailable()
if err != nil {
// this should never stop the application
log.Errorf(err.Error())
}
if isAvailable {
log.Infof("new version of %s is available: %s (current version is %s)", internal.ApplicationName, newVersion, version.FromBuild().Version)
bus.Publish(partybus.Event{
Type: event.AppUpdateAvailable,
Value: newVersion,
})
} else {
log.Debugf("no new %s update available", internal.ApplicationName)
}
}
func logApplicationConfig(app *config.Application) {
versionInfo := version.FromBuild()
log.Infof("%s version: %+v", internal.ApplicationName, versionInfo.Version)
log.Debugf("application config:\n%+v", color.Magenta.Sprint(app.String()))
}
func newLogWrapper(app *config.Application) {
cfg := logrus.Config{
EnableConsole: (app.Log.FileLocation == "" || app.Verbosity > 0) && !app.Quiet,
FileLocation: app.Log.FileLocation,
Level: app.Log.Level,
}
if app.Log.Structured {
cfg.Formatter = &logrusUpstream.JSONFormatter{
TimestampFormat: "2006-01-02 15:04:05",
DisableTimestamp: false,
DisableHTMLEscape: false,
PrettyPrint: false,
}
}
logWrapper, err := logrus.New(cfg)
if err != nil {
// this is kinda circular, but we can't return an error... ¯\_(ツ)_/¯
// I'm going to leave this here in case we one day have a different default logger other than the "discard" logger
log.Error("unable to initialize logger: %+v", err)
return
}
syft.SetLogger(logWrapper)
stereoscope.SetLogger(logWrapper.Nested("from-lib", "stereoscope"))
}
-54
View File
@@ -1,54 +0,0 @@
package cli
import (
"os"
"github.com/spf13/cobra"
)
func Completion() *cobra.Command {
cmd := &cobra.Command{
Use: "completion [bash|zsh|fish]",
Short: "Generate a shell completion for Syft (listing local docker images)",
Long: `To load completions (docker image list):
Bash:
$ source <(syft completion bash)
# To load completions for each session, execute once:
Linux:
$ syft completion bash > /etc/bash_completion.d/syft
MacOS:
$ syft completion bash > /usr/local/etc/bash_completion.d/syft
Zsh:
# If shell completion is not already enabled in your environment you will need
# to enable it. You can execute the following once:
$ echo "autoload -U compinit; compinit" >> ~/.zshrc
# To load completions for each session, execute once:
$ syft completion zsh > "${fpath[1]}/_syft"
# You will need to start a new shell for this setup to take effect.
Fish:
$ syft completion fish | source
# To load completions for each session, execute once:
$ syft completion fish > ~/.config/fish/completions/syft.fish
`,
DisableFlagsInUseLine: true,
ValidArgs: []string{"bash", "zsh", "fish"},
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
RunE: func(cmd *cobra.Command, args []string) error {
var err error
switch args[0] {
case "bash":
err = cmd.Root().GenBashCompletion(os.Stdout)
case "zsh":
err = cmd.Root().GenZshCompletion(os.Stdout)
case "fish":
err = cmd.Root().GenFishCompletion(os.Stdout, true)
}
if err != nil {
return err
}
return nil
},
}
return cmd
}
-56
View File
@@ -1,56 +0,0 @@
package cli
import (
"fmt"
"log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/anchore/syft/cmd/syft/cli/convert"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/config"
)
const (
convertExample = ` {{.appName}} {{.command}} img.syft.json -o spdx-json convert a syft SBOM to spdx-json, output goes to stdout in table format, by default
{{.appName}} {{.command}} img.syft.json -o cyclonedx-json=img.cdx.json convert a syft SBOM to CycloneDX, output goes to a file named img.cdx.json
`
)
//nolint:dupl
func Convert(v *viper.Viper, app *config.Application, ro *options.RootOptions, po *options.PackagesOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "convert [SOURCE-SBOM] -o [FORMAT]",
Short: "Convert between SBOM formats",
Long: "[Experimental] Convert SBOM files to, and from, SPDX, CycloneDX and Syft's format. For more info about data loss between formats see https://github.com/anchore/syft#format-conversion-experimental",
Example: internal.Tprintf(convertExample, map[string]interface{}{
"appName": internal.ApplicationName,
"command": "convert",
}),
Args: func(cmd *cobra.Command, args []string) error {
if err := app.LoadAllValues(v, ro.Config); err != nil {
return fmt.Errorf("invalid application config: %w", err)
}
newLogWrapper(app)
logApplicationConfig(app)
return validateArgs(cmd, args)
},
SilenceUsage: true,
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
if app.CheckForAppUpdate {
checkForApplicationUpdate()
}
return convert.Run(cmd.Context(), app, args)
},
}
err := po.AddFlags(cmd, v)
if err != nil {
log.Fatal(err)
}
return cmd
}
-41
View File
@@ -1,41 +0,0 @@
package convert
import (
"context"
"fmt"
"os"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/internal/config"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft"
)
func Run(ctx context.Context, app *config.Application, args []string) error {
log.Warn("convert is an experimental feature, run `syft convert -h` for help")
writer, err := options.MakeWriter(app.Outputs, app.File, "")
if err != nil {
return err
}
defer func() {
if err := writer.Close(); err != nil {
log.Warnf("unable to write to report destination: %w", err)
}
}()
// this can only be a SBOM file
userInput := args[0]
f, err := os.Open(userInput)
if err != nil {
return fmt.Errorf("failed to open SBOM file: %w", err)
}
defer f.Close()
sbom, _, err := syft.Decode(f)
if err != nil {
return fmt.Errorf("failed to decode SBOM: %w", err)
}
return writer.Write(*sbom)
}
-98
View File
@@ -1,98 +0,0 @@
package eventloop
import (
"errors"
"fmt"
"os"
"github.com/hashicorp/go-multierror"
"github.com/wagoodman/go-partybus"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/ui"
)
// eventLoop listens to worker errors (from execution path), worker events (from a partybus subscription), and
// signal interrupts. Is responsible for handling each event relative to a given UI an to coordinate eventing until
// an eventual graceful exit.
func EventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription *partybus.Subscription, cleanupFn func(), uxs ...ui.UI) error {
defer cleanupFn()
events := subscription.Events()
var err error
var ux ui.UI
if ux, err = setupUI(subscription.Unsubscribe, uxs...); err != nil {
return err
}
var retErr error
var forceTeardown bool
for {
if workerErrs == nil && events == nil {
break
}
select {
case err, isOpen := <-workerErrs:
if !isOpen {
workerErrs = nil
continue
}
if err != nil {
// capture the error from the worker and unsubscribe to complete a graceful shutdown
retErr = multierror.Append(retErr, err)
_ = subscription.Unsubscribe()
// the worker has exited, we may have been mid-handling events for the UI which should now be
// ignored, in which case forcing a teardown of the UI irregardless of the state is required.
forceTeardown = true
}
case e, isOpen := <-events:
if !isOpen {
events = nil
continue
}
if err := ux.Handle(e); err != nil {
if errors.Is(err, partybus.ErrUnsubscribe) {
events = nil
} else {
retErr = multierror.Append(retErr, err)
// TODO: should we unsubscribe? should we try to halt execution? or continue?
}
}
case <-signals:
// ignore further results from any event source and exit ASAP, but ensure that all cache is cleaned up.
// we ignore further errors since cleaning up the tmp directories will affect running catalogers that are
// reading/writing from/to their nested temp dirs. This is acceptable since we are bailing without result.
// TODO: potential future improvement would be to pass context into workers with a cancel function that is
// to the event loop. In this way we can have a more controlled shutdown even at the most nested levels
// of processing.
events = nil
workerErrs = nil
forceTeardown = true
}
}
if err := ux.Teardown(forceTeardown); err != nil {
retErr = multierror.Append(retErr, err)
}
return retErr
}
// setupUI takes one or more UIs that responds to events and takes a event bus unsubscribe function for use
// during teardown. With the given UIs, the first UI which the ui.Setup() function does not return an error
// will be utilized in execution. Providing a set of UIs allows for the caller to provide graceful fallbacks
// when there are environmental problem (e.g. unable to setup a TUI with the current TTY).
func setupUI(unsubscribe func() error, uis ...ui.UI) (ui.UI, error) {
for _, ux := range uis {
if err := ux.Setup(unsubscribe); err != nil {
log.Warnf("unable to setup given UI, falling back to alternative UI: %+v", err)
continue
}
return ux, nil
}
return nil, fmt.Errorf("unable to setup any UI")
}
-456
View File
@@ -1,456 +0,0 @@
package eventloop
import (
"fmt"
"os"
"syscall"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/wagoodman/go-partybus"
"github.com/anchore/syft/internal/ui"
"github.com/anchore/syft/syft/event"
)
var _ ui.UI = (*uiMock)(nil)
type uiMock struct {
t *testing.T
finalEvent partybus.Event
unsubscribe func() error
mock.Mock
}
func (u *uiMock) Setup(unsubscribe func() error) error {
u.t.Logf("UI Setup called")
u.unsubscribe = unsubscribe
return u.Called(unsubscribe).Error(0)
}
func (u *uiMock) Handle(event partybus.Event) error {
u.t.Logf("UI Handle called: %+v", event.Type)
if event == u.finalEvent {
assert.NoError(u.t, u.unsubscribe())
}
return u.Called(event).Error(0)
}
func (u *uiMock) Teardown(_ bool) error {
u.t.Logf("UI Teardown called")
return u.Called().Error(0)
}
func Test_EventLoop_gracefulExit(t *testing.T) {
test := func(t *testing.T) {
testBus := partybus.NewBus()
subscription := testBus.Subscribe()
t.Cleanup(testBus.Close)
finalEvent := partybus.Event{
Type: event.Exit,
}
worker := func() <-chan error {
ret := make(chan error)
go func() {
t.Log("worker running")
// send an empty item (which is ignored) ensuring we've entered the select statement,
// then close (a partial shutdown).
ret <- nil
t.Log("worker sent nothing")
close(ret)
t.Log("worker closed")
// do the other half of the shutdown
testBus.Publish(finalEvent)
t.Log("worker published final event")
}()
return ret
}
signaler := func() <-chan os.Signal {
return nil
}
ux := &uiMock{
t: t,
finalEvent: finalEvent,
}
// ensure the mock sees at least the final event
ux.On("Handle", finalEvent).Return(nil)
// ensure the mock sees basic setup/teardown events
ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
ux.On("Teardown").Return(nil)
var cleanupCalled bool
cleanupFn := func() {
t.Log("cleanup called")
cleanupCalled = true
}
assert.NoError(t,
EventLoop(
worker(),
signaler(),
subscription,
cleanupFn,
ux,
),
)
assert.True(t, cleanupCalled, "cleanup function not called")
ux.AssertExpectations(t)
}
// if there is a bug, then there is a risk of the event loop never returning
testWithTimeout(t, 5*time.Second, test)
}
func Test_EventLoop_workerError(t *testing.T) {
test := func(t *testing.T) {
testBus := partybus.NewBus()
subscription := testBus.Subscribe()
t.Cleanup(testBus.Close)
workerErr := fmt.Errorf("worker error")
worker := func() <-chan error {
ret := make(chan error)
go func() {
t.Log("worker running")
// send an empty item (which is ignored) ensuring we've entered the select statement,
// then close (a partial shutdown).
ret <- nil
t.Log("worker sent nothing")
ret <- workerErr
t.Log("worker sent error")
close(ret)
t.Log("worker closed")
// note: NO final event is fired
}()
return ret
}
signaler := func() <-chan os.Signal {
return nil
}
ux := &uiMock{
t: t,
}
// ensure the mock sees basic setup/teardown events
ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
ux.On("Teardown").Return(nil)
var cleanupCalled bool
cleanupFn := func() {
t.Log("cleanup called")
cleanupCalled = true
}
// ensure we see an error returned
assert.ErrorIs(t,
EventLoop(
worker(),
signaler(),
subscription,
cleanupFn,
ux,
),
workerErr,
"should have seen a worker error, but did not",
)
assert.True(t, cleanupCalled, "cleanup function not called")
ux.AssertExpectations(t)
}
// if there is a bug, then there is a risk of the event loop never returning
testWithTimeout(t, 5*time.Second, test)
}
func Test_EventLoop_unsubscribeError(t *testing.T) {
test := func(t *testing.T) {
testBus := partybus.NewBus()
subscription := testBus.Subscribe()
t.Cleanup(testBus.Close)
finalEvent := partybus.Event{
Type: event.Exit,
}
worker := func() <-chan error {
ret := make(chan error)
go func() {
t.Log("worker running")
// send an empty item (which is ignored) ensuring we've entered the select statement,
// then close (a partial shutdown).
ret <- nil
t.Log("worker sent nothing")
close(ret)
t.Log("worker closed")
// do the other half of the shutdown
testBus.Publish(finalEvent)
t.Log("worker published final event")
}()
return ret
}
signaler := func() <-chan os.Signal {
return nil
}
ux := &uiMock{
t: t,
finalEvent: finalEvent,
}
// ensure the mock sees at least the final event... note the unsubscribe error here
ux.On("Handle", finalEvent).Return(partybus.ErrUnsubscribe)
// ensure the mock sees basic setup/teardown events
ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
ux.On("Teardown").Return(nil)
var cleanupCalled bool
cleanupFn := func() {
t.Log("cleanup called")
cleanupCalled = true
}
// unsubscribe errors should be handled and ignored, not propagated. We are additionally asserting that
// this case is handled as a controlled shutdown (this test should not timeout)
assert.NoError(t,
EventLoop(
worker(),
signaler(),
subscription,
cleanupFn,
ux,
),
)
assert.True(t, cleanupCalled, "cleanup function not called")
ux.AssertExpectations(t)
}
// if there is a bug, then there is a risk of the event loop never returning
testWithTimeout(t, 5*time.Second, test)
}
func Test_EventLoop_handlerError(t *testing.T) {
test := func(t *testing.T) {
testBus := partybus.NewBus()
subscription := testBus.Subscribe()
t.Cleanup(testBus.Close)
finalEvent := partybus.Event{
Type: event.Exit,
Error: fmt.Errorf("an exit error occured"),
}
worker := func() <-chan error {
ret := make(chan error)
go func() {
t.Log("worker running")
// send an empty item (which is ignored) ensuring we've entered the select statement,
// then close (a partial shutdown).
ret <- nil
t.Log("worker sent nothing")
close(ret)
t.Log("worker closed")
// do the other half of the shutdown
testBus.Publish(finalEvent)
t.Log("worker published final event")
}()
return ret
}
signaler := func() <-chan os.Signal {
return nil
}
ux := &uiMock{
t: t,
finalEvent: finalEvent,
}
// ensure the mock sees at least the final event... note the event error is propagated
ux.On("Handle", finalEvent).Return(finalEvent.Error)
// ensure the mock sees basic setup/teardown events
ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
ux.On("Teardown").Return(nil)
var cleanupCalled bool
cleanupFn := func() {
t.Log("cleanup called")
cleanupCalled = true
}
// handle errors SHOULD propagate the event loop. We are additionally asserting that this case is
// handled as a controlled shutdown (this test should not timeout)
assert.ErrorIs(t,
EventLoop(
worker(),
signaler(),
subscription,
cleanupFn,
ux,
),
finalEvent.Error,
"should have seen a event error, but did not",
)
assert.True(t, cleanupCalled, "cleanup function not called")
ux.AssertExpectations(t)
}
// if there is a bug, then there is a risk of the event loop never returning
testWithTimeout(t, 5*time.Second, test)
}
func Test_EventLoop_signalsStopExecution(t *testing.T) {
test := func(t *testing.T) {
testBus := partybus.NewBus()
subscription := testBus.Subscribe()
t.Cleanup(testBus.Close)
worker := func() <-chan error {
// the worker will never return work and the event loop will always be waiting...
return make(chan error)
}
signaler := func() <-chan os.Signal {
ret := make(chan os.Signal)
go func() {
ret <- syscall.SIGINT
// note: we do NOT close the channel to ensure the event loop does not depend on that behavior to exit
}()
return ret
}
ux := &uiMock{
t: t,
}
// ensure the mock sees basic setup/teardown events
ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
ux.On("Teardown").Return(nil)
var cleanupCalled bool
cleanupFn := func() {
t.Log("cleanup called")
cleanupCalled = true
}
assert.NoError(t,
EventLoop(
worker(),
signaler(),
subscription,
cleanupFn,
ux,
),
)
assert.True(t, cleanupCalled, "cleanup function not called")
ux.AssertExpectations(t)
}
// if there is a bug, then there is a risk of the event loop never returning
testWithTimeout(t, 5*time.Second, test)
}
func Test_EventLoop_uiTeardownError(t *testing.T) {
test := func(t *testing.T) {
testBus := partybus.NewBus()
subscription := testBus.Subscribe()
t.Cleanup(testBus.Close)
finalEvent := partybus.Event{
Type: event.Exit,
}
worker := func() <-chan error {
ret := make(chan error)
go func() {
t.Log("worker running")
// send an empty item (which is ignored) ensuring we've entered the select statement,
// then close (a partial shutdown).
ret <- nil
t.Log("worker sent nothing")
close(ret)
t.Log("worker closed")
// do the other half of the shutdown
testBus.Publish(finalEvent)
t.Log("worker published final event")
}()
return ret
}
signaler := func() <-chan os.Signal {
return nil
}
ux := &uiMock{
t: t,
finalEvent: finalEvent,
}
teardownError := fmt.Errorf("sorry, dave, the UI doesn't want to be torn down")
// ensure the mock sees at least the final event... note the event error is propagated
ux.On("Handle", finalEvent).Return(nil)
// ensure the mock sees basic setup/teardown events
ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
ux.On("Teardown").Return(teardownError)
var cleanupCalled bool
cleanupFn := func() {
t.Log("cleanup called")
cleanupCalled = true
}
// ensure we see an error returned
assert.ErrorIs(t,
EventLoop(
worker(),
signaler(),
subscription,
cleanupFn,
ux,
),
teardownError,
"should have seen a UI teardown error, but did not",
)
assert.True(t, cleanupCalled, "cleanup function not called")
ux.AssertExpectations(t)
}
// if there is a bug, then there is a risk of the event loop never returning
testWithTimeout(t, 5*time.Second, test)
}
func testWithTimeout(t *testing.T, timeout time.Duration, test func(*testing.T)) {
done := make(chan bool)
go func() {
test(t)
done <- true
}()
select {
case <-time.After(timeout):
t.Fatal("test timed out")
case <-done:
}
}
-20
View File
@@ -1,20 +0,0 @@
package eventloop
import (
"os"
"os/signal"
"syscall"
)
func SetupSignals() <-chan os.Signal {
c := make(chan os.Signal, 1) // Note: A buffered channel is recommended for this; see https://golang.org/pkg/os/signal/#Notify
interruptions := []os.Signal{
syscall.SIGINT,
syscall.SIGTERM,
}
signal.Notify(c, interruptions...)
return c
}
-203
View File
@@ -1,203 +0,0 @@
package eventloop
import (
"crypto"
"fmt"
"github.com/anchore/syft/internal/config"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
type Task func(*sbom.Artifacts, *source.Source) ([]artifact.Relationship, error)
func Tasks(app *config.Application) ([]Task, error) {
var tasks []Task
generators := []func(app *config.Application) (Task, error){
generateCatalogPackagesTask,
generateCatalogFileMetadataTask,
generateCatalogFileDigestsTask,
generateCatalogSecretsTask,
generateCatalogContentsTask,
}
for _, generator := range generators {
task, err := generator(app)
if err != nil {
return nil, err
}
if task != nil {
tasks = append(tasks, task)
}
}
return tasks, nil
}
func generateCatalogPackagesTask(app *config.Application) (Task, error) {
if !app.Package.Cataloger.Enabled {
return nil, nil
}
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
packageCatalog, relationships, theDistro, err := syft.CatalogPackages(src, app.ToCatalogerConfig())
if err != nil {
return nil, err
}
results.PackageCatalog = packageCatalog
results.LinuxDistribution = theDistro
return relationships, nil
}
return task, nil
}
func generateCatalogFileMetadataTask(app *config.Application) (Task, error) {
if !app.FileMetadata.Cataloger.Enabled {
return nil, nil
}
metadataCataloger := file.NewMetadataCataloger()
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
resolver, err := src.FileResolver(app.FileMetadata.Cataloger.ScopeOpt)
if err != nil {
return nil, err
}
result, err := metadataCataloger.Catalog(resolver)
if err != nil {
return nil, err
}
results.FileMetadata = result
return nil, nil
}
return task, nil
}
func generateCatalogFileDigestsTask(app *config.Application) (Task, error) {
if !app.FileMetadata.Cataloger.Enabled {
return nil, nil
}
supportedHashAlgorithms := make(map[string]crypto.Hash)
for _, h := range []crypto.Hash{
crypto.MD5,
crypto.SHA1,
crypto.SHA256,
} {
supportedHashAlgorithms[file.DigestAlgorithmName(h)] = h
}
var hashes []crypto.Hash
for _, hashStr := range app.FileMetadata.Digests {
name := file.CleanDigestAlgorithmName(hashStr)
hashObj, ok := supportedHashAlgorithms[name]
if !ok {
return nil, fmt.Errorf("unsupported hash algorithm: %s", hashStr)
}
hashes = append(hashes, hashObj)
}
digestsCataloger, err := file.NewDigestsCataloger(hashes)
if err != nil {
return nil, err
}
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
resolver, err := src.FileResolver(app.FileMetadata.Cataloger.ScopeOpt)
if err != nil {
return nil, err
}
result, err := digestsCataloger.Catalog(resolver)
if err != nil {
return nil, err
}
results.FileDigests = result
return nil, nil
}
return task, nil
}
func generateCatalogSecretsTask(app *config.Application) (Task, error) {
if !app.Secrets.Cataloger.Enabled {
return nil, nil
}
patterns, err := file.GenerateSearchPatterns(file.DefaultSecretsPatterns, app.Secrets.AdditionalPatterns, app.Secrets.ExcludePatternNames)
if err != nil {
return nil, err
}
secretsCataloger, err := file.NewSecretsCataloger(patterns, app.Secrets.RevealValues, app.Secrets.SkipFilesAboveSize)
if err != nil {
return nil, err
}
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
resolver, err := src.FileResolver(app.Secrets.Cataloger.ScopeOpt)
if err != nil {
return nil, err
}
result, err := secretsCataloger.Catalog(resolver)
if err != nil {
return nil, err
}
results.Secrets = result
return nil, nil
}
return task, nil
}
func generateCatalogContentsTask(app *config.Application) (Task, error) {
if !app.FileContents.Cataloger.Enabled {
return nil, nil
}
contentsCataloger, err := file.NewContentsCataloger(app.FileContents.Globs, app.FileContents.SkipFilesAboveSize)
if err != nil {
return nil, err
}
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
resolver, err := src.FileResolver(app.FileContents.Cataloger.ScopeOpt)
if err != nil {
return nil, err
}
result, err := contentsCataloger.Catalog(resolver)
if err != nil {
return nil, err
}
results.FileContents = result
return nil, nil
}
return task, nil
}
func RunTask(t Task, a *sbom.Artifacts, src *source.Source, c chan<- artifact.Relationship, errs chan<- error) {
defer close(c)
relationships, err := t(a, src)
if err != nil {
errs <- err
return
}
for _, relationship := range relationships {
c <- relationship
}
}
-77
View File
@@ -1,77 +0,0 @@
package options
import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
const defaultKeyFileName = "cosign.key"
type AttestOptions struct {
Key string
Cert string
CertChain string
NoUpload bool
Force bool
Recursive bool
Rekor RekorOptions
Fulcio FulcioOptions
OIDC OIDCOptions
}
var _ Interface = (*AttestOptions)(nil)
func (o *AttestOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error {
if err := o.Rekor.AddFlags(cmd, v); err != nil {
return err
}
if err := o.Fulcio.AddFlags(cmd, v); err != nil {
return err
}
if err := o.OIDC.AddFlags(cmd, v); err != nil {
return err
}
cmd.Flags().StringVarP(&o.Key, "key", "", defaultKeyFileName,
"path to the private key file to use for attestation")
cmd.Flags().StringVarP(&o.Cert, "cert", "", "",
"path to the x.509 certificate in PEM format to include in the OCI Signature")
cmd.Flags().BoolVarP(&o.NoUpload, "no-upload", "", false,
"do not upload the generated attestation")
cmd.Flags().BoolVarP(&o.Force, "force", "", false,
"skip warnings and confirmations")
cmd.Flags().BoolVarP(&o.Recursive, "recursive", "", false,
"if a multi-arch image is specified, additionally sign each discrete image")
return bindAttestationConfigOptions(cmd.Flags(), v)
}
func bindAttestationConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error {
if err := v.BindPFlag("attest.key", flags.Lookup("key")); err != nil {
return err
}
if err := v.BindPFlag("attest.cert", flags.Lookup("cert")); err != nil {
return err
}
if err := v.BindPFlag("attest.no-upload", flags.Lookup("no-upload")); err != nil {
return err
}
if err := v.BindPFlag("attest.force", flags.Lookup("force")); err != nil {
return err
}
if err := v.BindPFlag("attest.recursive", flags.Lookup("recursive")); err != nil {
return err
}
return nil
}
-39
View File
@@ -1,39 +0,0 @@
package options
import (
"github.com/anchore/syft/syft/formats/cyclonedxjson"
"github.com/anchore/syft/syft/formats/cyclonedxxml"
"github.com/anchore/syft/syft/formats/github"
"github.com/anchore/syft/syft/formats/spdxjson"
"github.com/anchore/syft/syft/formats/spdxtagvalue"
"github.com/anchore/syft/syft/formats/syftjson"
"github.com/anchore/syft/syft/formats/table"
"github.com/anchore/syft/syft/formats/text"
"github.com/anchore/syft/syft/sbom"
)
func FormatAliases(ids ...sbom.FormatID) (aliases []string) {
for _, id := range ids {
switch id {
case syftjson.ID:
aliases = append(aliases, "syft-json")
case text.ID:
aliases = append(aliases, "text")
case table.ID:
aliases = append(aliases, "table")
case spdxjson.ID:
aliases = append(aliases, "spdx-json")
case spdxtagvalue.ID:
aliases = append(aliases, "spdx-tag-value")
case cyclonedxxml.ID:
aliases = append(aliases, "cyclonedx-xml")
case cyclonedxjson.ID:
aliases = append(aliases, "cyclonedx-json")
case github.ID:
aliases = append(aliases, "github", "github-json")
default:
aliases = append(aliases, string(id))
}
}
return aliases
}
-48
View File
@@ -1,48 +0,0 @@
package options
import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
const defaultFulcioURL = "https://fulcio.sigstore.dev"
// FulcioOptions is the wrapper for Fulcio related options.
type FulcioOptions struct {
URL string
IdentityToken string
InsecureSkipFulcioVerify bool
}
var _ Interface = (*FulcioOptions)(nil)
// AddFlags implements Interface
func (o *FulcioOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error {
// TODO: change this back to api.SigstorePublicServerURL after the v1 migration is complete.
cmd.Flags().StringVar(&o.URL, "fulcio-url", defaultFulcioURL,
"address of sigstore PKI server")
cmd.Flags().StringVar(&o.IdentityToken, "identity-token", "",
"identity token to use for certificate from fulcio")
cmd.Flags().BoolVar(&o.InsecureSkipFulcioVerify, "insecure-skip-verify", false,
"skip verifying fulcio certificat and the SCT (Signed Certificate Timestamp) (this should only be used for testing).")
return bindFulcioConfigOptions(cmd.Flags(), v)
}
func bindFulcioConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error {
if err := v.BindPFlag("attest.fulcio-url", flags.Lookup("fulcio-url")); err != nil {
return err
}
if err := v.BindPFlag("attest.fulcio-identity-token", flags.Lookup("identity-token")); err != nil {
return err
}
if err := v.BindPFlag("attest.insecure-skip-verify", flags.Lookup("insecure-skip-verify")); err != nil {
return err
}
return nil
}
-48
View File
@@ -1,48 +0,0 @@
package options
import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
const DefaultOIDCIssuerURL = "https://oauth2.sigstore.dev/auth"
// OIDCOptions is the wrapper for OIDC related options.
type OIDCOptions struct {
Issuer string
ClientID string
RedirectURL string
}
var _ Interface = (*OIDCOptions)(nil)
// AddFlags implements Interface
func (o *OIDCOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error {
cmd.Flags().StringVar(&o.Issuer, "oidc-issuer", DefaultOIDCIssuerURL,
"OIDC provider to be used to issue ID token")
cmd.Flags().StringVar(&o.ClientID, "oidc-client-id", "sigstore",
"OIDC client ID for application")
cmd.Flags().StringVar(&o.RedirectURL, "oidc-redirect-url", "",
"OIDC redirect URL (Optional)")
return bindOIDCConfigOptions(cmd.Flags(), v)
}
func bindOIDCConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error {
if err := v.BindPFlag("attest.oidc-issuer", flags.Lookup("oidc-issuer")); err != nil {
return err
}
if err := v.BindPFlag("attest.oidc-client-id", flags.Lookup("oidc-client-id")); err != nil {
return err
}
if err := v.BindPFlag("attest.oidc-redirect-url", flags.Lookup("oidc-redirect-url")); err != nil {
return err
}
return nil
}
-11
View File
@@ -1,11 +0,0 @@
package options
import (
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
type Interface interface {
// AddFlags adds this options' flags to the cobra command.
AddFlags(cmd *cobra.Command, v *viper.Viper) error
}
-93
View File
@@ -1,93 +0,0 @@
package options
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/formats/table"
"github.com/anchore/syft/syft/pkg/cataloger"
"github.com/anchore/syft/syft/source"
)
type PackagesOptions struct {
Scope string
Output []string
OutputTemplatePath string
File string
Platform string
Exclude []string
Catalogers []string
Name string
}
var _ Interface = (*PackagesOptions)(nil)
func (o *PackagesOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error {
cmd.Flags().StringVarP(&o.Scope, "scope", "s", cataloger.DefaultSearchConfig().Scope.String(),
fmt.Sprintf("selection of layers to catalog, options=%v", source.AllScopes))
cmd.Flags().StringArrayVarP(&o.Output, "output", "o", FormatAliases(table.ID),
fmt.Sprintf("report output format, options=%v", FormatAliases(syft.FormatIDs()...)))
cmd.Flags().StringVarP(&o.File, "file", "", "",
"file to write the default report output to (default is STDOUT)")
cmd.Flags().StringVarP(&o.OutputTemplatePath, "template", "t", "",
"specify the path to a Go template file")
cmd.Flags().StringVarP(&o.Platform, "platform", "", "",
"an optional platform specifier for container image sources (e.g. 'linux/arm64', 'linux/arm64/v8', 'arm64', 'linux')")
cmd.Flags().StringArrayVarP(&o.Exclude, "exclude", "", nil,
"exclude paths from being scanned using a glob expression")
cmd.Flags().StringArrayVarP(&o.Catalogers, "catalogers", "", nil,
"enable one or more package catalogers")
cmd.Flags().StringVarP(&o.Name, "name", "", "",
"set the name of the target being analyzed")
return bindPackageConfigOptions(cmd.Flags(), v)
}
func bindPackageConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error {
// Formatting & Input options //////////////////////////////////////////////
if err := v.BindPFlag("package.cataloger.scope", flags.Lookup("scope")); err != nil {
return err
}
if err := v.BindPFlag("file", flags.Lookup("file")); err != nil {
return err
}
if err := v.BindPFlag("exclude", flags.Lookup("exclude")); err != nil {
return err
}
if err := v.BindPFlag("catalogers", flags.Lookup("catalogers")); err != nil {
return err
}
if err := v.BindPFlag("name", flags.Lookup("name")); err != nil {
return err
}
if err := v.BindPFlag("output", flags.Lookup("output")); err != nil {
return err
}
if err := v.BindPFlag("output-template-path", flags.Lookup("template")); err != nil {
return err
}
if err := v.BindPFlag("platform", flags.Lookup("platform")); err != nil {
return err
}
return nil
}
-32
View File
@@ -1,32 +0,0 @@
package options
import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
const DefaultRekorURL = "https://rekor.sigstore.dev"
// RekorOptions is the wrapper for Rekor related options.
type RekorOptions struct {
URL string
}
var _ Interface = (*RekorOptions)(nil)
// AddFlags implements Interface
func (o *RekorOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error {
cmd.Flags().StringVar(&o.URL, "rekor-url", DefaultRekorURL,
"address of rekor STL server")
return bindRekorConfigOptions(cmd.Flags(), v)
}
func bindRekorConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error {
// TODO: config re-design
if err := v.BindPFlag("attest.rekor-url", flags.Lookup("rekor-url")); err != nil {
return err
}
return nil
}
-36
View File
@@ -1,36 +0,0 @@
package options
import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
type RootOptions struct {
Config string
Quiet bool
Verbose int
}
var _ Interface = (*RootOptions)(nil)
func (o *RootOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error {
cmd.PersistentFlags().StringVarP(&o.Config, "config", "c", "", "application config file")
cmd.PersistentFlags().CountVarP(&o.Verbose, "verbose", "v", "increase verbosity (-v = info, -vv = debug)")
cmd.PersistentFlags().BoolVarP(&o.Quiet, "quiet", "q", false, "suppress all logging output")
return bindRootConfigOptions(cmd.PersistentFlags(), v)
}
func bindRootConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error {
if err := v.BindPFlag("config", flags.Lookup("config")); err != nil {
return err
}
if err := v.BindPFlag("verbosity", flags.Lookup("verbose")); err != nil {
return err
}
if err := v.BindPFlag("quiet", flags.Lookup("quiet")); err != nil {
return err
}
return nil
}
-18
View File
@@ -1,18 +0,0 @@
package options
import (
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/config"
"github.com/anchore/syft/internal/log"
)
func IsVerbose(app *config.Application) (result bool) {
isPipedInput, err := internal.IsPipedInput()
if err != nil {
// since we can't tell if there was piped input we assume that there could be to disable the ETUI
log.Warnf("unable to determine if there is piped input: %+v", err)
return true
}
// verbosity should consider if there is piped input (in which case we should not show the ETUI)
return app.Verbosity > 0 || isPipedInput
}
-17
View File
@@ -1,17 +0,0 @@
package options
import (
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
type VersionOptions struct {
Output string
}
var _ Interface = (*PackagesOptions)(nil)
func (o *VersionOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error {
cmd.Flags().StringVarP(&o.Output, "output", "o", "text", "format to show version information (available=[text, json])")
return nil
}
-69
View File
@@ -1,69 +0,0 @@
package options
import (
"fmt"
"strings"
"github.com/hashicorp/go-multierror"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/formats/table"
"github.com/anchore/syft/syft/formats/template"
"github.com/anchore/syft/syft/sbom"
)
// makeWriter creates a sbom.Writer for output or returns an error. this will either return a valid writer
// or an error but neither both and if there is no error, sbom.Writer.Close() should be called
func MakeWriter(outputs []string, defaultFile, templateFilePath string) (sbom.Writer, error) {
outputOptions, err := parseOutputs(outputs, defaultFile, templateFilePath)
if err != nil {
return nil, err
}
writer, err := sbom.NewWriter(outputOptions...)
if err != nil {
return nil, err
}
return writer, nil
}
// parseOptions utility to parse command-line option strings and retain the existing behavior of default format and file
func parseOutputs(outputs []string, defaultFile, templateFilePath string) (out []sbom.WriterOption, errs error) {
// always should have one option -- we generally get the default of "table", but just make sure
if len(outputs) == 0 {
outputs = append(outputs, string(table.ID))
}
for _, name := range outputs {
name = strings.TrimSpace(name)
// split to at most two parts for <format>=<file>
parts := strings.SplitN(name, "=", 2)
// the format name is the first part
name = parts[0]
// default to the --file or empty string if not specified
file := defaultFile
// If a file is specified as part of the output formatName, use that
if len(parts) > 1 {
file = parts[1]
}
format := syft.FormatByName(name)
if format == nil {
errs = multierror.Append(errs, fmt.Errorf(`unsupported output format "%s", supported formats are: %+v`, name, FormatAliases(syft.FormatIDs()...)))
continue
}
if tmpl, ok := format.(template.OutputFormat); ok {
tmpl.SetTemplatePath(templateFilePath)
format = tmpl
}
out = append(out, sbom.NewWriterOption(format, file))
}
return out, errs
}
-34
View File
@@ -1,34 +0,0 @@
package options
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestIsSupportedFormat(t *testing.T) {
tests := []struct {
outputs []string
wantErr assert.ErrorAssertionFunc
}{
{
outputs: []string{"json"},
wantErr: assert.NoError,
},
{
outputs: []string{"table", "json"},
wantErr: assert.NoError,
},
{
outputs: []string{"unknown"},
wantErr: func(t assert.TestingT, err error, bla ...interface{}) bool {
return assert.ErrorContains(t, err, `unsupported output format "unknown", supported formats are: [`)
},
},
}
for _, tt := range tests {
_, err := MakeWriter(tt.outputs, "", "")
tt.wantErr(t, err)
}
}
-83
View File
@@ -1,83 +0,0 @@
package cli
import (
"fmt"
"log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/cmd/syft/cli/packages"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/config"
)
const (
packagesExample = ` {{.appName}} {{.command}} alpine:latest a summary of discovered packages
{{.appName}} {{.command}} alpine:latest -o json show all possible cataloging details
{{.appName}} {{.command}} alpine:latest -o cyclonedx show a CycloneDX formatted SBOM
{{.appName}} {{.command}} alpine:latest -o cyclonedx-json show a CycloneDX JSON formatted SBOM
{{.appName}} {{.command}} alpine:latest -o spdx show a SPDX 2.2 Tag-Value formatted SBOM
{{.appName}} {{.command}} alpine:latest -o spdx-json show a SPDX 2.2 JSON formatted SBOM
{{.appName}} {{.command}} alpine:latest -vv show verbose debug information
{{.appName}} {{.command}} alpine:latest -o template -t my_format.tmpl show a SBOM formatted according to given template file
Supports the following image sources:
{{.appName}} {{.command}} yourrepo/yourimage:tag defaults to using images from a Docker daemon. If Docker is not present, the image is pulled directly from the registry.
{{.appName}} {{.command}} path/to/a/file/or/dir a Docker tar, OCI tar, OCI directory, SIF container, or generic filesystem directory
`
schemeHelpHeader = "You can also explicitly specify the scheme to use:"
imageSchemeHelp = ` {{.appName}} {{.command}} docker:yourrepo/yourimage:tag explicitly use the Docker daemon
{{.appName}} {{.command}} podman:yourrepo/yourimage:tag explicitly use the Podman daemon
{{.appName}} {{.command}} registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required)
{{.appName}} {{.command}} docker-archive:path/to/yourimage.tar use a tarball from disk for archives created from "docker save"
{{.appName}} {{.command}} oci-archive:path/to/yourimage.tar use a tarball from disk for OCI archives (from Skopeo or otherwise)
{{.appName}} {{.command}} oci-dir:path/to/yourimage read directly from a path on disk for OCI layout directories (from Skopeo or otherwise)
{{.appName}} {{.command}} singularity:path/to/yourimage.sif read directly from a Singularity Image Format (SIF) container on disk
`
nonImageSchemeHelp = ` {{.appName}} {{.command}} dir:path/to/yourproject read directly from a path on disk (any directory)
{{.appName}} {{.command}} file:path/to/yourproject/file read directly from a path on disk (any single file)
`
packagesSchemeHelp = "\n" + indent + schemeHelpHeader + "\n" + imageSchemeHelp + nonImageSchemeHelp
packagesHelp = packagesExample + packagesSchemeHelp
)
//nolint:dupl
func Packages(v *viper.Viper, app *config.Application, ro *options.RootOptions, po *options.PackagesOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "packages [SOURCE]",
Short: "Generate a package SBOM",
Long: "Generate a packaged-based Software Bill Of Materials (SBOM) from container images and filesystems",
Example: internal.Tprintf(packagesHelp, map[string]interface{}{
"appName": internal.ApplicationName,
"command": "packages",
}),
Args: func(cmd *cobra.Command, args []string) error {
if err := app.LoadAllValues(v, ro.Config); err != nil {
return fmt.Errorf("invalid application config: %w", err)
}
// configure logging for command
newLogWrapper(app)
logApplicationConfig(app)
return validateArgs(cmd, args)
},
SilenceUsage: true,
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
if app.CheckForAppUpdate {
checkForApplicationUpdate()
}
return packages.Run(cmd.Context(), app, args)
},
}
err := po.AddFlags(cmd, v)
if err != nil {
log.Fatal(err)
}
return cmd
}
-151
View File
@@ -1,151 +0,0 @@
package packages
import (
"context"
"fmt"
"github.com/wagoodman/go-partybus"
"github.com/anchore/stereoscope"
"github.com/anchore/syft/cmd/syft/cli/eventloop"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/config"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/ui"
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/formats/template"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
func Run(ctx context.Context, app *config.Application, args []string) error {
err := validateOutputOptions(app)
if err != nil {
return err
}
writer, err := options.MakeWriter(app.Outputs, app.File, app.OutputTemplatePath)
if err != nil {
return err
}
defer func() {
if err := writer.Close(); err != nil {
log.Warnf("unable to write to report destination: %w", err)
}
}()
// could be an image or a directory, with or without a scheme
userInput := args[0]
si, err := source.ParseInputWithName(userInput, app.Platform, true, app.Name)
if err != nil {
return fmt.Errorf("could not generate source input for packages command: %w", err)
}
eventBus := partybus.NewBus()
stereoscope.SetBus(eventBus)
syft.SetBus(eventBus)
subscription := eventBus.Subscribe()
return eventloop.EventLoop(
execWorker(app, *si, writer),
eventloop.SetupSignals(),
subscription,
stereoscope.Cleanup,
ui.Select(options.IsVerbose(app), app.Quiet)...,
)
}
func execWorker(app *config.Application, si source.Input, writer sbom.Writer) <-chan error {
errs := make(chan error)
go func() {
defer close(errs)
src, cleanup, err := source.New(si, app.Registry.ToOptions(), app.Exclusions)
if cleanup != nil {
defer cleanup()
}
if err != nil {
errs <- fmt.Errorf("failed to construct source from user input %q: %w", si.UserInput, err)
return
}
s, err := GenerateSBOM(src, errs, app)
if err != nil {
errs <- err
return
}
if s == nil {
errs <- fmt.Errorf("no SBOM produced for %q", si.UserInput)
}
bus.Publish(partybus.Event{
Type: event.Exit,
Value: func() error { return writer.Write(*s) },
})
}()
return errs
}
func GenerateSBOM(src *source.Source, errs chan error, app *config.Application) (*sbom.SBOM, error) {
tasks, err := eventloop.Tasks(app)
if err != nil {
return nil, err
}
s := sbom.SBOM{
Source: src.Metadata,
Descriptor: sbom.Descriptor{
Name: internal.ApplicationName,
Version: version.FromBuild().Version,
Configuration: app,
},
}
buildRelationships(&s, src, tasks, errs)
return &s, nil
}
func buildRelationships(s *sbom.SBOM, src *source.Source, tasks []eventloop.Task, errs chan error) {
var relationships []<-chan artifact.Relationship
for _, task := range tasks {
c := make(chan artifact.Relationship)
relationships = append(relationships, c)
go eventloop.RunTask(task, &s.Artifacts, src, c, errs)
}
s.Relationships = append(s.Relationships, MergeRelationships(relationships...)...)
}
func MergeRelationships(cs ...<-chan artifact.Relationship) (relationships []artifact.Relationship) {
for _, c := range cs {
for n := range c {
relationships = append(relationships, n)
}
}
return relationships
}
func validateOutputOptions(app *config.Application) error {
var usesTemplateOutput bool
for _, o := range app.Outputs {
if o == template.ID.String() {
usesTemplateOutput = true
break
}
}
if usesTemplateOutput && app.OutputTemplatePath == "" {
return fmt.Errorf(`must specify path to template file when using "template" output format`)
}
return nil
}
-50
View File
@@ -1,50 +0,0 @@
package cli
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/cmd/syft/cli/poweruser"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/config"
)
const powerUserExample = ` {{.appName}} {{.command}} <image>
DEPRECATED - THIS COMMAND WILL BE REMOVED in v1.0.0
Only image sources are supported (e.g. docker: , podman: , docker-archive: , oci: , etc.), the directory source (dir:) is not supported, template outputs are not supported.
All behavior is controlled via application configuration and environment variables (see https://github.com/anchore/syft#configuration)
`
func PowerUser(v *viper.Viper, app *config.Application, ro *options.RootOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "power-user [IMAGE]",
Short: "Run bulk operations on container images",
Example: internal.Tprintf(powerUserExample, map[string]interface{}{
"appName": internal.ApplicationName,
"command": "power-user",
}),
Args: func(cmd *cobra.Command, args []string) error {
if err := app.LoadAllValues(v, ro.Config); err != nil {
return fmt.Errorf("invalid application config: %w", err)
}
// configure logging for command
newLogWrapper(app)
logApplicationConfig(app)
return validateArgs(cmd, args)
},
Hidden: true,
SilenceUsage: true,
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
if app.CheckForAppUpdate {
checkForApplicationUpdate()
}
return poweruser.Run(cmd.Context(), app, args)
},
}
return cmd
}
-118
View File
@@ -1,118 +0,0 @@
package poweruser
import (
"context"
"fmt"
"os"
"github.com/gookit/color"
"github.com/wagoodman/go-partybus"
"github.com/anchore/stereoscope"
"github.com/anchore/syft/cmd/syft/cli/eventloop"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/cmd/syft/cli/packages"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/config"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/ui"
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/formats/syftjson"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
func Run(ctx context.Context, app *config.Application, args []string) error {
writer, err := sbom.NewWriter(sbom.WriterOption{
Format: syftjson.Format(),
Path: app.File,
})
if err != nil {
return err
}
defer func() {
if err := writer.Close(); err != nil {
log.Warnf("unable to write to report destination: %+v", err)
}
// inform user at end of run that command will be removed
deprecated := color.Style{color.Red, color.OpBold}.Sprint("DEPRECATED: This command will be removed in v1.0.0")
fmt.Fprintln(os.Stderr, deprecated)
}()
userInput := args[0]
si, err := source.ParseInputWithName(userInput, app.Platform, true, app.Name)
if err != nil {
return fmt.Errorf("could not generate source input for packages command: %w", err)
}
eventBus := partybus.NewBus()
stereoscope.SetBus(eventBus)
syft.SetBus(eventBus)
subscription := eventBus.Subscribe()
return eventloop.EventLoop(
execWorker(app, *si, writer),
eventloop.SetupSignals(),
subscription,
stereoscope.Cleanup,
ui.Select(options.IsVerbose(app), app.Quiet)...,
)
}
func execWorker(app *config.Application, si source.Input, writer sbom.Writer) <-chan error {
errs := make(chan error)
go func() {
defer close(errs)
app.Secrets.Cataloger.Enabled = true
app.FileMetadata.Cataloger.Enabled = true
app.FileContents.Cataloger.Enabled = true
app.FileClassification.Cataloger.Enabled = true
tasks, err := eventloop.Tasks(app)
if err != nil {
errs <- err
return
}
src, cleanup, err := source.New(si, app.Registry.ToOptions(), app.Exclusions)
if err != nil {
errs <- err
return
}
if cleanup != nil {
defer cleanup()
}
s := sbom.SBOM{
Source: src.Metadata,
Descriptor: sbom.Descriptor{
Name: internal.ApplicationName,
Version: version.FromBuild().Version,
Configuration: app,
},
}
var relationships []<-chan artifact.Relationship
for _, task := range tasks {
c := make(chan artifact.Relationship)
relationships = append(relationships, c)
go eventloop.RunTask(task, &s.Artifacts, src, c, errs)
}
s.Relationships = append(s.Relationships, packages.MergeRelationships(relationships...)...)
bus.Publish(partybus.Event{
Type: event.Exit,
Value: func() error { return writer.Write(s) },
})
}()
return errs
}
-72
View File
@@ -1,72 +0,0 @@
package cli
import (
"encoding/json"
"fmt"
"log"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/config"
"github.com/anchore/syft/internal/version"
)
func Version(v *viper.Viper, app *config.Application) *cobra.Command {
o := &options.VersionOptions{}
cmd := &cobra.Command{
Use: "version",
Short: "show the version",
RunE: func(cmd *cobra.Command, args []string) error {
return printVersion(o.Output)
},
}
err := o.AddFlags(cmd, v)
if err != nil {
log.Fatal(err)
}
return cmd
}
func printVersion(output string) error {
versionInfo := version.FromBuild()
switch output {
case "text":
fmt.Println("Application: ", internal.ApplicationName)
fmt.Println("Version: ", versionInfo.Version)
fmt.Println("JsonSchemaVersion: ", internal.JSONSchemaVersion)
fmt.Println("BuildDate: ", versionInfo.BuildDate)
fmt.Println("GitCommit: ", versionInfo.GitCommit)
fmt.Println("GitDescription: ", versionInfo.GitDescription)
fmt.Println("Platform: ", versionInfo.Platform)
fmt.Println("GoVersion: ", versionInfo.GoVersion)
fmt.Println("Compiler: ", versionInfo.Compiler)
case "json":
enc := json.NewEncoder(os.Stdout)
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
err := enc.Encode(&struct {
version.Version
Application string `json:"application"`
}{
Version: versionInfo,
Application: internal.ApplicationName,
})
if err != nil {
fmt.Printf("failed to show version information: %+v\n", err)
os.Exit(1)
}
default:
fmt.Printf("unsupported output format: %s\n", output)
os.Exit(1)
}
return nil
}
-18
View File
@@ -1,18 +0,0 @@
package main
import (
"log"
"github.com/anchore/syft/cmd/syft/cli"
)
func main() {
cli, err := cli.New()
if err != nil {
log.Fatalf("error during command construction: %v", err)
}
if err := cli.Execute(); err != nil {
log.Fatalf("error during command execution: %v", err)
}
}
+39
View File
@@ -0,0 +1,39 @@
package cmd
import (
"fmt"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/version"
"github.com/spf13/cobra"
)
var showVerboseVersionInfo bool
var versionCmd = &cobra.Command{
Use: "version",
Short: "show the version",
Run: printVersion,
}
func init() {
versionCmd.Flags().BoolVarP(&showVerboseVersionInfo, "verbose", "v", false, "show additional version information")
rootCmd.AddCommand(versionCmd)
}
func printVersion(_ *cobra.Command, _ []string) {
versionInfo := version.FromBuild()
if showVerboseVersionInfo {
fmt.Println("Application: ", internal.ApplicationName)
fmt.Println("Version: ", versionInfo.Version)
fmt.Println("BuildDate: ", versionInfo.BuildDate)
fmt.Println("GitCommit: ", versionInfo.GitCommit)
fmt.Println("GitTreeState: ", versionInfo.GitTreeState)
fmt.Println("Platform: ", versionInfo.Platform)
fmt.Println("GoVersion: ", versionInfo.GoVersion)
fmt.Println("Compiler: ", versionInfo.Compiler)
} else {
fmt.Printf("%s %s\n", internal.ApplicationName, versionInfo.Version)
}
}
+37 -332
View File
@@ -1,342 +1,47 @@
module github.com/anchore/syft
go 1.18
go 1.14
require (
github.com/CycloneDX/cyclonedx-go v0.5.2
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
github.com/acobaugh/osrelease v0.1.0
github.com/adrg/xdg v0.3.3
github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
github.com/adrg/xdg v0.2.1
github.com/anchore/go-rpmdb v0.0.0-20200811175839-cbc751c28e8e
github.com/anchore/go-testutils v0.0.0-20200624184116-66aa578126db
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
github.com/anchore/packageurl-go v0.1.1-0.20220428202044-a072fa3cb6d7
github.com/bmatcuk/doublestar/v4 v4.0.2
github.com/anchore/stereoscope v0.0.0-20200903223322-a3806f515470
github.com/bmatcuk/doublestar v1.3.1
github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe // indirect
github.com/dustin/go-humanize v1.0.0
github.com/facebookincubator/nvdtools v0.1.4
github.com/go-test/deep v1.0.8
github.com/google/go-cmp v0.5.9
github.com/google/uuid v1.3.0
github.com/gookit/color v1.4.2
github.com/hashicorp/go-multierror v1.1.1
github.com/jinzhu/copier v0.3.2
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mholt/archiver/v3 v3.5.1
github.com/microsoft/go-rustaudit v0.0.0-20220730194248-4b17361d90a5
github.com/go-test/deep v1.0.6
github.com/golang/protobuf v1.4.2
github.com/google/go-containerregistry v0.1.1 // indirect
github.com/google/uuid v1.1.1
github.com/gookit/color v1.2.7
github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99 // indirect
github.com/hashicorp/go-hclog v0.14.1
github.com/hashicorp/go-multierror v1.1.0
github.com/hashicorp/go-plugin v1.3.0
github.com/hashicorp/go-version v1.2.0
github.com/mitchellh/go-homedir v1.1.0
github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/mitchellh/mapstructure v1.5.0
github.com/olekukonko/tablewriter v0.0.5
github.com/pelletier/go-toml v1.9.5
// pinned to pull in 386 arch fix: https://github.com/scylladb/go-set/commit/cc7b2070d91ebf40d233207b633e28f5bd8f03a5
github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e
github.com/sergi/go-diff v1.2.0
github.com/sirupsen/logrus v1.9.0
github.com/spdx/tools-golang v0.3.1-0.20221108182156-8a01147e6342
github.com/spf13/afero v1.8.2
github.com/spf13/cobra v1.6.0
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.13.0
github.com/stretchr/testify v1.8.0
github.com/vifraa/gopom v0.1.0
github.com/wagoodman/go-partybus v0.0.0-20210627031916-db1f5573bbc5
github.com/mitchellh/mapstructure v1.3.1
github.com/olekukonko/tablewriter v0.0.4
github.com/package-url/packageurl-go v0.1.0
github.com/pelletier/go-toml v1.8.0
github.com/rogpeppe/go-internal v1.5.2
github.com/sergi/go-diff v1.1.0
github.com/sirupsen/logrus v1.6.0
github.com/spf13/cobra v1.0.0
github.com/spf13/viper v1.7.0
github.com/wagoodman/go-partybus v0.0.0-20200526224238-eb215533f07d
github.com/wagoodman/go-progress v0.0.0-20200731105512-1020f39e6240
github.com/wagoodman/jotframe v0.0.0-20211129225309-56b0d0a4aebb
github.com/wagoodman/jotframe v0.0.0-20200730190914-3517092dd163
github.com/x-cray/logrus-prefixed-formatter v0.5.2
github.com/xeipuuv/gojsonschema v1.2.0
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4
golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458
golang.org/x/term v0.0.0-20220919170432-7a66f970e087
gopkg.in/yaml.v2 v2.4.0
)
require (
github.com/Masterminds/sprig/v3 v3.2.2
github.com/anchore/go-logger v0.0.0-20220728155337-03b66a5207d8
github.com/anchore/stereoscope v0.0.0-20221130153459-3b80d983223f
github.com/docker/docker v20.10.17+incompatible
github.com/google/go-containerregistry v0.11.0
github.com/in-toto/in-toto-golang v0.4.1-0.20221018183522-731d0640b65f
github.com/invopop/jsonschema v0.7.0
github.com/knqyf263/go-rpmdb v0.0.0-20221030135625-4082a22221ce
github.com/opencontainers/go-digest v1.0.0
github.com/sassoftware/go-rpmutils v0.2.0
github.com/sigstore/cosign v1.13.1
github.com/sigstore/rekor v0.12.1-0.20220915152154-4bb6f441c1b2
github.com/sigstore/sigstore v1.4.4
github.com/vbatts/go-mtree v0.5.0
gopkg.in/yaml.v3 v3.0.1
)
require (
bitbucket.org/creachadair/shell v0.0.7 // indirect
cloud.google.com/go/compute v1.10.0 // indirect
github.com/AliyunContainerService/ack-ram-tool/pkg/credentials/alibabacloudsdkgo/helper v0.2.0 // indirect
github.com/Azure/azure-sdk-for-go v66.0.0+incompatible // indirect
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
github.com/Azure/go-autorest/autorest v0.11.28 // indirect
github.com/Azure/go-autorest/autorest/adal v0.9.21 // indirect
github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 // indirect
github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
github.com/Azure/go-autorest/logger v0.2.1 // indirect
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/DataDog/zstd v1.4.5 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.1.1 // indirect
github.com/Microsoft/go-winio v0.5.2 // indirect
github.com/ThalesIgnite/crypto11 v1.2.5 // indirect
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 // indirect
github.com/alibabacloud-go/cr-20160607 v1.0.1 // indirect
github.com/alibabacloud-go/cr-20181201 v1.0.10 // indirect
github.com/alibabacloud-go/darabonba-openapi v0.1.18 // indirect
github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68 // indirect
github.com/alibabacloud-go/endpoint-util v1.1.1 // indirect
github.com/alibabacloud-go/openapi-util v0.0.11 // indirect
github.com/alibabacloud-go/tea v1.1.18 // indirect
github.com/alibabacloud-go/tea-utils v1.4.4 // indirect
github.com/alibabacloud-go/tea-xml v1.1.2 // indirect
github.com/aliyun/credentials-go v1.2.3 // indirect
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
github.com/aws/aws-sdk-go-v2 v1.16.16 // indirect
github.com/aws/aws-sdk-go-v2/config v1.17.8 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.12.21 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.23 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.24 // indirect
github.com/aws/aws-sdk-go-v2/service/ecr v1.15.0 // indirect
github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.12.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.17 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.11.23 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.6 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.16.19 // indirect
github.com/aws/smithy-go v1.13.3 // indirect
github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20220517224237-e6f29200ae04 // indirect
github.com/benbjohnson/clock v1.1.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bgentry/speakeasy v0.1.0 // indirect
github.com/blang/semver v3.5.1+incompatible // indirect
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/chrismellard/docker-credential-acr-env v0.0.0-20220119192733-fe33c00cee21 // indirect
github.com/clbanning/mxj/v2 v2.5.6 // indirect
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 // indirect
github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490 // indirect
github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be // indirect
github.com/containerd/containerd v1.6.8 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.12.0 // indirect
github.com/coreos/go-oidc/v3 v3.4.0 // indirect
github.com/coreos/go-semver v0.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/cyberphone/json-canonicalization v0.0.0-20210823021906-dc406ceaf94b // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dimchansky/utfbom v1.1.1 // indirect
github.com/docker/cli v20.10.17+incompatible // indirect
github.com/docker/distribution v2.8.1+incompatible // indirect
github.com/docker/docker-credential-helpers v0.6.4 // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1 // indirect
github.com/envoyproxy/protoc-gen-validate v0.6.2 // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/fullstorydev/grpcurl v1.8.7 // indirect
github.com/gabriel-vasile/mimetype v1.4.0 // indirect
github.com/go-chi/chi v4.1.2+incompatible // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/analysis v0.21.4 // indirect
github.com/go-openapi/errors v0.20.3 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect
github.com/go-openapi/loads v0.21.2 // indirect
github.com/go-openapi/runtime v0.24.2 // indirect
github.com/go-openapi/spec v0.20.7 // indirect
github.com/go-openapi/strfmt v0.21.3 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/go-openapi/validate v0.22.0 // indirect
github.com/go-piv/piv-go v1.10.0 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.11.0 // indirect
github.com/go-restruct/restruct v1.2.0-alpha // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang-jwt/jwt/v4 v4.4.2 // indirect
github.com/golang/glog v1.0.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/certificate-transparency-go v1.1.3 // indirect
github.com/google/go-github/v45 v45.2.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/trillian v1.5.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.0 // indirect
github.com/googleapis/gnostic v0.5.5 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/huandu/xstrings v1.3.2 // indirect
github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/jedisct1/go-minisign v0.0.0-20211028175153-1c139d1cc84b // indirect
github.com/jhump/protoreflect v1.13.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jonboulle/clockwork v0.3.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/klauspost/compress v1.15.9 // indirect
github.com/klauspost/pgzip v1.2.5 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/letsencrypt/boulder v0.0.0-20220929215747-76583552c2be // indirect
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/miekg/pkcs11 v1.1.1 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mozillazg/docker-credential-acr-helper v0.3.0 // indirect
github.com/nwaples/rardecode v1.1.0 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
github.com/pierrec/lz4/v4 v4.1.15 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.13.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sassoftware/relic v0.0.0-20210427151427-dfb082b79b74 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.4.0 // indirect
github.com/segmentio/ksuid v1.0.4 // indirect
github.com/shibumi/go-pathspec v1.3.0 // indirect
github.com/shopspring/decimal v1.2.0 // indirect
github.com/sigstore/fulcio v0.6.0 // indirect
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect
github.com/soheilhy/cmux v0.1.5 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spiffe/go-spiffe/v2 v2.1.1 // indirect
github.com/stretchr/objx v0.4.0 // indirect
github.com/subosito/gotenv v1.4.1 // indirect
github.com/sylabs/sif/v2 v2.8.1 // indirect
github.com/sylabs/squashfs v0.6.1 // indirect
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect
github.com/tent/canonical-json-go v0.0.0-20130607151641-96e4ba3a7613 // indirect
github.com/thales-e-security/pool v0.0.2 // indirect
github.com/therootcompany/xz v1.0.1 // indirect
github.com/theupdateframework/go-tuf v0.5.2-0.20220930112810-3890c1e7ace4 // indirect
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect
github.com/tjfoc/gmsm v1.3.2 // indirect
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect
github.com/transparency-dev/merkle v0.0.1 // indirect
github.com/ulikunitz/xz v0.5.10 // indirect
github.com/urfave/cli v1.22.7 // indirect
github.com/vbatts/tar-split v0.11.2 // indirect
github.com/xanzy/go-gitlab v0.73.1 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
github.com/zeebo/errs v1.2.2 // indirect
go.etcd.io/bbolt v1.3.6 // indirect
go.etcd.io/etcd/api/v3 v3.6.0-alpha.0 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.6.0-alpha.0 // indirect
go.etcd.io/etcd/client/v2 v2.306.0-alpha.0 // indirect
go.etcd.io/etcd/client/v3 v3.6.0-alpha.0 // indirect
go.etcd.io/etcd/etcdctl/v3 v3.6.0-alpha.0 // indirect
go.etcd.io/etcd/etcdutl/v3 v3.6.0-alpha.0 // indirect
go.etcd.io/etcd/pkg/v3 v3.6.0-alpha.0 // indirect
go.etcd.io/etcd/raft/v3 v3.6.0-alpha.0 // indirect
go.etcd.io/etcd/server/v3 v3.6.0-alpha.0 // indirect
go.etcd.io/etcd/tests/v3 v3.6.0-alpha.0 // indirect
go.etcd.io/etcd/v3 v3.6.0-alpha.0 // indirect
go.mongodb.org/mongo-driver v1.10.0 // indirect
go.opencensus.io v0.23.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.28.0 // indirect
go.opentelemetry.io/otel v1.7.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.7.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.7.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.7.0 // indirect
go.opentelemetry.io/otel/sdk v1.7.0 // indirect
go.opentelemetry.io/otel/trace v1.7.0 // indirect
go.opentelemetry.io/proto/otlp v0.16.0 // indirect
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/multierr v1.8.0 // indirect
go.uber.org/zap v1.23.0 // indirect
golang.org/x/exp v0.0.0-20220823124025-807a23277127 // indirect
golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1 // indirect
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0 // indirect
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect
golang.org/x/text v0.3.8-0.20211004125949-5bd84dd9b33b // indirect
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af // indirect
golang.org/x/tools v0.1.12 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/api v0.99.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e // indirect
google.golang.org/grpc v1.50.1 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/cheggaaa/pb.v1 v1.0.28 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
k8s.io/api v0.23.5 // indirect
k8s.io/apimachinery v0.23.5 // indirect
k8s.io/client-go v0.23.5 // indirect
k8s.io/klog/v2 v2.60.1-0.20220317184644-43cc75f9ae89 // indirect
k8s.io/kube-openapi v0.0.0-20220124234850-424119656bbf // indirect
k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect
lukechampine.com/uint128 v1.1.1 // indirect
modernc.org/cc/v3 v3.36.0 // indirect
modernc.org/ccgo/v3 v3.16.6 // indirect
modernc.org/libc v1.16.7 // indirect
modernc.org/mathutil v1.4.1 // indirect
modernc.org/memory v1.1.1 // indirect
modernc.org/opt v0.1.1 // indirect
modernc.org/sqlite v1.17.3 // indirect
modernc.org/strutil v1.1.1 // indirect
modernc.org/token v1.0.0 // indirect
sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect
sigs.k8s.io/release-utils v0.7.3 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)
require (
// we are hinting brotli to latest due to warning when installing archiver v3:
// go: warning: github.com/andybalholm/brotli@v1.0.1: retracted by module author: occasional panics and data corruption
github.com/andybalholm/brotli v1.0.4 // indirect
github.com/pkg/errors v0.9.1 // indirect
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be // indirect
)
retract (
v0.53.2
v0.53.1 // Published accidentally with incorrect license in depdencies
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
golang.org/x/net v0.0.0-20200625001655-4c5254603344 // indirect
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 // indirect
golang.org/x/sys v0.0.0-20200610111108-226ff32320da // indirect
google.golang.org/genproto v0.0.0-20200615140333-fd031eab31e7 // indirect
google.golang.org/grpc v1.31.1
gopkg.in/ini.v1 v1.57.0 // indirect
gopkg.in/yaml.v2 v2.3.0
)
+438 -2269
View File
File diff suppressed because it is too large Load Diff
+236 -560
View File
@@ -1,134 +1,189 @@
#!/bin/sh
# note: we require errors to propagate (don't set -e)
set -u
PROJECT_NAME="syft"
OWNER=anchore
REPO="${PROJECT_NAME}"
GITHUB_DOWNLOAD_PREFIX=https://github.com/${OWNER}/${REPO}/releases/download
INSTALL_SH_BASE_URL=https://raw.githubusercontent.com/${OWNER}/${PROJECT_NAME}
PROGRAM_ARGS=$@
# do not change the name of this parameter (this must always be backwards compatible)
DOWNLOAD_TAG_INSTALL_SCRIPT=${DOWNLOAD_TAG_INSTALL_SCRIPT:-true}
set -e
# Code generated by godownloader on 2020-08-10T20:55:46Z. DO NOT EDIT.
#
# usage [script-name]
#
usage() (
usage() {
this=$1
cat <<EOF
$this: download go binaries for anchore/syft
Usage: $this [-b] dir [-d] [tag]
-b the installation directory (dDefaults to ./bin)
-d turns on debug logging
-dd turns on trace logging
[tag] the specific release to use (if missing, then the latest will be used)
Usage: $this [-b] bindir [-d] [tag]
-b sets bindir or installation directory, Defaults to ./bin
-d turns on debug logging
[tag] is a tag from
https://github.com/anchore/syft/releases
If tag is missing, then the latest will be used.
Generated by godownloader
https://github.com/goreleaser/godownloader
EOF
exit 2
)
}
parse_args() {
#BINDIR is ./bin unless set be ENV
# over-ridden by flag below
# ------------------------------------------------------------------------
# https://github.com/client9/shlib - portable posix shell functions
# Public domain - http://unlicense.org
# https://github.com/client9/shlib/blob/master/LICENSE.md
# but credit (and pull requests) appreciated.
# ------------------------------------------------------------------------
BINDIR=${BINDIR:-./bin}
while getopts "b:dh?x" arg; do
case "$arg" in
b) BINDIR="$OPTARG" ;;
d) log_set_priority 10 ;;
h | \?) usage "$0" ;;
x) set -x ;;
esac
done
shift $((OPTIND - 1))
TAG=$1
}
# this function wraps all the destructive operations
# if a curl|bash cuts off the end of the script due to
# network, either nothing will happen or will syntax error
# out preventing half-done work
execute() {
tmpdir=$(mktemp -d)
log_debug "downloading files into ${tmpdir}"
http_download "${tmpdir}/${TARBALL}" "${TARBALL_URL}"
http_download "${tmpdir}/${CHECKSUM}" "${CHECKSUM_URL}"
hash_sha256_verify "${tmpdir}/${TARBALL}" "${tmpdir}/${CHECKSUM}"
srcdir="${tmpdir}"
(cd "${tmpdir}" && untar "${TARBALL}")
test ! -d "${BINDIR}" && install -d "${BINDIR}"
for binexe in $BINARIES; do
if [ "$OS" = "windows" ]; then
binexe="${binexe}.exe"
fi
install "${srcdir}/${binexe}" "${BINDIR}/"
log_info "installed ${BINDIR}/${binexe}"
done
rm -rf "${tmpdir}"
}
get_binaries() {
case "$PLATFORM" in
darwin/amd64) BINARIES="syft" ;;
linux/amd64) BINARIES="syft" ;;
windows/amd64) BINARIES="syft" ;;
*)
log_crit "platform $PLATFORM is not supported. Make sure this script is up-to-date and file request at https://github.com/${PREFIX}/issues/new"
exit 1
;;
esac
}
tag_to_version() {
if [ -z "${TAG}" ]; then
log_info "checking GitHub for latest tag"
else
log_info "checking GitHub for tag '${TAG}'"
fi
REALTAG=$(github_release "$OWNER/$REPO" "${TAG}") && true
if test -z "$REALTAG"; then
log_crit "unable to find '${TAG}' - use 'latest' or see https://github.com/${PREFIX}/releases for details"
exit 1
fi
# if version starts with 'v', remove it
TAG="$REALTAG"
VERSION=${TAG#v}
}
adjust_format() {
# change format (tar.gz or zip) based on OS
case ${OS} in
windows) FORMAT=zip ;;
esac
true
}
adjust_os() {
# adjust archive name based on OS
true
}
adjust_arch() {
# adjust archive name based on ARCH
true
}
is_command() (
cat /dev/null <<EOF
------------------------------------------------------------------------
https://github.com/client9/shlib - portable posix shell functions
Public domain - http://unlicense.org
https://github.com/client9/shlib/blob/master/LICENSE.md
but credit (and pull requests) appreciated.
------------------------------------------------------------------------
EOF
is_command() {
command -v "$1" >/dev/null
)
echo_stderr() (
}
echoerr() {
echo "$@" 1>&2
)
_logp=2
}
log_prefix() {
echo "$0"
}
_logp=6
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
}
init_colors
log_tag() (
log_tag() {
case $1 in
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]" ;;
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_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
}
log_debug() {
log_priority 7 || return 0
echoerr "$(log_prefix)" "$(log_tag 7)" "$@"
}
log_info() {
log_priority 6 || return 0
echoerr "$(log_prefix)" "$(log_tag 6)" "$@"
}
log_err() {
log_priority 3 || return 0
echoerr "$(log_prefix)" "$(log_tag 3)" "$@"
}
log_crit() {
log_priority 2 || return 0
echoerr "$(log_prefix)" "$(log_tag 2)" "$@"
}
uname_os() {
os=$(uname -s | tr '[:upper:]' '[:lower:]')
case "$os" in
cygwin_nt*) os="windows" ;;
mingw*) os="windows" ;;
msys_nt*) os="windows" ;;
esac
echo "$os"
}
uname_arch() {
arch=$(uname -m)
case $arch in
x86_64) arch="amd64" ;;
x86) arch="386" ;;
i686) arch="386" ;;
i386) arch="386" ;;
aarch64) arch="arm64" ;;
armv5*) arch="armv5" ;;
armv6*) arch="armv6" ;;
armv7*) arch="armv7" ;;
esac
echo ${arch}
}
uname_os_check() {
os=$(uname_os)
case "$os" in
darwin) return 0 ;;
dragonfly) return 0 ;;
@@ -142,12 +197,11 @@ uname_os_check() (
solaris) return 0 ;;
windows) return 0 ;;
esac
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"
log_crit "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib"
return 1
)
uname_arch_check() (
arch=$1
}
uname_arch_check() {
arch=$(uname_arch)
case "$arch" in
386) return 0 ;;
amd64) return 0 ;;
@@ -164,72 +218,48 @@ uname_arch_check() (
s390x) return 0 ;;
amd64p32) return 0 ;;
esac
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"
log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib"
return 1
)
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 -q "${archive}" ;;
*.dmg) extract_from_dmg "${archive}" ;;
}
untar() {
tarball=$1
case "${tarball}" in
*.tar.gz | *.tgz) tar --no-same-owner -xzf "${tarball}" ;;
*.tar) tar --no-same-owner -xf "${tarball}" ;;
*.zip) unzip "${tarball}" ;;
*)
log_err "unpack unknown archive format for ${archive}"
log_err "untar unknown archive format for ${tarball}"
return 1
;;
esac
)
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_err "received HTTP status=$code for url='$source_url'"
log_debug "http_download_curl received HTTP status $code"
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(url=$2)"
}
http_download() {
log_debug "http_download $2"
if is_command curl; then
http_download_curl "$@"
return
@@ -237,19 +267,28 @@ http_download() (
http_download_wget "$@"
return
fi
log_err "http_download unable to find wget or curl"
log_crit "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"
)
hash_sha256() (
}
github_release() {
owner_repo=$1
version=$2
test -z "$version" && version="latest"
giturl="https://github.com/${owner_repo}/releases/${version}"
json=$(http_copy "$giturl" "Accept:application/json")
test -z "$json" && return 1
version=$(echo "$json" | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//')
test -z "$version" && return 1
echo "$version"
}
hash_sha256() {
TARGET=${1:-/dev/stdin}
if is_command gsha256sum; then
hash=$(gsha256sum "$TARGET") || return 1
@@ -264,12 +303,11 @@ hash_sha256() (
hash=$(openssl -dst openssl dgst -sha256 "$TARGET") || return 1
echo "$hash" | cut -d ' ' -f a
else
log_err "hash_sha256 unable to find command to compute sha-256 hash"
log_crit "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
@@ -287,413 +325,51 @@ hash_sha256_verify() (
log_err "hash_sha256_verify checksum for '$TARGET' did not verify ${want} vs $got"
return 1
fi
)
# ------------------------------------------------------------------------
# End of functions from https://github.com/client9/shlib
# ------------------------------------------------------------------------
# asset_file_exists [path]
#
# returns 1 if the given file does not exist
#
asset_file_exists() (
path="$1"
if [ ! -f "${path}" ]; then
return 1
fi
)
# github_release_json [owner] [repo] [version]
#
# outputs release json string
#
github_release_json() (
owner=$1
repo=$2
version=$3
test -z "$version" && version="latest"
giturl="https://github.com/${owner}/${repo}/releases/${version}"
json=$(http_copy "$giturl" "Accept:application/json")
log_trace "github_release_json(owner=${owner}, repo=${repo}, version=${version}) returned '${json}'"
test -z "$json" && return 1
echo "${json}"
)
# extract_value [key-value-pair]
#
# outputs value from a colon delimited key-value pair
#
extract_value() (
key_value="$1"
IFS=':' read -r _ value << EOF
${key_value}
}
cat /dev/null <<EOF
------------------------------------------------------------------------
End of functions from https://github.com/client9/shlib
------------------------------------------------------------------------
EOF
echo "$value"
)
# extract_json_value [json] [key]
#
# outputs value of the key from the given json string
#
extract_json_value() (
json="$1"
key="$2"
key_value=$(echo "${json}" | grep -o "\"$key\":[^,]*[,}]" | tr -d '",}')
PROJECT_NAME="syft"
OWNER=anchore
REPO="syft"
BINARY=syft
FORMAT=tar.gz
OS=$(uname_os)
ARCH=$(uname_arch)
PREFIX="$OWNER/$REPO"
extract_value "$key_value"
)
# use in logging routines
log_prefix() {
echo "$PREFIX"
}
PLATFORM="${OS}/${ARCH}"
GITHUB_DOWNLOAD=https://github.com/${OWNER}/${REPO}/releases/download
# github_release_tag [release-json]
#
# outputs release tag string
#
github_release_tag() (
json="$1"
tag=$(extract_json_value "${json}" "tag_name")
test -z "$tag" && return 1
echo "$tag"
)
uname_os_check "$OS"
uname_arch_check "$ARCH"
# download_github_release_checksums [release-url-prefix] [name] [version] [output-dir]
#
# outputs path to the downloaded checksums file
#
download_github_release_checksums() (
download_url="$1"
name="$2"
version="$3"
output_dir="$4"
parse_args "$@"
log_trace "download_github_release_checksums(url=${download_url}, name=${name}, version=${version}, output_dir=${output_dir})"
get_binaries
checksum_filename=${name}_${version}_checksums.txt
checksum_url=${download_url}/${checksum_filename}
output_path="${output_dir}/${checksum_filename}"
tag_to_version
http_download "${output_path}" "${checksum_url}" ""
asset_file_exists "${output_path}"
adjust_format
log_trace "download_github_release_checksums() returned '${output_path}'"
adjust_os
echo "${output_path}"
)
adjust_arch
# search_for_asset [checksums-file-path] [name] [os] [arch] [format]
#
# outputs name of the asset to download
#
search_for_asset() (
checksum_path="$1"
name="$2"
os="$3"
arch="$4"
format="$5"
log_info "found version: ${VERSION} for ${TAG}/${OS}/${ARCH}"
log_trace "search_for_asset(checksum-path=${checksum_path}, name=${name}, os=${os}, arch=${arch}, format=${format})"
NAME=${PROJECT_NAME}_${VERSION}_${OS}_${ARCH}
TARBALL=${NAME}.${FORMAT}
TARBALL_URL=${GITHUB_DOWNLOAD}/${TAG}/${TARBALL}
CHECKSUM=${PROJECT_NAME}_${VERSION}_checksums.txt
CHECKSUM_URL=${GITHUB_DOWNLOAD}/${TAG}/${CHECKSUM}
asset_glob="${name}_.*_${os}_${arch}.${format}"
output_path=$(grep -o "${asset_glob}" "${checksum_path}" || true)
log_trace "search_for_asset() returned '${output_path}'"
echo "${output_path}"
)
# uname_os
#
# outputs an adjusted os value
#
uname_os() (
os=$(uname -s | tr '[:upper:]' '[:lower:]')
case "$os" in
cygwin_nt*) os="windows" ;;
mingw*) os="windows" ;;
msys_nt*) os="windows" ;;
esac
uname_os_check "$os"
log_trace "uname_os() returned '${os}'"
echo "$os"
)
# uname_arch
#
# outputs an adjusted architecture value
#
uname_arch() (
arch=$(uname -m)
case $arch in
x86_64) arch="amd64" ;;
x86) arch="386" ;;
i686) arch="386" ;;
i386) arch="386" ;;
aarch64) arch="arm64" ;;
armv5*) arch="armv5" ;;
armv6*) arch="armv6" ;;
armv7*) arch="armv7" ;;
esac
uname_arch_check "${arch}"
log_trace "uname_arch() returned '${arch}'"
echo "${arch}"
)
# get_release_tag [owner] [repo] [tag]
#
# outputs tag string
#
get_release_tag() (
owner="$1"
repo="$2"
tag="$3"
log_trace "get_release_tag(owner=${owner}, repo=${repo}, tag=${tag})"
json=$(github_release_json "${owner}" "${repo}" "${tag}")
real_tag=$(github_release_tag "${json}")
if test -z "${real_tag}"; then
return 1
fi
log_trace "get_release_tag() returned '${real_tag}'"
echo "${real_tag}"
)
# tag_to_version [tag]
#
# outputs version string
#
tag_to_version() (
tag="$1"
value="${tag#v}"
log_trace "tag_to_version(tag=${tag}) returned '${value}'"
echo "$value"
)
# get_binary_name [os] [arch] [default-name]
#
# outputs a the binary string name
#
get_binary_name() (
os="$1"
arch="$2"
binary="$3"
original_binary="${binary}"
case "${os}" in
windows) binary="${binary}.exe" ;;
esac
log_trace "get_binary_name(os=${os}, arch=${arch}, binary=${original_binary}) returned '${binary}'"
echo "${binary}"
)
# get_format_name [os] [arch] [default-format]
#
# outputs an adjusted file format
#
get_format_name() (
os="$1"
arch="$2"
format="$3"
original_format="${format}"
case ${os} in
windows) format=zip ;;
esac
log_trace "get_format_name(os=${os}, arch=${arch}, format=${original_format}) returned '${format}'"
echo "${format}"
)
# download_and_install_asset [release-url-prefix] [download-path] [install-path] [name] [os] [arch] [version] [format] [binary]
#
# attempts to download the archive and install it to the given path.
#
download_and_install_asset() (
download_url="$1"
download_path="$2"
install_path=$3
name="$4"
os="$5"
arch="$6"
version="$7"
format="$8"
binary="$9"
asset_filepath=$(download_asset "${download_url}" "${download_path}" "${name}" "${os}" "${arch}" "${version}" "${format}")
# don't continue if we couldn't download an asset
if [ -z "${asset_filepath}" ]; then
log_err "could not find release asset for os='${os}' arch='${arch}' format='${format}' "
return 1
fi
install_asset "${asset_filepath}" "${install_path}" "${binary}"
)
# download_asset [release-url-prefix] [download-path] [name] [os] [arch] [version] [format] [binary]
#
# outputs the path to the downloaded asset asset_filepath
#
download_asset() (
download_url="$1"
destination="$2"
name="$3"
os="$4"
arch="$5"
version="$6"
format="$7"
log_trace "download_asset(url=${download_url}, destination=${destination}, name=${name}, os=${os}, arch=${arch}, version=${version}, format=${format})"
checksums_filepath=$(download_github_release_checksums "${download_url}" "${name}" "${version}" "${destination}")
log_trace "checksums content:\n$(cat ${checksums_filepath})"
asset_filename=$(search_for_asset "${checksums_filepath}" "${name}" "${os}" "${arch}" "${format}")
# don't continue if we couldn't find a matching asset from the checksums file
if [ -z "${asset_filename}" ]; then
return 1
fi
asset_url="${download_url}/${asset_filename}"
asset_filepath="${destination}/${asset_filename}"
http_download "${asset_filepath}" "${asset_url}" ""
hash_sha256_verify "${asset_filepath}" "${checksums_filepath}"
log_trace "download_asset_by_checksums_file() returned '${asset_filepath}'"
echo "${asset_filepath}"
)
# install_asset [asset-path] [destination-path] [binary]
#
install_asset() (
asset_filepath="$1"
destination="$2"
binary="$3"
log_trace "install_asset(asset=${asset_filepath}, destination=${destination}, binary=${binary})"
# don't continue if we don't have anything to install
if [ -z "${asset_filepath}" ]; then
return
fi
archive_dir=$(dirname "${asset_filepath}")
# unarchive the downloaded archive to the temp dir
(cd "${archive_dir}" && unpack "${asset_filepath}")
# create the destination dir
test ! -d "${destination}" && install -d "${destination}"
# install the binary to the destination dir
install "${archive_dir}/${binary}" "${destination}/"
)
main() (
# parse arguments
# note: never change default install directory (this must always be backwards compatible)
install_dir=${install_dir:-./bin}
# note: never change the program flags or arguments (this must always be backwards compatible)
while getopts "b:dh?x" arg; do
case "$arg" in
b) install_dir="$OPTARG" ;;
d)
if [ "$_logp" = "$log_info_priority" ]; then
# -d == debug
log_set_priority $log_debug_priority
else
# -dd (or -ddd...) == trace
log_set_priority $log_trace_priority
fi
;;
h | \?) usage "$0" ;;
x) set -x ;;
esac
done
shift $((OPTIND - 1))
set +u
tag=$1
if [ -z "${tag}" ]; then
log_debug "checking github for the current release tag"
tag=""
else
log_debug "checking github for release tag='${tag}'"
fi
set -u
tag=$(get_release_tag "${OWNER}" "${REPO}" "${tag}")
if [ "$?" != "0" ]; then
log_err "unable to find tag='${tag}'"
log_err "do not specify a version or select a valid version from https://github.com/${OWNER}/${REPO}/releases"
return 1
fi
# run the application
version=$(tag_to_version "${tag}")
os=$(uname_os)
arch=$(uname_arch)
format=$(get_format_name "${os}" "${arch}" "tar.gz")
binary=$(get_binary_name "${os}" "${arch}" "${PROJECT_NAME}")
download_url="${GITHUB_DOWNLOAD_PREFIX}/${tag}"
# we always use the install.sh script that is associated with the tagged release. Why? the latest install.sh is not
# guaranteed to be able to install every version of the application. We use the DOWNLOAD_TAG_INSTALL_SCRIPT env var
# to indicate if we should continue processing with the existing script or to download the script from the given tag.
if [ "${DOWNLOAD_TAG_INSTALL_SCRIPT}" = "true" ]; then
export DOWNLOAD_TAG_INSTALL_SCRIPT=false
log_info "fetching release script for tag='${tag}'"
http_copy "${INSTALL_SH_BASE_URL}/${tag}/install.sh" "" | sh -s -- ${PROGRAM_ARGS}
exit $?
fi
log_info "using release tag='${tag}' version='${version}' os='${os}' arch='${arch}'"
download_dir=$(mktemp -d)
trap 'rm -rf -- "$download_dir"' EXIT
log_debug "downloading files into ${download_dir}"
download_and_install_asset "${download_url}" "${download_dir}" "${install_dir}" "${PROJECT_NAME}" "${os}" "${arch}" "${version}" "${format}" "${binary}"
# don't continue if we couldn't install the asset
if [ "$?" != "0" ]; then
log_err "failed to install ${PROJECT_NAME}"
return 1
fi
log_info "installed ${install_dir}/${binary}"
)
# entrypoint
set +u
if [ -z "${TEST_INSTALL_SH}" ]; then
set -u
main "$@"
fi
set -u
execute
-268
View File
@@ -1,268 +0,0 @@
package config
import (
"errors"
"fmt"
"path"
"reflect"
"sort"
"strings"
"github.com/adrg/xdg"
"github.com/mitchellh/go-homedir"
"github.com/spf13/viper"
"gopkg.in/yaml.v2"
"github.com/anchore/go-logger"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/pkg/cataloger"
)
var (
ErrApplicationConfigNotFound = fmt.Errorf("application config not found")
catalogerEnabledDefault = false
)
type defaultValueLoader interface {
loadDefaultValues(*viper.Viper)
}
type parser interface {
parseConfigValues() error
}
// Application is the main syft application configuration.
type Application struct {
// the location where the application config was read from (either from -c or discovered while loading); default .syft.yaml
ConfigPath string `yaml:"configPath,omitempty" json:"configPath" mapstructure:"config"`
Verbosity uint `yaml:"verbosity,omitempty" json:"verbosity" mapstructure:"verbosity"`
// -q, indicates to not show any status output to stderr (ETUI or logging UI)
Quiet bool `yaml:"quiet" json:"quiet" mapstructure:"quiet"`
Outputs []string `yaml:"output" json:"output" mapstructure:"output"` // -o, the format to use for output
OutputTemplatePath string `yaml:"output-template-path" json:"output-template-path" mapstructure:"output-template-path"` // -t template file to use for output
File string `yaml:"file" json:"file" mapstructure:"file"` // --file, the file to write report output to
CheckForAppUpdate bool `yaml:"check-for-app-update" json:"check-for-app-update" mapstructure:"check-for-app-update"` // whether to check for an application update on start up or not
Dev development `yaml:"dev" json:"dev" mapstructure:"dev"`
Log logging `yaml:"log" json:"log" mapstructure:"log"` // all logging-related options
Catalogers []string `yaml:"catalogers" json:"catalogers" mapstructure:"catalogers"`
Package pkg `yaml:"package" json:"package" mapstructure:"package"`
FileMetadata FileMetadata `yaml:"file-metadata" json:"file-metadata" mapstructure:"file-metadata"`
FileClassification fileClassification `yaml:"file-classification" json:"file-classification" mapstructure:"file-classification"`
FileContents fileContents `yaml:"file-contents" json:"file-contents" mapstructure:"file-contents"`
Secrets secrets `yaml:"secrets" json:"secrets" mapstructure:"secrets"`
Registry registry `yaml:"registry" json:"registry" mapstructure:"registry"`
Exclusions []string `yaml:"exclude" json:"exclude" mapstructure:"exclude"`
Attest attest `yaml:"attest" json:"attest" mapstructure:"attest"`
Platform string `yaml:"platform" json:"platform" mapstructure:"platform"`
Name string `yaml:"name" json:"name" mapstructure:"name"`
}
func (cfg Application) ToCatalogerConfig() cataloger.Config {
return cataloger.Config{
Search: cataloger.SearchConfig{
IncludeIndexedArchives: cfg.Package.SearchIndexedArchives,
IncludeUnindexedArchives: cfg.Package.SearchUnindexedArchives,
Scope: cfg.Package.Cataloger.ScopeOpt,
},
Catalogers: cfg.Catalogers,
}
}
func (cfg *Application) LoadAllValues(v *viper.Viper, configPath string) error {
// priority order: viper.Set, flag, env, config, kv, defaults
// flags have already been loaded into viper by command construction
// check if user specified config; otherwise read all possible paths
if err := loadConfig(v, configPath); err != nil {
var notFound *viper.ConfigFileNotFoundError
if errors.As(err, &notFound) {
log.Debugf("no config file found, using defaults")
} else {
return fmt.Errorf("unable to load config: %w", err)
}
}
// load default config values into viper
loadDefaultValues(v)
// load environment variables
v.SetEnvPrefix(internal.ApplicationName)
v.AllowEmptyEnv(true)
v.AutomaticEnv()
// unmarshal fully populated viper object onto config
err := v.Unmarshal(cfg)
if err != nil {
return err
}
// Convert all populated config options to their internal application values ex: scope string => scopeOpt source.Scope
return cfg.parseConfigValues()
}
func (cfg *Application) parseConfigValues() error {
// parse options on this struct
var catalogers []string
for _, c := range cfg.Catalogers {
for _, f := range strings.Split(c, ",") {
catalogers = append(catalogers, strings.TrimSpace(f))
}
}
sort.Strings(catalogers)
cfg.Catalogers = catalogers
// parse application config options
for _, optionFn := range []func() error{
cfg.parseLogLevelOption,
cfg.parseFile,
} {
if err := optionFn(); err != nil {
return err
}
}
// parse nested config options
// for each field in the configuration struct, see if the field implements the parser interface
// note: the app config is a pointer, so we need to grab the elements explicitly (to traverse the address)
value := reflect.ValueOf(cfg).Elem()
for i := 0; i < value.NumField(); i++ {
// note: since the interface method of parser is a pointer receiver we need to get the value of the field as a pointer.
if parsable, ok := value.Field(i).Addr().Interface().(parser); ok {
// the field implements parser, call it
if err := parsable.parseConfigValues(); err != nil {
return err
}
}
}
return nil
}
func (cfg *Application) parseLogLevelOption() error {
switch {
case cfg.Quiet:
// TODO: this is bad: quiet option trumps all other logging options (such as to a file on disk)
// we should be able to quiet the console logging and leave file logging alone...
// ... this will be an enhancement for later
cfg.Log.Level = logger.DisabledLevel
case cfg.Verbosity > 0:
cfg.Log.Level = logger.LevelFromVerbosity(int(cfg.Verbosity), logger.WarnLevel, logger.InfoLevel, logger.DebugLevel, logger.TraceLevel)
case cfg.Log.Level != "":
var err error
cfg.Log.Level, err = logger.LevelFromString(string(cfg.Log.Level))
if err != nil {
return err
}
if logger.IsVerbose(cfg.Log.Level) {
cfg.Verbosity = 1
}
default:
cfg.Log.Level = logger.WarnLevel
}
return nil
}
func (cfg *Application) parseFile() error {
if cfg.File != "" {
expandedPath, err := homedir.Expand(cfg.File)
if err != nil {
return fmt.Errorf("unable to expand file path=%q: %w", cfg.File, err)
}
cfg.File = expandedPath
}
return nil
}
// init loads the default configuration values into the viper instance (before the config values are read and parsed).
func loadDefaultValues(v *viper.Viper) {
// set the default values for primitive fields in this struct
v.SetDefault("quiet", false)
v.SetDefault("check-for-app-update", true)
v.SetDefault("catalogers", nil)
// for each field in the configuration struct, see if the field implements the defaultValueLoader interface and invoke it if it does
value := reflect.ValueOf(Application{})
for i := 0; i < value.NumField(); i++ {
// note: the defaultValueLoader method receiver is NOT a pointer receiver.
if loadable, ok := value.Field(i).Interface().(defaultValueLoader); ok {
// the field implements defaultValueLoader, call it
loadable.loadDefaultValues(v)
}
}
}
func (cfg Application) String() string {
// yaml is pretty human friendly (at least when compared to json)
appaStr, err := yaml.Marshal(&cfg)
if err != nil {
return err.Error()
}
return string(appaStr)
}
func loadConfig(v *viper.Viper, configPath string) error {
var err error
// use explicitly the given user config
if configPath != "" {
v.SetConfigFile(configPath)
if err := v.ReadInConfig(); err != nil {
return fmt.Errorf("unable to read application config=%q : %w", configPath, err)
}
v.Set("config", v.ConfigFileUsed())
// don't fall through to other options if the config path was explicitly provided
return nil
}
// start searching for valid configs in order...
// 1. look for .<appname>.yaml (in the current directory)
v.AddConfigPath(".")
v.SetConfigName("." + internal.ApplicationName)
if err = v.ReadInConfig(); err == nil {
v.Set("config", v.ConfigFileUsed())
return nil
} else if !errors.As(err, &viper.ConfigFileNotFoundError{}) {
return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err)
}
// 2. look for .<appname>/config.yaml (in the current directory)
v.AddConfigPath("." + internal.ApplicationName)
v.SetConfigName("config")
if err = v.ReadInConfig(); err == nil {
v.Set("config", v.ConfigFileUsed())
return nil
} else if !errors.As(err, &viper.ConfigFileNotFoundError{}) {
return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err)
}
// 3. look for ~/.<appname>.yaml
home, err := homedir.Dir()
if err == nil {
v.AddConfigPath(home)
v.SetConfigName("." + internal.ApplicationName)
if err = v.ReadInConfig(); err == nil {
v.Set("config", v.ConfigFileUsed())
return nil
} else if !errors.As(err, &viper.ConfigFileNotFoundError{}) {
return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err)
}
}
// 4. look for <appname>/config.yaml in xdg locations (starting with xdg home config dir, then moving upwards)
v.AddConfigPath(path.Join(xdg.ConfigHome, internal.ApplicationName))
for _, dir := range xdg.ConfigDirs {
v.AddConfigPath(path.Join(dir, internal.ApplicationName))
}
v.SetConfigName("config")
if err = v.ReadInConfig(); err == nil {
v.Set("config", v.ConfigFileUsed())
return nil
} else if !errors.As(err, &viper.ConfigFileNotFoundError{}) {
return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err)
}
return nil
}
-56
View File
@@ -1,56 +0,0 @@
package config
import (
"fmt"
"os"
"github.com/mitchellh/go-homedir"
"github.com/sigstore/cosign/cmd/cosign/cli/options"
"github.com/spf13/viper"
)
// IMPORTANT: do not show the password in any YAML/JSON output (sensitive information)
type attest struct {
KeyRef string `yaml:"key" json:"key" mapstructure:"key"` // same as --key, file path to the private key
Cert string `yaml:"cert" json:"cert" mapstructure:"cert"`
NoUpload bool `yaml:"no_upload" json:"noUpload" mapstructure:"no_upload"`
Force bool `yaml:"force" json:"force" mapstructure:"force"`
Recursive bool `yaml:"recursive" json:"recursive" mapstructure:"recursive"`
Replace bool `yaml:"replace" json:"replace" mapstructure:"replace"`
Password string `yaml:"-" json:"-" mapstructure:"password"` // password for the private key
FulcioURL string `yaml:"fulcio_url" json:"fulcioUrl" mapstructure:"fulcio_url"`
FulcioIdentityToken string `yaml:"fulcio_identity_token" json:"fulcio_identity_token" mapstructure:"fulcio_identity_token"`
InsecureSkipFulcioVerify bool `yaml:"insecure_skip_verify" json:"insecure_skip_verify" mapstructure:"insecure_skip_verify"`
RekorURL string `yaml:"rekor_url" json:"rekorUrl" mapstructure:"rekor_url"`
OIDCIssuer string `yaml:"oidc_issuer" json:"oidcIssuer" mapstructure:"oidc_issuer"`
OIDCClientID string `yaml:"oidc_client_id" json:"oidcClientId" mapstructure:"oidc_client_id"`
OIDCRedirectURL string `yaml:"oidc_redirect_url" json:"OIDCRedirectURL" mapstructure:"oidc_redirect_url"`
}
func (cfg *attest) parseConfigValues() error {
if cfg.KeyRef != "" {
expandedPath, err := homedir.Expand(cfg.KeyRef)
if err != nil {
return fmt.Errorf("unable to expand key path=%q: %w", cfg.KeyRef, err)
}
cfg.KeyRef = expandedPath
}
if cfg.Password == "" {
// we allow for configuration via syft config/env vars and additionally interop with known cosign config env vars
if pw, ok := os.LookupEnv("COSIGN_PASSWORD"); ok {
cfg.Password = pw
}
}
return nil
}
func (cfg attest) loadDefaultValues(v *viper.Viper) {
v.SetDefault("attest.key", "")
v.SetDefault("attest.password", "")
v.SetDefault("attest.fulcio_url", options.DefaultFulcioURL)
v.SetDefault("attest.rekor_url", options.DefaultRekorURL)
v.SetDefault("attest.oidc_issuer", options.DefaultOIDCIssuerURL)
v.SetDefault("attest.oidc_client_id", "sigstore")
}
-29
View File
@@ -1,29 +0,0 @@
package config
import (
"fmt"
"github.com/spf13/viper"
"github.com/anchore/syft/syft/source"
)
type catalogerOptions struct {
Enabled bool `yaml:"enabled" json:"enabled" mapstructure:"enabled"`
Scope string `yaml:"scope" json:"scope" mapstructure:"scope"`
ScopeOpt source.Scope `yaml:"-" json:"-"`
}
func (cfg catalogerOptions) loadDefaultValues(v *viper.Viper) {
v.SetDefault("package.cataloger.enabled", true)
}
func (cfg *catalogerOptions) parseConfigValues() error {
scopeOption := source.ParseScope(cfg.Scope)
if scopeOption == source.UnknownScope {
return fmt.Errorf("bad scope value %q", cfg.Scope)
}
cfg.ScopeOpt = scopeOption
return nil
}
+182
View File
@@ -0,0 +1,182 @@
package config
import (
"fmt"
"path"
"strings"
"github.com/adrg/xdg"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/presenter"
"github.com/anchore/syft/syft/scope"
"github.com/mitchellh/go-homedir"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
)
type CliOnlyOptions struct {
ConfigPath string
Verbosity int
}
type Application struct {
ConfigPath string
PresenterOpt presenter.Option
Output string `mapstructure:"output"`
ScopeOpt scope.Option
Scope string `mapstructure:"scope"`
Quiet bool `mapstructure:"quiet"`
Log Logging `mapstructure:"log"`
CliOptions CliOnlyOptions
CheckForAppUpdate bool `mapstructure:"check-for-app-update"`
Plugins Plugins `mapstructure:"plugins"`
}
type Plugins struct {
Enabled bool `mapstructure:"enabled"`
Directory string `mapstructure:"dir"`
}
type Logging struct {
Structured bool `mapstructure:"structured"`
LevelOpt logrus.Level
Level string `mapstructure:"level"`
FileLocation string `mapstructure:"file"`
}
func setNonCliDefaultValues(v *viper.Viper) {
v.SetDefault("log.level", "")
v.SetDefault("log.file", "")
v.SetDefault("log.structured", false)
v.SetDefault("check-for-app-update", true)
}
func LoadConfigFromFile(v *viper.Viper, cliOpts *CliOnlyOptions) (*Application, error) {
// the user may not have a config, and this is OK, we can use the default config + default cobra cli values instead
setNonCliDefaultValues(v)
if cliOpts != nil {
_ = readConfig(v, cliOpts.ConfigPath)
} else {
_ = readConfig(v, "")
}
config := &Application{
CliOptions: *cliOpts,
}
err := v.Unmarshal(config)
if err != nil {
return nil, fmt.Errorf("unable to parse config: %w", err)
}
config.ConfigPath = v.ConfigFileUsed()
err = config.Build()
if err != nil {
return nil, fmt.Errorf("invalid config: %w", err)
}
return config, nil
}
func (cfg *Application) Build() error {
// set the presenter
presenterOption := presenter.ParseOption(cfg.Output)
if presenterOption == presenter.UnknownPresenter {
return fmt.Errorf("bad --output value '%s'", cfg.Output)
}
cfg.PresenterOpt = presenterOption
// set the scope
scopeOption := scope.ParseOption(cfg.Scope)
if scopeOption == scope.UnknownScope {
return fmt.Errorf("bad --scope value '%s'", cfg.Scope)
}
cfg.ScopeOpt = scopeOption
if cfg.Quiet {
// TODO: this is bad: quiet option trumps all other logging options
// we should be able to quiet the console logging and leave file logging alone...
// ... this will be an enhancement for later
cfg.Log.LevelOpt = logrus.PanicLevel
} else {
if cfg.Log.Level != "" {
if cfg.CliOptions.Verbosity > 0 {
return fmt.Errorf("cannot explicitly set log level (cfg file or env var) and use -v flag together")
}
lvl, err := logrus.ParseLevel(strings.ToLower(cfg.Log.Level))
if err != nil {
return fmt.Errorf("bad log level configured (%q): %w", cfg.Log.Level, err)
}
// set the log level explicitly
cfg.Log.LevelOpt = lvl
} else {
// set the log level implicitly
switch v := cfg.CliOptions.Verbosity; {
case v == 1:
cfg.Log.LevelOpt = logrus.InfoLevel
case v >= 2:
cfg.Log.LevelOpt = logrus.DebugLevel
default:
cfg.Log.LevelOpt = logrus.ErrorLevel
}
}
}
return nil
}
func readConfig(v *viper.Viper, configPath string) error {
v.AutomaticEnv()
v.SetEnvPrefix(internal.ApplicationName)
// allow for nested options to be specified via environment variables
// e.g. pod.context = APPNAME_POD_CONTEXT
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
// use explicitly the given user config
if configPath != "" {
v.SetConfigFile(configPath)
if err := v.ReadInConfig(); err == nil {
return nil
}
// don't fall through to other options if this fails
return fmt.Errorf("unable to read config: %v", configPath)
}
// start searching for valid configs in order...
// 1. look for .<appname>.yaml (in the current directory)
v.AddConfigPath(".")
v.SetConfigName(internal.ApplicationName)
if err := v.ReadInConfig(); err == nil {
return nil
}
// 2. look for .<appname>/config.yaml (in the current directory)
v.AddConfigPath("." + internal.ApplicationName)
v.SetConfigName("config")
if err := v.ReadInConfig(); err == nil {
return nil
}
// 3. look for ~/.<appname>.yaml
home, err := homedir.Dir()
if err == nil {
v.AddConfigPath(home)
v.SetConfigName("." + internal.ApplicationName)
if err := v.ReadInConfig(); err == nil {
return nil
}
}
// 4. look for <appname>/config.yaml in xdg locations (starting with xdg home config dir, then moving upwards)
v.AddConfigPath(path.Join(xdg.ConfigHome, internal.ApplicationName))
for _, dir := range xdg.ConfigDirs {
v.AddConfigPath(path.Join(dir, internal.ApplicationName))
}
v.SetConfigName("config")
if err := v.ReadInConfig(); err == nil {
return nil
}
return fmt.Errorf("application config not found")
}
-13
View File
@@ -1,13 +0,0 @@
package config
import "github.com/spf13/viper"
type development struct {
ProfileCPU bool `yaml:"profile-cpu" json:"profile-cpu" mapstructure:"profile-cpu"`
ProfileMem bool `yaml:"profile-mem" json:"profile-mem" mapstructure:"profile-mem"`
}
func (cfg development) loadDefaultValues(v *viper.Viper) {
v.SetDefault("dev.profile-cpu", false)
v.SetDefault("dev.profile-mem", false)
}
-20
View File
@@ -1,20 +0,0 @@
package config
import (
"github.com/spf13/viper"
"github.com/anchore/syft/syft/source"
)
type fileClassification struct {
Cataloger catalogerOptions `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"`
}
func (cfg fileClassification) loadDefaultValues(v *viper.Viper) {
v.SetDefault("file-classification.cataloger.enabled", catalogerEnabledDefault)
v.SetDefault("file-classification.cataloger.scope", source.SquashedScope)
}
func (cfg *fileClassification) parseConfigValues() error {
return cfg.Cataloger.parseConfigValues()
}
-25
View File
@@ -1,25 +0,0 @@
package config
import (
"github.com/spf13/viper"
"github.com/anchore/syft/internal/file"
"github.com/anchore/syft/syft/source"
)
type fileContents struct {
Cataloger catalogerOptions `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"`
SkipFilesAboveSize int64 `yaml:"skip-files-above-size" json:"skip-files-above-size" mapstructure:"skip-files-above-size"`
Globs []string `yaml:"globs" json:"globs" mapstructure:"globs"`
}
func (cfg fileContents) loadDefaultValues(v *viper.Viper) {
v.SetDefault("file-contents.cataloger.enabled", catalogerEnabledDefault)
v.SetDefault("file-contents.cataloger.scope", source.SquashedScope)
v.SetDefault("file-contents.skip-files-above-size", 1*file.MB)
v.SetDefault("file-contents.globs", []string{})
}
func (cfg *fileContents) parseConfigValues() error {
return cfg.Cataloger.parseConfigValues()
}
-22
View File
@@ -1,22 +0,0 @@
package config
import (
"github.com/spf13/viper"
"github.com/anchore/syft/syft/source"
)
type FileMetadata struct {
Cataloger catalogerOptions `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"`
Digests []string `yaml:"digests" json:"digests" mapstructure:"digests"`
}
func (cfg FileMetadata) loadDefaultValues(v *viper.Viper) {
v.SetDefault("file-metadata.cataloger.enabled", catalogerEnabledDefault)
v.SetDefault("file-metadata.cataloger.scope", source.SquashedScope)
v.SetDefault("file-metadata.digests", []string{"sha256"})
}
func (cfg *FileMetadata) parseConfigValues() error {
return cfg.Cataloger.parseConfigValues()
}
-34
View File
@@ -1,34 +0,0 @@
package config
import (
"fmt"
"github.com/mitchellh/go-homedir"
"github.com/spf13/viper"
"github.com/anchore/go-logger"
)
// logging contains all logging-related configuration options available to the user via the application config.
type logging struct {
Structured bool `yaml:"structured" json:"structured" mapstructure:"structured"` // show all log entries as JSON formatted strings
Level logger.Level `yaml:"level" json:"level" mapstructure:"level"` // the log level string hint
FileLocation string `yaml:"file" json:"file-location" mapstructure:"file"` // the file path to write logs to
}
func (cfg *logging) parseConfigValues() error {
if cfg.FileLocation != "" {
expandedPath, err := homedir.Expand(cfg.FileLocation)
if err != nil {
return fmt.Errorf("unable to expand log file path=%q: %w", cfg.FileLocation, err)
}
cfg.FileLocation = expandedPath
}
return nil
}
func (cfg logging) loadDefaultValues(v *viper.Viper) {
v.SetDefault("log.structured", false)
v.SetDefault("log.file", "")
v.SetDefault("log.level", string(logger.WarnLevel))
}
-24
View File
@@ -1,24 +0,0 @@
package config
import (
"github.com/spf13/viper"
"github.com/anchore/syft/syft/pkg/cataloger"
)
type pkg struct {
Cataloger catalogerOptions `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"`
SearchUnindexedArchives bool `yaml:"search-unindexed-archives" json:"search-unindexed-archives" mapstructure:"search-unindexed-archives"`
SearchIndexedArchives bool `yaml:"search-indexed-archives" json:"search-indexed-archives" mapstructure:"search-indexed-archives"`
}
func (cfg pkg) loadDefaultValues(v *viper.Viper) {
cfg.Cataloger.loadDefaultValues(v)
c := cataloger.DefaultSearchConfig()
v.SetDefault("package.search-unindexed-archives", c.IncludeUnindexedArchives)
v.SetDefault("package.search-indexed-archives", c.IncludeIndexedArchives)
}
func (cfg *pkg) parseConfigValues() error {
return cfg.Cataloger.parseConfigValues()
}
-75
View File
@@ -1,75 +0,0 @@
package config
import (
"os"
"github.com/spf13/viper"
"github.com/anchore/stereoscope/pkg/image"
)
type RegistryCredentials struct {
Authority string `yaml:"authority" json:"authority" mapstructure:"authority"`
// IMPORTANT: do not show the username in any YAML/JSON output (sensitive information)
Username string `yaml:"-" json:"-" mapstructure:"username"`
// IMPORTANT: do not show the password in any YAML/JSON output (sensitive information)
Password string `yaml:"-" json:"-" mapstructure:"password"`
// IMPORTANT: do not show the token in any YAML/JSON output (sensitive information)
Token string `yaml:"-" json:"-" mapstructure:"token"`
}
type registry struct {
InsecureSkipTLSVerify bool `yaml:"insecure-skip-tls-verify" json:"insecure-skip-tls-verify" mapstructure:"insecure-skip-tls-verify"`
InsecureUseHTTP bool `yaml:"insecure-use-http" json:"insecure-use-http" mapstructure:"insecure-use-http"`
Auth []RegistryCredentials `yaml:"auth" json:"auth" mapstructure:"auth"`
}
func (cfg registry) loadDefaultValues(v *viper.Viper) {
v.SetDefault("registry.insecure-skip-tls-verify", false)
v.SetDefault("registry.insecure-use-http", false)
v.SetDefault("registry.auth", []RegistryCredentials{})
}
//nolint:unparam
func (cfg *registry) parseConfigValues() error {
// there may be additional credentials provided by env var that should be appended to the set of credentials
authority, username, password, token :=
os.Getenv("SYFT_REGISTRY_AUTH_AUTHORITY"),
os.Getenv("SYFT_REGISTRY_AUTH_USERNAME"),
os.Getenv("SYFT_REGISTRY_AUTH_PASSWORD"),
os.Getenv("SYFT_REGISTRY_AUTH_TOKEN")
if hasNonEmptyCredentials(username, password, token) {
// note: we prepend the credentials such that the environment variables take precedence over on-disk configuration.
cfg.Auth = append([]RegistryCredentials{
{
Authority: authority,
Username: username,
Password: password,
Token: token,
},
}, cfg.Auth...)
}
return nil
}
func hasNonEmptyCredentials(username, password, token string) bool {
return password != "" && username != "" || token != ""
}
func (cfg *registry) ToOptions() *image.RegistryOptions {
var auth = make([]image.RegistryCredentials, len(cfg.Auth))
for i, a := range cfg.Auth {
auth[i] = image.RegistryCredentials{
Authority: a.Authority,
Username: a.Username,
Password: a.Password,
Token: a.Token,
}
}
return &image.RegistryOptions{
InsecureSkipTLSVerify: cfg.InsecureSkipTLSVerify,
InsecureUseHTTP: cfg.InsecureUseHTTP,
Credentials: auth,
}
}
-112
View File
@@ -1,112 +0,0 @@
package config
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/anchore/stereoscope/pkg/image"
)
func TestHasNonEmptyCredentials(t *testing.T) {
tests := []struct {
username, password, token string
expected bool
}{
{
"", "", "",
false,
},
{
"user", "", "",
false,
},
{
"", "pass", "",
false,
},
{
"", "pass", "tok",
true,
},
{
"user", "", "tok",
true,
},
{
"", "", "tok",
true,
},
{
"user", "pass", "tok",
true,
},
{
"user", "pass", "",
true,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%+v", test), func(t *testing.T) {
assert.Equal(t, test.expected, hasNonEmptyCredentials(test.username, test.password, test.token))
})
}
}
func Test_registry_ToOptions(t *testing.T) {
tests := []struct {
name string
input registry
expected image.RegistryOptions
}{
{
name: "no registry options",
input: registry{},
expected: image.RegistryOptions{
Credentials: []image.RegistryCredentials{},
},
},
{
name: "set InsecureSkipTLSVerify",
input: registry{
InsecureSkipTLSVerify: true,
},
expected: image.RegistryOptions{
InsecureSkipTLSVerify: true,
Credentials: []image.RegistryCredentials{},
},
},
{
name: "set InsecureUseHTTP",
input: registry{
InsecureUseHTTP: true,
},
expected: image.RegistryOptions{
InsecureUseHTTP: true,
Credentials: []image.RegistryCredentials{},
},
},
{
name: "set all bool options",
input: registry{
InsecureSkipTLSVerify: true,
InsecureUseHTTP: true,
},
expected: image.RegistryOptions{
InsecureSkipTLSVerify: true,
InsecureUseHTTP: true,
Credentials: []image.RegistryCredentials{},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, &test.expected, test.input.ToOptions())
})
}
}
-29
View File
@@ -1,29 +0,0 @@
package config
import (
"github.com/spf13/viper"
"github.com/anchore/syft/internal/file"
"github.com/anchore/syft/syft/source"
)
type secrets struct {
Cataloger catalogerOptions `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"`
AdditionalPatterns map[string]string `yaml:"additional-patterns" json:"additional-patterns" mapstructure:"additional-patterns"`
ExcludePatternNames []string `yaml:"exclude-pattern-names" json:"exclude-pattern-names" mapstructure:"exclude-pattern-names"`
RevealValues bool `yaml:"reveal-values" json:"reveal-values" mapstructure:"reveal-values"`
SkipFilesAboveSize int64 `yaml:"skip-files-above-size" json:"skip-files-above-size" mapstructure:"skip-files-above-size"`
}
func (cfg secrets) loadDefaultValues(v *viper.Viper) {
v.SetDefault("secrets.cataloger.enabled", catalogerEnabledDefault)
v.SetDefault("secrets.cataloger.scope", source.AllLayersScope)
v.SetDefault("secrets.reveal-values", false)
v.SetDefault("secrets.skip-files-above-size", 1*file.MB)
v.SetDefault("secrets.additional-patterns", map[string]string{})
v.SetDefault("secrets.exclude-pattern-names", []string{})
}
func (cfg *secrets) parseConfigValues() error {
return cfg.Cataloger.parseConfigValues()
}
+2 -8
View File
@@ -1,10 +1,4 @@
package internal
const (
// ApplicationName is the non-capitalized name of the application (do not change this)
ApplicationName = "syft"
// JSONSchemaVersion is the current schema version output by the JSON encoder
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
JSONSchemaVersion = "6.0.0"
)
// note: do not change this
const ApplicationName = "syft"
-4
View File
@@ -1,4 +0,0 @@
/*
Package internal contains miscellaneous functions and objects useful within syft but should not be used externally.
*/
package internal
-40
View File
@@ -1,40 +0,0 @@
package internal
import (
"errors"
"fmt"
"io"
"os"
"github.com/anchore/syft/internal/log"
)
// CloseAndLogError closes the given io.Closer and reports any errors found as a warning in the log
func CloseAndLogError(closer io.Closer, location string) {
if err := closer.Close(); err != nil {
log.Warnf("unable to close file for location=%q: %+v", location, err)
}
}
type ErrPath struct {
Context string
Path string
Err error
}
func (e ErrPath) Error() string {
return fmt.Sprintf("%s unable to observe contents of %+v: %v", e.Context, e.Path, e.Err)
}
func IsErrPath(err error) bool {
var pathErr ErrPath
return errors.As(err, &pathErr)
}
func IsErrPathPermission(err error) bool {
var pathErr ErrPath
if errors.As(err, &pathErr) {
return os.IsPermission(pathErr.Err)
}
return false
}
-19
View File
@@ -1,19 +0,0 @@
package file
import (
"errors"
"fmt"
"io"
)
const perFileReadLimit = 2 * GB
// safeCopy limits the copy from the reader. This is useful when extracting files from archives to
// protect against decompression bomb attacks.
func safeCopy(writer io.Writer, reader io.Reader) error {
numBytes, err := io.Copy(writer, io.LimitReader(reader, perFileReadLimit))
if numBytes >= perFileReadLimit || errors.Is(err, io.EOF) {
return fmt.Errorf("zip read limit hit (potential decompression bomb attack)")
}
return nil
}
-1
View File
@@ -1,6 +1,5 @@
package file
// GlobMatch evaluates the given glob pattern against the given "name" string, indicating if there is a match or not.
// Source: https://research.swtch.com/glob.go
func GlobMatch(pattern, name string) bool {
px := 0
-16
View File
@@ -1,16 +0,0 @@
package file
import (
"io"
"os"
)
// Opener is an object that stores a path to later be opened as a file.
type Opener struct {
path string
}
// Open the stored path as a io.ReadCloser.
func (o Opener) Open() (io.ReadCloser, error) {
return os.Open(o.path)
}
-64
View File
@@ -1,64 +0,0 @@
package file
import (
"fmt"
"os"
"path/filepath"
"github.com/bmatcuk/doublestar/v4"
"github.com/mholt/archiver/v3"
)
// ExtractGlobsFromTarToUniqueTempFile extracts paths matching the given globs within the given archive to a temporary directory, returning file openers for each file extracted.
func ExtractGlobsFromTarToUniqueTempFile(archivePath, dir string, globs ...string) (map[string]Opener, error) {
results := make(map[string]Opener)
// don't allow for full traversal, only select traversal from given paths
if len(globs) == 0 {
return results, nil
}
visitor := func(file archiver.File) error {
defer file.Close()
// ignore directories
if file.FileInfo.IsDir() {
return nil
}
// ignore any filename that doesn't match the given globs...
if !matchesAnyGlob(file.Name(), globs...) {
return nil
}
// we have a file we want to extract....
tempfilePrefix := filepath.Base(filepath.Clean(file.Name())) + "-"
tempFile, err := os.CreateTemp(dir, tempfilePrefix)
if err != nil {
return fmt.Errorf("unable to create temp file: %w", err)
}
// we shouldn't try and keep the tempfile open as the returned result may have several files, which takes up
// resources (leading to "too many open files"). Instead we'll return a file opener to the caller which
// provides a ReadCloser. It is up to the caller to handle closing the file explicitly.
defer tempFile.Close()
if err := safeCopy(tempFile, file.ReadCloser); err != nil {
return fmt.Errorf("unable to copy source=%q for tar=%q: %w", file.Name(), archivePath, err)
}
results[file.Name()] = Opener{path: tempFile.Name()}
return nil
}
return results, archiver.Walk(archivePath, visitor)
}
func matchesAnyGlob(name string, globs ...string) bool {
for _, glob := range globs {
if matches, err := doublestar.PathMatch(glob, name); err == nil && matches {
return true
}
}
return false
}
@@ -1,12 +0,0 @@
#!/usr/bin/env bash
set -eux
# $1 —— absolute path to destination file, should end with .zip, ideally
# $2 —— absolute path to directory from which to add entries to the archive
# $3 —— if files should be zip64 or not
if [[$3]]; then
pushd "$2" && find . -print | zip -fz "$1" -@ && popd
else
pushd "$2" && find . -print | zip "$1" -@ && popd
fi
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -eux
zip -r "$1" zip-source
-119
View File
@@ -1,119 +0,0 @@
package file
import (
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"
"syscall"
"testing"
"github.com/stretchr/testify/assert"
)
var expectedZipArchiveEntries = []string{
"some-dir" + string(os.PathSeparator),
filepath.Join("some-dir", "a-file.txt"),
"b-file.txt",
"nested.zip",
}
// createZipArchive creates a new ZIP archive file at destinationArchivePath based on the directory found at
// sourceDirPath. It forces a zip64 archive if zip64 is "0".
func createZipArchive(t testing.TB, sourceDirPath, destinationArchivePath string, zip64 bool) {
t.Helper()
cwd, err := os.Getwd()
if err != nil {
t.Fatalf("unable to get cwd: %+v", err)
}
zip64Arg := "0"
if zip64 {
zip64Arg = "1"
}
cmd := exec.Command("./generate-zip-fixture-from-source-dir.sh", destinationArchivePath, path.Base(sourceDirPath), zip64Arg)
cmd.Dir = filepath.Join(cwd, "test-fixtures")
if err := cmd.Start(); err != nil {
t.Fatalf("unable to start generate zip fixture script: %+v", err)
}
if err := cmd.Wait(); err != nil {
if exiterr, ok := err.(*exec.ExitError); ok {
// The program has exited with an exit code != 0
// This works on both Unix and Windows. Although package
// syscall is generally platform dependent, WaitStatus is
// defined for both Unix and Windows and in both cases has
// an ExitStatus() method with the same signature.
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
if status.ExitStatus() != 0 {
t.Fatalf("failed to generate fixture: rc=%d", status.ExitStatus())
}
}
} else {
t.Fatalf("unable to get generate fixture script result: %+v", err)
}
}
}
func assertNoError(t testing.TB, fn func() error) func() {
return func() {
assert.NoError(t, fn())
}
}
// setupZipFileTest encapsulates common test setup work for zip file tests. It returns a cleanup function,
// which should be called (typically deferred) by the caller, the path of the created zip archive, and an error,
// which should trigger a fatal test failure in the consuming test. The returned cleanup function will never be nil
// (even if there's an error), and it should always be called.
func setupZipFileTest(t testing.TB, sourceDirPath string, zip64 bool) string {
t.Helper()
archivePrefix, err := ioutil.TempFile("", "syft-ziputil-archive-TEST-")
if err != nil {
t.Fatalf("unable to create tempfile: %+v", err)
}
t.Cleanup(
assertNoError(t,
func() error {
return os.Remove(archivePrefix.Name())
},
),
)
destinationArchiveFilePath := archivePrefix.Name() + ".zip"
t.Logf("archive path: %s", destinationArchiveFilePath)
createZipArchive(t, sourceDirPath, destinationArchiveFilePath, zip64)
t.Cleanup(
assertNoError(t,
func() error {
return os.Remove(destinationArchiveFilePath)
},
),
)
cwd, err := os.Getwd()
if err != nil {
t.Fatalf("unable to get cwd: %+v", err)
}
t.Logf("running from: %s", cwd)
return destinationArchiveFilePath
}
// TODO: Consider moving any non-git asset generation to a task (e.g. make) that's run ahead of running go tests.
func ensureNestedZipExists(t *testing.T, sourceDirPath string) error {
t.Helper()
nestedArchiveFilePath := path.Join(sourceDirPath, "nested.zip")
createZipArchive(t, sourceDirPath, nestedArchiveFilePath, false)
return nil
}
+20 -32
View File
@@ -1,54 +1,32 @@
package file
import (
"archive/zip"
"fmt"
"os"
"sort"
"strings"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
)
// ZipFileManifest is a collection of paths and their file metadata.
type ZipFileManifest map[string]os.FileInfo
// NewZipFileManifest creates and returns a new ZipFileManifest populated with path and metadata from the given zip archive path.
func NewZipFileManifest(archivePath string) (ZipFileManifest, error) {
zipReader, err := OpenZip(archivePath)
manifest := make(ZipFileManifest)
if err != nil {
return manifest, fmt.Errorf("unable to open zip archive (%s): %w", archivePath, err)
}
defer func() {
err = zipReader.Close()
if err != nil {
log.Warnf("unable to close zip archive (%s): %+v", archivePath, err)
}
}()
for _, file := range zipReader.Reader.File {
manifest.Add(file.Name, file.FileInfo())
}
return manifest, nil
func newZipManifest() ZipFileManifest {
return make(ZipFileManifest)
}
// Add a new path and it's file metadata to the collection.
func (z ZipFileManifest) Add(entry string, info os.FileInfo) {
z[entry] = info
}
// GlobMatch returns the path keys that match the given value(s).
func (z ZipFileManifest) GlobMatch(patterns ...string) []string {
uniqueMatches := internal.NewStringSet()
for _, pattern := range patterns {
for entry := range z {
// We want to match globs as if entries begin with a leading slash (akin to an absolute path)
// so that glob logic is consistent inside and outside of ZIP archives
normalizedEntry := normalizeZipEntryName(entry)
if GlobMatch(pattern, normalizedEntry) {
if GlobMatch(pattern, entry) {
uniqueMatches.Add(entry)
}
}
@@ -60,11 +38,21 @@ func (z ZipFileManifest) GlobMatch(patterns ...string) []string {
return results
}
// normalizeZipEntryName takes the given path entry and ensures it is prefixed with "/".
func normalizeZipEntryName(entry string) string {
if !strings.HasPrefix(entry, "/") {
return "/" + entry
func NewZipFileManifest(archivePath string) (ZipFileManifest, error) {
zipReader, err := zip.OpenReader(archivePath)
manifest := newZipManifest()
if err != nil {
return manifest, fmt.Errorf("unable to open zip archive (%s): %w", archivePath, err)
}
defer func() {
err = zipReader.Close()
if err != nil {
log.Errorf("unable to close zip archive (%s): %w", archivePath, err)
}
}()
return entry
for _, file := range zipReader.Reader.File {
manifest.Add(file.Name, file.FileInfo())
}
return manifest, nil
}
-138
View File
@@ -1,138 +0,0 @@
//go:build !windows
// +build !windows
package file
import (
"encoding/json"
"os"
"path"
"testing"
)
func TestNewZipFileManifest(t *testing.T) {
cwd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
sourceDirPath := path.Join(cwd, "test-fixtures", "zip-source")
err = ensureNestedZipExists(t, sourceDirPath)
if err != nil {
t.Fatal(err)
}
archiveFilePath := setupZipFileTest(t, sourceDirPath, false)
actual, err := NewZipFileManifest(archiveFilePath)
if err != nil {
t.Fatalf("unable to extract from unzip archive: %+v", err)
}
if len(expectedZipArchiveEntries) != len(actual) {
t.Fatalf("mismatched manifest: %d != %d", len(actual), len(expectedZipArchiveEntries))
}
for _, e := range expectedZipArchiveEntries {
_, ok := actual[e]
if !ok {
t.Errorf("missing path: %s", e)
}
}
if t.Failed() {
b, err := json.MarshalIndent(actual, "", " ")
if err != nil {
t.Fatalf("can't show results: %+v", err)
}
t.Errorf("full result: %s", string(b))
}
}
func TestNewZip64FileManifest(t *testing.T) {
cwd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
sourceDirPath := path.Join(cwd, "test-fixtures", "zip-source")
archiveFilePath := setupZipFileTest(t, sourceDirPath, true)
actual, err := NewZipFileManifest(archiveFilePath)
if err != nil {
t.Fatalf("unable to extract from unzip archive: %+v", err)
}
if len(expectedZipArchiveEntries) != len(actual) {
t.Fatalf("mismatched manifest: %d != %d", len(actual), len(expectedZipArchiveEntries))
}
for _, e := range expectedZipArchiveEntries {
_, ok := actual[e]
if !ok {
t.Errorf("missing path: %s", e)
}
}
if t.Failed() {
b, err := json.MarshalIndent(actual, "", " ")
if err != nil {
t.Fatalf("can't show results: %+v", err)
}
t.Errorf("full result: %s", string(b))
}
}
func TestZipFileManifest_GlobMatch(t *testing.T) {
cwd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
sourceDirPath := path.Join(cwd, "test-fixtures", "zip-source")
err = ensureNestedZipExists(t, sourceDirPath)
if err != nil {
t.Fatal(err)
}
archiveFilePath := setupZipFileTest(t, sourceDirPath, false)
z, err := NewZipFileManifest(archiveFilePath)
if err != nil {
t.Fatalf("unable to extract from unzip archive: %+v", err)
}
cases := []struct {
glob string
expected string
}{
{
"/b*",
"b-file.txt",
},
{
"*/a-file.txt",
"some-dir/a-file.txt",
},
{
"**/*.zip",
"nested.zip",
},
}
for _, tc := range cases {
t.Run(tc.glob, func(t *testing.T) {
glob := tc.glob
results := z.GlobMatch(glob)
if len(results) == 1 && results[0] == tc.expected {
return
}
t.Errorf("unexpected results for glob '%s': %+v", glob, results)
})
}
}
+48 -50
View File
@@ -3,7 +3,10 @@ package file
import (
"archive/zip"
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
@@ -12,21 +15,13 @@ import (
)
const (
// represents the order of bytes
_ = iota
KB = 1 << (10 * iota)
MB
GB
)
type errZipSlipDetected struct {
Prefix string
JoinArgs []string
}
func (e *errZipSlipDetected) Error() string {
return fmt.Sprintf("paths are not allowed to resolve outside of the root prefix (%q). Destination: %q", e.Prefix, e.JoinArgs)
}
const perFileReadLimit = 2 * GB
type zipTraversalRequest map[string]struct{}
@@ -38,18 +33,17 @@ func newZipTraverseRequest(paths ...string) zipTraversalRequest {
return results
}
// TraverseFilesInZip enumerates all paths stored within a zip archive using the visitor pattern.
func TraverseFilesInZip(archivePath string, visitor func(*zip.File) error, paths ...string) error {
request := newZipTraverseRequest(paths...)
zipReader, err := OpenZip(archivePath)
zipReader, err := zip.OpenReader(archivePath)
if err != nil {
return fmt.Errorf("unable to open zip archive (%s): %w", archivePath, err)
}
defer func() {
err = zipReader.Close()
if err != nil {
log.Errorf("unable to close zip archive (%s): %+v", archivePath, err)
log.Errorf("unable to close zip archive (%s): %w", archivePath, err)
}
}()
@@ -69,9 +63,8 @@ func TraverseFilesInZip(archivePath string, visitor func(*zip.File) error, paths
return nil
}
// ExtractFromZipToUniqueTempFile extracts select paths for the given archive to a temporary directory, returning file openers for each file extracted.
func ExtractFromZipToUniqueTempFile(archivePath, dir string, paths ...string) (map[string]Opener, error) {
results := make(map[string]Opener)
func ExtractFromZipToUniqueTempFile(archivePath, dir string, paths ...string) (map[string]io.Reader, error) {
results := make(map[string]io.Reader)
// don't allow for full traversal, only select traversal from given paths
if len(paths) == 0 {
@@ -81,43 +74,47 @@ func ExtractFromZipToUniqueTempFile(archivePath, dir string, paths ...string) (m
visitor := func(file *zip.File) error {
tempfilePrefix := filepath.Base(filepath.Clean(file.Name)) + "-"
tempFile, err := os.CreateTemp(dir, tempfilePrefix)
tempFile, err := ioutil.TempFile(dir, tempfilePrefix)
if err != nil {
return fmt.Errorf("unable to create temp file: %w", err)
}
// we shouldn't try and keep the tempfile open as the returned result may have several files, which takes up
// resources (leading to "too many open files"). Instead we'll return a file opener to the caller which
// provides a ReadCloser. It is up to the caller to handle closing the file explicitly.
defer tempFile.Close()
zippedFile, err := file.Open()
if err != nil {
return fmt.Errorf("unable to read file=%q from zip=%q: %w", file.Name, archivePath, err)
}
defer func() {
err := zippedFile.Close()
if err != nil {
log.Errorf("unable to close source file=%q from zip=%q: %+v", file.Name, archivePath, err)
}
}()
if file.FileInfo().IsDir() {
return fmt.Errorf("unable to extract directories, only files: %s", file.Name)
}
if err := safeCopy(tempFile, zippedFile); err != nil {
// limit the zip reader on each file read to prevent decompression bomb attacks
numBytes, err := io.Copy(tempFile, io.LimitReader(zippedFile, perFileReadLimit))
if numBytes >= perFileReadLimit || errors.Is(err, io.EOF) {
return fmt.Errorf("zip read limit hit (potential decompression bomb attack)")
}
if err != nil {
return fmt.Errorf("unable to copy source=%q for zip=%q: %w", file.Name, archivePath, err)
}
results[file.Name] = Opener{path: tempFile.Name()}
// the file pointer is at the end due to the copy operation, reset back to the beginning
_, err = tempFile.Seek(0, io.SeekStart)
if err != nil {
return fmt.Errorf("unable to reset file pointer (%s): %w", tempFile.Name(), err)
}
results[file.Name] = tempFile
err = zippedFile.Close()
if err != nil {
return fmt.Errorf("unable to close source file=%q from zip=%q: %w", file.Name, archivePath, err)
}
return nil
}
return results, TraverseFilesInZip(archivePath, visitor, paths...)
}
// ContentsFromZip extracts select paths for the given archive and returns a set of string contents for each path.
func ContentsFromZip(archivePath string, paths ...string) (map[string]string, error) {
results := make(map[string]string)
@@ -137,7 +134,13 @@ func ContentsFromZip(archivePath string, paths ...string) (map[string]string, er
}
var buffer bytes.Buffer
if err := safeCopy(&buffer, zippedFile); err != nil {
// limit the zip reader on each file read to prevent decompression bomb attacks
numBytes, err := io.Copy(&buffer, io.LimitReader(zippedFile, perFileReadLimit))
if numBytes >= perFileReadLimit || errors.Is(err, io.EOF) {
return fmt.Errorf("zip read limit hit (potential decompression bomb attack)")
}
if err != nil {
return fmt.Errorf("unable to copy source=%q for zip=%q: %w", file.Name, archivePath, err)
}
@@ -153,15 +156,19 @@ func ContentsFromZip(archivePath string, paths ...string) (map[string]string, er
return results, TraverseFilesInZip(archivePath, visitor, paths...)
}
// UnzipToDir extracts a zip archive to a target directory.
func UnzipToDir(archivePath, targetDir string) error {
visitor := func(file *zip.File) error {
joinedPath, err := safeJoin(targetDir, file.Name)
if err != nil {
return err
// the zip-slip attack protection is still being erroneously detected
// nolint:gosec
expandedFilePath := filepath.Clean(filepath.Join(targetDir, file.Name))
// protect against zip slip attacks (traversing unintended parent paths from maliciously crafted relative-path entries)
if !strings.HasPrefix(expandedFilePath, filepath.Clean(targetDir)+string(os.PathSeparator)) {
return fmt.Errorf("potential zip slip attack: %q", expandedFilePath)
}
if err = extractSingleFile(file, joinedPath, archivePath); err != nil {
err := extractSingleFile(file, expandedFilePath, archivePath)
if err != nil {
return err
}
return nil
@@ -170,20 +177,6 @@ func UnzipToDir(archivePath, targetDir string) error {
return TraverseFilesInZip(archivePath, visitor)
}
// safeJoin ensures that any destinations do not resolve to a path above the prefix path.
func safeJoin(prefix string, dest ...string) (string, error) {
joinResult := filepath.Join(append([]string{prefix}, dest...)...)
cleanJoinResult := filepath.Clean(joinResult)
if !strings.HasPrefix(cleanJoinResult, filepath.Clean(prefix)) {
return "", &errZipSlipDetected{
Prefix: prefix,
JoinArgs: dest,
}
}
// why not return the clean path? the called may not be expected it from what should only be a join operation.
return joinResult, nil
}
func extractSingleFile(file *zip.File, expandedFilePath, archivePath string) error {
zippedFile, err := file.Open()
if err != nil {
@@ -206,7 +199,12 @@ func extractSingleFile(file *zip.File, expandedFilePath, archivePath string) err
return fmt.Errorf("unable to create dest file=%q from zip=%q: %w", expandedFilePath, archivePath, err)
}
if err := safeCopy(outputFile, zippedFile); err != nil {
// limit the zip reader on each file read to prevent decompression bomb attacks
numBytes, err := io.Copy(outputFile, io.LimitReader(zippedFile, perFileReadLimit))
if numBytes >= perFileReadLimit || errors.Is(err, io.EOF) {
return fmt.Errorf("zip read limit hit (potential decompression bomb attack)")
}
if err != nil {
return fmt.Errorf("unable to copy source=%q to dest=%q for zip=%q: %w", file.Name, outputFile.Name(), archivePath, err)
}
+133 -200
View File
@@ -1,25 +1,52 @@
//go:build !windows
// +build !windows
package file
import (
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path"
"os/exec"
"path/filepath"
"strings"
"syscall"
"testing"
"github.com/go-test/deep"
"github.com/stretchr/testify/assert"
)
func generateFixture(t *testing.T, archivePath string) {
cwd, err := os.Getwd()
if err != nil {
t.Errorf("unable to get cwd: %+v", err)
}
cmd := exec.Command("./generate-zip-fixture.sh", archivePath)
cmd.Dir = filepath.Join(cwd, "test-fixtures")
if err := cmd.Start(); err != nil {
t.Fatalf("unable to start generate zip fixture script: %+v", err)
}
if err := cmd.Wait(); err != nil {
if exiterr, ok := err.(*exec.ExitError); ok {
// The program has exited with an exit code != 0
// This works on both Unix and Windows. Although package
// syscall is generally platform dependent, WaitStatus is
// defined for both Unix and Windows and in both cases has
// an ExitStatus() method with the same signature.
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
if status.ExitStatus() != 0 {
t.Fatalf("failed to generate fixture: rc=%d", status.ExitStatus())
}
}
} else {
t.Fatalf("unable to get generate fixture script result: %+v", err)
}
}
}
func equal(r1, r2 io.Reader) (bool, error) {
w1 := sha256.New()
w2 := sha256.New()
@@ -40,48 +67,54 @@ func equal(r1, r2 io.Reader) (bool, error) {
}
func TestUnzipToDir(t *testing.T) {
cwd, err := os.Getwd()
archivePrefix, err := ioutil.TempFile("", "syft-ziputil-archive-TEST-")
if err != nil {
t.Fatal(err)
t.Fatalf("unable to create tempfile: %+v", err)
}
defer os.Remove(archivePrefix.Name())
// the zip utility will add ".zip" to the end of the given name
archivePath := archivePrefix.Name() + ".zip"
defer os.Remove(archivePath)
t.Logf("archive path: %s", archivePath)
goldenRootDir := filepath.Join(cwd, "test-fixtures")
sourceDirPath := path.Join(goldenRootDir, "zip-source")
archiveFilePath := setupZipFileTest(t, sourceDirPath, false)
generateFixture(t, archivePrefix.Name())
unzipDestinationDir, err := ioutil.TempDir("", "syft-ziputil-contents-TEST-")
t.Cleanup(assertNoError(t, func() error {
return os.RemoveAll(unzipDestinationDir)
}))
contentsDir, err := ioutil.TempDir("", "syft-ziputil-contents-TEST-")
if err != nil {
t.Fatalf("unable to create tempdir: %+v", err)
}
defer os.RemoveAll(contentsDir)
t.Logf("content path: %s", unzipDestinationDir)
t.Logf("content path: %s", contentsDir)
expectedPaths := len(expectedZipArchiveEntries)
cwd, err := os.Getwd()
if err != nil {
t.Errorf("unable to get cwd: %+v", err)
}
t.Logf("running from: %s", cwd)
// note: zip utility already includes "zip-source" as a parent dir for all contained files
goldenRootDir := filepath.Join(cwd, "test-fixtures")
expectedPaths := 4
observedPaths := 0
err = UnzipToDir(archiveFilePath, unzipDestinationDir)
err = UnzipToDir(archivePath, contentsDir)
if err != nil {
t.Fatalf("unable to unzip archive: %+v", err)
}
// compare the source dir tree and the unzipped tree
err = filepath.Walk(unzipDestinationDir,
err = filepath.Walk(filepath.Join(contentsDir, "zip-source"),
func(path string, info os.FileInfo, err error) error {
// We don't unzip the root archive dir, since there's no archive entry for it
if path != unzipDestinationDir {
t.Logf("unzipped path: %s", path)
observedPaths++
}
t.Logf("unzipped path: %s", path)
observedPaths++
if err != nil {
t.Fatalf("this should not happen")
return err
}
goldenPath := filepath.Join(sourceDirPath, strings.TrimPrefix(path, unzipDestinationDir))
goldenPath := filepath.Join(goldenRootDir, strings.TrimPrefix(path, contentsDir))
if info.IsDir() {
i, err := os.Stat(goldenPath)
@@ -123,124 +156,44 @@ func TestUnzipToDir(t *testing.T) {
}
if observedPaths != expectedPaths {
t.Errorf("missed test paths: %d != %d", observedPaths, expectedPaths)
t.Errorf("missed test paths: %d!=%d", observedPaths, expectedPaths)
}
}
func TestContentsFromZip(t *testing.T) {
tests := []struct {
name string
archivePrep func(tb testing.TB) string
}{
{
name: "standard, non-nested zip",
archivePrep: prepZipSourceFixture,
},
{
name: "zip with prepended bytes",
archivePrep: prependZipSourceFixtureWithString(t, "junk at the beginning of the file..."),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
archivePath := test.archivePrep(t)
expected := zipSourceFixtureExpectedContents()
var paths []string
for p := range expected {
paths = append(paths, p)
}
actual, err := ContentsFromZip(archivePath, paths...)
if err != nil {
t.Fatalf("unable to extract from unzip archive: %+v", err)
}
assertZipSourceFixtureContents(t, actual, expected)
})
}
}
func prependZipSourceFixtureWithString(tb testing.TB, value string) func(tb testing.TB) string {
if len(value) == 0 {
tb.Fatalf("no bytes given to prefix")
}
return func(t testing.TB) string {
archivePath := prepZipSourceFixture(t)
// create a temp file
tmpFile, err := ioutil.TempFile("", "syft-ziputil-prependZipSourceFixtureWithString-")
if err != nil {
t.Fatalf("unable to create tempfile: %+v", err)
}
defer tmpFile.Close()
// write value to the temp file
if _, err := tmpFile.WriteString(value); err != nil {
t.Fatalf("unable to write to tempfile: %+v", err)
}
// open the original archive
sourceFile, err := os.Open(archivePath)
if err != nil {
t.Fatalf("unable to read source file: %+v", err)
}
// copy all contents from the archive to the temp file
if _, err := io.Copy(tmpFile, sourceFile); err != nil {
t.Fatalf("unable to copy source to dest: %+v", err)
}
sourceFile.Close()
// remove the original archive and replace it with the temp file
if err := os.Remove(archivePath); err != nil {
t.Fatalf("unable to remove original source archive (%q): %+v", archivePath, err)
}
if err := os.Rename(tmpFile.Name(), archivePath); err != nil {
t.Fatalf("unable to move new archive to old path (%q): %+v", tmpFile.Name(), err)
}
return archivePath
}
}
func prepZipSourceFixture(t testing.TB) string {
t.Helper()
archivePrefix, err := ioutil.TempFile("", "syft-ziputil-prepZipSourceFixture-")
func TestExtractFilesFromZipFile(t *testing.T) {
archivePrefix, err := ioutil.TempFile("", "syft-ziputil-archive-TEST-")
if err != nil {
t.Fatalf("unable to create tempfile: %+v", err)
}
t.Cleanup(func() {
assert.NoError(t, os.Remove(archivePrefix.Name()))
})
defer os.Remove(archivePrefix.Name())
// the zip utility will add ".zip" to the end of the given name
archivePath := archivePrefix.Name() + ".zip"
t.Cleanup(func() {
assert.NoError(t, os.Remove(archivePath))
})
defer os.Remove(archivePath)
t.Logf("archive path: %s", archivePath)
createZipArchive(t, "zip-source", archivePrefix.Name(), false)
generateFixture(t, archivePrefix.Name())
return archivePath
}
cwd, err := os.Getwd()
if err != nil {
t.Errorf("unable to get cwd: %+v", err)
}
func zipSourceFixtureExpectedContents() map[string]string {
return map[string]string{
filepath.Join("some-dir", "a-file.txt"): "A file! nice!",
filepath.Join("b-file.txt"): "B file...",
t.Logf("running from: %s", cwd)
aFilePath := filepath.Join("zip-source", "some-dir", "a-file.txt")
bFilePath := filepath.Join("zip-source", "b-file.txt")
expected := map[string]string{
aFilePath: "A file! nice!",
bFilePath: "B file...",
}
actual, err := ContentsFromZip(archivePath, aFilePath, bFilePath)
if err != nil {
t.Fatalf("unable to extract from unzip archive: %+v", err)
}
}
func assertZipSourceFixtureContents(t testing.TB, actual map[string]string, expected map[string]string) {
t.Helper()
diffs := deep.Equal(actual, expected)
if len(diffs) > 0 {
for _, d := range diffs {
@@ -254,80 +207,60 @@ func assertZipSourceFixtureContents(t testing.TB, actual map[string]string, expe
t.Errorf("full result: %s", string(b))
}
}
// looks like there isn't a helper for this yet? https://github.com/stretchr/testify/issues/497
func assertErrorAs(expectedErr interface{}) assert.ErrorAssertionFunc {
return func(t assert.TestingT, actualErr error, i ...interface{}) bool {
return errors.As(actualErr, &expectedErr)
func TestZipFileManifest(t *testing.T) {
archivePrefix, err := ioutil.TempFile("", "syft-ziputil-archive-TEST-")
if err != nil {
t.Fatalf("unable to create tempfile: %+v", err)
}
}
defer os.Remove(archivePrefix.Name())
// the zip utility will add ".zip" to the end of the given name
archivePath := archivePrefix.Name() + ".zip"
defer os.Remove(archivePath)
t.Logf("archive path: %s", archivePath)
func TestSafeJoin(t *testing.T) {
tests := []struct {
prefix string
args []string
expected string
errAssertion assert.ErrorAssertionFunc
}{
// go cases...
{
prefix: "/a/place",
args: []string{
"somewhere/else",
},
expected: "/a/place/somewhere/else",
errAssertion: assert.NoError,
},
{
prefix: "/a/place",
args: []string{
"somewhere/../else",
},
expected: "/a/place/else",
errAssertion: assert.NoError,
},
{
prefix: "/a/../place",
args: []string{
"somewhere/else",
},
expected: "/place/somewhere/else",
errAssertion: assert.NoError,
},
// zip slip examples....
{
prefix: "/a/place",
args: []string{
"../../../etc/passwd",
},
expected: "",
errAssertion: assertErrorAs(&errZipSlipDetected{}),
},
{
prefix: "/a/place",
args: []string{
"../",
"../",
},
expected: "",
errAssertion: assertErrorAs(&errZipSlipDetected{}),
},
{
prefix: "/a/place",
args: []string{
"../",
},
expected: "",
errAssertion: assertErrorAs(&errZipSlipDetected{}),
},
generateFixture(t, archivePrefix.Name())
cwd, err := os.Getwd()
if err != nil {
t.Errorf("unable to get cwd: %+v", err)
}
for _, test := range tests {
t.Run(fmt.Sprintf("%+v:%+v", test.prefix, test.args), func(t *testing.T) {
actual, err := safeJoin(test.prefix, test.args...)
test.errAssertion(t, err)
assert.Equal(t, test.expected, actual)
})
t.Logf("running from: %s", cwd)
expected := []string{
filepath.Join("zip-source") + string(os.PathSeparator),
filepath.Join("zip-source", "some-dir") + string(os.PathSeparator),
filepath.Join("zip-source", "some-dir", "a-file.txt"),
filepath.Join("zip-source", "b-file.txt"),
}
actual, err := NewZipFileManifest(archivePath)
if err != nil {
t.Fatalf("unable to extract from unzip archive: %+v", err)
}
if len(expected) != len(actual) {
t.Fatalf("mismatched manifest: %d != %d", len(actual), len(expected))
}
for _, e := range expected {
_, ok := actual[e]
if !ok {
t.Errorf("missing path: %s", e)
}
}
if t.Failed() {
b, err := json.MarshalIndent(actual, "", " ")
if err != nil {
t.Fatalf("can't show results: %+v", err)
}
t.Errorf("full result: %s", string(b))
}
}
-223
View File
@@ -1,223 +0,0 @@
package file
import (
"archive/zip"
"encoding/binary"
"errors"
"fmt"
"io"
"os"
)
// directoryEndLen, readByf, directoryEnd, and findSignatureInBlock were copied from the golang stdlib, specifically:
// - https://github.com/golang/go/blob/go1.16.4/src/archive/zip/struct.go
// - https://github.com/golang/go/blob/go1.16.4/src/archive/zip/reader.go
// findArchiveStartOffset is derived from the same stdlib utils, specifically the readDirectoryEnd function.
const (
directoryEndLen = 22
directory64LocLen = 20
directory64EndLen = 56
directory64LocSignature = 0x07064b50
directory64EndSignature = 0x06064b50
)
// ZipReadCloser is a drop-in replacement for zip.ReadCloser (from zip.OpenReader) that additionally considers zips
// that have bytes prefixed to the front of the archive (common with self-extracting jars).
type ZipReadCloser struct {
*zip.Reader
io.Closer
}
// OpenZip provides a ZipReadCloser for the given filepath.
func OpenZip(filepath string) (*ZipReadCloser, error) {
f, err := os.Open(filepath)
if err != nil {
return nil, err
}
fi, err := f.Stat()
if err != nil {
f.Close()
return nil, err
}
// some archives may have bytes prepended to the front of the archive, such as with self executing JARs. We first
// need to find the start of the archive and keep track of this offset.
offset, err := findArchiveStartOffset(f, fi.Size())
if err != nil {
return nil, fmt.Errorf("cannot find beginning of zip archive=%q : %w", filepath, err)
}
if _, err := f.Seek(0, io.SeekStart); err != nil {
return nil, fmt.Errorf("unable to seek to beginning of archive: %w", err)
}
size := fi.Size() - int64(offset)
r, err := zip.NewReader(io.NewSectionReader(f, int64(offset), size), size)
if err != nil {
return nil, fmt.Errorf("unable to open ZipReadCloser @ %q: %w", filepath, err)
}
return &ZipReadCloser{
Reader: r,
Closer: f,
}, nil
}
type readBuf []byte
func (b *readBuf) uint16() uint16 {
v := binary.LittleEndian.Uint16(*b)
*b = (*b)[2:]
return v
}
func (b *readBuf) uint32() uint32 {
v := binary.LittleEndian.Uint32(*b)
*b = (*b)[4:]
return v
}
func (b *readBuf) uint64() uint64 {
v := binary.LittleEndian.Uint64(*b)
*b = (*b)[8:]
return v
}
type directoryEnd struct {
diskNbr uint32 // unused
dirDiskNbr uint32 // unused
dirRecordsThisDisk uint64 // unused
directoryRecords uint64
directorySize uint64
directoryOffset uint64 // relative to file
}
// note: this is derived from readDirectoryEnd within the archive/zip package
//
//nolint:gocognit
func findArchiveStartOffset(r io.ReaderAt, size int64) (startOfArchive uint64, err error) {
// look for directoryEndSignature in the last 1k, then in the last 65k
var buf []byte
var directoryEndOffset int64
for i, bLen := range []int64{1024, 65 * 1024} {
if bLen > size {
bLen = size
}
buf = make([]byte, int(bLen))
if _, err := r.ReadAt(buf, size-bLen); err != nil && !errors.Is(err, io.EOF) {
return 0, err
}
if p := findSignatureInBlock(buf); p >= 0 {
buf = buf[p:]
directoryEndOffset = size - bLen + int64(p)
break
}
if i == 1 || bLen == size {
return 0, zip.ErrFormat
}
}
if buf == nil {
// we were unable to find the directoryEndSignature block
return 0, zip.ErrFormat
}
// read header into struct
b := readBuf(buf[4:]) // skip signature
d := &directoryEnd{
diskNbr: uint32(b.uint16()),
dirDiskNbr: uint32(b.uint16()),
dirRecordsThisDisk: uint64(b.uint16()),
directoryRecords: uint64(b.uint16()),
directorySize: uint64(b.uint32()),
directoryOffset: uint64(b.uint32()),
}
// Calculate where the zip data actually begins
// These values mean that the file can be a zip64 file
if d.directoryRecords == 0xffff || d.directorySize == 0xffff || d.directoryOffset == 0xffffffff {
p, err := findDirectory64End(r, directoryEndOffset)
if err == nil && p >= 0 {
directoryEndOffset = p
err = readDirectory64End(r, p, d)
}
if err != nil {
return 0, err
}
startOfArchive = 0 // Prefixed data not supported
}
startOfArchive = uint64(directoryEndOffset) - d.directorySize - d.directoryOffset
// Make sure directoryOffset points to somewhere in our file.
if o := int64(d.directoryOffset); o < 0 || o >= size {
return 0, zip.ErrFormat
}
return startOfArchive, nil
}
// findDirectory64End tries to read the zip64 locator just before the
// directory end and returns the offset of the zip64 directory end if
// found.
func findDirectory64End(r io.ReaderAt, directoryEndOffset int64) (int64, error) {
locOffset := directoryEndOffset - directory64LocLen
if locOffset < 0 {
return -1, nil // no need to look for a header outside the file
}
buf := make([]byte, directory64LocLen)
if _, err := r.ReadAt(buf, locOffset); err != nil {
return -1, err
}
b := readBuf(buf)
if sig := b.uint32(); sig != directory64LocSignature {
return -1, nil
}
if b.uint32() != 0 { // number of the disk with the start of the zip64 end of central directory
return -1, nil // the file is not a valid zip64-file
}
p := b.uint64() // relative offset of the zip64 end of central directory record
if b.uint32() != 1 { // total number of disks
return -1, nil // the file is not a valid zip64-file
}
return int64(p), nil
}
// readDirectory64End reads the zip64 directory end and updates the
// directory end with the zip64 directory end values.
func readDirectory64End(r io.ReaderAt, offset int64, d *directoryEnd) (err error) {
buf := make([]byte, directory64EndLen)
if _, err := r.ReadAt(buf, offset); err != nil {
return err
}
b := readBuf(buf)
if sig := b.uint32(); sig != directory64EndSignature {
return errors.New("could not read directory64End")
}
b = b[12:] // skip dir size, version and version needed (uint64 + 2x uint16)
d.diskNbr = b.uint32() // number of this disk
d.dirDiskNbr = b.uint32() // number of the disk with the start of the central directory
d.dirRecordsThisDisk = b.uint64() // total number of entries in the central directory on this disk
d.directoryRecords = b.uint64() // total number of entries in the central directory
d.directorySize = b.uint64() // size of the central directory
d.directoryOffset = b.uint64() // offset of start of central directory with respect to the starting disk number
return nil
}
func findSignatureInBlock(b []byte) int {
for i := len(b) - directoryEndLen; i >= 0; i-- {
// defined from directoryEndSignature
if b[i] == 'P' && b[i+1] == 'K' && b[i+2] == 0x05 && b[i+3] == 0x06 {
// n is length of comment
n := int(b[i+directoryEndLen-2]) | int(b[i+directoryEndLen-1])<<8
if n+directoryEndLen+i <= len(b) {
return i
}
}
}
return -1
}
-50
View File
@@ -1,50 +0,0 @@
//go:build !windows
// +build !windows
package file
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestFindArchiveStartOffset(t *testing.T) {
tests := []struct {
name string
archivePrep func(tb testing.TB) string
expected uint64
}{
{
name: "standard, non-nested zip",
archivePrep: prepZipSourceFixture,
expected: 0,
},
{
name: "zip with prepended bytes",
archivePrep: prependZipSourceFixtureWithString(t, "junk at the beginning of the file..."),
expected: 36,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
archivePath := test.archivePrep(t)
f, err := os.Open(archivePath)
if err != nil {
t.Fatalf("could not open archive %q: %+v", archivePath, err)
}
fi, err := os.Stat(f.Name())
if err != nil {
t.Fatalf("unable to stat archive: %+v", err)
}
actual, err := findArchiveStartOffset(f, fi.Size())
if err != nil {
t.Fatalf("unable to find offset: %+v", err)
}
assert.Equal(t, test.expected, actual)
})
}
}
+21
View File
@@ -0,0 +1,21 @@
package format
import "fmt"
const (
DefaultColor Color = iota + 30
Red
Green
Yellow
Blue
Magenta
Cyan
White
)
type Color uint8
// TODO: not cross platform (windows...)
func (c Color) Format(s string) string {
return fmt.Sprintf("\x1b[%dm%s\x1b[0m", c, s)
}
-26
View File
@@ -1,26 +0,0 @@
package internal
import (
"fmt"
"os"
)
// IsPipedInput returns true if there is no input device, which means the user **may** be providing input via a pipe.
func IsPipedInput() (bool, error) {
fi, err := os.Stdin.Stat()
if err != nil {
return false, fmt.Errorf("unable to determine if there is piped input: %w", err)
}
// note: we should NOT use the absence of a character device here as the hint that there may be input expected
// on stdin, as running syft as a subprocess you would expect no character device to be present but input can
// be from either stdin or indicated by the CLI. Checking if stdin is a pipe is the most direct way to determine
// if there *may* be bytes that will show up on stdin that should be used for the analysis source.
return fi.Mode()&os.ModeNamedPipe != 0, nil
}
// IsTerminal returns true if there is a terminal present.
func IsTerminal() bool {
stat, _ := os.Stdin.Stat()
return (stat.Mode() & os.ModeCharDevice) != 0
}
+2 -43
View File
@@ -1,72 +1,31 @@
/*
Package log contains the singleton object and helper functions for facilitating logging within the syft library.
*/
package log
import (
"github.com/anchore/go-logger"
"github.com/anchore/go-logger/adapter/discard"
"github.com/anchore/syft/syft/logger"
)
// Log is the singleton used to facilitate logging internally within syft
var Log logger.Logger = discard.New()
var Log logger.Logger = &nopLogger{}
// Errorf takes a formatted template string and template arguments for the error logging level.
func Errorf(format string, args ...interface{}) {
Log.Errorf(format, args...)
}
// Error logs the given arguments at the error logging level.
func Error(args ...interface{}) {
Log.Error(args...)
}
// Warnf takes a formatted template string and template arguments for the warning logging level.
func Warnf(format string, args ...interface{}) {
Log.Warnf(format, args...)
}
// Warn logs the given arguments at the warning logging level.
func Warn(args ...interface{}) {
Log.Warn(args...)
}
// Infof takes a formatted template string and template arguments for the info logging level.
func Infof(format string, args ...interface{}) {
Log.Infof(format, args...)
}
// Info logs the given arguments at the info logging level.
func Info(args ...interface{}) {
Log.Info(args...)
}
// Debugf takes a formatted template string and template arguments for the debug logging level.
func Debugf(format string, args ...interface{}) {
Log.Debugf(format, args...)
}
// Debug logs the given arguments at the debug logging level.
func Debug(args ...interface{}) {
Log.Debug(args...)
}
// Tracef takes a formatted template string and template arguments for the trace logging level.
func Tracef(format string, args ...interface{}) {
Log.Tracef(format, args...)
}
// Trace logs the given arguments at the trace logging level.
func Trace(args ...interface{}) {
Log.Trace(args...)
}
// WithFields returns a message logger with multiple key-value fields.
func WithFields(fields ...interface{}) logger.MessageLogger {
return Log.WithFields(fields...)
}
// Nested returns a new logger with hard coded key-value pairs
func Nested(fields ...interface{}) logger.Logger {
return Log.Nested(fields...)
}
+10
View File
@@ -0,0 +1,10 @@
package log
type nopLogger struct{}
func (l *nopLogger) Errorf(format string, args ...interface{}) {}
func (l *nopLogger) Error(args ...interface{}) {}
func (l *nopLogger) Infof(format string, args ...interface{}) {}
func (l *nopLogger) Info(args ...interface{}) {}
func (l *nopLogger) Debugf(format string, args ...interface{}) {}
func (l *nopLogger) Debug(args ...interface{}) {}
+78
View File
@@ -0,0 +1,78 @@
package logger
import (
"bytes"
"strings"
"github.com/hashicorp/go-hclog"
)
// Source: https://github.com/hashicorp/go-hclog/blob/master/stdlog.go (MIT License)
// Provides a io.Writer to shim the data out of *log.Logger
// and back into our Logger. This is basically the only way to
// build upon *log.Logger.
type HCLogStdlogAdapter struct {
log hclog.Logger
inferLevels bool
forceLevel hclog.Level
}
// Take the data, infer the levels if configured, and send it through
// a regular Logger.
func (s *HCLogStdlogAdapter) Write(data []byte) (int, error) {
str := string(bytes.TrimRight(data, " \t\n"))
if s.forceLevel != hclog.NoLevel {
// Use pickLevel to strip log levels included in the line since we are
// forcing the level
_, str := s.pickLevel(str)
// Log at the forced level
s.dispatch(str, s.forceLevel)
} else if s.inferLevels {
level, str := s.pickLevel(str)
s.dispatch(str, level)
} else {
s.log.Info(str)
}
return len(data), nil
}
func (s *HCLogStdlogAdapter) dispatch(str string, level hclog.Level) {
switch level {
case hclog.Trace:
s.log.Trace(str)
case hclog.Debug:
s.log.Debug(str)
case hclog.Info:
s.log.Info(str)
case hclog.Warn:
s.log.Warn(str)
case hclog.Error:
s.log.Error(str)
default:
s.log.Info(str)
}
}
// Detect, based on conventions, what log level this is.
func (s *HCLogStdlogAdapter) pickLevel(str string) (hclog.Level, string) {
switch {
case strings.HasPrefix(str, "[DEBUG]"):
return hclog.Debug, strings.TrimSpace(str[7:])
case strings.HasPrefix(str, "[TRACE]"):
return hclog.Trace, strings.TrimSpace(str[7:])
case strings.HasPrefix(str, "[INFO]"):
return hclog.Info, strings.TrimSpace(str[6:])
case strings.HasPrefix(str, "[WARN]"):
return hclog.Warn, strings.TrimSpace(str[7:])
case strings.HasPrefix(str, "[ERROR]"):
return hclog.Error, strings.TrimSpace(str[7:])
case strings.HasPrefix(str, "[ERR]"):
return hclog.Error, strings.TrimSpace(str[5:])
default:
return hclog.Info, str
}
}
+148
View File
@@ -0,0 +1,148 @@
package logger
import (
"io"
"log"
"github.com/anchore/syft/syft/logger"
"github.com/hashicorp/go-hclog"
)
// integrity check
var _ hclog.Logger = &LoggerHCLogAdapter{}
// LoggerHCLogAdapter is used to (partially) adapt a syft logger.Logger object to the hclog.Logger interface for syft plugins.
// Note: this does not implement all functionality required by the hclog.Logger.
type LoggerHCLogAdapter struct {
Logger logger.Logger
}
func NewLoggerHCLogAdapter(logger logger.Logger) *LoggerHCLogAdapter {
return &LoggerHCLogAdapter{
Logger: logger,
}
}
// Args are alternating key, val pairs
// keys must be strings
// vals can be any type, but display is implementation specific
// Emit a message and key/value pairs at a provided log level
func (l *LoggerHCLogAdapter) Log(level hclog.Level, msg string, args ...interface{}) {
l.Logger.Info(append([]interface{}{msg}, args...)...)
}
// Emit a message and key/value pairs at the TRACE level
func (l *LoggerHCLogAdapter) Trace(msg string, args ...interface{}) {
l.Logger.Debug(append([]interface{}{msg}, args...)...)
}
// Emit a message and key/value pairs at the DEBUG level
func (l *LoggerHCLogAdapter) Debug(msg string, args ...interface{}) {
l.Logger.Debug(append([]interface{}{msg}, args...)...)
}
// Emit a message and key/value pairs at the INFO level
func (l *LoggerHCLogAdapter) Info(msg string, args ...interface{}) {
l.Logger.Info(append([]interface{}{msg}, args...)...)
}
// Emit a message and key/value pairs at the WARN level
func (l *LoggerHCLogAdapter) Warn(msg string, args ...interface{}) {
l.Logger.Error(append([]interface{}{msg}, args...)...)
}
// Emit a message and key/value pairs at the ERROR level
func (l *LoggerHCLogAdapter) Error(msg string, args ...interface{}) {
l.Logger.Error(append([]interface{}{msg}, args...)...)
}
// Indicate if TRACE logs would be emitted. This and the other Is* guards
// are used to elide expensive logging code based on the current level.
func (l *LoggerHCLogAdapter) IsTrace() bool {
// TODO: not implemented
return true
}
// Indicate if DEBUG logs would be emitted. This and the other Is* guards
// are used to elide expensive logging code based on the current level.
func (l *LoggerHCLogAdapter) IsDebug() bool {
// TODO: not implemented
return true
}
// Indicate if INFO logs would be emitted. This and the other Is* guards
// are used to elide expensive logging code based on the current level.
func (l *LoggerHCLogAdapter) IsInfo() bool {
// TODO: not implemented
return true
}
// Indicate if WARN logs would be emitted. This and the other Is* guards
// are used to elide expensive logging code based on the current level.
func (l *LoggerHCLogAdapter) IsWarn() bool {
// TODO: not implemented
return true
}
// Indicate if ERROR logs would be emitted. This and the other Is* guards
// are used to elide expensive logging code based on the current level.
func (l *LoggerHCLogAdapter) IsError() bool {
// TODO: not implemented
return true
}
// ImpliedArgs returns With key/value pairs
func (l *LoggerHCLogAdapter) ImpliedArgs() []interface{} {
// TODO: not implemented
return nil
}
// Creates a sublogger that will always have the given key/value pairs
func (l *LoggerHCLogAdapter) With(args ...interface{}) hclog.Logger {
// TODO: not implemented
return l
}
// Returns the Name of the logger
func (l *LoggerHCLogAdapter) Name() string {
// TODO: not implemented
return ""
}
// Create a logger that will prepend the name string on the front of all messages.
// If the logger already has a name, the new value will be appended to the current
// name. That way, a major subsystem can use this to decorate all it's own logs
// without losing context.
func (l *LoggerHCLogAdapter) Named(name string) hclog.Logger {
// TODO: not implemented
return l
}
// Create a logger that will prepend the name string on the front of all messages.
// This sets the name of the logger to the value directly, unlike Named which honor
// the current name as well.
func (l *LoggerHCLogAdapter) ResetNamed(name string) hclog.Logger {
// TODO: not implemented
return l
}
// Updates the level. This should affect all sub-loggers as well. If an
// implementation cannot update the level on the fly, it should no-op.
func (l *LoggerHCLogAdapter) SetLevel(level hclog.Level) {
// TODO: not implemented
}
// Return a value that conforms to the stdlib log.Logger interface
func (l *LoggerHCLogAdapter) StandardLogger(opts *hclog.StandardLoggerOptions) *log.Logger {
return log.New(l.StandardWriter(opts), "", log.LstdFlags)
}
// Return a value that conforms to io.Writer, which can be passed into log.SetOutput()
func (l *LoggerHCLogAdapter) StandardWriter(opts *hclog.StandardLoggerOptions) io.Writer {
return &HCLogStdlogAdapter{
log: l,
inferLevels: opts.InferLevels,
forceLevel: opts.ForceLevel,
}
}
+130
View File
@@ -0,0 +1,130 @@
package logger
import (
"fmt"
"io"
"io/ioutil"
"os"
"github.com/anchore/syft/syft/logger"
"github.com/sirupsen/logrus"
prefixed "github.com/x-cray/logrus-prefixed-formatter"
)
// integrity check
var _ logger.Logger = &LogrusLogger{}
var _ logger.Logger = &LogrusNestedLogger{}
type LogrusConfig struct {
EnableConsole bool
EnableFile bool
Structured bool
Level logrus.Level
FileLocation string
}
type LogrusLogger struct {
Config LogrusConfig
Logger *logrus.Logger
Output io.Writer
}
type LogrusNestedLogger struct {
Logger *logrus.Entry
}
func NewLogrusLogger(cfg LogrusConfig) *LogrusLogger {
appLogger := logrus.New()
var output io.Writer
switch {
case cfg.EnableConsole && cfg.EnableFile:
logFile, err := os.OpenFile(cfg.FileLocation, os.O_WRONLY|os.O_CREATE, 0755)
if err != nil {
panic(fmt.Errorf("unable to setup log file: %w", err))
}
output = io.MultiWriter(os.Stderr, logFile)
case cfg.EnableConsole:
output = os.Stderr
case cfg.EnableFile:
logFile, err := os.OpenFile(cfg.FileLocation, os.O_WRONLY|os.O_CREATE, 0755)
if err != nil {
panic(fmt.Errorf("unable to setup log file: %w", err))
}
output = logFile
default:
output = ioutil.Discard
}
appLogger.SetOutput(output)
appLogger.SetLevel(cfg.Level)
if cfg.Structured {
appLogger.SetFormatter(&logrus.JSONFormatter{
TimestampFormat: "2006-01-02 15:04:05",
DisableTimestamp: false,
DisableHTMLEscape: false,
PrettyPrint: false,
})
} else {
appLogger.SetFormatter(&prefixed.TextFormatter{
TimestampFormat: "2006-01-02 15:04:05",
ForceColors: true,
ForceFormatting: true,
})
}
return &LogrusLogger{
Config: cfg,
Logger: appLogger,
Output: output,
}
}
func (l *LogrusLogger) Errorf(format string, args ...interface{}) {
l.Logger.Errorf(format, args...)
}
func (l *LogrusLogger) Infof(format string, args ...interface{}) {
l.Logger.Infof(format, args...)
}
func (l *LogrusLogger) Debugf(format string, args ...interface{}) {
l.Logger.Debugf(format, args...)
}
func (l *LogrusLogger) Error(args ...interface{}) {
l.Logger.Error(args...)
}
func (l *LogrusLogger) Info(args ...interface{}) {
l.Logger.Info(args...)
}
func (l *LogrusLogger) Debug(args ...interface{}) {
l.Logger.Debug(args...)
}
func (l *LogrusNestedLogger) Errorf(format string, args ...interface{}) {
l.Logger.Errorf(format, args...)
}
func (l *LogrusNestedLogger) Infof(format string, args ...interface{}) {
l.Logger.Infof(format, args...)
}
func (l *LogrusNestedLogger) Debugf(format string, args ...interface{}) {
l.Logger.Debugf(format, args...)
}
func (l *LogrusNestedLogger) Error(args ...interface{}) {
l.Logger.Error(args...)
}
func (l *LogrusNestedLogger) Info(args ...interface{}) {
l.Logger.Info(args...)
}
func (l *LogrusNestedLogger) Debug(args ...interface{}) {
l.Logger.Debug(args...)
}
+181
View File
@@ -0,0 +1,181 @@
package logger
import (
"fmt"
"io"
"log"
"github.com/hashicorp/go-hclog"
"github.com/sirupsen/logrus"
)
// integrity check
var _ hclog.Logger = &LogrusHCLogAdapter{}
var levels = map[hclog.Level]logrus.Level{
hclog.NoLevel: logrus.PanicLevel,
hclog.Error: logrus.ErrorLevel,
hclog.Warn: logrus.WarnLevel,
hclog.Info: logrus.InfoLevel,
hclog.Debug: logrus.DebugLevel,
hclog.Trace: logrus.TraceLevel,
}
// LogrusHCLogAdapter is used to adapt a logrus logger object to the hclog.Logger interface for syft plugins.
type LogrusHCLogAdapter struct {
logger *logrus.Logger
entry *logrus.Entry
}
func NewLogrusHCLogAdapter(logger *logrus.Logger, fields map[string]interface{}) *LogrusHCLogAdapter {
if fields != nil {
return &LogrusHCLogAdapter{
logger: logger,
entry: logger.WithFields(fields),
}
}
return &LogrusHCLogAdapter{
logger: logger,
}
}
func (l *LogrusHCLogAdapter) log(level hclog.Level, msg string, args ...interface{}) {
if l.entry != nil {
l.entry.WithFields(makeArgFields(args...)).Log(levels[level], msg)
return
}
l.logger.WithFields(makeArgFields(args...)).Log(levels[level], msg)
}
// Args are alternating key, val pairs
// keys must be strings
// vals can be any type, but display is implementation specific
// Emit a message and key/value pairs at a provided log level
func (l *LogrusHCLogAdapter) Log(level hclog.Level, msg string, args ...interface{}) {
l.log(level, msg, args...)
}
// Emit a message and key/value pairs at the TRACE level
func (l *LogrusHCLogAdapter) Trace(msg string, args ...interface{}) {
l.log(hclog.Trace, msg, args...)
}
// Emit a message and key/value pairs at the DEBUG level
func (l *LogrusHCLogAdapter) Debug(msg string, args ...interface{}) {
l.log(hclog.Debug, msg, args...)
}
// Emit a message and key/value pairs at the INFO level
func (l *LogrusHCLogAdapter) Info(msg string, args ...interface{}) {
l.log(hclog.Info, msg, args...)
}
// Emit a message and key/value pairs at the WARN level
func (l *LogrusHCLogAdapter) Warn(msg string, args ...interface{}) {
l.log(hclog.Warn, msg, args...)
}
// Emit a message and key/value pairs at the ERROR level
func (l *LogrusHCLogAdapter) Error(msg string, args ...interface{}) {
l.log(hclog.Error, msg, args...)
}
// Indicate if TRACE logs would be emitted. This and the other Is* guards
// are used to elide expensive logging code based on the current level.
func (l *LogrusHCLogAdapter) IsTrace() bool {
return l.logger.Level <= logrus.TraceLevel
}
// Indicate if DEBUG logs would be emitted. This and the other Is* guards
// are used to elide expensive logging code based on the current level.
func (l *LogrusHCLogAdapter) IsDebug() bool {
return l.logger.Level <= logrus.DebugLevel
}
// Indicate if INFO logs would be emitted. This and the other Is* guards
// are used to elide expensive logging code based on the current level.
func (l *LogrusHCLogAdapter) IsInfo() bool {
return l.logger.Level <= logrus.InfoLevel
}
// Indicate if WARN logs would be emitted. This and the other Is* guards
// are used to elide expensive logging code based on the current level.
func (l *LogrusHCLogAdapter) IsWarn() bool {
return l.logger.Level <= logrus.WarnLevel
}
// Indicate if ERROR logs would be emitted. This and the other Is* guards
// are used to elide expensive logging code based on the current level.
func (l *LogrusHCLogAdapter) IsError() bool {
return l.logger.Level <= logrus.ErrorLevel
}
// ImpliedArgs returns With key/value pairs
func (l *LogrusHCLogAdapter) ImpliedArgs() []interface{} {
// TODO: not implemented
return nil
}
// Creates a sublogger that will always have the given key/value pairs
func (l *LogrusHCLogAdapter) With(args ...interface{}) hclog.Logger {
l.entry = l.logger.WithFields(makeArgFields(args...))
return l
}
// Returns the Name of the logger
func (l *LogrusHCLogAdapter) Name() string {
// TODO: not implemented
return ""
}
// Create a logger that will prepend the name string on the front of all messages.
// If the logger already has a name, the new value will be appended to the current
// name. That way, a major subsystem can use this to decorate all it's own logs
// without losing context.
func (l *LogrusHCLogAdapter) Named(name string) hclog.Logger {
return NewLogrusHCLogAdapter(l.logger, logrus.Fields{"name": name})
}
// Create a logger that will prepend the name string on the front of all messages.
// This sets the name of the logger to the value directly, unlike Named which honor
// the current name as well.
func (l *LogrusHCLogAdapter) ResetNamed(name string) hclog.Logger {
return NewLogrusHCLogAdapter(l.logger, logrus.Fields{"name": name})
}
// Updates the level. This should affect all sub-loggers as well. If an
// implementation cannot update the level on the fly, it should no-op.
func (l *LogrusHCLogAdapter) SetLevel(level hclog.Level) {
l.logger.SetLevel(levels[level])
}
// Return a value that conforms to the stdlib log.Logger interface
func (l *LogrusHCLogAdapter) StandardLogger(opts *hclog.StandardLoggerOptions) *log.Logger {
return log.New(l.StandardWriter(opts), "", log.LstdFlags)
}
// Return a value that conforms to io.Writer, which can be passed into log.SetOutput()
func (l *LogrusHCLogAdapter) StandardWriter(opts *hclog.StandardLoggerOptions) io.Writer {
return &HCLogStdlogAdapter{
log: l,
inferLevels: opts.InferLevels,
forceLevel: opts.ForceLevel,
}
}
func makeArgFields(args ...interface{}) logrus.Fields {
if len(args)%2 != 0 {
panic(fmt.Errorf("odd number of logger key-value pairs (%d): %+v", len(args), args))
}
var key string
var fields = make(logrus.Fields)
for i, arg := range args {
if i%2 == 0 {
key = arg.(string)
} else {
fields[key] = arg
}
}
return fields
}
-73
View File
@@ -1,73 +0,0 @@
package internal
import "github.com/scylladb/go-set/strset"
var (
ArchiveMIMETypeSet = strset.New(
// derived from https://en.wikipedia.org/wiki/List_of_archive_formats
[]string{
// archive only
"application/x-archive",
"application/x-cpio",
"application/x-shar",
"application/x-iso9660-image",
"application/x-sbx",
"application/x-tar",
// compression only
"application/x-bzip2",
"application/gzip",
"application/x-lzip",
"application/x-lzma",
"application/x-lzop",
"application/x-snappy-framed",
"application/x-xz",
"application/x-compress",
"application/zstd",
// archiving and compression
"application/x-7z-compressed",
"application/x-ace-compressed",
"application/x-astrotite-afa",
"application/x-alz-compressed",
"application/vnd.android.package-archive",
"application/x-freearc",
"application/x-arj",
"application/x-b1",
"application/vnd.ms-cab-compressed",
"application/x-cfs-compressed",
"application/x-dar",
"application/x-dgc-compressed",
"application/x-apple-diskimage",
"application/x-gca-compressed",
"application/java-archive",
"application/x-lzh",
"application/x-lzx",
"application/x-rar-compressed",
"application/x-stuffit",
"application/x-stuffitx",
"application/x-gtar",
"application/x-ms-wim",
"application/x-xar",
"application/zip",
"application/x-zoo",
}...,
)
ExecutableMIMETypeSet = strset.New(
[]string{
"application/x-executable",
"application/x-mach-binary",
"application/x-elf",
"application/x-sharedlib",
"application/vnd.microsoft.portable-executable",
"application/x-executable",
}...,
)
)
func IsArchive(mimeType string) bool {
return ArchiveMIMETypeSet.Has(mimeType)
}
func IsExecutable(mimeType string) bool {
return ExecutableMIMETypeSet.Has(mimeType)
}
-57
View File
@@ -1,57 +0,0 @@
package internal
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_IsArchive(t *testing.T) {
tests := []struct {
name string
mimeType string
expected bool
}{
{
name: "not an archive",
mimeType: "application/vnd.unknown",
expected: false,
},
{
name: "archive",
mimeType: "application/x-rar-compressed",
expected: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, IsArchive(test.mimeType))
})
}
}
func Test_IsExecutable(t *testing.T) {
tests := []struct {
name string
mimeType string
expected bool
}{
{
name: "not an executable",
mimeType: "application/vnd.unknown",
expected: false,
},
{
name: "executable",
mimeType: "application/x-mach-binary",
expected: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, IsExecutable(test.mimeType))
})
}
}
-45
View File
@@ -1,45 +0,0 @@
package internal
import "regexp"
// MatchNamedCaptureGroups takes a regular expression and string and returns all of the named capture group results in a map.
// This is only for the first match in the regex. Callers shouldn't be providing regexes with multiple capture groups with the same name.
func MatchNamedCaptureGroups(regEx *regexp.Regexp, content string) map[string]string {
// note: we are looking across all matches and stopping on the first non-empty match. Why? Take the following example:
// input: "cool something to match against" pattern: `((?P<name>match) (?P<version>against))?`. Since the pattern is
// encapsulated in an optional capture group, there will be results for each character, but the results will match
// on nothing. The only "true" match will be at the end ("match against").
allMatches := regEx.FindAllStringSubmatch(content, -1)
var results map[string]string
for _, match := range allMatches {
// fill a candidate results map with named capture group results, accepting empty values, but not groups with
// no names
for nameIdx, name := range regEx.SubexpNames() {
if nameIdx > len(match) || len(name) == 0 {
continue
}
if results == nil {
results = make(map[string]string)
}
results[name] = match[nameIdx]
}
// note: since we are looking for the first best potential match we should stop when we find the first one
// with non-empty results.
if !isEmptyMap(results) {
break
}
}
return results
}
func isEmptyMap(m map[string]string) bool {
if len(m) == 0 {
return true
}
for _, value := range m {
if value != "" {
return false
}
}
return true
}

Some files were not shown because too many files have changed in this diff Show More