Compare commits

...
20 Commits
Author SHA1 Message Date
Christopher Angelo Phillips 7a005fec06 update to point at new script for new release (#551)
* update to point at local script release

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
2021-10-15 13:26:34 -04:00
Dan Luhring c0f55866ec Announce upcoming webinar (#553)
Signed-off-by: Dan Luhring <dan.luhring@anchore.com>
2021-10-15 12:13:28 -04:00
Christopher Angelo Phillips 9ea6330ebc update split in py package_cataloger to cap at 2 (#549)
Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
2021-10-14 12:32:06 -04:00
Christopher Angelo Phillips 7d13fbf0d4 540 add additional go package metadata (#546)
* update with distinct golang metadata types

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
2021-10-13 11:00:28 -04:00
Dan Luhring 10368f6fe8 Add Syft logo (#545)
Signed-off-by: Dan Luhring <dan.luhring@anchore.com>
2021-10-11 21:06:27 +00:00
Josh Bressers f2840029d1 Allow Syft to build with podman (#537)
* Don't check the Built-By flag

Signed-off-by: Josh Bressers <josh@bress.net>

* Remove alpine pinning to resolve conflict with main

Signed-off-by: Josh Bressers <josh@bress.net>
2021-10-11 12:39:34 -04:00
Christopher Angelo Phillips b25f5b664b (464) - remove mod and cargo from image cataloger (#539)
* remove mod and cargo from image cataloger

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>

* update test error messages for clear failures

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
2021-10-07 15:18:20 -04:00
Christopher Angelo PhillipsandAlex Goodman 3462e18af3 478 identify go binaries and extract mod information (#534)
* add query by MIME type to source.FileResolver

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* import stereoscope lib changes to find mime type

- add bin cataloger
- add bin parser
- add mime type go utils
- import new resolver

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>

* add go std library code to unpack bin

- keep them in their own (original) files
- add note for "this code was copied from"
- comment the lines the required changing

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
Co-authored-by: Alex Goodman <alex.goodman@anchore.com>
2021-10-07 12:16:38 -04:00
Alex Goodman 05ac3f1eff split application configuration parsing to multiple methods (#532)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-10-06 09:48:53 -04:00
Alex Goodman a000a2926b update linter + fix whitespace (#536)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-10-06 13:46:55 +00:00
Alex Goodman 9189ed68df Add query by MIME type to source.FileResolver (#529)
* add query by MIME type to source.FileResolver

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* pull in stereoscope MIME type feature

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* fix tests

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-10-05 20:50:53 -04:00
Alex Goodman ba1cd8d753 specify optional file.Digests instances to abbreviate json output (#531)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-10-05 15:35:28 -04:00
Alex Goodman dd2c178085 add --file option to readme (#533)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-10-05 15:34:32 -04:00
Alex Goodman 1b23a94015 Add option to output SBOM report to a file (#530)
* add output to file option

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* log errors on close of the report destination

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* remove file option from persistent args

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* update file option comments and logging

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* allow for multiple UI fallback options

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* update UI select signatures + tests

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-10-05 14:47:24 -04:00
Christopher Angelo Phillips f47a6a88b1 (#495) Update documentNamespace uniqueness for spdx-json output (#528)
* add unique namespace identifier

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
2021-10-05 13:10:49 -04:00
Dan Luhring 4b7217f052 Improve documentation (#525)
* Make installation methods more obvious

Signed-off-by: Dan Luhring <dan.luhring@anchore.com>

* Create linkable section headers

Signed-off-by: Dan Luhring <dan.luhring@anchore.com>

* Add badge for joining Slack

Signed-off-by: Dan Luhring <dan.luhring@anchore.com>

* Document requirement for signed commits

Signed-off-by: Dan Luhring <dan.luhring@anchore.com>
2021-10-04 09:29:27 -04:00
Christopher Angelo Phillips 5e4b668e5a 497 stable sorted CPE array (JSON and SPDX) (#522)
* add small sorting change to our specificity

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
2021-10-01 15:31:17 -04:00
Dan Luhring 6480f06173 Update description for Slack link (#523)
Signed-off-by: Dan Luhring <dan.luhring@anchore.com>
2021-09-30 21:43:43 +00:00
Christopher Angelo Phillips 75aed5f3ec (#460) Extend license mapping for common SPDX license names (#509)
Fixes #460 
Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
2021-09-30 16:54:36 -04:00
Jamie Magee 270fbb7665 Add mariner to distro types (#520)
Signed-off-by: Jamie Magee <jamie.magee@gmail.com>
2021-09-30 13:20:15 -04:00
89 changed files with 3453 additions and 900 deletions
+1 -1
View File
@@ -3,4 +3,4 @@ contact_links:
- name: Join the Slack community 💬
# link to our community Slack registration page
url: https://anchore.com/slack
about: 'Be sure to check out the channels #syft-help (ask us questions) and #syft-dev (collaborate with us on design and development)!'
about: 'Come chat with us! Ask for help, join our software development efforts, or just give us feedback!'
+405
View File
@@ -0,0 +1,405 @@
#!/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/all) BINARIES="goreleaser" ;;
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
os=$(uname_os)
case "$os" in
darwin) arch="all" ;;
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 ;;
all) 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 ;;
all) 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"
+5 -3
View File
@@ -15,6 +15,7 @@ linters:
- dogsled
- dupl
- errcheck
- exportloopref
- funlen
- gocognit
- goconst
@@ -22,7 +23,6 @@ linters:
- gocyclo
- gofmt
- goimports
- golint
- goprintffuncname
- gosec
- gosimple
@@ -31,8 +31,8 @@ linters:
- misspell
- nakedret
- nolintlint
- revive
- rowserrcheck
- scopelint
- staticcheck
- structcheck
- stylecheck
@@ -49,11 +49,13 @@ linters:
# - godot
# - godox
# - goerr113
# - golint # deprecated
# - gomnd # this is too aggressive
# - interfacer # this is a good idea, but is no longer supported and is prone to false positives
# - lll # without a way to specify per-line exception cases, this is not usable
# - maligned # this is an excellent linter, but tricky to optimize and we are not sensitive to memory layout optimizations
# - nestif
# - prealloc # following this rule isn't consistently a good idea, as it sometimes forces unnecessary allocations that result in less idiomatic code
# - scopelint # deprecated
# - testpackage
# - wsl
# - wsl # this doens't have an auto-fixer yet and is pretty noisy (https://github.com/bombsimon/wsl/issues/90)
+11 -6
View File
@@ -1,12 +1,12 @@
# Contributing to `syft`
# Contributing to Syft
If you are looking to contribute to this project and want to open a Github pull request ("PR"), there are a few guidelines of what we are looking for in patches. Make sure you go through this document and ensure that your code proposal is aligned.
If you are looking to contribute to this project and want to open a GitHub pull request ("PR"), there are a few guidelines of what we are looking for in patches. Make sure you go through this document and ensure that your code proposal is aligned.
## Sign your work
## Sign off your work
The `sign-off` is an added line at the end of the explanation for the commit, certifying that you wrote it or otherwise have the right to submit it as an open-source patch. By submitting a contribution, you agree to be bound by the terms of the DCO Version 1.1 and Apache License Version 2.0.
Signing a commit certifies the below Developer's Certificate of Origin (DCO):
Signing off a commit certifies the below Developer's Certificate of Origin (DCO):
```text
Developer's Certificate of Origin 1.1
@@ -51,13 +51,13 @@ $ git config --global user.name "John Doe"
$ git config --global user.email "john.doe@example.com"
```
Creating a signed commit is then possible with `-s`:
Creating a signed-off commit is then possible with `-s` or `--signoff`:
```text
$ git commit -s -m "this is a commit message"
```
To double check if the commit was signed, look at the log output:
To double-check that the commit was signed-off, look at the log output:
```text
$ git log -1
@@ -82,6 +82,11 @@ Date: Mon Aug 1 11:27:13 2020 -0400
[//]: # (TODO: PR title and description)
## Sign your commits
To ensure the authenticity and integrity of code contributions, **we require that all commits are signed**. Signing commits proves that your commits were truly created by you, as the holder of a private key.
Configuring git to sign your commits is a straightforward process. To get set up, see [GitHub's documentation on signing commits](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits).
## Test your changes
+2 -2
View File
@@ -101,9 +101,9 @@ $(TEMPDIR):
.PHONY: bootstrap-tools
bootstrap-tools: $(TEMPDIR)
GO111MODULE=off GOBIN=$(shell realpath $(TEMPDIR)) go get -u golang.org/x/perf/cmd/benchstat
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(TEMPDIR)/ v1.26.0
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(TEMPDIR)/ v1.42.1
curl -sSfL https://raw.githubusercontent.com/wagoodman/go-bouncer/master/bouncer.sh | sh -s -- -b $(TEMPDIR)/ v0.2.0
curl -sfL https://install.goreleaser.com/github.com/goreleaser/goreleaser.sh | sh -s -- -b $(TEMPDIR)/ v0.177.0
.github/scripts/goreleaser-install.sh -b $(TEMPDIR)/ v0.182.1
.PHONY: bootstrap-go
bootstrap-go:
+38 -25
View File
@@ -1,24 +1,23 @@
# syft
<p align="center">
<img src="https://user-images.githubusercontent.com/5199289/136844524-1527b09f-c5cb-4aa9-be54-5aa92a6086c1.png" width="271">
</p>
[![Validations](https://github.com/anchore/syft/actions/workflows/validations.yaml/badge.svg)](https://github.com/anchore/syft/actions/workflows/validations.yaml)
[![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)
[![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/anchore/syft/blob/main/LICENSE)
[![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 tool like [Grype](https://github.com/anchore/grype).
## We'll be at KubeCon 2021!
Attending KubeCon 2021 in person? Join us for a meetup on **Tuesday, October 12th**.
We’ll have free swag, giveaways, snacks, and sips. Space will be limited, so make sure to [save your seat](https://get.anchore.com/2021-kubecon-na-opensource-happy-hour/)!
**Announcement:** Join us on October 20, 11am-noon PT for a webinar where we’ll discuss Anchore’s open source and enterprise solutions. [Save your seat!](https://get.anchore.com/anchore-open-source-vs-enterprise/)
---
![syft-demo](https://user-images.githubusercontent.com/590471/90277200-2a253000-de33-11ea-893f-32c219eea11a.gif)
**Features**
## Features
- Catalog container images and filesystems to discover packages and libraries.
- Supports packages and libraries from various ecosystems (APK, DEB, RPM, Ruby Bundles, Python Wheel/Egg/requirements.txt, JavaScript NPM/Yarn, Java JAR/EAR/WAR, Jenkins plugins JPI/HPI, Go modules)
- Linux distribution identification (supports Alpine, BusyBox, CentOS/RedHat, Debian/Ubuntu flavored distributions)
@@ -28,6 +27,27 @@ We’ll have free swag, giveaways, snacks, and sips. Space will be limited, so m
If you encounter an issue, please [let us know using the issue tracker](https://github.com/anchore/syft/issues).
## Installation
### Recommended
```bash
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
```
...or, you can specify a release version and destination directory for the installation:
```
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b <DESTINATION_DIR> <RELEASE_VERSION>
```
### Homebrew
```bash
brew tap anchore/syft
brew install syft
```
**Note**: Currently, Syft is built only for macOS and Linux.
## Getting started
To generate an SBOM for a Docker or OCI image:
@@ -47,7 +67,10 @@ To include software from all image layers in the SBOM, regardless of its presenc
syft packages <image> --scope all-layers
```
### Supported sources
Syft can generate a SBOM from a variety of sources:
```
# catalog a container image archive (from the result of `docker image save ...`, `podman save ...`, or `skopeo copy` commands)
syft packages path/to/image.tar
@@ -57,6 +80,7 @@ syft packages path/to/dir
```
Sources can be explicitly provided with a scheme:
```
docker:yourrepo/yourimage:tag use images from the Docker daemon
docker-archive:path/to/yourimage.tar use a tarball from disk for archives created from "docker save"
@@ -66,7 +90,10 @@ dir:path/to/yourproject read directly from a path on disk (any di
registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required)
```
### Output formats
The output format for Syft is configurable as well:
```
syft packages <image> -o <format>
```
@@ -79,23 +106,6 @@ Where the `format`s available are:
- `spdx-json`: A JSON report conforming to the [SPDX 2.2 JSON Schema](https://github.com/spdx/spdx-spec/blob/v2.2/schemas/spdx-schema.json).
- `table`: A columnar summary (default).
## Installation
**Recommended (macOS and Linux)**
```bash
# install the latest version to /usr/local/bin
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
# install a specific version into a specific dir
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b <SOME_BIN_PATH> <RELEASE_VERSION>
```
**Homebrew (macOS)**
```bash
brew tap anchore/syft
brew install syft
```
## Configuration
Configuration search paths:
@@ -116,6 +126,9 @@ output: "table"
# same as -q ; SYFT_QUIET env var
quiet: false
# same as --file; write output report to a file (default is to write to stdout)
file: ""
# enable/disable checking for application updates on startup
# same as SYFT_CHECK_FOR_APP_UPDATE env var
check-for-app-update: true
+16 -10
View File
@@ -2,6 +2,7 @@ package cmd
import (
"errors"
"fmt"
"os"
"github.com/anchore/syft/internal/log"
@@ -14,11 +15,13 @@ import (
// signal interrupts. Is responsible for handling each event relative to a given UI an to coordinate eventing until
// an eventual graceful exit.
// nolint:gocognit,funlen
func eventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription *partybus.Subscription, ux ui.UI, cleanupFn func()) error {
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
if ux, err = setupUI(subscription.Unsubscribe, ux); err != nil {
var ux ui.UI
if ux, err = setupUI(subscription.Unsubscribe, uxs...); err != nil {
return err
}
@@ -78,15 +81,18 @@ func eventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription *
return retErr
}
func setupUI(unsubscribe func() error, ux ui.UI) (ui.UI, error) {
if err := ux.Setup(unsubscribe); err != nil {
// replace the existing UI with a (simpler) logger UI
ux = ui.NewLoggerUI()
// 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 {
// something is very wrong, bail.
return ux, err
log.Warnf("unable to setup given UI, falling back to alternative UI: %+v", err)
continue
}
log.Errorf("unable to setup given UI, falling back to logger: %+v", err)
return ux, nil
}
return ux, nil
return nil, fmt.Errorf("unable to setup any UI")
}
+6 -6
View File
@@ -96,8 +96,8 @@ func Test_eventLoop_gracefulExit(t *testing.T) {
worker(),
signaler(),
subscription,
ux,
cleanupFn,
ux,
),
)
@@ -159,8 +159,8 @@ func Test_eventLoop_workerError(t *testing.T) {
worker(),
signaler(),
subscription,
ux,
cleanupFn,
ux,
),
workerErr,
"should have seen a worker error, but did not",
@@ -230,8 +230,8 @@ func Test_eventLoop_unsubscribeError(t *testing.T) {
worker(),
signaler(),
subscription,
ux,
cleanupFn,
ux,
),
)
@@ -300,8 +300,8 @@ func Test_eventLoop_handlerError(t *testing.T) {
worker(),
signaler(),
subscription,
ux,
cleanupFn,
ux,
),
finalEvent.Error,
"should have seen a event error, but did not",
@@ -355,8 +355,8 @@ func Test_eventLoop_signalsStopExecution(t *testing.T) {
worker(),
signaler(),
subscription,
ux,
cleanupFn,
ux,
),
)
@@ -425,8 +425,8 @@ func Test_eventLoop_uiTeardownError(t *testing.T) {
worker(),
signaler(),
subscription,
ux,
cleanupFn,
ux,
),
teardownError,
"should have seen a UI teardown error, but did not",
+27 -6
View File
@@ -35,7 +35,7 @@ const (
Supports the following image sources:
{{.appName}} {{.command}} yourrepo/yourimage:tag defaults to using images from a Docker daemon. If Docker is not present, the image is pulled directly from the registry.
{{.appName}} {{.command}} path/to/a/file/or/dir a Docker tar, OCI tar, OCI directory, or generic filesystem directory
{{.appName}} {{.command}} path/to/a/file/or/dir a Docker tar, OCI tar, OCI directory, or generic filesystem directory
You can also explicitly specify the scheme to use:
{{.appName}} {{.command}} docker:yourrepo/yourimage:tag explicitly use the Docker daemon
@@ -102,7 +102,7 @@ func init() {
}
func setPackageFlags(flags *pflag.FlagSet) {
///////// Formatting & Input options //////////////////////////////////////////////
// Formatting & Input options //////////////////////////////////////////////
flags.StringP(
"scope", "s", source.SquashedScope.String(),
@@ -113,7 +113,12 @@ func setPackageFlags(flags *pflag.FlagSet) {
fmt.Sprintf("report output formatter, options=%v", packages.AllPresenters),
)
///////// Upload options //////////////////////////////////////////////////////////
flags.StringP(
"file", "", "",
"file to write the report output to (default is STDOUT)",
)
// Upload options //////////////////////////////////////////////////////////
flags.StringP(
"host", "H", "",
"the hostname or URL of the Anchore Enterprise instance to upload to",
@@ -146,7 +151,7 @@ func setPackageFlags(flags *pflag.FlagSet) {
}
func bindPackagesConfigOptions(flags *pflag.FlagSet) error {
///////// Formatting & Input options //////////////////////////////////////////////
// Formatting & Input options //////////////////////////////////////////////
if err := viper.BindPFlag("package.cataloger.scope", flags.Lookup("scope")); err != nil {
return err
@@ -156,7 +161,11 @@ func bindPackagesConfigOptions(flags *pflag.FlagSet) error {
return err
}
///////// Upload options //////////////////////////////////////////////////////////
if err := viper.BindPFlag("file", flags.Lookup("file")); err != nil {
return err
}
// Upload options //////////////////////////////////////////////////////////
if err := viper.BindPFlag("anchore.host", flags.Lookup("host")); err != nil {
return err
@@ -188,12 +197,24 @@ func bindPackagesConfigOptions(flags *pflag.FlagSet) error {
func packagesExec(_ *cobra.Command, args []string) error {
// could be an image or a directory, with or without a scheme
userInput := args[0]
reporter, closer, err := reportWriter()
defer func() {
if err := closer(); err != nil {
log.Warnf("unable to write to report destination: %+v", err)
}
}()
if err != nil {
return err
}
return eventLoop(
packagesExecWorker(userInput),
setupSignals(),
eventSubscription,
ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet),
stereoscope.Cleanup,
ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet, reporter)...,
)
}
+14 -1
View File
@@ -7,6 +7,7 @@ import (
"github.com/anchore/stereoscope"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/presenter/poweruser"
"github.com/anchore/syft/internal/ui"
"github.com/anchore/syft/syft/event"
@@ -73,12 +74,24 @@ func init() {
func powerUserExec(_ *cobra.Command, args []string) error {
// could be an image or a directory, with or without a scheme
userInput := args[0]
reporter, closer, err := reportWriter()
defer func() {
if err := closer(); err != nil {
log.Warnf("unable to write to report destination: %+v", err)
}
}()
if err != nil {
return err
}
return eventLoop(
powerUserExecWorker(userInput),
setupSignals(),
eventSubscription,
ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet),
stereoscope.Cleanup,
ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet, reporter)...,
)
}
+33
View File
@@ -0,0 +1,33 @@
package cmd
import (
"fmt"
"io"
"os"
"strings"
"github.com/anchore/syft/internal/log"
)
func reportWriter() (io.Writer, func() error, error) {
nop := func() error { return nil }
path := strings.TrimSpace(appConfig.File)
switch len(path) {
case 0:
return os.Stdout, nop, nil
default:
reportFile, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return nil, nop, fmt.Errorf("unable to create report file: %w", err)
}
return reportFile, func() error {
log.Infof("report written to file=%q", path)
return reportFile.Close()
}, nil
}
}
+7 -6
View File
@@ -11,25 +11,26 @@ require (
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
github.com/anchore/packageurl-go v0.0.0-20210922164639-b3fa992ebd29
github.com/anchore/stereoscope v0.0.0-20210817160504-0f4abc2a5a5a
github.com/anchore/stereoscope v0.0.0-20211005213828-538011008578
github.com/antihax/optional v1.0.0
github.com/bmatcuk/doublestar/v2 v2.0.4
github.com/docker/docker v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible
github.com/dustin/go-humanize v1.0.0
github.com/facebookincubator/nvdtools v0.1.4
github.com/go-test/deep v1.0.7
github.com/google/uuid v1.1.1
github.com/google/go-cmp v0.4.1
github.com/google/uuid v1.2.0
github.com/gookit/color v1.2.7
github.com/hashicorp/go-multierror v1.1.0
github.com/hashicorp/go-version v1.2.0
github.com/mitchellh/go-homedir v1.1.0
github.com/mitchellh/mapstructure v1.3.1
github.com/olekukonko/tablewriter v0.0.4
github.com/pelletier/go-toml v1.8.0
github.com/pelletier/go-toml v1.8.1
github.com/pkg/profile v1.5.0
github.com/scylladb/go-set v1.0.2
github.com/sergi/go-diff v1.1.0
github.com/sirupsen/logrus v1.6.0
github.com/sirupsen/logrus v1.8.1
github.com/spdx/tools-golang v0.1.0
github.com/spf13/afero v1.2.2
github.com/spf13/cobra v1.0.1-0.20200909172742-8a63648dd905
@@ -42,8 +43,8 @@ require (
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/crypto v0.0.0-20200622213623-75b288015ac9
golang.org/x/mod v0.3.0
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d
gopkg.in/yaml.v2 v2.3.0
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1
gopkg.in/yaml.v2 v2.4.0
)
+15 -12
View File
@@ -73,7 +73,6 @@ github.com/Azure/go-autorest/autorest/validation v0.1.0/go.mod h1:Ha3z/SqBeaalWQ
github.com/Azure/go-autorest/autorest/validation v0.2.0/go.mod h1:3EEqHnBxQGHXRYq3HT1WyXAvT7LLY3tl70hw6tQIbjI=
github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Djarvur/go-err113 v0.0.0-20200410182137-af658d038157/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs=
@@ -117,8 +116,8 @@ github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZV
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E=
github.com/anchore/packageurl-go v0.0.0-20210922164639-b3fa992ebd29 h1:K9LfnxwhqvihqU0+MF325FNy7fsKV9EGaUxdfR4gnWk=
github.com/anchore/packageurl-go v0.0.0-20210922164639-b3fa992ebd29/go.mod h1:Oc1UkGaJwY6ND6vtAqPSlYrptKRJngHwkwB6W7l1uP0=
github.com/anchore/stereoscope v0.0.0-20210817160504-0f4abc2a5a5a h1:RQb+Gft1MKxjDfJCnHP/f1mwfy0Jz50Kp9QGgSWKQiY=
github.com/anchore/stereoscope v0.0.0-20210817160504-0f4abc2a5a5a/go.mod h1:165DfE5jApgEkHTWwu7Bijeml9fofudrgcpuWaD9+tk=
github.com/anchore/stereoscope v0.0.0-20211005213828-538011008578 h1:gSpftl0RWfdTwlPmsOLgEawHIw16xwwG1mFD/wrVDRE=
github.com/anchore/stereoscope v0.0.0-20211005213828-538011008578/go.mod h1:kL7jfbXblrDcBhu5ja/s+VTYL3Mzof4eQNMJiSqcwXQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
@@ -242,6 +241,8 @@ github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHqu
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/gabriel-vasile/mimetype v1.3.0 h1:4YOHITFLyYwF+iqG0ybSLGArRItynpfwdlWRmJnd75E=
github.com/gabriel-vasile/mimetype v1.3.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8=
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-critic/go-critic v0.4.1/go.mod h1:7/14rZGnZbY6E38VEGk2kVhoq6itzc1E68facVDK23g=
@@ -372,8 +373,9 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4
github.com/google/rpmpack v0.0.0-20191226140753-aa36bfddb3a0/go.mod h1:RaTPr0KUf2K7fnZYLNDrr8rxAamWs3iNywJLtQ2AzBg=
github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/wire v0.3.0/go.mod h1:i1DMg/Lu8Sz5yYl25iOdmc5CT5qusaa+zmRWs16741s=
github.com/google/wire v0.4.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU=
github.com/googleapis/gax-go v2.0.2+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
@@ -477,7 +479,6 @@ github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM52
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -593,8 +594,9 @@ github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.8.0 h1:Keo9qb7iRJs2voHvunFtuuYFsbWeOBh8/P9v/kVMFtw=
github.com/pelletier/go-toml v1.8.0/go.mod h1:D6yutnOGMveHEPV7VQOuvI/gXY61bv+9bAOTRnLElKs=
github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM=
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d/go.mod h1:3OzsM7FXDQlpCiw2j81fOmAwQLnZnLGXVKUzeKQXIAw=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -658,8 +660,9 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v1.0.0 h1:UVQPSSmc3qtTi+zPPkCXvZX9VvW/xT/NsRvKfwY81a8=
github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
@@ -798,9 +801,8 @@ golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 h1:cg5LA/zNPRzIXIWSCxQW10Rvpy94aQh3LT/ShoCpkHw=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -872,7 +874,7 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d h1:LO7XpTYMwTqxjLcGWPijK3vRXg1aWdlNOVOHRq45d7c=
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -939,10 +941,10 @@ golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1137,8 +1139,9 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
+37 -21
View File
@@ -29,6 +29,7 @@ type parser interface {
type Application struct {
ConfigPath string `yaml:",omitempty" json:"configPath"` // the location where the application config was read from (either from -c or discovered while loading)
Output string `yaml:"output" json:"output" mapstructure:"output"` // -o, the Presenter hint string to use for report formatting
File string `yaml:"file" json:"file" mapstructure:"file"` // --file, the file to write report output to
Quiet bool `yaml:"quiet" json:"quiet" mapstructure:"quiet"` // -q, indicates to not show any status output to stderr (ETUI or logging UI)
CheckForAppUpdate bool `yaml:"check-for-app-update" json:"check-for-app-update" mapstructure:"check-for-app-update"` // whether to check for an application update on start up or not
Anchore anchore `yaml:"anchore" json:"anchore" mapstructure:"anchore"` // options for interacting with Anchore Engine/Enterprise
@@ -88,10 +89,43 @@ func (cfg Application) loadDefaultValues(v *viper.Viper) {
}
}
// build inflates simple config values into syft native objects (or other complex objects) after the config is fully read in.
func (cfg *Application) parseConfigValues() error {
// parse application config options
for _, optionFn := range []func() error{
cfg.parseUploadOptions,
cfg.parseLogLevelOption,
} {
if err := optionFn(); err != nil {
return err
}
}
// parse nested config options
// for each field in the configuration struct, see if the field implements the parser interface
// note: the app config is a pointer, so we need to grab the elements explicitly (to traverse the address)
value := reflect.ValueOf(cfg).Elem()
for i := 0; i < value.NumField(); i++ {
// note: since the interface method of parser is a pointer receiver we need to get the value of the field as a pointer.
if parsable, ok := value.Field(i).Addr().Interface().(parser); ok {
// the field implements parser, call it
if err := parsable.parseConfigValues(); err != nil {
return err
}
}
}
return nil
}
func (cfg *Application) parseUploadOptions() error {
if cfg.Anchore.Host == "" && cfg.Anchore.Dockerfile != "" {
return fmt.Errorf("cannot provide dockerfile option without enabling upload")
}
return nil
}
func (cfg *Application) parseLogLevelOption() error {
if cfg.Quiet {
// TODO: this is bad: quiet option trumps all other logging options
// TODO: this is bad: quiet option trumps all other logging options (such as to a file on disk)
// we should be able to quiet the console logging and leave file logging alone...
// ... this will be an enhancement for later
cfg.Log.LevelOpt = logrus.PanicLevel
@@ -115,28 +149,10 @@ func (cfg *Application) parseConfigValues() error {
case v >= 2:
cfg.Log.LevelOpt = logrus.DebugLevel
default:
cfg.Log.LevelOpt = logrus.WarnLevel
cfg.Log.LevelOpt = logrus.ErrorLevel
}
}
}
if cfg.Anchore.Host == "" && cfg.Anchore.Dockerfile != "" {
return fmt.Errorf("cannot provide dockerfile option without enabling upload")
}
// 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
}
@@ -10,7 +10,7 @@ type CreationInfo struct {
Created time.Time `json:"created"`
// Identify who (or what, in the case of a tool) created the SPDX file. If the SPDX file was created by an
// individual, indicate the person's name. If the SPDX file was created on behalf of a company or organization,
//indicate the entity name. If the SPDX file was created using a software tool, indicate the name and version
// indicate the entity name. If the SPDX file was created using a software tool, indicate the name and version
// for that tool. If multiple participants or tools were involved, use multiple instances of this field. Person
// name or organization name may be designated as “anonymous” if appropriate.
Creators []string `json:"creators"`
@@ -95,6 +95,15 @@ func Test_getSPDXLicense(t *testing.T) {
},
expected: "GPL-3.0",
},
{
name: "debian to spdx conversion",
input: pkg.Package{
Licenses: []string{
"GPL-2",
},
},
expected: "GPL-2.0",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
@@ -4,6 +4,8 @@ import (
"encoding/json"
"fmt"
"io"
"path"
"strings"
"time"
"github.com/anchore/syft/internal"
@@ -12,8 +14,11 @@ import (
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
"github.com/google/uuid"
)
const anchoreNamespace = "https://anchore.com/syft"
// SPDXJsonPresenter is a SPDX presentation object for the syft results (see https://github.com/spdx/spdx-spec)
type SPDXJsonPresenter struct {
catalog *pkg.Catalog
@@ -41,14 +46,25 @@ func (pres *SPDXJsonPresenter) Present(output io.Writer) error {
// newSPDXJsonDocument creates and populates a new JSON document struct that follows the SPDX 2.2 spec from the given cataloging results.
func newSPDXJsonDocument(catalog *pkg.Catalog, srcMetadata source.Metadata) spdx22.Document {
var name string
uniqueID := uuid.Must(uuid.NewRandom())
var name, input, identifier string
switch srcMetadata.Scheme {
case source.ImageScheme:
name = srcMetadata.ImageMetadata.UserInput
name = cleanSPDXName(srcMetadata.ImageMetadata.UserInput)
input = "image"
case source.DirectoryScheme:
name = srcMetadata.Path
name = cleanSPDXName(srcMetadata.Path)
input = "dir"
}
if name != "." {
identifier = path.Join(input, fmt.Sprintf("%s-%s", name, uniqueID.String()))
} else {
identifier = path.Join(input, uniqueID.String())
}
namespace := path.Join(anchoreNamespace, identifier)
packages, files, relationships := newSPDXJsonElements(catalog)
return spdx22.Document{
@@ -67,7 +83,7 @@ func newSPDXJsonDocument(catalog *pkg.Catalog, srcMetadata source.Metadata) spdx
LicenseListVersion: spdxlicense.Version,
},
DataLicense: "CC0-1.0",
DocumentNamespace: fmt.Sprintf("https://anchore.com/syft/image/%s", srcMetadata.ImageMetadata.UserInput),
DocumentNamespace: namespace,
Packages: packages,
Files: files,
Relationships: relationships,
@@ -113,3 +129,14 @@ func newSPDXJsonElements(catalog *pkg.Catalog) ([]spdx22.Package, []spdx22.File,
return packages, files, relationships
}
func cleanSPDXName(name string) string {
// remove # according to specification
name = strings.ReplaceAll(name, "#", "-")
// remove : for url construction
name = strings.ReplaceAll(name, ":", "-")
// clean relative pathing
return path.Clean(name)
}
@@ -31,6 +31,10 @@ func TestSPDXJSONImagePresenter(t *testing.T) {
func spdxJsonRedactor(s []byte) []byte {
// each SBOM reports the time it was generated, which is not useful during snapshot testing
s = regexp.MustCompile(`"created": .*`).ReplaceAll(s, []byte("redacted"))
// each SBOM reports a unique documentNamespace when generated, this is not useful for snapshot testing
s = regexp.MustCompile(`"documentNamespace": .*`).ReplaceAll(s, []byte("redacted"))
// the license list will be updated periodically, the value here should not be directly tested in snapshot tests
return regexp.MustCompile(`"licenseListVersion": .*`).ReplaceAll(s, []byte("redacted"))
}
@@ -32,7 +32,7 @@ func (pres *TextPresenter) Present(output io.Writer) error {
switch pres.srcMetadata.Scheme {
case source.DirectoryScheme:
fmt.Fprintln(w, fmt.Sprintf("[Path: %s]", pres.srcMetadata.Path))
fmt.Fprintf(w, "[Path: %s]\n", pres.srcMetadata.Path)
case source.ImageScheme:
fmt.Fprintln(w, "[Image]")
@@ -51,7 +51,7 @@ func (pres *TextPresenter) Present(output io.Writer) error {
// populate artifacts...
rows := 0
for _, p := range pres.catalog.Sorted() {
fmt.Fprintln(w, fmt.Sprintf("[%s]", p.Name))
fmt.Fprintf(w, "[%s]\n", p.Name)
fmt.Fprintln(w, " Version:\t", p.Version)
fmt.Fprintln(w, " Type:\t", string(p.Type))
fmt.Fprintln(w, " Found by:\t", p.FoundBy)
@@ -22,6 +22,7 @@ type JSONFileMetadataEntry struct {
UserID int `json:"userID"`
GroupID int `json:"groupID"`
Digests []file.Digest `json:"digests,omitempty"`
MIMEType string `json:"mimeType"`
}
func NewJSONFileMetadata(data map[source.Location]source.FileMetadata, digests map[source.Location][]file.Digest) ([]JSONFileMetadata, error) {
@@ -46,6 +47,7 @@ func NewJSONFileMetadata(data map[source.Location]source.FileMetadata, digests m
UserID: metadata.UserID,
GroupID: metadata.GroupID,
Digests: digestResults,
MIMEType: metadata.MIMEType,
},
})
}
@@ -16,7 +16,8 @@
"mode": 775,
"type": "directory",
"userID": 0,
"groupID": 0
"groupID": 0,
"mimeType": ""
}
},
{
@@ -33,7 +34,8 @@
"algorithm": "sha256",
"value": "366a3f5653e34673b875891b021647440d0127c2ef041e3b1a22da2a7d4f3703"
}
]
],
"mimeType": ""
}
},
{
@@ -45,7 +47,8 @@
"type": "symbolicLink",
"linkDestination": "/c",
"userID": 0,
"groupID": 0
"groupID": 0,
"mimeType": ""
}
},
{
@@ -62,7 +65,8 @@
"algorithm": "sha256",
"value": "1b3722da2a7d90d033b87581a2a3f12021647445653e34666ef041e3b4f3707c"
}
]
],
"mimeType": ""
}
}
],
@@ -168,6 +172,7 @@
"configuration": {
"configPath": "",
"output": "",
"file": "",
"quiet": false,
"check-for-app-update": false,
"anchore": {
@@ -0,0 +1,190 @@
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"regexp"
"sort"
"strings"
"text/template"
"time"
"github.com/scylladb/go-set/strset"
)
// This program generates license_list.go.
const (
source = "license_list.go"
url = "https://spdx.org/licenses/licenses.json"
)
var tmp = template.Must(template.New("").Parse(`// Code generated by go generate; DO NOT EDIT.
// This file was generated by robots at {{ .Timestamp }}
// using data from {{ .URL }}
package spdxlicense
const Version = {{ printf "%q" .Version }}
var licenseIDs = map[string]string{
{{- range $k, $v := .LicenseIDs }}
{{ printf "%q" $k }}: {{ printf "%q" $v }},
{{- end }}
}
`))
var versionMatch = regexp.MustCompile(`-([0-9]+)\.?([0-9]+)?\.?([0-9]+)?\.?`)
type LicenseList struct {
Version string `json:"licenseListVersion"`
Licenses []struct {
ID string `json:"licenseId"`
Name string `json:"name"`
Text string `json:"licenseText"`
Deprecated bool `json:"isDeprecatedLicenseId"`
OSIApproved bool `json:"isOsiApproved"`
SeeAlso []string `json:"seeAlso"`
} `json:"licenses"`
}
func main() {
if err := run(); err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
}
func run() error {
resp, err := http.Get(url)
if err != nil {
return fmt.Errorf("unable to get licenses list: %+v", err)
}
var result LicenseList
if err = json.NewDecoder(resp.Body).Decode(&result); err != nil {
return fmt.Errorf("unable to decode license list: %+v", err)
}
defer func() {
if err := resp.Body.Close(); err != nil {
log.Fatalf("unable to close body: %+v", err)
}
}()
f, err := os.Create(source)
if err != nil {
return fmt.Errorf("unable to create %q: %+v", source, err)
}
defer func() {
if err := f.Close(); err != nil {
log.Fatalf("unable to close %q: %+v", source, err)
}
}()
licenseIDs := processSPDXLicense(result)
err = tmp.Execute(f, struct {
Timestamp time.Time
URL string
Version string
LicenseIDs map[string]string
}{
Timestamp: time.Now(),
URL: url,
Version: result.Version,
LicenseIDs: licenseIDs,
})
if err != nil {
return fmt.Errorf("unable to generate template: %+v", err)
}
return nil
}
// Parsing the provided SPDX license list necessitates a two pass approach.
// The first pass is only related to what SPDX considers the truth. These K:V pairs will never be overwritten.
// The second pass attempts to generate known short/long version listings for each key.
// For info on some short name conventions see this document:
// https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/#license-short-name.
// The short long listing generation attempts to build all license permutations for a given key.
// The new keys are then also associated with their relative SPDX value. If a key has already been entered
// we know to ignore it since it came from the first pass which is considered the SPDX source of truth.
// We also sort the licenses for the second pass so that cases like `GPL-1` associate to `GPL-1.0` and not `GPL-1.1`.
func processSPDXLicense(result LicenseList) map[string]string {
// first pass build map
var licenseIDs = make(map[string]string)
for _, l := range result.Licenses {
cleanID := strings.ToLower(l.ID)
if _, exists := licenseIDs[cleanID]; exists {
log.Fatalf("duplicate license ID found: %q", cleanID)
}
licenseIDs[cleanID] = l.ID
}
sort.Slice(result.Licenses, func(i, j int) bool {
return result.Licenses[i].ID < result.Licenses[j].ID
})
// second pass build exceptions
// do not overwrite if already exists
for _, l := range result.Licenses {
var multipleID []string
cleanID := strings.ToLower(l.ID)
multipleID = append(multipleID, buildLicensePermutations(cleanID)...)
for _, id := range multipleID {
if _, exists := licenseIDs[id]; !exists {
licenseIDs[id] = l.ID
}
}
}
return licenseIDs
}
func buildLicensePermutations(license string) (perms []string) {
lv := findLicenseVersion(license)
vp := versionPermutations(lv)
version := strings.Join(lv, ".")
for _, p := range vp {
perms = append(perms, strings.Replace(license, version, p, 1))
}
return perms
}
func findLicenseVersion(license string) (version []string) {
versionList := versionMatch.FindAllStringSubmatch(license, -1)
if len(versionList) == 0 {
return version
}
for i, v := range versionList[0] {
if v != "" && i != 0 {
version = append(version, v)
}
}
return version
}
func versionPermutations(version []string) []string {
ver := append([]string(nil), version...)
perms := strset.New()
for i := 1; i <= 3; i++ {
if len(ver) < i+1 {
ver = append(ver, "0")
}
perm := strings.Join(ver[:i], ".")
badCount := strings.Count(perm, "0") + strings.Count(perm, ".")
if badCount != len(perm) {
perms.Add(perm)
}
}
return perms.List()
}
@@ -0,0 +1,146 @@
package main
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestLicensePermutations(t *testing.T) {
var tests = []struct {
shortName string
permutations []string
}{
{
"GPL-1-only",
[]string{
"GPL-1-only",
"GPL-1.0-only",
"GPL-1.0.0-only",
},
},
{
"GPL-2",
[]string{
"GPL-2",
"GPL-2.0",
"GPL-2.0.0",
},
},
{
"GPL-2.0+",
[]string{
"GPL-2+",
"GPL-2.0+",
"GPL-2.0.0+",
},
},
{
"GPL-3.0.0-or-later",
[]string{
"GPL-3-or-later",
"GPL-3.0-or-later",
"GPL-3.0.0-or-later",
},
},
{
"oldap-2.0",
[]string{
"oldap-2",
"oldap-2.0",
"oldap-2.0.0",
},
},
}
for _, test := range tests {
t.Run(test.shortName, func(t *testing.T) {
perms := buildLicensePermutations(test.shortName)
assert.ElementsMatch(t, test.permutations, perms)
})
}
}
func TestVersionPermutations(t *testing.T) {
var tests = []struct {
version []string
permutations []string
}{
{
[]string{"1", "0"},
[]string{"1", "1.0", "1.0.0"},
},
{
[]string{"2"},
[]string{"2", "2.0", "2.0.0"},
},
{
[]string{"2", "0"},
[]string{"2", "2.0", "2.0.0"},
},
{
[]string{"3", "0", "0"},
[]string{"3", "3.0", "3.0.0"},
},
{
[]string{"0", "3"},
[]string{"0.3", "0.3.0"},
},
{
[]string{"0", "0", "3"},
[]string{"0.0.3"},
},
}
for _, test := range tests {
t.Run(strings.Join(test.version, "."), func(t *testing.T) {
got := versionPermutations(test.version)
assert.ElementsMatch(t, test.permutations, got)
})
}
}
func TestFindLicenseVersion(t *testing.T) {
var tests = []struct {
license string
version []string
}{
{
"GPL-1.0-only",
[]string{"1", "0"},
},
{
"GPL-2.0",
[]string{"2", "0"},
},
{
"GPL-2.0.0",
[]string{"2", "0", "0"},
},
{
"GPL-2",
[]string{"2"},
},
{
"bzip2-1",
[]string{"1"},
},
{
"php-3.01",
[]string{"3", "01"},
},
{
"oldap-2.0",
[]string{"2", "0"},
},
}
for _, test := range tests {
t.Run(test.license, func(t *testing.T) {
got := findLicenseVersion(test.license)
assert.Equal(t, test.version, got)
})
}
}
@@ -1,93 +0,0 @@
//go:build ignore
// +build ignore
package main
import (
"encoding/json"
"log"
"net/http"
"os"
"strings"
"text/template"
"time"
)
// This program generates license_list.go.
const (
source = "license_list.go"
url = "https://spdx.org/licenses/licenses.json"
)
var tmp = template.Must(template.New("").Parse(`// Code generated by go generate; DO NOT EDIT.
// This file was generated by robots at {{ .Timestamp }}
// using data from {{ .URL }}
package spdxlicense
const Version = {{ printf "%q" .Version }}
var licenseIDs = map[string]string{
{{- range $k, $v := .LicenseIDs }}
{{ printf "%q" $k }}: {{ printf "%q" $v }},
{{- end }}
}
`))
type LicenseList struct {
Version string `json:"licenseListVersion"`
Licenses []struct {
ID string `json:"licenseId"`
Name string `json:"name"`
Text string `json:"licenseText"`
Deprecated bool `json:"isDeprecatedLicenseId"`
OSIApproved bool `json:"isOsiApproved"`
SeeAlso []string `json:"seeAlso"`
} `json:"licenses"`
}
func main() {
resp, err := http.Get(url)
if err != nil {
log.Fatalf("unable to get licenses list: %+v", err)
}
var result LicenseList
if err = json.NewDecoder(resp.Body).Decode(&result); err != nil {
log.Fatalf("unable to decode license list: %+v", err)
}
f, err := os.Create(source)
if err != nil {
log.Fatalf("unable to create %q: %+v", source, err)
}
defer func() {
if err := f.Close(); err != nil {
log.Fatalf("unable to close %q: %+v", source, err)
}
}()
var licenseIDs = make(map[string]string)
for _, l := range result.Licenses {
cleanID := strings.ToLower(l.ID)
if _, exists := licenseIDs[cleanID]; exists {
log.Fatalf("duplicate license ID found: %q", cleanID)
}
licenseIDs[cleanID] = l.ID
}
err = tmp.Execute(f, struct {
Timestamp time.Time
URL string
Version string
LicenseIDs map[string]string
}{
Timestamp: time.Now(),
URL: url,
Version: result.Version,
LicenseIDs: licenseIDs,
})
if err != nil {
log.Fatalf("unable to generate template: %+v", err)
}
}
+9 -1
View File
@@ -4,7 +4,15 @@ import (
"strings"
)
//go:generate go run generate_license_list.go
// https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/#license-short-name
// License generated in license_list.go uses a regular expression to help resolve cases where
// x.0.0 and x are supplied as version numbers. For SPDX compatibility, versions with trailing
// dot-zeroes are considered to be equivalent to versions without (e.g., “2.0.0” is considered equal to “2.0” and “2”).
// EX: gpl-2+ ---> GPL-2.0+
// EX: gpl-2.0.0-only ---> GPL-2.0-only
// See the debian link for more details on the spdx license differences
//go:generate go run generate/generate_license_list.go
func ID(id string) (string, bool) {
value, exists := licenseIDs[strings.ToLower(id)]
File diff suppressed because it is too large Load Diff
@@ -10,5 +10,6 @@ func TestLicenceListIDs(t *testing.T) {
// do a sanity check on the generated data
assert.Equal(t, "0BSD", licenseIDs["0bsd"])
assert.Equal(t, "ZPL-2.1", licenseIDs["zpl-2.1"])
assert.Equal(t, "GPL-2.0", licenseIDs["gpl-2"])
assert.NotEmpty(t, Version)
}
+60
View File
@@ -0,0 +1,60 @@
package spdxlicense
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestIDParse(t *testing.T) {
var tests = []struct {
shortName string
spdx string
}{
{
"GPL-1-only",
"GPL-1.0-only",
},
{
"GPL-2",
"GPL-2.0",
},
{
"GPL-2+",
"GPL-2.0+",
},
{
"GPL-3.0.0-or-later",
"GPL-3.0-or-later",
},
{
"GPL-3-with-autoconf-exception",
"GPL-3.0-with-autoconf-exception",
},
{
"CC-by-nc-3-de",
"CC-BY-NC-3.0-DE",
},
// the below few cases are NOT expected, however, seem unavoidable given the current approach
{
"w3c-20150513.0.0",
"W3C-20150513",
},
{
"spencer-86.0.0",
"Spencer-86",
},
{
"unicode-dfs-2015.0.0",
"Unicode-DFS-2015",
},
}
for _, test := range tests {
t.Run(test.shortName, func(t *testing.T) {
got, exists := ID(test.shortName)
assert.True(t, exists)
assert.Equal(t, test.spdx, got)
})
}
}
+19 -16
View File
@@ -33,25 +33,28 @@ import (
// or in the shared ui package as a function on the main handler object. All handler functions should be completed
// processing an event before the ETUI exits (coordinated with a sync.WaitGroup)
type ephemeralTerminalUI struct {
unsubscribe func() error
handler *ui.Handler
waitGroup *sync.WaitGroup
frame *frame.Frame
logBuffer *bytes.Buffer
output *os.File
unsubscribe func() error
handler *ui.Handler
waitGroup *sync.WaitGroup
frame *frame.Frame
logBuffer *bytes.Buffer
uiOutput *os.File
reportOutput io.Writer
}
func NewEphemeralTerminalUI() UI {
// NewEphemeralTerminalUI writes all events to a TUI and writes the final report to the given writer.
func NewEphemeralTerminalUI(reportWriter io.Writer) UI {
return &ephemeralTerminalUI{
handler: ui.NewHandler(),
waitGroup: &sync.WaitGroup{},
output: os.Stderr,
handler: ui.NewHandler(),
waitGroup: &sync.WaitGroup{},
uiOutput: os.Stderr,
reportOutput: reportWriter,
}
}
func (h *ephemeralTerminalUI) Setup(unsubscribe func() error) error {
h.unsubscribe = unsubscribe
hideCursor(h.output)
hideCursor(h.uiOutput)
// prep the logger to not clobber the screen from now on (logrus only)
h.logBuffer = bytes.NewBufferString("")
@@ -81,7 +84,7 @@ func (h *ephemeralTerminalUI) Handle(event partybus.Event) error {
// are about to write bytes to stdout, so we should reset the terminal state first
h.closeScreen(false)
if err := handleCatalogerPresenterReady(event); err != nil {
if err := handleCatalogerPresenterReady(event, h.reportOutput); err != nil {
log.Errorf("unable to show %s event: %+v", event.Type, err)
}
@@ -95,7 +98,7 @@ func (h *ephemeralTerminalUI) openScreen() error {
config := frame.Config{
PositionPolicy: frame.PolicyFloatForward,
// only report output to stderr, reserve report output for stdout
Output: h.output,
Output: h.uiOutput,
}
fr, err := frame.New(config)
@@ -128,15 +131,15 @@ func (h *ephemeralTerminalUI) flushLog() {
logWrapper, ok := log.Log.(*logger.LogrusLogger)
if ok {
fmt.Fprint(logWrapper.Output, h.logBuffer.String())
logWrapper.Logger.SetOutput(h.output)
logWrapper.Logger.SetOutput(h.uiOutput)
} else {
fmt.Fprint(h.output, h.logBuffer.String())
fmt.Fprint(h.uiOutput, h.logBuffer.String())
}
}
func (h *ephemeralTerminalUI) Teardown(force bool) error {
h.closeScreen(force)
showCursor(h.output)
showCursor(h.uiOutput)
return nil
}
+2 -3
View File
@@ -4,7 +4,6 @@ import (
"context"
"fmt"
"io"
"os"
"sync"
"github.com/anchore/syft/internal"
@@ -17,14 +16,14 @@ import (
// handleCatalogerPresenterReady is a UI function for processing the CatalogerFinished bus event, displaying the catalog
// via the given presenter to stdout.
func handleCatalogerPresenterReady(event partybus.Event) error {
func handleCatalogerPresenterReady(event partybus.Event, reportOutput io.Writer) error {
// show the report to stdout
pres, err := syftEventParsers.ParsePresenterReady(event)
if err != nil {
return fmt.Errorf("bad CatalogerFinished event: %w", err)
}
if err := pres.Present(os.Stdout); err != nil {
if err := pres.Present(reportOutput); err != nil {
return fmt.Errorf("unable to show package catalog report: %w", err)
}
return nil
+10 -4
View File
@@ -1,17 +1,23 @@
package ui
import (
"io"
"github.com/anchore/syft/internal/log"
syftEvent "github.com/anchore/syft/syft/event"
"github.com/wagoodman/go-partybus"
)
type loggerUI struct {
unsubscribe func() error
unsubscribe func() error
reportOutput io.Writer
}
func NewLoggerUI() UI {
return &loggerUI{}
// NewLoggerUI writes all events to the common application logger and writes the final report to the given writer.
func NewLoggerUI(reportWriter io.Writer) UI {
return &loggerUI{
reportOutput: reportWriter,
}
}
func (l *loggerUI) Setup(unsubscribe func() error) error {
@@ -25,7 +31,7 @@ func (l loggerUI) Handle(event partybus.Event) error {
return nil
}
if err := handleCatalogerPresenterReady(event); err != nil {
if err := handleCatalogerPresenterReady(event, l.reportOutput); err != nil {
log.Warnf("unable to show catalog image finished event: %+v", err)
}
+12 -10
View File
@@ -1,29 +1,31 @@
package ui
import (
"io"
"os"
"runtime"
"golang.org/x/crypto/ssh/terminal"
"golang.org/x/term"
)
// TODO: build tags to exclude options from windows
// Select is responsible for determining the specific UI function given select user option, the current platform
// config values, and environment status (such as a TTY being present).
func Select(verbose, quiet bool) UI {
var ui UI
isStdoutATty := terminal.IsTerminal(int(os.Stdout.Fd()))
isStderrATty := terminal.IsTerminal(int(os.Stderr.Fd()))
// config values, and environment status (such as a TTY being present). The first UI in the returned slice of UIs
// is intended to be used and the UIs that follow are meant to be attempted only in a fallback posture when there
// are environmental problems (e.g. cannot write to the terminal). A writer is provided to capture the output of
// the final SBOM report.
func Select(verbose, quiet bool, reportWriter io.Writer) (uis []UI) {
isStdoutATty := term.IsTerminal(int(os.Stdout.Fd()))
isStderrATty := term.IsTerminal(int(os.Stderr.Fd()))
notATerminal := !isStderrATty && !isStdoutATty
switch {
case runtime.GOOS == "windows" || verbose || quiet || notATerminal || !isStderrATty:
ui = NewLoggerUI()
uis = append(uis, NewLoggerUI(reportWriter))
default:
ui = NewEphemeralTerminalUI()
uis = append(uis, NewEphemeralTerminalUI(reportWriter), NewLoggerUI(reportWriter))
}
return ui
return uis
}
+1 -5
View File
@@ -45,11 +45,7 @@ func build() *jsonschema.Schema {
reflector := &jsonschema.Reflector{
AllowAdditionalProperties: true,
TypeNamer: func(r reflect.Type) string {
name := r.Name()
if strings.HasPrefix(name, "JSON") {
name = strings.TrimPrefix(name, "JSON")
}
return name
return strings.TrimPrefix(r.Name(), "JSON")
},
}
documentSchema := reflector.ReflectFromType(reflect.TypeOf(&poweruser.JSONDocument{}))
+5
View File
@@ -118,6 +118,11 @@ func TestIdentifyDistro(t *testing.T) {
Type: CentOS,
Version: "5.7.0",
},
{
fixture: "test-fixtures/os/mariner",
Type: Mariner,
Version: "1.0.0",
},
}
observedDistros := internal.NewStringSet()
@@ -0,0 +1,9 @@
NAME="Common Base Linux Mariner"
VERSION="1.0.20210901"
ID=mariner
VERSION_ID=1.0
PRETTY_NAME="CBL-Mariner/Linux"
ANSI_COLOR="1;34"
HOME_URL="https://aka.ms/cbl-mariner"
BUG_REPORT_URL="https://aka.ms/cbl-mariner"
SUPPORT_URL="https://aka.ms/cbl-mariner"
+3
View File
@@ -20,6 +20,7 @@ const (
SLES Type = "sles"
Photon Type = "photon"
Windows Type = "windows"
Mariner Type = "mariner"
)
// All contains all Linux distribution options
@@ -38,6 +39,7 @@ var All = []Type{
SLES,
Photon,
Windows,
Mariner,
}
// IDMapping connects a distro ID like "ubuntu" to a Distro type
@@ -56,6 +58,7 @@ var IDMapping = map[string]Type{
"sles": SLES,
"photon": Photon,
"windows": Windows,
"mariner": Mariner,
}
// String returns the string representation of the given Linux distribution.
+1 -1
View File
@@ -31,7 +31,7 @@ func (i *ClassificationCataloger) Catalog(resolver source.FileResolver) (map[sou
}
}
}
log.Debugf("classification cataloger discovered %d results", numResults)
log.Debugf("file classifier discovered %d results", numResults)
return results, nil
}
+1 -1
View File
@@ -99,7 +99,7 @@ func DigestAlgorithmName(hash crypto.Hash) string {
func CleanDigestAlgorithmName(name string) string {
lower := strings.ToLower(name)
return strings.Replace(lower, "-", "", -1)
return strings.ReplaceAll(lower, "-", "")
}
func digestsCatalogingProgress(locations int64) (*progress.Stage, *progress.Manual) {
+28 -21
View File
@@ -49,11 +49,12 @@ func TestFileMetadataCataloger(t *testing.T) {
path: "/file-1.txt",
exists: true,
expected: source.FileMetadata{
Mode: 0644,
Type: "RegularFile",
UserID: 1,
GroupID: 2,
Size: 7,
Mode: 0644,
Type: "RegularFile",
UserID: 1,
GroupID: 2,
Size: 7,
MIMEType: "text/plain",
},
},
{
@@ -65,6 +66,7 @@ func TestFileMetadataCataloger(t *testing.T) {
LinkDestination: "file-1.txt",
UserID: 1,
GroupID: 2,
MIMEType: "",
},
},
{
@@ -76,46 +78,51 @@ func TestFileMetadataCataloger(t *testing.T) {
LinkDestination: "file-1.txt",
UserID: 0,
GroupID: 0,
MIMEType: "",
},
},
{
path: "/char-device-1",
exists: true,
expected: source.FileMetadata{
Mode: 0644 | os.ModeDevice | os.ModeCharDevice,
Type: "CharacterDevice",
UserID: 0,
GroupID: 0,
Mode: 0644 | os.ModeDevice | os.ModeCharDevice,
Type: "CharacterDevice",
UserID: 0,
GroupID: 0,
MIMEType: "",
},
},
{
path: "/block-device-1",
exists: true,
expected: source.FileMetadata{
Mode: 0644 | os.ModeDevice,
Type: "BlockDevice",
UserID: 0,
GroupID: 0,
Mode: 0644 | os.ModeDevice,
Type: "BlockDevice",
UserID: 0,
GroupID: 0,
MIMEType: "",
},
},
{
path: "/fifo-1",
exists: true,
expected: source.FileMetadata{
Mode: 0644 | os.ModeNamedPipe,
Type: "FIFONode",
UserID: 0,
GroupID: 0,
Mode: 0644 | os.ModeNamedPipe,
Type: "FIFONode",
UserID: 0,
GroupID: 0,
MIMEType: "",
},
},
{
path: "/bin",
exists: true,
expected: source.FileMetadata{
Mode: 0755 | os.ModeDir,
Type: "Directory",
UserID: 0,
GroupID: 0,
Mode: 0755 | os.ModeDir,
Type: "Directory",
UserID: 0,
GroupID: 0,
MIMEType: "",
},
},
}
+6 -6
View File
@@ -9,7 +9,7 @@ import (
"github.com/scylladb/go-set/strset"
)
const ApkDbGlob = "**/lib/apk/db/installed"
const ApkDBGlob = "**/lib/apk/db/installed"
var _ FileOwner = (*ApkMetadata)(nil)
@@ -37,11 +37,11 @@ type ApkMetadata struct {
// ApkFileRecord represents a single file listing and metadata from a APK DB entry (which may have many of these file records).
type ApkFileRecord struct {
Path string `json:"path"`
OwnerUID string `json:"ownerUid,omitempty"`
OwnerGID string `json:"ownerGid,omitempty"`
Permissions string `json:"permissions,omitempty"`
Digest file.Digest `json:"digest,omitempty"`
Path string `json:"path"`
OwnerUID string `json:"ownerUid,omitempty"`
OwnerGID string `json:"ownerGid,omitempty"`
Permissions string `json:"permissions,omitempty"`
Digest *file.Digest `json:"digest,omitempty"`
}
// PackageURL returns the PURL for the specific Alpine package (see https://github.com/package-url/purl-spec)
+1 -1
View File
@@ -11,7 +11,7 @@ import (
// NewApkdbCataloger returns a new Alpine DB cataloger object.
func NewApkdbCataloger() *common.GenericCataloger {
globParsers := map[string]common.ParserFn{
pkg.ApkDbGlob: parseApkDB,
pkg.ApkDBGlob: parseApkDB,
}
return common.NewGenericCataloger(nil, globParsers, "apkdb-cataloger")
+1 -1
View File
@@ -128,7 +128,7 @@ func parseApkDBEntry(reader io.Reader) (*pkg.ApkMetadata, error) {
log.Warnf("checksum field with no parent record: %q", value)
continue
}
fileRecord.Digest = file.Digest{
fileRecord.Digest = &file.Digest{
Algorithm: "sha1",
Value: value,
}
+38 -38
View File
@@ -41,7 +41,7 @@ func TestExtraFileAttributes(t *testing.T) {
OwnerUID: "0",
OwnerGID: "0",
Permissions: "755",
Digest: file.Digest{
Digest: &file.Digest{
Algorithm: "sha1",
Value: "Q1M0C9qfC/+kdRiOodeihG2GMRtkE=",
},
@@ -110,7 +110,7 @@ func TestSinglePackageDetails(t *testing.T) {
OwnerUID: "0",
OwnerGID: "0",
Permissions: "755",
Digest: file.Digest{
Digest: &file.Digest{
Algorithm: "sha1",
Value: "Q1Kja2+POZKxEkUOZqwSjC6kmaED4=",
},
@@ -126,7 +126,7 @@ func TestSinglePackageDetails(t *testing.T) {
OwnerUID: "0",
OwnerGID: "0",
Permissions: "755",
Digest: file.Digest{
Digest: &file.Digest{
Algorithm: "sha1",
Value: "Q1CVmFbdY+Hv6/jAHl1gec2Kbx1EY=",
},
@@ -136,7 +136,7 @@ func TestSinglePackageDetails(t *testing.T) {
OwnerUID: "0",
OwnerGID: "0",
Permissions: "755",
Digest: file.Digest{
Digest: &file.Digest{
Algorithm: "sha1",
Value: "Q1yFAhGggmL7ERgbIA7KQxyTzf3ks=",
},
@@ -146,7 +146,7 @@ func TestSinglePackageDetails(t *testing.T) {
OwnerUID: "0",
OwnerGID: "0",
Permissions: "755",
Digest: file.Digest{
Digest: &file.Digest{
Algorithm: "sha1",
Value: "Q1dAdYK8M/INibRQF5B3Rw7cmNDDA=",
},
@@ -156,7 +156,7 @@ func TestSinglePackageDetails(t *testing.T) {
OwnerUID: "0",
OwnerGID: "0",
Permissions: "755",
Digest: file.Digest{
Digest: &file.Digest{
Algorithm: "sha1",
Value: "Q1eR2Dz/WylabgbWMTkd2+hGmEya4=",
},
@@ -195,49 +195,49 @@ func TestSinglePackageDetails(t *testing.T) {
},
{
Path: "/etc/fstab",
Digest: file.Digest{
Digest: &file.Digest{
Algorithm: "sha1",
Value: "Q11Q7hNe8QpDS531guqCdrXBzoA/o=",
},
},
{
Path: "/etc/group",
Digest: file.Digest{
Digest: &file.Digest{
Algorithm: "sha1",
Value: "Q1oJ16xWudgKOrXIEquEDzlF2Lsm4=",
},
},
{
Path: "/etc/hostname",
Digest: file.Digest{
Digest: &file.Digest{
Algorithm: "sha1",
Value: "Q16nVwYVXP/tChvUPdukVD2ifXOmc=",
},
},
{
Path: "/etc/hosts",
Digest: file.Digest{
Digest: &file.Digest{
Algorithm: "sha1",
Value: "Q1BD6zJKZTRWyqGnPi4tSfd3krsMU=",
},
},
{
Path: "/etc/inittab",
Digest: file.Digest{
Digest: &file.Digest{
Algorithm: "sha1",
Value: "Q1TsthbhW7QzWRe1E/NKwTOuD4pHc=",
},
},
{
Path: "/etc/modules",
Digest: file.Digest{
Digest: &file.Digest{
Algorithm: "sha1",
Value: "Q1toogjUipHGcMgECgPJX64SwUT1M=",
},
},
{
Path: "/etc/motd",
Digest: file.Digest{
Digest: &file.Digest{
Algorithm: "sha1",
Value: "Q1XmduVVNURHQ27TvYp1Lr5TMtFcA=",
},
@@ -247,35 +247,35 @@ func TestSinglePackageDetails(t *testing.T) {
OwnerUID: "0",
OwnerGID: "0",
Permissions: "777",
Digest: file.Digest{
Digest: &file.Digest{
Algorithm: "sha1",
Value: "Q1kiljhXXH1LlQroHsEJIkPZg2eiw=",
},
},
{
Path: "/etc/passwd",
Digest: file.Digest{
Digest: &file.Digest{
Algorithm: "sha1",
Value: "Q1TchuuLUfur0izvfZQZxgN/LJhB8=",
},
},
{
Path: "/etc/profile",
Digest: file.Digest{
Digest: &file.Digest{
Algorithm: "sha1",
Value: "Q1KpFb8kl5LvwXWlY3e58FNsjrI34=",
},
},
{
Path: "/etc/protocols",
Digest: file.Digest{
Digest: &file.Digest{
Algorithm: "sha1",
Value: "Q13FqXUnvuOpMDrH/6rehxuYAEE34=",
},
},
{
Path: "/etc/services",
Digest: file.Digest{
Digest: &file.Digest{
Algorithm: "sha1",
Value: "Q1C6HJNgQvLWqt5VY+n7MZJ1rsDuY=",
},
@@ -285,21 +285,21 @@ func TestSinglePackageDetails(t *testing.T) {
OwnerUID: "0",
OwnerGID: "42",
Permissions: "640",
Digest: file.Digest{
Digest: &file.Digest{
Algorithm: "sha1",
Value: "Q1ltrPIAW2zHeDiajsex2Bdmq3uqA=",
},
},
{
Path: "/etc/shells",
Digest: file.Digest{
Digest: &file.Digest{
Algorithm: "sha1",
Value: "Q1ojm2YdpCJ6B/apGDaZ/Sdb2xJkA=",
},
},
{
Path: "/etc/sysctl.conf",
Digest: file.Digest{
Digest: &file.Digest{
Algorithm: "sha1",
Value: "Q14upz3tfnNxZkIEsUhWn7Xoiw96g=",
},
@@ -318,7 +318,7 @@ func TestSinglePackageDetails(t *testing.T) {
OwnerUID: "0",
OwnerGID: "0",
Permissions: "600",
Digest: file.Digest{
Digest: &file.Digest{
Algorithm: "sha1",
Value: "Q1vfk1apUWI4yLJGhhNRd0kJixfvY=",
},
@@ -331,28 +331,28 @@ func TestSinglePackageDetails(t *testing.T) {
},
{
Path: "/etc/modprobe.d/aliases.conf",
Digest: file.Digest{
Digest: &file.Digest{
Algorithm: "sha1",
Value: "Q1WUbh6TBYNVK7e4Y+uUvLs/7viqk=",
},
},
{
Path: "/etc/modprobe.d/blacklist.conf",
Digest: file.Digest{
Digest: &file.Digest{
Algorithm: "sha1",
Value: "Q1xxYGU6S6TLQvb7ervPrWWwAWqMg=",
},
},
{
Path: "/etc/modprobe.d/i386.conf",
Digest: file.Digest{
Digest: &file.Digest{
Algorithm: "sha1",
Value: "Q1pnay/njn6ol9cCssL7KiZZ8etlc=",
},
},
{
Path: "/etc/modprobe.d/kms.conf",
Digest: file.Digest{
Digest: &file.Digest{
Algorithm: "sha1",
Value: "Q1ynbLn3GYDpvajba/ldp1niayeog=",
},
@@ -401,14 +401,14 @@ func TestSinglePackageDetails(t *testing.T) {
},
{
Path: "/etc/profile.d/color_prompt",
Digest: file.Digest{
Digest: &file.Digest{
Algorithm: "sha1",
Value: "Q10wL23GuSCVfumMRgakabUI6EsSk=",
},
},
{
Path: "/etc/profile.d/locale",
Digest: file.Digest{
Digest: &file.Digest{
Algorithm: "sha1",
Value: "Q1R4bIEpnKxxOSrlnZy9AoawqZ5DU=",
},
@@ -436,7 +436,7 @@ func TestSinglePackageDetails(t *testing.T) {
},
{
Path: "/lib/sysctl.d/00-alpine.conf",
Digest: file.Digest{
Digest: &file.Digest{
Algorithm: "sha1",
Value: "Q1HpElzW1xEgmKfERtTy7oommnq6c=",
},
@@ -479,7 +479,7 @@ func TestSinglePackageDetails(t *testing.T) {
OwnerUID: "0",
OwnerGID: "0",
Permissions: "755",
Digest: file.Digest{
Digest: &file.Digest{
Algorithm: "sha1",
Value: "Q1YeuSmC7iDbEWrusPzA/zUQF6YSg=",
},
@@ -537,7 +537,7 @@ func TestSinglePackageDetails(t *testing.T) {
OwnerUID: "0",
OwnerGID: "0",
Permissions: "777",
Digest: file.Digest{
Digest: &file.Digest{
Algorithm: "sha1",
Value: "Q11/SNZz/8cK2dSKK+cJpVrZIuF4Q=",
},
@@ -586,7 +586,7 @@ func TestSinglePackageDetails(t *testing.T) {
OwnerUID: "0",
OwnerGID: "0",
Permissions: "777",
Digest: file.Digest{
Digest: &file.Digest{
Algorithm: "sha1",
Value: "Q1dzbdazYZA2nTzSIG3YyNw7d4Juc=",
},
@@ -599,7 +599,7 @@ func TestSinglePackageDetails(t *testing.T) {
OwnerUID: "0",
OwnerGID: "0",
Permissions: "777",
Digest: file.Digest{
Digest: &file.Digest{
Algorithm: "sha1",
Value: "Q1OFZt+ZMp7j0Gny0rqSKuWJyqYmA=",
},
@@ -704,7 +704,7 @@ func TestMultiplePackages(t *testing.T) {
OwnerUID: "0",
OwnerGID: "0",
Permissions: "755",
Digest: file.Digest{
Digest: &file.Digest{
Algorithm: "sha1",
Value: "Q1Kja2+POZKxEkUOZqwSjC6kmaED4=",
},
@@ -720,7 +720,7 @@ func TestMultiplePackages(t *testing.T) {
OwnerUID: "0",
OwnerGID: "0",
Permissions: "755",
Digest: file.Digest{
Digest: &file.Digest{
Algorithm: "sha1",
Value: "Q1CVmFbdY+Hv6/jAHl1gec2Kbx1EY=",
},
@@ -730,7 +730,7 @@ func TestMultiplePackages(t *testing.T) {
OwnerUID: "0",
OwnerGID: "0",
Permissions: "755",
Digest: file.Digest{
Digest: &file.Digest{
Algorithm: "sha1",
Value: "Q1yFAhGggmL7ERgbIA7KQxyTzf3ks=",
},
@@ -740,7 +740,7 @@ func TestMultiplePackages(t *testing.T) {
OwnerUID: "0",
OwnerGID: "0",
Permissions: "755",
Digest: file.Digest{
Digest: &file.Digest{
Algorithm: "sha1",
Value: "Q1dAdYK8M/INibRQF5B3Rw7cmNDDA=",
},
@@ -750,7 +750,7 @@ func TestMultiplePackages(t *testing.T) {
OwnerUID: "0",
OwnerGID: "0",
Permissions: "755",
Digest: file.Digest{
Digest: &file.Digest{
Algorithm: "sha1",
Value: "Q1eR2Dz/WylabgbWMTkd2+hGmEya4=",
},
+3 -3
View File
@@ -39,8 +39,7 @@ func ImageCatalogers() []Cataloger {
rpmdb.NewRpmdbCataloger(),
java.NewJavaCataloger(),
apkdb.NewApkdbCataloger(),
golang.NewGoModCataloger(),
rust.NewCargoLockCataloger(),
golang.NewGoModuleBinaryCataloger(),
}
}
@@ -55,7 +54,8 @@ func DirectoryCatalogers() []Cataloger {
rpmdb.NewRpmdbCataloger(),
java.NewJavaCataloger(),
apkdb.NewApkdbCataloger(),
golang.NewGoModCataloger(),
golang.NewGoModuleBinaryCataloger(),
golang.NewGoModFileCataloger(),
rust.NewCargoLockCataloger(),
}
}
@@ -29,20 +29,20 @@ var defaultCandidateAdditions = buildCandidateLookup(
{
// example image: docker.io/nuxeo:latest
pkg.JavaPkg,
candidateKey{PkgName: "elasticsearch"}, //, Vendor: "elasticsearch"},
candidateKey{PkgName: "elasticsearch"}, // , Vendor: "elasticsearch"},
candidateAddition{AdditionalVendors: []string{"elastic"}},
},
{
// example image: docker.io/kaazing-gateway:latest
pkg.JavaPkg,
candidateKey{PkgName: "log4j"}, //, Vendor: "apache-software-foundation"},
candidateKey{PkgName: "log4j"}, // , Vendor: "apache-software-foundation"},
candidateAddition{AdditionalVendors: []string{"apache"}},
},
{
// example image: cassandra:latest
pkg.JavaPkg,
candidateKey{PkgName: "apache-cassandra"}, //, Vendor: "apache"},
candidateKey{PkgName: "apache-cassandra"}, // , Vendor: "apache"},
candidateAddition{AdditionalProducts: []string{"cassandra"}},
},
@@ -18,10 +18,18 @@ func (c BySpecificity) Less(i, j int) bool {
iScore := weightedCountForSpecifiedFields(c[i])
jScore := weightedCountForSpecifiedFields(c[j])
if iScore == jScore {
// check weighted sort first
if iScore != jScore {
return iScore > jScore
}
// sort longer fields to top
if countFieldLength(c[i]) != countFieldLength(c[j]) {
return countFieldLength(c[i]) > countFieldLength(c[j])
}
return iScore > jScore
// if score and length are equal then text sort
return c[i].BindToFmtString() < c[j].BindToFmtString()
}
func countFieldLength(cpe wfn.Attributes) int {
@@ -80,6 +80,25 @@ func TestCPESpecificity(t *testing.T) {
mustCPE("cpe:2.3:a:1:*:333:*:*:*:*:*:*:*"),
},
},
{
name: "sort by mix of field length, specificity, dash",
input: []pkg.CPE{
mustCPE("cpe:2.3:a:alpine:alpine_keys:2.3-r1:*:*:*:*:*:*:*"),
mustCPE("cpe:2.3:a:alpine_keys:alpine_keys:2.3-r1:*:*:*:*:*:*:*"),
mustCPE("cpe:2.3:a:alpine-keys:alpine_keys:2.3-r1:*:*:*:*:*:*:*"),
mustCPE("cpe:2.3:a:alpine:alpine-keys:2.3-r1:*:*:*:*:*:*:*"),
mustCPE("cpe:2.3:a:alpine-keys:alpine-keys:2.3-r1:*:*:*:*:*:*:*"),
mustCPE("cpe:2.3:a:alpine_keys:alpine-keys:2.3-r1:*:*:*:*:*:*:*"),
},
expected: []pkg.CPE{
mustCPE("cpe:2.3:a:alpine-keys:alpine-keys:2.3-r1:*:*:*:*:*:*:*"),
mustCPE("cpe:2.3:a:alpine-keys:alpine_keys:2.3-r1:*:*:*:*:*:*:*"),
mustCPE("cpe:2.3:a:alpine_keys:alpine-keys:2.3-r1:*:*:*:*:*:*:*"),
mustCPE("cpe:2.3:a:alpine_keys:alpine_keys:2.3-r1:*:*:*:*:*:*:*"),
mustCPE("cpe:2.3:a:alpine:alpine-keys:2.3-r1:*:*:*:*:*:*:*"),
mustCPE("cpe:2.3:a:alpine:alpine_keys:2.3-r1:*:*:*:*:*:*:*"),
},
},
}
for _, test := range tests {
+7 -14
View File
@@ -36,9 +36,8 @@ func (c *Cataloger) Name() string {
}
// Catalog is given an object to resolve file references and content, this function returns any discovered Packages after analyzing dpkg support files.
// nolint:funlen
func (c *Cataloger) Catalog(resolver source.FileResolver) ([]pkg.Package, error) {
dbFileMatches, err := resolver.FilesByGlob(pkg.DpkgDbGlob)
dbFileMatches, err := resolver.FilesByGlob(pkg.DpkgDBGlob)
if err != nil {
return nil, fmt.Errorf("failed to find dpkg status files's by glob: %w", err)
}
@@ -80,15 +79,13 @@ func addLicenses(resolver source.FileResolver, dbLocation source.Location, p *pk
// get license information from the copyright file
copyrightReader, copyrightLocation := fetchCopyrightContents(resolver, dbLocation, p)
if copyrightReader != nil {
if copyrightReader != nil && copyrightLocation != nil {
defer internal.CloseAndLogError(copyrightReader, copyrightLocation.VirtualPath)
// attach the licenses
p.Licenses = parseLicensesFromCopyright(copyrightReader)
// keep a record of the file where this was discovered
if copyrightLocation != nil {
p.Locations = append(p.Locations, *copyrightLocation)
}
p.Locations = append(p.Locations, *copyrightLocation)
}
}
@@ -127,28 +124,24 @@ func getAdditionalFileListing(resolver source.FileResolver, dbLocation source.Lo
md5Reader, md5Location := fetchMd5Contents(resolver, dbLocation, p)
if md5Reader != nil {
if md5Reader != nil && md5Location != nil {
defer internal.CloseAndLogError(md5Reader, md5Location.VirtualPath)
// attach the file list
files = append(files, parseDpkgMD5Info(md5Reader)...)
// keep a record of the file where this was discovered
if md5Location != nil {
locations = append(locations, *md5Location)
}
locations = append(locations, *md5Location)
}
conffilesReader, conffilesLocation := fetchConffileContents(resolver, dbLocation, p)
if conffilesReader != nil {
if conffilesReader != nil && conffilesLocation != nil {
defer internal.CloseAndLogError(conffilesReader, conffilesLocation.VirtualPath)
// attach the file list
files = append(files, parseDpkgConffileInfo(md5Reader)...)
// keep a record of the file where this was discovered
if conffilesLocation != nil {
locations = append(locations, *conffilesLocation)
}
locations = append(locations, *conffilesLocation)
}
return files, locations
@@ -0,0 +1,61 @@
/*
Package golang provides a concrete Cataloger implementation for go.mod files.
*/
package golang
import (
"fmt"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
const catalogerName = "go-module-binary-cataloger"
// current mime types to search by to discover go binaries
var mimeTypes = []string{
"application/x-executable",
"application/x-mach-binary",
"application/x-elf",
"application/x-sharedlib",
"application/vnd.microsoft.portable-executable",
}
type Cataloger struct{}
// NewGoModuleBinaryCataloger returns a new Golang cataloger object.
func NewGoModuleBinaryCataloger() *Cataloger {
return &Cataloger{}
}
// Name returns a string that uniquely describes a cataloger
func (c *Cataloger) Name() string {
return catalogerName
}
// Catalog is given an object to resolve file references and content, this function returns any discovered Packages after analyzing rpm db installation.
func (c *Cataloger) Catalog(resolver source.FileResolver) ([]pkg.Package, error) {
pkgs := make([]pkg.Package, 0)
fileMatches, err := resolver.FilesByMIMEType(mimeTypes...)
if err != nil {
return pkgs, fmt.Errorf("failed to find bin by mime types: %w", err)
}
for _, location := range fileMatches {
r, err := resolver.FileContentsByLocation(location)
if err != nil {
return pkgs, fmt.Errorf("failed to resolve file contents by location: %w", err)
}
goPkgs, err := parseGoBin(location.RealPath, r)
if err != nil {
log.Infof("could not parse go bin for: %w", err)
}
r.Close()
pkgs = append(pkgs, goPkgs...)
}
return pkgs, nil
}
+271
View File
@@ -0,0 +1,271 @@
// This code was copied from the Go std library.
// https://github.com/golang/go/blob/master/src/cmd/go/internal/version/exe.go
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//nolint
package golang
import (
"bytes"
"debug/elf"
"debug/macho"
"debug/pe"
"fmt"
"io"
)
// An exe is a generic interface to an OS executable (ELF, Mach-O, PE, XCOFF).
type exe interface {
// Close closes the underlying file.
Close() error
// ReadData reads and returns up to size byte starting at virtual address addr.
ReadData(addr, size uint64) ([]byte, error)
// DataStart returns the writable data segment start address.
DataStart() uint64
}
// openExe opens file and returns it as an exe.
// we changed this signature from accpeting a string
// to a ReadCloser so we could adapt the code to the
// stereoscope api. We removed the file open methods.
func openExe(file io.ReadCloser) (exe, error) {
/*
f, err := os.Open(file)
if err != nil {
return nil, err
}
data := make([]byte, 16)
if _, err := io.ReadFull(f, data); err != nil {
return nil, err
}
f.Seek(0, 0)
*/
data, err := io.ReadAll(file)
if err != nil {
return nil, err
}
r := bytes.NewReader(data)
f := io.NewSectionReader(r, 0, int64(len(data)))
if bytes.HasPrefix(data, []byte("\x7FELF")) {
e, err := elf.NewFile(f)
if err != nil {
return nil, err
}
return &elfExe{file, e}, nil
}
if bytes.HasPrefix(data, []byte("MZ")) {
e, err := pe.NewFile(f)
if err != nil {
return nil, err
}
return &peExe{file, e}, nil
}
if bytes.HasPrefix(data, []byte("\xFE\xED\xFA")) || bytes.HasPrefix(data[1:], []byte("\xFA\xED\xFE")) {
e, err := macho.NewFile(f)
if err != nil {
return nil, err
}
return &machoExe{file, e}, nil
}
return nil, fmt.Errorf("unrecognized executable format")
}
// elfExe is the ELF implementation of the exe interface.
// updated os to be io.ReadCloser to interopt with stereoscope
type elfExe struct {
os io.ReadCloser
f *elf.File
}
func (x *elfExe) Close() error {
return x.os.Close()
}
func (x *elfExe) ReadData(addr, size uint64) ([]byte, error) {
for _, prog := range x.f.Progs {
if prog.Vaddr <= addr && addr <= prog.Vaddr+prog.Filesz-1 {
n := prog.Vaddr + prog.Filesz - addr
if n > size {
n = size
}
data := make([]byte, n)
_, err := prog.ReadAt(data, int64(addr-prog.Vaddr))
if err != nil {
return nil, err
}
return data, nil
}
}
return nil, fmt.Errorf("address not mapped")
}
func (x *elfExe) DataStart() uint64 {
for _, s := range x.f.Sections {
if s.Name == ".go.buildinfo" {
return s.Addr
}
}
for _, p := range x.f.Progs {
if p.Type == elf.PT_LOAD && p.Flags&(elf.PF_X|elf.PF_W) == elf.PF_W {
return p.Vaddr
}
}
return 0
}
// peExe is the PE (Windows Portable Executable) implementation of the exe interface.
type peExe struct {
os io.ReadCloser
f *pe.File
}
func (x *peExe) Close() error {
return x.os.Close()
}
func (x *peExe) imageBase() uint64 {
switch oh := x.f.OptionalHeader.(type) {
case *pe.OptionalHeader32:
return uint64(oh.ImageBase)
case *pe.OptionalHeader64:
return oh.ImageBase
}
return 0
}
func (x *peExe) ReadData(addr, size uint64) ([]byte, error) {
addr -= x.imageBase()
for _, sect := range x.f.Sections {
if uint64(sect.VirtualAddress) <= addr && addr <= uint64(sect.VirtualAddress+sect.Size-1) {
n := uint64(sect.VirtualAddress+sect.Size) - addr
if n > size {
n = size
}
data := make([]byte, n)
_, err := sect.ReadAt(data, int64(addr-uint64(sect.VirtualAddress)))
if err != nil {
return nil, err
}
return data, nil
}
}
return nil, fmt.Errorf("address not mapped")
}
func (x *peExe) DataStart() uint64 {
// Assume data is first writable section.
const (
IMAGE_SCN_CNT_CODE = 0x00000020
IMAGE_SCN_CNT_INITIALIZED_DATA = 0x00000040
IMAGE_SCN_CNT_UNINITIALIZED_DATA = 0x00000080
IMAGE_SCN_MEM_EXECUTE = 0x20000000
IMAGE_SCN_MEM_READ = 0x40000000
IMAGE_SCN_MEM_WRITE = 0x80000000
IMAGE_SCN_MEM_DISCARDABLE = 0x2000000
IMAGE_SCN_LNK_NRELOC_OVFL = 0x1000000
IMAGE_SCN_ALIGN_32BYTES = 0x600000
)
for _, sect := range x.f.Sections {
if sect.VirtualAddress != 0 && sect.Size != 0 &&
sect.Characteristics&^IMAGE_SCN_ALIGN_32BYTES == IMAGE_SCN_CNT_INITIALIZED_DATA|IMAGE_SCN_MEM_READ|IMAGE_SCN_MEM_WRITE {
return uint64(sect.VirtualAddress) + x.imageBase()
}
}
return 0
}
// machoExe is the Mach-O (Apple macOS/iOS) implementation of the exe interface.
type machoExe struct {
os io.ReadCloser
f *macho.File
}
func (x *machoExe) Close() error {
return x.os.Close()
}
func (x *machoExe) ReadData(addr, size uint64) ([]byte, error) {
for _, load := range x.f.Loads {
seg, ok := load.(*macho.Segment)
if !ok {
continue
}
if seg.Addr <= addr && addr <= seg.Addr+seg.Filesz-1 {
if seg.Name == "__PAGEZERO" {
continue
}
n := seg.Addr + seg.Filesz - addr
if n > size {
n = size
}
data := make([]byte, n)
_, err := seg.ReadAt(data, int64(addr-seg.Addr))
if err != nil {
return nil, err
}
return data, nil
}
}
return nil, fmt.Errorf("address not mapped")
}
func (x *machoExe) DataStart() uint64 {
// Look for section named "__go_buildinfo".
for _, sec := range x.f.Sections {
if sec.Name == "__go_buildinfo" {
return sec.Addr
}
}
// Try the first non-empty writable segment.
const RW = 3
for _, load := range x.f.Loads {
seg, ok := load.(*macho.Segment)
if ok && seg.Addr != 0 && seg.Filesz != 0 && seg.Prot == RW && seg.Maxprot == RW {
return seg.Addr
}
}
return 0
}
/*
// xcoffExe is the XCOFF (AIX eXtended COFF) implementation of the exe interface.
type xcoffExe struct {
os *os.File
f *xcoff.File
}
func (x *xcoffExe) Close() error {
return x.os.Close()
}
func (x *xcoffExe) ReadData(addr, size uint64) ([]byte, error) {
for _, sect := range x.f.Sections {
if uint64(sect.VirtualAddress) <= addr && addr <= uint64(sect.VirtualAddress+sect.Size-1) {
n := uint64(sect.VirtualAddress+sect.Size) - addr
if n > size {
n = size
}
data := make([]byte, n)
_, err := sect.ReadAt(data, int64(addr-uint64(sect.VirtualAddress)))
if err != nil {
return nil, err
}
return data, nil
}
}
return nil, fmt.Errorf("address not mapped")
}
func (x *xcoffExe) DataStart() uint64 {
return x.f.SectionByType(xcoff.STYP_DATA).VirtualAddress
}
*/
@@ -7,11 +7,11 @@ import (
"github.com/anchore/syft/syft/pkg/cataloger/common"
)
// NewGoModCataloger returns a new Go module cataloger object.
func NewGoModCataloger() *common.GenericCataloger {
// NewGoModFileCataloger returns a new Go module cataloger object.
func NewGoModFileCataloger() *common.GenericCataloger {
globParsers := map[string]common.ParserFn{
"**/go.mod": parseGoMod,
}
return common.NewGenericCataloger(nil, globParsers, "go-cataloger")
return common.NewGenericCataloger(nil, globParsers, "go-mod-file-cataloger")
}
+65
View File
@@ -0,0 +1,65 @@
package golang
import (
"bufio"
"io"
"strings"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
const (
packageIdentifier = "dep"
replaceIdentifier = "=>"
)
func parseGoBin(path string, reader io.ReadCloser) ([]pkg.Package, error) {
// Identify if bin was compiled by go
x, err := openExe(reader)
if err != nil {
return nil, err
}
goVersion, mod := findVers(x)
pkgs := buildGoPkgInfo(path, mod, goVersion)
return pkgs, nil
}
func buildGoPkgInfo(path, mod, goVersion string) []pkg.Package {
pkgsSlice := make([]pkg.Package, 0)
scanner := bufio.NewScanner(strings.NewReader(mod))
// filter mod dependencies: [dep, name, version, sha]
for scanner.Scan() {
fields := strings.Fields(scanner.Text())
// must have dep, name, version, sha
if len(fields) < 4 {
continue
}
if fields[0] == packageIdentifier || fields[0] == replaceIdentifier {
pkgsSlice = append(pkgsSlice, pkg.Package{
Name: fields[1],
Version: fields[2],
Language: pkg.Go,
Type: pkg.GoModulePkg,
Locations: []source.Location{
{
RealPath: path,
},
},
MetadataType: pkg.GolangBinMetadataType,
Metadata: pkg.GolangBinMetadata{
GoCompiledVersion: goVersion,
H1Digest: fields[3],
},
})
}
}
return pkgsSlice
}
@@ -0,0 +1,122 @@
package golang
import (
"testing"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
"github.com/stretchr/testify/assert"
)
const goCompiledVersion = "1.17"
func TestBuildGoPkgInfo(t *testing.T) {
tests := []struct {
name string
mod string
expected []pkg.Package
}{
{
name: "buildGoPkgInfo parses a blank mod string and returns no packages",
mod: "",
expected: make([]pkg.Package, 0),
},
{
name: "buildGoPkgInfo parses a populated mod string and returns packages but no source info",
mod: `path github.com/anchore/syft mod github.com/anchore/syft (devel)
dep github.com/adrg/xdg v0.2.1 h1:VSVdnH7cQ7V+B33qSJHTCRlNgra1607Q8PzEmnvb2Ic=
dep github.com/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf h1:DYssiUV1pBmKqzKsm4mqXx8artqC0Q8HgZsVI3lMsAg=`,
expected: []pkg.Package{
{
Name: "github.com/adrg/xdg",
Version: "v0.2.1",
Language: pkg.Go,
Type: pkg.GoModulePkg,
Locations: []source.Location{
{},
},
MetadataType: pkg.GolangBinMetadataType,
Metadata: pkg.GolangBinMetadata{
GoCompiledVersion: goCompiledVersion,
H1Digest: "h1:VSVdnH7cQ7V+B33qSJHTCRlNgra1607Q8PzEmnvb2Ic=",
},
},
{
Name: "github.com/anchore/client-go",
Version: "v0.0.0-20210222170800-9c70f9b80bcf",
Language: pkg.Go,
Type: pkg.GoModulePkg,
Locations: []source.Location{
{},
},
MetadataType: pkg.GolangBinMetadataType,
Metadata: pkg.GolangBinMetadata{
GoCompiledVersion: goCompiledVersion,
H1Digest: "h1:DYssiUV1pBmKqzKsm4mqXx8artqC0Q8HgZsVI3lMsAg=",
},
},
},
},
{
name: "buildGoPkgInfo parses a populated mod string and returns packages when a replace directive exists",
mod: `path github.com/anchore/test
mod github.com/anchore/test (devel)
dep golang.org/x/net v0.0.0-20211006190231-62292e806868 h1:KlOXYy8wQWTUJYFgkUI40Lzr06ofg5IRXUK5C7qZt1k=
dep golang.org/x/sys v0.0.0-20211006194710-c8a6f5223071 h1:PjhxBct4MZii8FFR8+oeS7QOvxKOTZXgk63EU2XpfJE=
dep golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
=> golang.org/x/term v0.0.0-20210916214954-140adaaadfaf h1:Ihq/mm/suC88gF8WFcVwk+OV6Tq+wyA1O0E5UEvDglI=`,
expected: []pkg.Package{
{
Name: "golang.org/x/net",
Version: "v0.0.0-20211006190231-62292e806868",
Language: pkg.Go,
Type: pkg.GoModulePkg,
Locations: []source.Location{
{},
},
MetadataType: pkg.GolangBinMetadataType,
Metadata: pkg.GolangBinMetadata{
GoCompiledVersion: goCompiledVersion,
H1Digest: "h1:KlOXYy8wQWTUJYFgkUI40Lzr06ofg5IRXUK5C7qZt1k=",
},
},
{
Name: "golang.org/x/sys",
Version: "v0.0.0-20211006194710-c8a6f5223071",
Language: pkg.Go,
Type: pkg.GoModulePkg,
Locations: []source.Location{
{},
},
MetadataType: pkg.GolangBinMetadataType,
Metadata: pkg.GolangBinMetadata{
GoCompiledVersion: goCompiledVersion,
H1Digest: "h1:PjhxBct4MZii8FFR8+oeS7QOvxKOTZXgk63EU2XpfJE=",
},
},
{
Name: "golang.org/x/term",
Version: "v0.0.0-20210916214954-140adaaadfaf",
Language: pkg.Go,
Type: pkg.GoModulePkg,
Locations: []source.Location{
{},
},
MetadataType: pkg.GolangBinMetadataType,
Metadata: pkg.GolangBinMetadata{
GoCompiledVersion: goCompiledVersion,
H1Digest: "h1:Ihq/mm/suC88gF8WFcVwk+OV6Tq+wyA1O0E5UEvDglI=",
},
},
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
pkgs := buildGoPkgInfo("", tt.mod, goCompiledVersion)
assert.Equal(t, tt.expected, pkgs)
})
}
}
+226
View File
@@ -0,0 +1,226 @@
// This code was copied from the Go std library.
// https://github.com/golang/go/blob/master/src/cmd/go/internal/version/version.go
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package version implements the ``go version'' command.
package golang
import (
"bytes"
"encoding/binary"
// "cmd/go/internal/base"
)
/*
var CmdVersion = &base.Command{
UsageLine: "go version [-m] [-v] [file ...]",
Short: "print Go version",
Long: `Version prints the build information for Go executables.
Go version reports the Go version used to build each of the named
executable files.
If no files are named on the command line, go version prints its own
version information.
If a directory is named, go version walks that directory, recursively,
looking for recognized Go binaries and reporting their versions.
By default, go version does not report unrecognized files found
during a directory scan. The -v flag causes it to report unrecognized files.
The -m flag causes go version to print each executable's embedded
module version information, when available. In the output, the module
information consists of multiple lines following the version line, each
indented by a leading tab character.
See also: go doc runtime/debug.BuildInfo.
`,
}
func init() {
CmdVersion.Run = runVersion // break init cycle
}
var (
versionM = CmdVersion.Flag.Bool("m", false, "")
versionV = CmdVersion.Flag.Bool("v", false, "")
)
func runVersion(ctx context.Context, cmd *base.Command, args []string) {
if len(args) == 0 {
// If any of this command's flags were passed explicitly, error
// out, because they only make sense with arguments.
//
// Don't error if the flags came from GOFLAGS, since that can be
// a reasonable use case. For example, imagine GOFLAGS=-v to
// turn "verbose mode" on for all Go commands, which should not
// break "go version".
var argOnlyFlag string
if !base.InGOFLAGS("-m") && *versionM {
argOnlyFlag = "-m"
} else if !base.InGOFLAGS("-v") && *versionV {
argOnlyFlag = "-v"
}
if argOnlyFlag != "" {
fmt.Fprintf(os.Stderr, "go: 'go version' only accepts %s flag with arguments\n", argOnlyFlag)
base.SetExitStatus(2)
return
}
fmt.Printf("go version %s %s/%s\n", runtime.Version(), runtime.GOOS, runtime.GOARCH)
return
}
for _, arg := range args {
info, err := os.Stat(arg)
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
base.SetExitStatus(1)
continue
}
if info.IsDir() {
scanDir(arg)
} else {
scanFile(arg, info, true)
}
}
}
// scanDir scans a directory for executables to run scanFile on.
func scanDir(dir string) {
filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
if d.Type().IsRegular() || d.Type()&fs.ModeSymlink != 0 {
info, err := d.Info()
if err != nil {
if *versionV {
fmt.Fprintf(os.Stderr, "%s: %v\n", path, err)
}
return nil
}
scanFile(path, info, *versionV)
}
return nil
})
}
// isExe reports whether the file should be considered executable.
func isExe(file string, info fs.FileInfo) bool {
if runtime.GOOS == "windows" {
return strings.HasSuffix(strings.ToLower(file), ".exe")
}
return info.Mode().IsRegular() && info.Mode()&0111 != 0
}
// scanFile scans file to try to report the Go and module versions.
// If mustPrint is true, scanFile will report any error reading file.
// Otherwise (mustPrint is false, because scanFile is being called
// by scanDir) scanFile prints nothing for non-Go executables.
func scanFile(file string, info fs.FileInfo, mustPrint bool) {
if info.Mode()&fs.ModeSymlink != 0 {
// Accept file symlinks only.
i, err := os.Stat(file)
if err != nil || !i.Mode().IsRegular() {
if mustPrint {
fmt.Fprintf(os.Stderr, "%s: symlink\n", file)
}
return
}
info = i
}
if !isExe(file, info) {
if mustPrint {
fmt.Fprintf(os.Stderr, "%s: not executable file\n", file)
}
return
}
x, err := openExe(file)
if err != nil {
if mustPrint {
fmt.Fprintf(os.Stderr, "%s: %v\n", file, err)
}
return
}
defer x.Close()
vers, mod := findVers(x)
if vers == "" {
if mustPrint {
fmt.Fprintf(os.Stderr, "%s: go version not found\n", file)
}
return
}
fmt.Printf("%s: %s\n", file, vers)
if *versionM && mod != "" {
fmt.Printf("\t%s\n", strings.ReplaceAll(mod[:len(mod)-1], "\n", "\n\t"))
}
}
*/
// The build info blob left by the linker is identified by
// a 16-byte header, consisting of buildInfoMagic (14 bytes),
// the binary's pointer size (1 byte),
// and whether the binary is big endian (1 byte).
var buildInfoMagic = []byte("\xff Go buildinf:")
// findVers finds and returns the Go version and module version information
// in the executable x.
func findVers(x exe) (vers, mod string) {
// Read the first 64kB of text to find the build info blob.
text := x.DataStart()
data, err := x.ReadData(text, 64*1024)
if err != nil {
return
}
for ; !bytes.HasPrefix(data, buildInfoMagic); data = data[32:] {
if len(data) < 32 {
return
}
}
// Decode the blob.
ptrSize := int(data[14])
bigEndian := data[15] != 0
var bo binary.ByteOrder
if bigEndian {
bo = binary.BigEndian
} else {
bo = binary.LittleEndian
}
var readPtr func([]byte) uint64
if ptrSize == 4 {
readPtr = func(b []byte) uint64 { return uint64(bo.Uint32(b)) }
} else {
readPtr = bo.Uint64
}
vers = readString(x, ptrSize, readPtr, readPtr(data[16:]))
if vers == "" {
return
}
mod = readString(x, ptrSize, readPtr, readPtr(data[16+ptrSize:]))
if len(mod) >= 33 && mod[len(mod)-17] == '\n' {
// Strip module framing.
mod = mod[16 : len(mod)-16]
} else {
mod = ""
}
return vers, mod
}
// readString returns the string at address addr in the executable x.
func readString(x exe, ptrSize int, readPtr func([]byte) uint64, addr uint64) string {
hdr, err := x.ReadData(addr, uint64(2*ptrSize))
if err != nil || len(hdr) < 2*ptrSize {
return ""
}
dataAddr := readPtr(hdr)
dataLen := readPtr(hdr[ptrSize:])
data, err := x.ReadData(dataAddr, dataLen)
if err != nil || uint64(len(data)) < dataLen {
return ""
}
return string(data)
}
@@ -90,6 +90,7 @@ func TestParseJar(t *testing.T) {
fixture: "test-fixtures/java-builds/packages/example-jenkins-plugin.hpi",
ignoreExtras: []string{
"Plugin-Version", // has dynamic date
"Built-By", // podman returns the real UID
"Build-Jdk", // can't guarantee the JDK used at build time
},
expected: map[string]pkg.Package{
@@ -112,7 +113,7 @@ func TestParseJar(t *testing.T) {
"Plugin-License-Url": "https://opensource.org/licenses/MIT",
"Plugin-License-Name": "MIT License",
"Created-By": "Apache Maven",
"Built-By": "?",
//"Built-By": "?",
//"Build-Jdk": "14.0.1",
"Jenkins-Version": "2.164.3",
"Minimum-Java-Version": "1.8",
@@ -162,6 +163,7 @@ func TestParseJar(t *testing.T) {
fixture: "test-fixtures/java-builds/packages/example-java-app-maven-0.1.0.jar",
ignoreExtras: []string{
"Build-Jdk", // can't guarantee the JDK used at build time
"Built-By", // podman returns the real UID
},
expected: map[string]pkg.Package{
"example-java-app-maven": {
@@ -178,7 +180,7 @@ func TestParseJar(t *testing.T) {
// extra fields...
"Archiver-Version": "Plexus Archiver",
"Created-By": "Apache Maven 3.6.3",
"Built-By": "?",
//"Built-By": "?",
//"Build-Jdk": "14.0.1",
"Main-Class": "hello.HelloWorld",
},
@@ -78,6 +78,8 @@ func TestPythonPackageWheelCataloger(t *testing.T) {
{Path: "Pygments-2.6.1.dist-info/RECORD"},
{Path: "pygments/__pycache__/__init__.cpython-38.pyc"},
{Path: "pygments/util.py", Digest: &pkg.PythonFileDigest{"sha256", "586xXHiJGGZxqk5PMBu3vBhE68DLuAe5MBARWrSPGxA"}, Size: "10778"},
{Path: "pygments/x_util.py", Digest: &pkg.PythonFileDigest{"sha256", "qpzzsOW31KT955agi-7NS--90I0iNiJCyLJQnRCHgKI="}, Size: "10778"},
},
TopLevelPackages: []string{"pygments", "something_else"},
},
@@ -6,6 +6,7 @@ import (
"io"
"strings"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/pkg"
)
@@ -38,10 +39,10 @@ func parseWheelOrEggRecord(reader io.Reader) ([]pkg.PythonFileRecord, error) {
if item == "" {
continue
}
fields := strings.Split(item, "=")
fields := strings.SplitN(item, "=", 2)
if len(fields) != 2 {
return nil, fmt.Errorf("unexpected python record digest: %q", item)
log.Warnf("unexpected python record digest: %q", item)
continue
}
record.Digest = &pkg.PythonFileDigest{
@@ -32,6 +32,7 @@ func TestParseWheelEggRecord(t *testing.T) {
{Path: "Pygments-2.6.1.dist-info/RECORD"},
{Path: "pygments/__pycache__/__init__.cpython-38.pyc"},
{Path: "pygments/util.py", Digest: &pkg.PythonFileDigest{"sha256", "586xXHiJGGZxqk5PMBu3vBhE68DLuAe5MBARWrSPGxA"}, Size: "10778"},
{Path: "pygments/x_util.py", Digest: &pkg.PythonFileDigest{"sha256", "qpzzsOW31KT955agi-7NS--90I0iNiJCyLJQnRCHgKI="}, Size: "10778"},
},
},
}
@@ -2,4 +2,5 @@
Pygments-2.6.1.dist-info/AUTHORS,sha256=PVpa2_Oku6BGuiUvutvuPnWGpzxqFy2I8-NIrqCvqUY,8449
Pygments-2.6.1.dist-info/RECORD,,
pygments/__pycache__/__init__.cpython-38.pyc,,
pygments/util.py,sha256=586xXHiJGGZxqk5PMBu3vBhE68DLuAe5MBARWrSPGxA,10778
pygments/util.py,sha256=586xXHiJGGZxqk5PMBu3vBhE68DLuAe5MBARWrSPGxA,10778
pygments/x_util.py,sha256=qpzzsOW31KT955agi-7NS--90I0iNiJCyLJQnRCHgKI=,10778
+1 -1
View File
@@ -28,7 +28,7 @@ func (c *Cataloger) Name() string {
// Catalog is given an object to resolve file references and content, this function returns any discovered Packages after analyzing rpm db installation.
func (c *Cataloger) Catalog(resolver source.FileResolver) ([]pkg.Package, error) {
fileMatches, err := resolver.FilesByGlob(pkg.RpmDbGlob)
fileMatches, err := resolver.FilesByGlob(pkg.RpmDBGlob)
if err != nil {
return nil, fmt.Errorf("failed to find rpmdb's by glob: %w", err)
}
+1 -1
View File
@@ -93,7 +93,7 @@ func extractRpmdbFileRecords(resolver source.FilePathResolver, entry *rpmdb.Pack
var records = make([]pkg.RpmdbFileRecord, 0)
for _, record := range entry.Files {
//only persist RPMDB file records which exist in the image/directory, otherwise ignore them
// only persist RPMDB file records which exist in the image/directory, otherwise ignore them
if resolver.HasPath(record.Path) {
records = append(records, pkg.RpmdbFileRecord{
Path: record.Path,
@@ -45,11 +45,16 @@ func (r *rpmdbTestFileResolverMock) FilesByPath(paths ...string) ([]source.Locat
func (r *rpmdbTestFileResolverMock) FilesByGlob(...string) ([]source.Location, error) {
return nil, fmt.Errorf("not implemented")
}
func (r *rpmdbTestFileResolverMock) RelativeFileByPath(source.Location, string) *source.Location {
panic(fmt.Errorf("not implemented"))
return nil
}
func (r *rpmdbTestFileResolverMock) FilesByMIMEType(...string) ([]source.Location, error) {
return nil, fmt.Errorf("not implemented")
}
func TestParseRpmDB(t *testing.T) {
dbLocation := source.NewLocation("test-path")
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"github.com/scylladb/go-set/strset"
)
const DpkgDbGlob = "**/var/lib/dpkg/{status,status.d/**}"
const DpkgDBGlob = "**/var/lib/dpkg/{status,status.d/**}"
var _ FileOwner = (*DpkgMetadata)(nil)
+7
View File
@@ -0,0 +1,7 @@
package pkg
// GolangBinMetadata represents all captured data for a Golang Binary
type GolangBinMetadata struct {
GoCompiledVersion string
H1Digest string
}
+2
View File
@@ -15,6 +15,7 @@ const (
PythonPackageMetadataType MetadataType = "PythonPackageMetadata"
RustCargoPackageMetadataType MetadataType = "RustCargoPackageMetadata"
KbPackageMetadataType MetadataType = "KbPackageMetadata"
GolangBinMetadataType MetadataType = "GolangBinMetadata"
)
var AllMetadataTypes = []MetadataType{
@@ -27,4 +28,5 @@ var AllMetadataTypes = []MetadataType{
PythonPackageMetadataType,
RustCargoPackageMetadataType,
KbPackageMetadataType,
GolangBinMetadataType,
}
+3 -3
View File
@@ -9,9 +9,9 @@ import (
var globsForbiddenFromBeingOwned = []string{
// any OS DBs should automatically be ignored to prevent cyclic issues (e.g. the "rpm" RPM owns the path to the
// RPM DB, so if not ignored that package would own all other packages on the system).
ApkDbGlob,
DpkgDbGlob,
RpmDbGlob,
ApkDBGlob,
DpkgDBGlob,
RpmDBGlob,
// DEB packages share common copyright info between, this does not mean that sharing these paths implies ownership.
"/usr/share/doc/**/copyright",
}
+1 -1
View File
@@ -13,7 +13,7 @@ import (
"github.com/anchore/syft/syft/distro"
)
const RpmDbGlob = "**/var/lib/rpm/Packages"
const RpmDBGlob = "**/var/lib/rpm/Packages"
var _ FileOwner = (*RpmdbMetadata)(nil)
+18 -1
View File
@@ -120,7 +120,6 @@ func (r *allLayersResolver) FilesByPath(paths ...string) ([]Location, error) {
}
// FilesByGlob returns all file.References that match the given path glob pattern from any layer in the image.
// nolint:gocognit
func (r *allLayersResolver) FilesByGlob(patterns ...string) ([]Location, error) {
uniqueFileIDs := file.NewFileReferenceSet()
uniqueLocations := make([]Location, 0)
@@ -188,6 +187,24 @@ func (r *allLayersResolver) FileContentsByLocation(location Location) (io.ReadCl
return r.img.FileContentsByRef(location.ref)
}
func (r *allLayersResolver) FilesByMIMEType(types ...string) ([]Location, error) {
var locations []Location
for _, layerIdx := range r.layers {
layer := r.img.Layers[layerIdx]
refs, err := layer.FilesByMIMEType(types...)
if err != nil {
return nil, err
}
for _, ref := range refs {
locations = append(locations, NewLocationFromReference(ref))
}
}
return locations, nil
}
func (r *allLayersResolver) AllLocations() <-chan Location {
results := make(chan Location)
go func() {
+33
View File
@@ -3,6 +3,8 @@ package source
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/anchore/stereoscope/pkg/imagetest"
)
@@ -239,3 +241,34 @@ func TestAllLayersResolver_FilesByGlob(t *testing.T) {
})
}
}
func Test_imageAllLayersResolver_FilesByMIMEType(t *testing.T) {
tests := []struct {
fixtureName string
mimeType string
expectedPaths []string
}{
{
fixtureName: "image-duplicate-path",
mimeType: "text/plain",
expectedPaths: []string{"/somefile-1.txt", "/somefile-1.txt"},
},
}
for _, test := range tests {
t.Run(test.fixtureName, func(t *testing.T) {
img := imagetest.GetFixtureImage(t, "docker-archive", test.fixtureName)
resolver, err := newAllLayersResolver(img)
assert.NoError(t, err)
locations, err := resolver.FilesByMIMEType(test.mimeType)
assert.NoError(t, err)
assert.Len(t, test.expectedPaths, len(locations))
for idx, l := range locations {
assert.Equal(t, test.expectedPaths[idx], l.RealPath, "does not have path %q", l.RealPath)
}
})
}
}
+31 -25
View File
@@ -8,7 +8,6 @@ import (
"path"
"path/filepath"
"strings"
"syscall"
"github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/stereoscope/pkg/filetree"
@@ -35,10 +34,11 @@ type directoryResolver struct {
path string
cwd string
fileTree *filetree.FileTree
infos map[file.ID]os.FileInfo
metadata map[file.ID]FileMetadata
// TODO: wire up to report these paths in the json report
pathFilterFns []pathFilterFn
errPaths map[string]error
pathFilterFns []pathFilterFn
refsByMIMEType map[string][]file.Reference
errPaths map[string]error
}
func newDirectoryResolver(root string, pathFilters ...pathFilterFn) (*directoryResolver, error) {
@@ -52,12 +52,13 @@ func newDirectoryResolver(root string, pathFilters ...pathFilterFn) (*directoryR
}
resolver := directoryResolver{
path: root,
cwd: cwd,
fileTree: filetree.NewFileTree(),
infos: make(map[file.ID]os.FileInfo),
pathFilterFns: pathFilters,
errPaths: make(map[string]error),
path: root,
cwd: cwd,
fileTree: filetree.NewFileTree(),
metadata: make(map[file.ID]FileMetadata),
pathFilterFns: pathFilters,
refsByMIMEType: make(map[string][]file.Reference),
errPaths: make(map[string]error),
}
return &resolver, indexAllRoots(root, resolver.indexTree)
@@ -158,7 +159,12 @@ func (r directoryResolver) addPathToIndex(p string, info os.FileInfo) (string, e
}
}
r.infos[ref.ID()] = info
metadata := fileMetadataFromPath(p, info)
if ref != nil {
r.refsByMIMEType[metadata.MIMEType] = append(r.refsByMIMEType[metadata.MIMEType], *ref)
}
r.metadata[ref.ID()] = metadata
return newRoot, nil
}
@@ -207,6 +213,7 @@ func (r directoryResolver) FilesByPath(userPaths ...string) ([]Location, error)
log.Warnf("unable to get file by path=%q : %+v", userPath, err)
continue
}
// TODO: why not use stored metadata?
fileMeta, err := os.Stat(userStrPath)
if os.IsNotExist(err) {
continue
@@ -280,25 +287,24 @@ func (r *directoryResolver) AllLocations() <-chan Location {
}
func (r *directoryResolver) FileMetadataByLocation(location Location) (FileMetadata, error) {
info, exists := r.infos[location.ref.ID()]
metadata, exists := r.metadata[location.ref.ID()]
if !exists {
return FileMetadata{}, fmt.Errorf("location: %+v : %w", location, os.ErrNotExist)
}
uid := -1
gid := -1
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
uid = int(stat.Uid)
gid = int(stat.Gid)
}
return metadata, nil
}
return FileMetadata{
Mode: info.Mode(),
Type: newFileTypeFromMode(info.Mode()),
// unsupported across platforms
UserID: uid,
GroupID: gid,
}, nil
func (r *directoryResolver) FilesByMIMEType(types ...string) ([]Location, error) {
var locations []Location
for _, ty := range types {
if refs, ok := r.refsByMIMEType[ty]; ok {
for _, ref := range refs {
locations = append(locations, NewLocationFromDirectory(r.responsePath(string(ref.RealPath)), ref))
}
}
}
return locations, nil
}
func isUnixSystemRuntimePath(path string) bool {
+31 -1
View File
@@ -7,6 +7,8 @@ import (
"strings"
"testing"
"github.com/scylladb/go-set/strset"
"github.com/anchore/stereoscope/pkg/file"
"github.com/stretchr/testify/assert"
"github.com/wagoodman/go-progress"
@@ -303,7 +305,7 @@ func Test_directoryResolver_index(t *testing.T) {
if assert.NoError(t, err) {
return
}
assert.Equal(t, info, r.infos[ref.ID()])
assert.Equal(t, info, r.metadata[ref.ID()])
})
}
}
@@ -429,3 +431,31 @@ func Test_indexAllRoots(t *testing.T) {
})
}
}
func Test_directoryResolver_FilesByMIMEType(t *testing.T) {
tests := []struct {
fixturePath string
mimeType string
expectedPaths *strset.Set
}{
{
fixturePath: "./test-fixtures/image-simple",
mimeType: "text/plain",
expectedPaths: strset.New("test-fixtures/image-simple/file-1.txt", "test-fixtures/image-simple/file-2.txt", "test-fixtures/image-simple/target/really/nested/file-3.txt", "test-fixtures/image-simple/Dockerfile"),
},
}
for _, test := range tests {
t.Run(test.fixturePath, func(t *testing.T) {
resolver, err := newDirectoryResolver(test.fixturePath)
assert.NoError(t, err)
locations, err := resolver.FilesByMIMEType(test.mimeType)
assert.NoError(t, err)
assert.Equal(t, test.expectedPaths.Size(), len(locations))
for _, l := range locations {
assert.True(t, test.expectedPaths.Has(l.RealPath), "does not have path %q", l.RealPath)
}
})
}
}
+37
View File
@@ -2,6 +2,11 @@ package source
import (
"os"
"syscall"
"github.com/anchore/syft/internal/log"
"github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/stereoscope/pkg/image"
)
@@ -13,6 +18,7 @@ type FileMetadata struct {
GroupID int
LinkDestination string
Size int64
MIMEType string
}
func fileMetadataByLocation(img *image.Image, location Location) (FileMetadata, error) {
@@ -28,5 +34,36 @@ func fileMetadataByLocation(img *image.Image, location Location) (FileMetadata,
GroupID: entry.Metadata.GroupID,
LinkDestination: entry.Metadata.Linkname,
Size: entry.Metadata.Size,
MIMEType: entry.Metadata.MIMEType,
}, nil
}
func fileMetadataFromPath(path string, info os.FileInfo) FileMetadata {
uid := -1
gid := -1
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
uid = int(stat.Uid)
gid = int(stat.Gid)
}
f, err := os.Open(path)
if err != nil {
// TODO: it may be that the file is inaccessible, however, this is not an error or a warning. In the future we need to track these as known-unknowns
f = nil
} else {
defer func() {
if err := f.Close(); err != nil {
log.Warnf("unable to close file while obtaining metadata: %s", path)
}
}()
}
return FileMetadata{
Mode: info.Mode(),
Type: newFileTypeFromMode(info.Mode()),
// unsupported across platforms
UserID: uid,
GroupID: gid,
MIMEType: file.MIMEType(f),
}
}
+2
View File
@@ -29,6 +29,8 @@ type FilePathResolver interface {
FilesByPath(paths ...string) ([]Location, error)
// FilesByGlob fetches a set of file references which the given glob matches
FilesByGlob(patterns ...string) ([]Location, error)
// FilesByMIMEType fetches a set of file references which the contents have been classified as one of the given MIME Types
FilesByMIMEType(types ...string) ([]Location, error)
// RelativeFileByPath fetches a single file at the given path relative to the layer squash of the given reference.
// This is helpful when attempting to find a file that is in the same layer or lower as another file.
RelativeFileByPath(_ Location, path string) *Location
+14
View File
@@ -151,6 +151,20 @@ func (r *imageSquashResolver) AllLocations() <-chan Location {
return results
}
func (r *imageSquashResolver) FilesByMIMEType(types ...string) ([]Location, error) {
refs, err := r.img.FilesByMIMETypeFromSquash(types...)
if err != nil {
return nil, err
}
var locations []Location
for _, ref := range refs {
locations = append(locations, NewLocationFromReference(ref))
}
return locations, nil
}
func (r *imageSquashResolver) FileMetadataByLocation(location Location) (FileMetadata, error) {
return fileMetadataByLocation(r.img, location)
}
+34
View File
@@ -3,6 +3,9 @@ package source
import (
"testing"
"github.com/scylladb/go-set/strset"
"github.com/stretchr/testify/assert"
"github.com/anchore/stereoscope/pkg/imagetest"
)
@@ -225,3 +228,34 @@ func TestImageSquashResolver_FilesByGlob(t *testing.T) {
})
}
}
func Test_imageSquashResolver_FilesByMIMEType(t *testing.T) {
tests := []struct {
fixtureName string
mimeType string
expectedPaths *strset.Set
}{
{
fixtureName: "image-simple",
mimeType: "text/plain",
expectedPaths: strset.New("/somefile-1.txt", "/somefile-2.txt", "/really/nested/file-3.txt"),
},
}
for _, test := range tests {
t.Run(test.fixtureName, func(t *testing.T) {
img := imagetest.GetFixtureImage(t, "docker-archive", test.fixtureName)
resolver, err := newImageSquashResolver(img)
assert.NoError(t, err)
locations, err := resolver.FilesByMIMEType(test.mimeType)
assert.NoError(t, err)
assert.Equal(t, test.expectedPaths.Size(), len(locations))
for _, l := range locations {
assert.True(t, test.expectedPaths.Has(l.RealPath), "does not have path %q", l.RealPath)
}
})
}
}
+30 -12
View File
@@ -14,8 +14,9 @@ var _ FileResolver = (*MockResolver)(nil)
// It provides an implementation that can resolve local filesystem paths using only a provided discrete list of file
// paths, which are typically paths to test fixtures.
type MockResolver struct {
Locations []Location
Metadata map[Location]FileMetadata
locations []Location
metadata map[Location]FileMetadata
mimeTypeIndex map[string][]Location
}
// NewMockResolverForPaths creates a new MockResolver, where the only resolvable
@@ -26,21 +27,30 @@ func NewMockResolverForPaths(paths ...string) *MockResolver {
locations = append(locations, NewLocation(p))
}
return &MockResolver{Locations: locations}
return &MockResolver{
locations: locations,
metadata: make(map[Location]FileMetadata),
}
}
func NewMockResolverForPathsWithMetadata(metadata map[Location]FileMetadata) *MockResolver {
var locations []Location
for p := range metadata {
locations = append(locations, p)
var mimeTypeIndex = make(map[string][]Location)
for l, m := range metadata {
locations = append(locations, l)
mimeTypeIndex[m.MIMEType] = append(mimeTypeIndex[m.MIMEType], l)
}
return &MockResolver{Locations: locations, Metadata: metadata}
return &MockResolver{
locations: locations,
metadata: metadata,
mimeTypeIndex: mimeTypeIndex,
}
}
// HasPath indicates if the given path exists in the underlying source.
func (r MockResolver) HasPath(path string) bool {
for _, l := range r.Locations {
for _, l := range r.locations {
if l.RealPath == path {
return true
}
@@ -50,13 +60,13 @@ func (r MockResolver) HasPath(path string) bool {
// String returns the string representation of the MockResolver.
func (r MockResolver) String() string {
return fmt.Sprintf("mock:(%s,...)", r.Locations[0].RealPath)
return fmt.Sprintf("mock:(%s,...)", r.locations[0].RealPath)
}
// FileContentsByLocation fetches file contents for a single location. If the
// path does not exist, an error is returned.
func (r MockResolver) FileContentsByLocation(location Location) (io.ReadCloser, error) {
for _, l := range r.Locations {
for _, l := range r.locations {
if l == location {
return os.Open(location.RealPath)
}
@@ -69,7 +79,7 @@ func (r MockResolver) FileContentsByLocation(location Location) (io.ReadCloser,
func (r MockResolver) FilesByPath(paths ...string) ([]Location, error) {
var results []Location
for _, p := range paths {
for _, location := range r.Locations {
for _, location := range r.locations {
if p == location.RealPath {
results = append(results, NewLocation(p))
}
@@ -83,7 +93,7 @@ func (r MockResolver) FilesByPath(paths ...string) ([]Location, error) {
func (r MockResolver) FilesByGlob(patterns ...string) ([]Location, error) {
var results []Location
for _, pattern := range patterns {
for _, location := range r.Locations {
for _, location := range r.locations {
matches, err := doublestar.Match(pattern, location.RealPath)
if err != nil {
return nil, err
@@ -115,7 +125,7 @@ func (r MockResolver) AllLocations() <-chan Location {
results := make(chan Location)
go func() {
defer close(results)
for _, l := range r.Locations {
for _, l := range r.locations {
results <- l
}
}()
@@ -142,3 +152,11 @@ func (r MockResolver) FileMetadataByLocation(l Location) (FileMetadata, error) {
Size: info.Size(),
}, nil
}
func (r MockResolver) FilesByMIMEType(types ...string) ([]Location, error) {
var locations []Location
for _, ty := range types {
locations = append(r.mimeTypeIndex[ty], locations...)
}
return locations, nil
}
@@ -0,0 +1,4 @@
# Note: changes to this file will result in updating several test values. Consider making a new image fixture instead of editing this one.
FROM scratch
ADD file-1.txt /somefile-1.txt
ADD file-2.txt /somefile-1.txt
@@ -0,0 +1 @@
this file has contents
@@ -0,0 +1 @@
file-2 contents!
+18 -18
View File
@@ -131,6 +131,24 @@ var dirOnlyTestCases = []testCase{
"someotherpkg": "3.19.0",
},
},
{
name: "find golang modules",
pkgType: pkg.GoModulePkg,
pkgLanguage: pkg.Go,
pkgInfo: map[string]string{
"github.com/bmatcuk/doublestar": "v1.3.1",
},
},
{
name: "find rust crates",
pkgType: pkg.RustPkg,
pkgLanguage: pkg.Rust,
pkgInfo: map[string]string{
"memchr": "2.3.3",
"nom": "4.2.3",
"version_check": "0.1.5",
},
},
}
var commonTestCases = []testCase{
@@ -177,22 +195,4 @@ var commonTestCases = []testCase{
"libc-utils": "0.7.2-r0",
},
},
{
name: "find golang modules",
pkgType: pkg.GoModulePkg,
pkgLanguage: pkg.Go,
pkgInfo: map[string]string{
"github.com/bmatcuk/doublestar": "v1.3.1",
},
},
{
name: "find rust crates",
pkgType: pkg.RustPkg,
pkgLanguage: pkg.Rust,
pkgInfo: map[string]string{
"memchr": "2.3.3",
"nom": "4.2.3",
"version_check": "0.1.5",
},
},
}
+17 -12
View File
@@ -5,12 +5,11 @@ import (
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg/cataloger"
"github.com/google/go-cmp/cmp"
"github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/syft/source"
"github.com/go-test/deep"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/pkg"
)
@@ -58,12 +57,20 @@ func TestPkgCoverageImage(t *testing.T) {
definedLanguages.Add(l.String())
}
// for image we remove the go mod and rust support by default
definedLanguages.Remove(pkg.Go.String())
definedLanguages.Remove(pkg.Rust.String())
observedPkgs := internal.NewStringSet()
definedPkgs := internal.NewStringSet()
for _, p := range pkg.AllPkgs {
definedPkgs.Add(string(p))
}
// for image we remove the go-module and rust-crate support by default
definedPkgs.Remove(string(pkg.KbPkg))
definedPkgs.Remove(string(pkg.GoModulePkg))
definedPkgs.Remove(string(pkg.RustPkg))
var cases []testCase
cases = append(cases, commonTestCases...)
@@ -75,9 +82,11 @@ func TestPkgCoverageImage(t *testing.T) {
for a := range catalog.Enumerate(c.pkgType) {
observedLanguages.Add(a.Language.String())
observedPkgs.Add(string(a.Type))
if a.Language.String() != "" {
observedLanguages.Add(a.Language.String())
}
observedPkgs.Add(string(a.Type))
expectedVersion, ok := c.pkgInfo[a.Name]
if !ok {
t.Errorf("unexpected package found: %s", a.Name)
@@ -114,18 +123,14 @@ func TestPkgCoverageImage(t *testing.T) {
definedPkgs.Remove(string(pkg.UnknownPkg))
// ensure that integration test cases stay in sync with the available catalogers
if len(observedLanguages) < len(definedLanguages) {
if diff := cmp.Diff(definedLanguages, observedLanguages); diff != "" {
t.Errorf("language coverage incomplete (languages=%d, coverage=%d)", len(definedLanguages), len(observedLanguages))
for _, d := range deep.Equal(observedLanguages, definedLanguages) {
t.Errorf("diff: %+v", d)
}
t.Errorf("definedLanguages mismatch observedLanguages (-want +got):\n%s", diff)
}
if len(observedPkgs) < len(definedPkgs) {
if diff := cmp.Diff(definedPkgs, observedPkgs); diff != "" {
t.Errorf("package coverage incomplete (packages=%d, coverage=%d)", len(definedPkgs), len(observedPkgs))
for _, d := range deep.Equal(observedPkgs, definedPkgs) {
t.Errorf("diff: %+v", d)
}
t.Errorf("definedPkgs mismatch observedPkgs (-want +got):\n%s", diff)
}
}
@@ -0,0 +1,48 @@
package integration
import (
"strings"
"testing"
"github.com/anchore/syft/syft/pkg"
)
func TestRegressionGoArchDiscovery(t *testing.T) {
const (
expectedELFPkg = 3
expectedWINPkg = 3
expectedMACOSPkg = 3
)
// This is a regression test to make sure the way we detect go binary packages
// stays consistent and reproducible as the tool chain evolves
catalog, _, _ := catalogFixtureImage(t, "image-go-bin-arch-coverage")
var actualELF, actualWIN, actualMACOS int
for p := range catalog.Enumerate(pkg.GoModulePkg) {
for _, l := range p.Locations {
switch {
case strings.Contains(l.RealPath, "elf"):
actualELF++
case strings.Contains(l.RealPath, "win"):
actualWIN++
case strings.Contains(l.RealPath, "macos"):
actualMACOS++
default:
}
}
}
if actualELF != expectedELFPkg {
t.Errorf("unexpected number of elf packages: %d != %d", expectedELFPkg, actualELF)
}
if actualWIN != expectedWINPkg {
t.Errorf("unexpected number of win packages: %d != %d", expectedWINPkg, actualWIN)
}
if actualMACOS != expectedMACOSPkg {
t.Errorf("unexpected number of macos packages: %d != %d", expectedMACOSPkg, actualMACOS)
}
}
@@ -0,0 +1,16 @@
FROM golang:latest as builder
WORKDIR /app
COPY go.sum go.mod app.go ./
RUN GOOS=linux go build -o elf .
RUN GOOS=windows go build -o win .
RUN GOOS=darwin go build -o macos .
FROM scratch
WORKDIR /tmp
COPY --from=builder /app/elf /
COPY --from=builder /app/win /
COPY --from=builder /app/macos /
ENTRYPOINT ["/elf"]
@@ -0,0 +1,18 @@
//build:ignore
package main
import (
"fmt"
"os"
"golang.org/x/net/html"
"golang.org/x/term"
)
var test = html.ErrBufferExceeded
func main() {
t := term.NewTerminal(os.Stdout, "foo")
t.Write([]byte("hello anchore"))
t.Write([]byte(fmt.Sprintf("%v", test)))
}
@@ -0,0 +1,14 @@
module github.com/anchore/test
go 1.17
require (
golang.org/x/net v0.0.0-20211006190231-62292e806868
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
)
require golang.org/x/sys v0.0.0-20211006194710-c8a6f5223071 // indirect
exclude golang.org/x/net v0.0.0-20211005215030-d2e5035098b3
replace golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 => golang.org/x/term v0.0.0-20210916214954-140adaaadfaf
@@ -0,0 +1,12 @@
golang.org/x/net v0.0.0-20211006190231-62292e806868 h1:KlOXYy8wQWTUJYFgkUI40Lzr06ofg5IRXUK5C7qZt1k=
golang.org/x/net v0.0.0-20211006190231-62292e806868/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211006194710-c8a6f5223071 h1:PjhxBct4MZii8FFR8+oeS7QOvxKOTZXgk63EU2XpfJE=
golang.org/x/sys v0.0.0-20211006194710-c8a6f5223071/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210916214954-140adaaadfaf h1:Ihq/mm/suC88gF8WFcVwk+OV6Tq+wyA1O0E5UEvDglI=
golang.org/x/term v0.0.0-20210916214954-140adaaadfaf/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
Binary file not shown.
@@ -1,5 +1,8 @@
FROM alpine@sha256:d9a7354e3845ea8466bb00b22224d9116b183e594527fb5b6c3d30bc01a20378
# we keep these unpinned so that if alpine
# changes our integration tests can adapt
RUN apk add --no-cache \
tzdata=2021a-r0 \
vim=8.2.2320-r0 \
alpine-sdk=1.0-r0
tzdata \
vim \
alpine-sdk
-4
View File
@@ -184,7 +184,6 @@ func PullDockerImageHandler(ctx context.Context, fr *frame.Frame, event partybus
}
// FetchImageHandler periodically writes a the image save and write-to-disk process in the form of a progress bar.
// nolint:dupl
func FetchImageHandler(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error {
_, prog, err := stereoEventParsers.ParseFetchImage(event)
if err != nil {
@@ -313,7 +312,6 @@ func PackageCatalogerStartedHandler(ctx context.Context, fr *frame.Frame, event
}
// SecretsCatalogerStartedHandler shows the intermittent secrets searching progress.
// nolint:dupl
func SecretsCatalogerStartedHandler(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error {
prog, err := syftEventParsers.ParseSecretsCatalogingStarted(event)
if err != nil {
@@ -401,7 +399,6 @@ func FileMetadataCatalogerStartedHandler(ctx context.Context, fr *frame.Frame, e
}
// FileIndexingStartedHandler shows the intermittent indexing progress from a directory resolver.
// nolint:dupl
func FileIndexingStartedHandler(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error {
path, prog, err := syftEventParsers.ParseFileIndexingStarted(event)
if err != nil {
@@ -487,7 +484,6 @@ func FileDigestsCatalogerStartedHandler(ctx context.Context, fr *frame.Frame, ev
}
// ImportStartedHandler shows the intermittent upload progress to Anchore Enterprise.
// nolint:dupl
func ImportStartedHandler(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error {
host, prog, err := syftEventParsers.ParseImportStarted(event)
if err != nil {