mirror of
https://github.com/anchore/syft.git
synced 2026-10-12 06:07:20 +02:00
Compare commits
352
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4fb9970481 | ||
|
|
3aae316456 | ||
|
|
063e9da65d | ||
|
|
bf1102c3f1 | ||
|
|
bbd2d42dbb | ||
|
|
d84120f499 | ||
|
|
9480f10ccd | ||
|
|
1e4d26f526 | ||
|
|
e1c1832f84 | ||
|
|
9a73380f29 | ||
|
|
2e718cf865 | ||
|
|
4000a84624 | ||
|
|
99d172f0d1 | ||
|
|
3f5c601620 | ||
|
|
8478e0bef7 | ||
|
|
0327fdc88a | ||
|
|
88b3d1e9bb | ||
|
|
204b790012 | ||
|
|
35699f6fdc | ||
|
|
2e7fd031d4 | ||
|
|
32296f5943 | ||
|
|
4fc17edd14 | ||
|
|
38efe4ec5f | ||
|
|
05a61897f2 | ||
|
|
5a7c200911 | ||
|
|
72616db81f | ||
|
|
4ab9f393fc | ||
|
|
9744f4c009 | ||
|
|
d21fa84335 | ||
|
|
d5d95da3b6 | ||
|
|
c0c089ffd5 | ||
|
|
376c42893b | ||
|
|
81d8019207 | ||
|
|
8ce88e11fd | ||
|
|
f8b832e6c3 | ||
|
|
a00a3df10c | ||
|
|
cfbb9f703b | ||
|
|
6280146c81 | ||
|
|
e8f7108e6e | ||
|
|
023ca1be32 | ||
|
|
2e3c7fa158 | ||
|
|
4da3be864f | ||
|
|
608dbded06 | ||
|
|
791d1f9552 | ||
|
|
e5e97b5c4e | ||
|
|
8219f8d55b | ||
|
|
026be3c0f1 | ||
|
|
0d4f19043e | ||
|
|
38b47e484c | ||
|
|
7943c73d3f | ||
|
|
25ce245c03 | ||
|
|
7de7a7990a | ||
|
|
f79cb9587f | ||
|
|
c27d5b11d4 | ||
|
|
5d54e6e847 | ||
|
|
631d50d038 | ||
|
|
269006bf04 | ||
|
|
e2ed89f700 | ||
|
|
badb957888 | ||
|
|
a1bba36d51 | ||
|
|
c019cd51da | ||
|
|
5406d8a366 | ||
|
|
098c255a2d | ||
|
|
2c5d64ac9e | ||
|
|
1764e1c3f6 | ||
|
|
c560ffd811 | ||
|
|
7d1b292ad0 | ||
|
|
f07581f504 | ||
|
|
d676e5e781 | ||
|
|
903d29b6f7 | ||
|
|
79a955b1a9 | ||
|
|
1bd9de9047 | ||
|
|
68f8df9594 | ||
|
|
c69cdd9f4a | ||
|
|
5842fc2a64 | ||
|
|
f0307fdd62 | ||
|
|
74013d7da7 | ||
|
|
6afbffce28 | ||
|
|
07e76907f6 | ||
|
|
4bf17a94b9 | ||
|
|
4ac8fdf6df | ||
|
|
087a6356b9 | ||
|
|
26c201f7f7 | ||
|
|
a3c5550217 | ||
|
|
798af57853 | ||
|
|
f50302b2ba | ||
|
|
b09cf6c6b5 | ||
|
|
334a775cb9 | ||
|
|
f1b6f38ea8 | ||
|
|
f6f8332b7f | ||
|
|
74351567ab | ||
|
|
51d4c9b4ab | ||
|
|
4601ca3735 | ||
|
|
1a2a49840b | ||
|
|
42fa9e4965 | ||
|
|
8046f09562 | ||
|
|
b4ed599481 | ||
|
|
e925d9d4a5 | ||
|
|
da3624644a | ||
|
|
0580328ad9 | ||
|
|
291da8cd12 | ||
|
|
8a3cbf2fdd | ||
|
|
ef08d0fa39 | ||
|
|
75d625b697 | ||
|
|
88ba8b78fc | ||
|
|
3f19aa589c | ||
|
|
630c18e0d3 | ||
|
|
1860bab24b | ||
|
|
e31839a370 | ||
|
|
0f1aed4477 | ||
|
|
ddb338d834 | ||
|
|
354c72bbf4 | ||
|
|
d63a1f5f80 | ||
|
|
645206735e | ||
|
|
95a04cadea | ||
|
|
dd458a2b33 | ||
|
|
5f3d4d285b | ||
|
|
10c3cc27e8 | ||
|
|
a07bfe7dfa | ||
|
|
451cb9d5ca | ||
|
|
fd02bef0a3 | ||
|
|
02bd52728e | ||
|
|
c038f13d44 | ||
|
|
8102ad4edc | ||
|
|
13485ca5e7 | ||
|
|
b2b332e8b2 | ||
|
|
a42bac6fcc | ||
|
|
98a6c6efbe | ||
|
|
55a90a2ee0 | ||
|
|
6e835fd8fc | ||
|
|
ee80349ea0 | ||
|
|
52b54bbad9 | ||
|
|
66d9c5637b | ||
|
|
244b797a19 | ||
|
|
95176d7e0c | ||
|
|
5a7bab972c | ||
|
|
b69259534d | ||
|
|
44422853be | ||
|
|
cc731c7b19 | ||
|
|
5d156b8241 | ||
|
|
05715489c4 | ||
|
|
661d256b85 | ||
|
|
dd30c99bc2 | ||
|
|
16ebcb2455 | ||
|
|
305838582b | ||
|
|
a260fb2774 | ||
|
|
f83cae35f2 | ||
|
|
0fed17f1c8 | ||
|
|
da44db92e9 | ||
|
|
4a499c946e | ||
|
|
99c28a94a4 | ||
|
|
f7ac4e98af | ||
|
|
394ec8d215 | ||
|
|
7845381331 | ||
|
|
7464079a09 | ||
|
|
8a574c9ed9 | ||
|
|
681d250fdc | ||
|
|
2022ffa0e5 | ||
|
|
dfcc07e512 | ||
|
|
2fa238af7c | ||
|
|
63bbd1e3ed | ||
|
|
81b87dd108 | ||
|
|
f473bb75a8 | ||
|
|
539bc2afcb | ||
|
|
9fd532246a | ||
|
|
11e926ab2f | ||
|
|
168c5aed51 | ||
|
|
d02c56aa5f | ||
|
|
829a71cd92 | ||
|
|
34ace36a9e | ||
|
|
100cf1003d | ||
|
|
f11a7b5e9f | ||
|
|
434aa7fd46 | ||
|
|
5fb0423b72 | ||
|
|
7998520848 | ||
|
|
d05000ff21 | ||
|
|
f66e77e2c6 | ||
|
|
928c4a55ff | ||
|
|
1899eb50d0 | ||
|
|
b5ec4d4f08 | ||
|
|
61362c04fa | ||
|
|
e3140063d4 | ||
|
|
1d9ef34ec7 | ||
|
|
302735097e | ||
|
|
cc0a376aba | ||
|
|
b379dd9f27 | ||
|
|
a81e0c8008 | ||
|
|
41cbbe09b2 | ||
|
|
7714bc0521 | ||
|
|
096d2b7bff | ||
|
|
7cfdffab5f | ||
|
|
f43953d225 | ||
|
|
eea1b48cbb | ||
|
|
a063cf300b | ||
|
|
b73903519c | ||
|
|
304be4a5a1 | ||
|
|
c4cbe211a3 | ||
|
|
01230aa766 | ||
|
|
5f90d03718 | ||
|
|
e2ebc9769f | ||
|
|
2e6e3b0c74 | ||
|
|
24584a4d27 | ||
|
|
8e1205f7ab | ||
|
|
e92b0fa629 | ||
|
|
bcc0751a40 | ||
|
|
f1169e56fc | ||
|
|
98e737fc27 | ||
|
|
ff34594284 | ||
|
|
9e953b1da3 | ||
|
|
fbda21f4f4 | ||
|
|
d23b4d4cbd | ||
|
|
f3acff81f3 | ||
|
|
fa0a9fe8f9 | ||
|
|
284bae9d5f | ||
|
|
3ee1af0dc6 | ||
|
|
0c5f03235e | ||
|
|
5e8aa4da5e | ||
|
|
abfec62219 | ||
|
|
0c05855131 | ||
|
|
1150772d06 | ||
|
|
669fee84d5 | ||
|
|
aa151da5fe | ||
|
|
4bf677d555 | ||
|
|
f5e20521e0 | ||
|
|
8f6a317fef | ||
|
|
d339ffdcb5 | ||
|
|
e84ffc6003 | ||
|
|
bb52a25c8a | ||
|
|
4b7b709473 | ||
|
|
2ef2eed382 | ||
|
|
f6716092af | ||
|
|
0076b19893 | ||
|
|
2642a36161 | ||
|
|
1981b249f1 | ||
|
|
a3ebb31e3c | ||
|
|
9b9a7d6c98 | ||
|
|
57a13ae355 | ||
|
|
890fb3f0e8 | ||
|
|
e236054668 | ||
|
|
3013c8b691 | ||
|
|
88c81d33ed | ||
|
|
988041ba6d | ||
|
|
550e2fc7c3 | ||
|
|
43b53ac173 | ||
|
|
08804842fa | ||
|
|
284814153d | ||
|
|
8847ba5d0b | ||
|
|
48528efff3 | ||
|
|
8d856a7c7b | ||
|
|
38a090c218 | ||
|
|
deb7052f41 | ||
|
|
9650473298 | ||
|
|
95201840d2 | ||
|
|
6ba595344a | ||
|
|
9995950c70 | ||
|
|
b6a496f18c | ||
|
|
0853a50f4d | ||
|
|
a1b82c9664 | ||
|
|
ad8604c223 | ||
|
|
d80ee966dc | ||
|
|
1530ef354f | ||
|
|
cdac2245b5 | ||
|
|
8dba4c33dd | ||
|
|
b81c9805dc | ||
|
|
ba55963104 | ||
|
|
8c91605541 | ||
|
|
4c0aef09b8 | ||
|
|
02fb757c21 | ||
|
|
674a54512c | ||
|
|
21ba5d0806 | ||
|
|
0ba57a5936 | ||
|
|
3269bc98d4 | ||
|
|
7f3382f7eb | ||
|
|
65e5ff63f0 | ||
|
|
d287c22b69 | ||
|
|
e8be93a8eb | ||
|
|
e58050bac0 | ||
|
|
36a0945c95 | ||
|
|
396441e921 | ||
|
|
972e4cdaeb | ||
|
|
99f55f6a81 | ||
|
|
285112fe29 | ||
|
|
f29bea5921 | ||
|
|
39cdbc42aa | ||
|
|
27b62ce833 | ||
|
|
499e7c4e16 | ||
|
|
0f75f975c8 | ||
|
|
7427445fe9 | ||
|
|
09a5baf523 | ||
|
|
33c08c8545 | ||
|
|
fd002db802 | ||
|
|
cb3e4b8e49 | ||
|
|
5917f8d8f9 | ||
|
|
70e6d0f2e3 | ||
|
|
31a763c46d | ||
|
|
ae6c9c2e97 | ||
|
|
f6a0dd33d1 | ||
|
|
b77c104aa6 | ||
|
|
10ca7f56ab | ||
|
|
6b2dc08ffb | ||
|
|
03971ace43 | ||
|
|
07aee798b0 | ||
|
|
6cf668f749 | ||
|
|
2ec4371c95 | ||
|
|
fc4d28f365 | ||
|
|
5290dfb9c2 | ||
|
|
05611c283d | ||
|
|
934644232a | ||
|
|
641bccc79b | ||
|
|
ac94bf530c | ||
|
|
e87cfe7319 | ||
|
|
260cb4c72d | ||
|
|
85bddaa43d | ||
|
|
a864dc9505 | ||
|
|
44e8ae2577 | ||
|
|
ac8f72fdd1 | ||
|
|
e063471c66 | ||
|
|
645debe7a4 | ||
|
|
4bfb849310 | ||
|
|
d524bd5fc3 | ||
|
|
a8416d674b | ||
|
|
725529f43f | ||
|
|
e480443c8c | ||
|
|
db386baf81 | ||
|
|
795a63f1c9 | ||
|
|
d4f9993b8d | ||
|
|
bb6fc6525c | ||
|
|
bc1edb9c8a | ||
|
|
64be0a1072 | ||
|
|
e3d6ffd30e | ||
|
|
8d36b21237 | ||
|
|
6a7d6e6071 | ||
|
|
e1e489a284 | ||
|
|
b125ea83ba | ||
|
|
3690f979b3 | ||
|
|
5dd726fc86 | ||
|
|
c8b8b1ca11 | ||
|
|
7b08608adb | ||
|
|
82f32c7301 | ||
|
|
4ffbeeeea5 | ||
|
|
b1d6dae203 | ||
|
|
0f1e8fca14 | ||
|
|
8b38549b79 | ||
|
|
23a3173c9f | ||
|
|
17aa8287e6 | ||
|
|
730d3e3187 | ||
|
|
997fbdfcf3 | ||
|
|
13ceed9336 | ||
|
|
668f102340 | ||
|
|
f1a124209a | ||
|
|
5dbb3fc41d | ||
|
|
614ea00905 |
@@ -0,0 +1 @@
|
||||
enforce-v0: true # don't make breaking-change label bump major version before 1.0.
|
||||
@@ -7,12 +7,12 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Please provide a set of steps on how to reproduce the issue**
|
||||
|
||||
**What happened**:
|
||||
|
||||
**What you expected to happen**:
|
||||
|
||||
**Steps to reproduce the issue**:
|
||||
|
||||
**Anything else we need to know?**:
|
||||
|
||||
**Environment**:
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
name: "Bootstrap"
|
||||
description: "Bootstrap all tools and dependencies"
|
||||
inputs:
|
||||
go-version:
|
||||
description: "Go version to install"
|
||||
required: true
|
||||
default: "1.20.x"
|
||||
use-go-cache:
|
||||
description: "Restore go cache"
|
||||
required: true
|
||||
default: "true"
|
||||
cache-key-prefix:
|
||||
description: "Prefix all cache keys with this value"
|
||||
required: true
|
||||
default: "831180ac25"
|
||||
build-cache-key-prefix:
|
||||
description: "Prefix build cache key with this value"
|
||||
required: true
|
||||
default: "f8b6d31dea"
|
||||
bootstrap-apt-packages:
|
||||
description: "Space delimited list of tools to install via apt"
|
||||
default: "libxml2-utils"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ inputs.go-version }}
|
||||
|
||||
- name: Restore tool cache
|
||||
id: tool-cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ github.workspace }}/.tmp
|
||||
key: ${{ inputs.cache-key-prefix }}-${{ runner.os }}-tool-${{ hashFiles('Makefile') }}
|
||||
|
||||
# note: we need to keep restoring the go mod cache before bootstrapping tools since `go install` is used in
|
||||
# some installations of project tools.
|
||||
- name: Restore go module cache
|
||||
id: go-mod-cache
|
||||
if: inputs.use-go-cache == 'true'
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
key: ${{ inputs.cache-key-prefix }}-${{ runner.os }}-go-${{ inputs.go-version }}-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ inputs.cache-key-prefix }}-${{ runner.os }}-go-${{ inputs.go-version }}-
|
||||
|
||||
- name: (cache-miss) Bootstrap project tools
|
||||
shell: bash
|
||||
if: steps.tool-cache.outputs.cache-hit != 'true'
|
||||
run: make bootstrap-tools
|
||||
|
||||
- name: Restore go build cache
|
||||
id: go-cache
|
||||
if: inputs.use-go-cache == 'true'
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cache/go-build
|
||||
key: ${{ inputs.cache-key-prefix }}-${{ inputs.build-cache-key-prefix }}-${{ runner.os }}-go-${{ inputs.go-version }}-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ inputs.cache-key-prefix }}-${{ inputs.build-cache-key-prefix }}-${{ runner.os }}-go-${{ inputs.go-version }}-
|
||||
|
||||
- name: (cache-miss) Bootstrap go dependencies
|
||||
shell: bash
|
||||
if: steps.go-mod-cache.outputs.cache-hit != 'true' && inputs.use-go-cache == 'true'
|
||||
run: make bootstrap-go
|
||||
|
||||
- name: Install apt packages
|
||||
if: inputs.bootstrap-apt-packages != ''
|
||||
shell: bash
|
||||
run: |
|
||||
DEBIAN_FRONTEND=noninteractive sudo apt update && sudo -E apt install -y ${{ inputs.bootstrap-apt-packages }}
|
||||
|
||||
- name: Create all cache fingerprints
|
||||
shell: bash
|
||||
run: make fingerprints
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
Executable
+11
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
red=$(tput setaf 1)
|
||||
bold=$(tput bold)
|
||||
normal=$(tput sgr0)
|
||||
|
||||
# assert we are running in CI (or die!)
|
||||
if [[ -z "$CI" ]]; then
|
||||
echo "${bold}${red}This step should ONLY be run in CI. Exiting...${normal}"
|
||||
exit 1
|
||||
fi
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env python3
|
||||
import subprocess
|
||||
import sys
|
||||
import shlex
|
||||
|
||||
|
||||
class bcolors:
|
||||
HEADER = '\033[95m'
|
||||
OKBLUE = '\033[94m'
|
||||
OKCYAN = '\033[96m'
|
||||
OKGREEN = '\033[92m'
|
||||
WARNING = '\033[93m'
|
||||
FAIL = '\033[91m'
|
||||
ENDC = '\033[0m'
|
||||
BOLD = '\033[1m'
|
||||
UNDERLINE = '\033[4m'
|
||||
|
||||
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: coverage.py [threshold] [go-coverage-report]")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
threshold = float(sys.argv[1])
|
||||
report = sys.argv[2]
|
||||
|
||||
|
||||
args = shlex.split(f"go tool cover -func {report}")
|
||||
p = subprocess.run(args, capture_output=True, text=True)
|
||||
|
||||
percent_coverage = float(p.stdout.splitlines()[-1].split()[-1].replace("%", ""))
|
||||
print(f"{bcolors.BOLD}Coverage: {percent_coverage}%{bcolors.ENDC}")
|
||||
|
||||
if percent_coverage < threshold:
|
||||
print(f"{bcolors.BOLD}{bcolors.FAIL}Coverage below threshold of {threshold}%{bcolors.ENDC}")
|
||||
sys.exit(1)
|
||||
@@ -4,19 +4,18 @@ set -eu
|
||||
ORIGINAL_STATE_DIR=$(mktemp -d "TEMP-original-state-XXXXXXXXX")
|
||||
TIDY_STATE_DIR=$(mktemp -d "TEMP-tidy-state-XXXXXXXXX")
|
||||
|
||||
trap "cp -v ${ORIGINAL_STATE_DIR}/* ./ && rm -fR ${ORIGINAL_STATE_DIR} ${TIDY_STATE_DIR}" EXIT
|
||||
trap "cp -p ${ORIGINAL_STATE_DIR}/* ./ && git update-index -q --refresh && rm -fR ${ORIGINAL_STATE_DIR} ${TIDY_STATE_DIR}" EXIT
|
||||
|
||||
echo "Capturing original state of files..."
|
||||
cp -v go.mod go.sum "${ORIGINAL_STATE_DIR}"
|
||||
# capturing original state of files...
|
||||
cp go.mod go.sum "${ORIGINAL_STATE_DIR}"
|
||||
|
||||
echo "Capturing state of go.mod and go.sum after running go mod tidy..."
|
||||
# capturing state of go.mod and go.sum after running go mod tidy...
|
||||
go mod tidy
|
||||
cp -v go.mod go.sum "${TIDY_STATE_DIR}"
|
||||
echo ""
|
||||
cp go.mod go.sum "${TIDY_STATE_DIR}"
|
||||
|
||||
set +e
|
||||
|
||||
# Detect difference between the git HEAD state and the go mod tidy state
|
||||
# detect difference between the git HEAD state and the go mod tidy state
|
||||
DIFF_MOD=$(diff -u "${ORIGINAL_STATE_DIR}/go.mod" "${TIDY_STATE_DIR}/go.mod")
|
||||
DIFF_SUM=$(diff -u "${ORIGINAL_STATE_DIR}/go.sum" "${TIDY_STATE_DIR}/go.sum")
|
||||
|
||||
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
set -u
|
||||
|
||||
if [ "$(git status --porcelain | wc -l)" -ne "0" ]; then
|
||||
echo " 🔴 there are uncommitted changes, please commit them before running this check"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! make generate-json-schema; then
|
||||
echo "Generating json schema failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$(git status --porcelain | wc -l)" -ne "0" ]; then
|
||||
echo " 🔴 there are uncommitted changes, please commit them before running this check"
|
||||
exit 1
|
||||
fi
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eu
|
||||
|
||||
bold=$(tput bold)
|
||||
normal=$(tput sgr0)
|
||||
|
||||
if ! [ -x "$(command -v gh)" ]; then
|
||||
echo "The GitHub CLI could not be found. To continue follow the instructions at https://github.com/cli/cli#installation"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
gh auth status
|
||||
|
||||
# we need all of the git state to determine the next version. Since tagging is done by
|
||||
# the release pipeline it is possible to not have all of the tags from previous releases.
|
||||
git fetch --tags
|
||||
|
||||
# populates the CHANGELOG.md and VERSION files
|
||||
echo "${bold}Generating changelog...${normal}"
|
||||
make changelog 2> /dev/null
|
||||
|
||||
NEXT_VERSION=$(cat VERSION)
|
||||
|
||||
if [[ "$NEXT_VERSION" == "" || "${NEXT_VERSION}" == "(Unreleased)" ]]; then
|
||||
echo "Could not determine the next version to release. Exiting..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
while true; do
|
||||
read -p "${bold}Do you want to trigger a release for version '${NEXT_VERSION}'?${normal} [y/n] " yn
|
||||
case $yn in
|
||||
[Yy]* ) echo; break;;
|
||||
[Nn]* ) echo; echo "Cancelling release..."; exit;;
|
||||
* ) echo "Please answer yes or no.";;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "${bold}Kicking off release for ${NEXT_VERSION}${normal}..."
|
||||
echo
|
||||
gh workflow run release.yaml -f version=${NEXT_VERSION}
|
||||
|
||||
echo
|
||||
echo "${bold}Waiting for release to start...${normal}"
|
||||
sleep 10
|
||||
|
||||
set +e
|
||||
|
||||
echo "${bold}Head to the release workflow to monitor the release:${normal} $(gh run list --workflow=release.yaml --limit=1 --json url --jq '.[].url')"
|
||||
id=$(gh run list --workflow=release.yaml --limit=1 --json databaseId --jq '.[].databaseId')
|
||||
gh run watch $id --exit-status || (echo ; echo "${bold}Logs of failed step:${normal}" && GH_PAGER="" gh run view $id --log-failed)
|
||||
@@ -0,0 +1,58 @@
|
||||
name: "Benchmark testing"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
|
||||
Benchmark-Test:
|
||||
name: "Benchmark tests"
|
||||
runs-on: ubuntu-20.04
|
||||
# note: we want benchmarks to run on pull_request events in order to publish results to a sticky comment, and
|
||||
# we also want to run on push such that merges to main are recorded to the cache. For this reason we don't filter
|
||||
# the job by event.
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Bootstrap environment
|
||||
uses: ./.github/actions/bootstrap
|
||||
|
||||
- name: Restore base benchmark result
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: test/results/benchmark-main.txt
|
||||
# use base sha for PR or new commit hash for main push in benchmark result key
|
||||
key: ${{ runner.os }}-bench-${{ (github.event.pull_request.base.sha != github.event.after) && github.event.pull_request.base.sha || github.event.after }}
|
||||
|
||||
- name: Run benchmark tests
|
||||
id: benchmark
|
||||
run: |
|
||||
REF_NAME=${GITHUB_REF##*/} make benchmark
|
||||
OUTPUT=$(make show-benchstat)
|
||||
OUTPUT="${OUTPUT//'%'/'%25'}" # URL encode all '%' characters
|
||||
OUTPUT="${OUTPUT//$'\n'/'%0A'}" # URL encode all '\n' characters
|
||||
OUTPUT="${OUTPUT//$'\r'/'%0D'}" # URL encode all '\r' characters
|
||||
echo "result=$OUTPUT" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: benchmark-test-results
|
||||
path: test/results/**/*
|
||||
|
||||
- name: Update PR benchmark results comment
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
continue-on-error: true
|
||||
with:
|
||||
header: benchmark
|
||||
message: |
|
||||
### Benchmark Test Results
|
||||
|
||||
<details>
|
||||
<summary>Benchmark results from the latest changes vs base branch</summary>
|
||||
|
||||
```
|
||||
${{ steps.benchmark.outputs.result }}
|
||||
```
|
||||
|
||||
</details>
|
||||
@@ -30,11 +30,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -59,4 +59,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
uses: github/codeql-action/analyze@v2
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
name: Add to OSS board
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- transferred
|
||||
- labeled
|
||||
|
||||
jobs:
|
||||
|
||||
run:
|
||||
uses: "anchore/workflows/.github/workflows/oss-project-board-add.yaml@main"
|
||||
secrets:
|
||||
token: ${{ secrets.OSS_PROJECT_GH_TOKEN }}
|
||||
@@ -1,32 +1,29 @@
|
||||
name: "Release"
|
||||
on:
|
||||
push:
|
||||
# take no actions on push to any branch...
|
||||
branches-ignore:
|
||||
- "**"
|
||||
# ... only act on release tags
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: tag the latest commit on main with the given version (prefixed with v)
|
||||
required: true
|
||||
|
||||
env:
|
||||
GO_VERSION: "1.18.x"
|
||||
GO_VERSION: "1.19.x"
|
||||
|
||||
jobs:
|
||||
quality-gate:
|
||||
environment: release
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
# we don't want to release commits that have been pushed and tagged, but not necessarily merged onto main
|
||||
- name: Ensure tagged commit is on main
|
||||
- name: Check if tag already exists
|
||||
# note: this will fail if the tag already exists
|
||||
run: |
|
||||
echo "Tag: ${GITHUB_REF##*/}"
|
||||
git fetch origin main
|
||||
git merge-base --is-ancestor ${GITHUB_REF##*/} origin/main && echo "${GITHUB_REF##*/} is a commit on main!"
|
||||
[[ "${{ github.event.inputs.version }}" == v* ]] || (echo "version '${{ github.event.inputs.version }}' does not have a 'v' prefix" && exit 1)
|
||||
git tag ${{ github.event.inputs.version }}
|
||||
|
||||
- name: Check static analysis results
|
||||
uses: fountainhead/action-wait-for-check@v1.0.0
|
||||
uses: fountainhead/action-wait-for-check@v1.1.0
|
||||
id: static-analysis
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -35,7 +32,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
- name: Check unit test results
|
||||
uses: fountainhead/action-wait-for-check@v1.0.0
|
||||
uses: fountainhead/action-wait-for-check@v1.1.0
|
||||
id: unit
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -44,7 +41,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
- name: Check integration test results
|
||||
uses: fountainhead/action-wait-for-check@v1.0.0
|
||||
uses: fountainhead/action-wait-for-check@v1.1.0
|
||||
id: integration
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -53,7 +50,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
- name: Check acceptance test results (linux)
|
||||
uses: fountainhead/action-wait-for-check@v1.0.0
|
||||
uses: fountainhead/action-wait-for-check@v1.1.0
|
||||
id: acceptance-linux
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -62,7 +59,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
- name: Check acceptance test results (mac)
|
||||
uses: fountainhead/action-wait-for-check@v1.0.0
|
||||
uses: fountainhead/action-wait-for-check@v1.1.0
|
||||
id: acceptance-mac
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -71,7 +68,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
- name: Check cli test results (linux)
|
||||
uses: fountainhead/action-wait-for-check@v1.0.0
|
||||
uses: fountainhead/action-wait-for-check@v1.1.0
|
||||
id: cli-linux
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -97,33 +94,15 @@ jobs:
|
||||
contents: write
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Restore tool cache
|
||||
id: tool-cache
|
||||
uses: actions/cache@v2.1.3
|
||||
- name: Bootstrap environment
|
||||
uses: ./.github/actions/bootstrap
|
||||
with:
|
||||
path: ${{ github.workspace }}/.tmp
|
||||
key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }}
|
||||
|
||||
- name: Restore go cache
|
||||
id: go-cache
|
||||
uses: actions/cache@v2.1.3
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-${{ env.GO_VERSION }}-
|
||||
|
||||
- name: (cache-miss) Bootstrap all project dependencies
|
||||
if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true'
|
||||
run: make bootstrap
|
||||
# use the same cache we used for building snapshots
|
||||
build-cache-key-prefix: "snapshot"
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
@@ -138,8 +117,15 @@ jobs:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Tag release
|
||||
run: |
|
||||
git tag ${{ github.event.inputs.version }}
|
||||
git push origin --tags
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build & publish release artifacts
|
||||
run: make release
|
||||
run: make ci-release
|
||||
env:
|
||||
# for mac signing and notarization...
|
||||
QUILL_SIGN_P12: ${{ secrets.ANCHORE_APPLE_DEVELOPER_ID_CERT_CHAIN }}
|
||||
@@ -165,12 +151,12 @@ jobs:
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
fields: repo,workflow,action,eventName
|
||||
text: "A new Syft release has been published: https://github.com/anchore/syft/releases/tag/${{ github.ref_name }}"
|
||||
text: "A new Syft release has been published: https://github.com/anchore/syft/releases/tag/${{ github.event.inputs.version }}"
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }}
|
||||
if: ${{ success() }}
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: artifacts
|
||||
path: dist/**/*
|
||||
|
||||
@@ -6,7 +6,7 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
GO_VERSION: "1.18.x"
|
||||
GO_VERSION: "1.20.x"
|
||||
GO_STABLE_VERSION: true
|
||||
|
||||
jobs:
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
stable: ${{ env.GO_STABLE_VERSION }}
|
||||
@@ -28,29 +28,28 @@ jobs:
|
||||
GORELEASER_LATEST_VERSION=$(go list -m -json github.com/goreleaser/goreleaser@latest 2>/dev/null | jq -r '.Version')
|
||||
GOSIMPORTS_LATEST_VERSION=$(go list -m -json github.com/rinchsan/gosimports@latest 2>/dev/null | jq -r '.Version')
|
||||
YAJSV_LATEST_VERSION=$(go list -m -json github.com/neilpa/yajsv@latest 2>/dev/null | jq -r '.Version')
|
||||
COSIGN_LATEST_VERSION=$(go list -m -json github.com/sigstore/cosign@latest 2>/dev/null | jq -r '.Version')
|
||||
COSIGN_LATEST_VERSION=$(go list -m -json github.com/sigstore/cosign/v2@latest 2>/dev/null | jq -r '.Version')
|
||||
GLOW_LATEST_VERSION=$(go list -m -json github.com/charmbracelet/glow@latest 2>/dev/null | jq -r '.Version')
|
||||
|
||||
# update version variables in the Makefile
|
||||
sed -r -i -e 's/^(GOLANGCILINT_VERSION = ).*/\1'${GOLANGCILINT_LATEST_VERSION}'/' Makefile
|
||||
sed -r -i -e 's/^(BOUNCER_VERSION = ).*/\1'${BOUNCER_LATEST_VERSION}'/' Makefile
|
||||
sed -r -i -e 's/^(CHRONICLE_VERSION = ).*/\1'${CHRONICLE_LATEST_VERSION}'/' Makefile
|
||||
sed -r -i -e 's/^(GORELEASER_VERSION = ).*/\1'${GORELEASER_LATEST_VERSION}'/' Makefile
|
||||
sed -r -i -e 's/^(GOSIMPORTS_VERSION = ).*/\1'${GOSIMPORTS_LATEST_VERSION}'/' Makefile
|
||||
sed -r -i -e 's/^(YAJSV_VERSION = ).*/\1'${YAJSV_LATEST_VERSION}'/' Makefile
|
||||
sed -r -i -e 's/^(COSIGN_VERSION = ).*/\1'${COSIGN_LATEST_VERSION}'/' Makefile
|
||||
|
||||
# update cosign in go.mod as well
|
||||
go get github.com/sigstore/cosign@$COSIGN_LATEST_VERSION
|
||||
go mod tidy
|
||||
sed -r -i -e 's/^(GOLANGCILINT_VERSION := ).*/\1'${GOLANGCILINT_LATEST_VERSION}'/' Makefile
|
||||
sed -r -i -e 's/^(BOUNCER_VERSION := ).*/\1'${BOUNCER_LATEST_VERSION}'/' Makefile
|
||||
sed -r -i -e 's/^(CHRONICLE_VERSION := ).*/\1'${CHRONICLE_LATEST_VERSION}'/' Makefile
|
||||
sed -r -i -e 's/^(GORELEASER_VERSION := ).*/\1'${GORELEASER_LATEST_VERSION}'/' Makefile
|
||||
sed -r -i -e 's/^(GOSIMPORTS_VERSION := ).*/\1'${GOSIMPORTS_LATEST_VERSION}'/' Makefile
|
||||
sed -r -i -e 's/^(YAJSV_VERSION := ).*/\1'${YAJSV_LATEST_VERSION}'/' Makefile
|
||||
sed -r -i -e 's/^(COSIGN_VERSION := ).*/\1'${COSIGN_LATEST_VERSION}'/' Makefile
|
||||
sed -r -i -e 's/^(GLOW_VERSION := ).*/\1'${GLOW_LATEST_VERSION}'/' Makefile
|
||||
|
||||
# export the versions for use with create-pull-request
|
||||
echo "::set-output name=GOLANGCILINT::$GOLANGCILINT_LATEST_VERSION"
|
||||
echo "::set-output name=BOUNCER::$BOUNCER_LATEST_VERSION"
|
||||
echo "::set-output name=CHRONICLE::$CHRONICLE_LATEST_VERSION"
|
||||
echo "::set-output name=GORELEASER::$GORELEASER_LATEST_VERSION"
|
||||
echo "::set-output name=GOSIMPORTS::$GOSIMPORTS_LATEST_VERSION"
|
||||
echo "::set-output name=YAJSV::$YAJSV_LATEST_VERSION"
|
||||
echo "::set-output name=COSIGN::$COSIGN_LATEST_VERSION"
|
||||
echo "GOLANGCILINT=$GOLANGCILINT_LATEST_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "BOUNCER=$BOUNCER_LATEST_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "CHRONICLE=$CHRONICLE_LATEST_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "GORELEASER=$GORELEASER_LATEST_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "GOSIMPORTS=$GOSIMPORTS_LATEST_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "YAJSV=$YAJSV_LATEST_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "COSIGN=$COSIGN_LATEST_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "GLOW=GLOW_LATEST_VERSION" >> $GITHUB_OUTPUT
|
||||
id: latest-versions
|
||||
|
||||
- uses: tibdex/github-app-token@v1
|
||||
@@ -59,14 +58,14 @@ jobs:
|
||||
app_id: ${{ secrets.TOKEN_APP_ID }}
|
||||
private_key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }}
|
||||
|
||||
- uses: peter-evans/create-pull-request@v4
|
||||
- uses: peter-evans/create-pull-request@v5
|
||||
with:
|
||||
signoff: true
|
||||
delete-branch: true
|
||||
branch: auto/latest-bootstrap-tools
|
||||
labels: dependencies
|
||||
commit-message: 'Update syft bootstrap tools to latest versions.'
|
||||
title: 'Update syft bootstrap tools to latest versions.'
|
||||
commit-message: 'chore(deps): update bootstrap tools to latest versions'
|
||||
title: 'chore(deps): update bootstrap tools to latest versions'
|
||||
body: |
|
||||
- [golangci-lint ${{ steps.latest-versions.outputs.GOLANGCILINT }}](https://github.com/golangci/golangci-lint/releases/tag/${{ steps.latest-versions.outputs.GOLANGCILINT }})
|
||||
- [bouncer ${{ steps.latest-versions.outputs.BOUNCER }}](https://github.com/wagoodman/go-bouncer/releases/tag/${{ steps.latest-versions.outputs.BOUNCER }})
|
||||
@@ -75,5 +74,6 @@ jobs:
|
||||
- [gosimports ${{ steps.latest-versions.outputs.GOSIMPORTS }}](https://github.com/rinchsan/gosimports/releases/tag/${{ steps.latest-versions.outputs.GOSIMPORTS }})
|
||||
- [yajsv ${{ steps.latest-versions.outputs.YAJSV }}](https://github.com/neilpa/yajsv/releases/tag/${{ steps.latest-versions.outputs.YAJSV }})
|
||||
- [cosign ${{ steps.latest-versions.outputs.COSIGN }}](https://github.com/sigstore/cosign/releases/tag/${{ steps.latest-versions.outputs.COSIGN }})
|
||||
- [glow ${{ steps.latest-versions.outputs.GLOW }}](https://github.com/charmbracelet/glow/releases/tag/${{ steps.latest-versions.outputs.GLOW }})
|
||||
This is an auto-generated pull request to update all of the bootstrap tools to the latest versions.
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
name: PR to update CPE dictionary index
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 1 * * 1" # every monday at 1 AM
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
GO_VERSION: "1.20.x"
|
||||
GO_STABLE_VERSION: true
|
||||
|
||||
jobs:
|
||||
upgrade-cpe-dictionary-index:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'anchore/syft' # only run for main repo
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
stable: ${{ env.GO_STABLE_VERSION }}
|
||||
|
||||
- run: |
|
||||
make generate-cpe-dictionary-index
|
||||
|
||||
- uses: tibdex/github-app-token@v1
|
||||
id: generate-token
|
||||
with:
|
||||
app_id: ${{ secrets.TOKEN_APP_ID }}
|
||||
private_key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }}
|
||||
|
||||
- uses: peter-evans/create-pull-request@v5
|
||||
with:
|
||||
signoff: true
|
||||
delete-branch: true
|
||||
branch: auto/latest-cpe-dictionary-index
|
||||
labels: dependencies
|
||||
commit-message: "chore(deps): update CPE dictionary index"
|
||||
title: "chore(deps): update CPE dictionary index"
|
||||
body: |
|
||||
Update CPE dictionary index based on the latest available CPE dictionary
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
@@ -6,7 +6,7 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
GO_VERSION: "1.18.x"
|
||||
GO_VERSION: "1.20.x"
|
||||
GO_STABLE_VERSION: true
|
||||
|
||||
jobs:
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
stable: ${{ env.GO_STABLE_VERSION }}
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
go mod tidy
|
||||
|
||||
# export the version for use with create-pull-request
|
||||
echo "::set-output name=LATEST_VERSION::$LATEST_VERSION"
|
||||
echo "LATEST_VERSION=$LATEST_VERSION" >> $GITHUB_OUTPUT
|
||||
id: latest-version
|
||||
|
||||
- uses: tibdex/github-app-token@v1
|
||||
@@ -38,14 +38,14 @@ jobs:
|
||||
app_id: ${{ secrets.TOKEN_APP_ID }}
|
||||
private_key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }}
|
||||
|
||||
- uses: peter-evans/create-pull-request@v4
|
||||
- uses: peter-evans/create-pull-request@v5
|
||||
with:
|
||||
signoff: true
|
||||
delete-branch: true
|
||||
branch: auto/latest
|
||||
branch: auto/latest-stereoscope
|
||||
labels: dependencies
|
||||
commit-message: "Update Stereoscope to ${{ steps.latest-version.outputs.LATEST_VERSION }}"
|
||||
title: "Update Stereoscope to ${{ steps.latest-version.outputs.LATEST_VERSION }}"
|
||||
commit-message: "chore(deps): update stereoscope to ${{ steps.latest-version.outputs.LATEST_VERSION }}"
|
||||
title: "chore(deps): update stereoscope to ${{ steps.latest-version.outputs.LATEST_VERSION }}"
|
||||
body: |
|
||||
Update Stereoscope to ${{ steps.latest-version.outputs.LATEST_VERSION }}
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
name: "Validations"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
GO_VERSION: "1.18.x"
|
||||
GO_STABLE_VERSION: true
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -17,155 +14,74 @@ jobs:
|
||||
name: "Static analysis"
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
stable: ${{ env.GO_STABLE_VERSION }}
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Restore tool cache
|
||||
id: tool-cache
|
||||
uses: actions/cache@v2.1.3
|
||||
with:
|
||||
path: ${{ github.workspace }}/.tmp
|
||||
key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }}
|
||||
|
||||
- name: Restore go cache
|
||||
id: go-cache
|
||||
uses: actions/cache@v2.1.3
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-${{ env.GO_VERSION }}-
|
||||
|
||||
- name: (cache-miss) Bootstrap all project dependencies
|
||||
if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true'
|
||||
run: make bootstrap
|
||||
|
||||
- name: Bootstrap CI environment dependencies
|
||||
run: make ci-bootstrap
|
||||
- name: Bootstrap environment
|
||||
uses: ./.github/actions/bootstrap
|
||||
|
||||
- name: Run static analysis
|
||||
run: make static-analysis
|
||||
|
||||
|
||||
Unit-Test:
|
||||
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
|
||||
name: "Unit tests"
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
stable: ${{ env.GO_STABLE_VERSION }}
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Restore tool cache
|
||||
id: tool-cache
|
||||
uses: actions/cache@v2.1.3
|
||||
with:
|
||||
path: ${{ github.workspace }}/.tmp
|
||||
key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }}
|
||||
|
||||
- name: Restore go cache
|
||||
id: go-cache
|
||||
uses: actions/cache@v2.1.3
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-${{ env.GO_VERSION }}-
|
||||
|
||||
- name: (cache-miss) Bootstrap all project dependencies
|
||||
if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true'
|
||||
run: make bootstrap
|
||||
|
||||
- name: Bootstrap CI environment dependencies
|
||||
run: make ci-bootstrap
|
||||
|
||||
- name: Build cache key for java test-fixture blobs (for unit tests)
|
||||
run: make java-packages-fingerprint
|
||||
- name: Bootstrap environment
|
||||
uses: ./.github/actions/bootstrap
|
||||
|
||||
- name: Restore Java test-fixture cache
|
||||
id: unit-java-cache
|
||||
uses: actions/cache@v2.1.3
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: syft/pkg/cataloger/java/test-fixtures/java-builds/packages
|
||||
key: ${{ runner.os }}-unit-java-cache-${{ hashFiles( 'syft/pkg/cataloger/java/test-fixtures/java-builds/packages.fingerprint' ) }}
|
||||
|
||||
- name: Build cache key for rpm test-fixture blobs (for unit tests)
|
||||
run: make rpm-binaries-fingerprint
|
||||
key: ${{ runner.os }}-unit-java-cache-${{ hashFiles( 'syft/pkg/cataloger/java/test-fixtures/java-builds/cache.fingerprint' ) }}
|
||||
|
||||
- name: Restore RPM test-fixture cache
|
||||
id: unit-rpm-cache
|
||||
uses: actions/cache@v2.1.3
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: syft/pkg/cataloger/rpm/test-fixtures/rpms
|
||||
key: ${{ runner.os }}-unit-rpm-cache-${{ hashFiles( 'syft/pkg/cataloger/rpm/test-fixtures/rpms.fingerprint' ) }}
|
||||
|
||||
- name: Build cache key for go binary test-fixture blobs (for unit tests)
|
||||
run: make go-binaries-fingerprint
|
||||
|
||||
- name: Restore Go binary test-fixture cache
|
||||
id: unit-go-binary-cache
|
||||
uses: actions/cache@v2.1.3
|
||||
- name: Restore go binary test-fixture cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: syft/pkg/cataloger/golang/test-fixtures/archs/binaries
|
||||
key: ${{ runner.os }}-unit-go-binaries-cache-${{ hashFiles( 'syft/pkg/cataloger/golang/test-fixtures/archs/binaries.fingerprint' ) }}
|
||||
|
||||
- name: Restore binary cataloger test-fixture cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: syft/pkg/cataloger/binary/test-fixtures/classifiers/dynamic
|
||||
key: ${{ runner.os }}-unit-binary-cataloger-cache-${{ hashFiles( 'syft/pkg/cataloger/binary/test-fixtures/cache.fingerprint' ) }}
|
||||
|
||||
- name: Restore Kernel test-fixture cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: syft/pkg/cataloger/kernel/test-fixtures/cache
|
||||
key: ${{ runner.os }}-unit-kernel-cache-${{ hashFiles( 'syft/pkg/cataloger/kernel/test-fixtures/cache.fingerprint' ) }}
|
||||
|
||||
- name: Run unit tests
|
||||
run: make unit
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: unit-test-results
|
||||
path: test/results/**/*
|
||||
|
||||
Integration-Test:
|
||||
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
|
||||
name: "Integration tests"
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
stable: ${{ env.GO_STABLE_VERSION }}
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Restore tool cache
|
||||
id: tool-cache
|
||||
uses: actions/cache@v2.1.3
|
||||
with:
|
||||
path: ${{ github.workspace }}/.tmp
|
||||
key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }}
|
||||
|
||||
- name: Restore go cache
|
||||
id: go-cache
|
||||
uses: actions/cache@v2.1.3
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-${{ env.GO_VERSION }}-
|
||||
|
||||
- name: (cache-miss) Bootstrap all project dependencies
|
||||
if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true'
|
||||
run: make bootstrap
|
||||
|
||||
- name: Bootstrap CI environment dependencies
|
||||
run: make ci-bootstrap
|
||||
- name: Bootstrap environment
|
||||
uses: ./.github/actions/bootstrap
|
||||
|
||||
- name: Validate syft output against the CycloneDX schema
|
||||
run: make validate-cyclonedx-schema
|
||||
|
||||
- name: Build key for tar cache
|
||||
run: make integration-fingerprint
|
||||
|
||||
- name: Restore integration test cache
|
||||
uses: actions/cache@v2.1.3
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ github.workspace }}/test/integration/test-fixtures/cache
|
||||
key: ${{ runner.os }}-integration-test-cache-${{ hashFiles('test/integration/test-fixtures/cache.fingerprint') }}
|
||||
@@ -173,123 +89,35 @@ jobs:
|
||||
- name: Run integration tests
|
||||
run: make integration
|
||||
|
||||
Benchmark-Test:
|
||||
name: "Benchmark tests"
|
||||
runs-on: ubuntu-20.04
|
||||
# note: we want benchmarks to run on pull_request events in order to publish results to a sticky comment, and
|
||||
# we also want to run on push such that merges to main are recorded to the cache. For this reason we don't filter
|
||||
# the job by event.
|
||||
steps:
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
stable: ${{ env.GO_STABLE_VERSION }}
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Restore tool cache
|
||||
id: tool-cache
|
||||
uses: actions/cache@v2.1.3
|
||||
with:
|
||||
path: ${{ github.workspace }}/.tmp
|
||||
key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }}
|
||||
|
||||
- name: Restore go cache
|
||||
id: go-cache
|
||||
uses: actions/cache@v2.1.3
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-${{ env.GO_VERSION }}-
|
||||
|
||||
- name: (cache-miss) Bootstrap all project dependencies
|
||||
if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true'
|
||||
run: make bootstrap
|
||||
|
||||
- name: Bootstrap CI environment dependencies
|
||||
run: make ci-bootstrap
|
||||
|
||||
- name: Restore base benchmark result
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: test/results/benchmark-main.txt
|
||||
# use base sha for PR or new commit hash for main push in benchmark result key
|
||||
key: ${{ runner.os }}-bench-${{ (github.event.pull_request.base.sha != github.event.after) && github.event.pull_request.base.sha || github.event.after }}
|
||||
|
||||
- name: Run benchmark tests
|
||||
id: benchmark
|
||||
run: |
|
||||
REF_NAME=${GITHUB_REF##*/} make benchmark
|
||||
OUTPUT=$(make show-benchstat)
|
||||
OUTPUT="${OUTPUT//'%'/'%25'}" # URL encode all '%' characters
|
||||
OUTPUT="${OUTPUT//$'\n'/'%0A'}" # URL encode all '\n' characters
|
||||
OUTPUT="${OUTPUT//$'\r'/'%0D'}" # URL encode all '\r' characters
|
||||
echo "::set-output name=result::$OUTPUT"
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: benchmark-test-results
|
||||
path: test/results/**/*
|
||||
|
||||
- name: Update PR benchmark results comment
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
continue-on-error: true
|
||||
with:
|
||||
header: benchmark
|
||||
message: |
|
||||
### Benchmark Test Results
|
||||
|
||||
<details>
|
||||
<summary>Benchmark results from the latest changes vs base branch</summary>
|
||||
|
||||
```
|
||||
${{ steps.benchmark.outputs.result }}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
Build-Snapshot-Artifacts:
|
||||
name: "Build snapshot artifacts"
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Bootstrap environment
|
||||
uses: ./.github/actions/bootstrap
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
stable: ${{ env.GO_STABLE_VERSION }}
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
- name: Restore tool cache
|
||||
id: tool-cache
|
||||
uses: actions/cache@v2.1.3
|
||||
with:
|
||||
path: ${{ github.workspace }}/.tmp
|
||||
key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }}
|
||||
|
||||
- name: Restore go cache
|
||||
id: go-cache
|
||||
uses: actions/cache@v2.1.3
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-${{ env.GO_VERSION }}-
|
||||
|
||||
- name: (cache-miss) Bootstrap all project dependencies
|
||||
if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true'
|
||||
run: make bootstrap
|
||||
# why have another build cache key? We don't want unit/integration/etc test build caches to replace
|
||||
# the snapshot build cache, which includes builds for all OSs and architectures. As long as this key is
|
||||
# unique from the build-cache-key-prefix in other CI jobs, we should be fine.
|
||||
#
|
||||
# note: ideally this value should match what is used in release (just to help with build times).
|
||||
build-cache-key-prefix: "snapshot"
|
||||
bootstrap-apt-packages: ""
|
||||
|
||||
- name: Build snapshot artifacts
|
||||
run: make snapshot
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
# why not use actions/upload-artifact? It is very slow (3 minutes to upload ~600MB of data, vs 10 seconds with this approach).
|
||||
# see https://github.com/actions/upload-artifact/issues/199 for more info
|
||||
- name: Upload snapshot artifacts
|
||||
uses: actions/cache/save@v3
|
||||
with:
|
||||
name: artifacts
|
||||
path: snapshot/**/*
|
||||
path: snapshot
|
||||
key: snapshot-build-${{ github.run_id }}
|
||||
|
||||
|
||||
Acceptance-Linux:
|
||||
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
|
||||
@@ -297,22 +125,20 @@ jobs:
|
||||
needs: [Build-Snapshot-Artifacts]
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/download-artifact@v2
|
||||
- name: Download snapshot build
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
name: artifacts
|
||||
path: snapshot
|
||||
key: snapshot-build-${{ github.run_id }}
|
||||
|
||||
- name: Run comparison tests (Linux)
|
||||
run: make compare-linux
|
||||
|
||||
- name: Build key for image cache
|
||||
run: make install-fingerprint
|
||||
|
||||
- name: Restore install.sh test image cache
|
||||
id: install-test-image-cache
|
||||
uses: actions/cache@v2.1.3
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ github.workspace }}/test/install/cache
|
||||
key: ${{ runner.os }}-install-test-image-cache-${{ hashFiles('test/install/cache.fingerprint') }}
|
||||
@@ -335,16 +161,17 @@ jobs:
|
||||
needs: [Build-Snapshot-Artifacts]
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/download-artifact@v2
|
||||
- name: Download snapshot build
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
name: artifacts
|
||||
path: snapshot
|
||||
key: snapshot-build-${{ github.run_id }}
|
||||
|
||||
- name: Restore docker image cache for compare testing
|
||||
id: mac-compare-testing-cache
|
||||
uses: actions/cache@v2.1.3
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: image.tar
|
||||
key: ${{ runner.os }}-${{ hashFiles('test/compare/mac.sh') }}
|
||||
@@ -355,52 +182,29 @@ jobs:
|
||||
- name: Run install.sh tests (Mac)
|
||||
run: make install-test-ci-mac
|
||||
|
||||
|
||||
Cli-Linux:
|
||||
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
|
||||
name: "CLI tests (Linux)"
|
||||
needs: [Build-Snapshot-Artifacts]
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
stable: ${{ env.GO_STABLE_VERSION }}
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- name: Bootstrap environment
|
||||
uses: ./.github/actions/bootstrap
|
||||
|
||||
- name: Restore go cache
|
||||
id: go-cache
|
||||
uses: actions/cache@v2.1.3
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-${{ env.GO_VERSION }}-
|
||||
|
||||
- name: Restore tool cache
|
||||
id: tool-cache
|
||||
uses: actions/cache@v2.1.3
|
||||
with:
|
||||
path: ${{ github.workspace }}/.tmp
|
||||
key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }}
|
||||
|
||||
- name: (cache-miss) Bootstrap all project dependencies
|
||||
if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true'
|
||||
run: make bootstrap
|
||||
|
||||
- name: Build key for tar cache
|
||||
run: make cli-fingerprint
|
||||
|
||||
- name: Restore CLI test cache
|
||||
uses: actions/cache@v2.1.3
|
||||
- name: Restore CLI test-fixture cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ github.workspace }}/test/cli/test-fixtures/cache
|
||||
key: ${{ runner.os }}-cli-test-cache-${{ hashFiles('test/cli/test-fixtures/cache.fingerprint') }}
|
||||
|
||||
- uses: actions/download-artifact@v2
|
||||
- name: Download snapshot build
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
name: artifacts
|
||||
path: snapshot
|
||||
key: snapshot-build-${{ github.run_id }}
|
||||
|
||||
- name: Run CLI Tests (Linux)
|
||||
run: make cli
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
go.work
|
||||
go.work.sum
|
||||
/bin
|
||||
/.bin
|
||||
/build
|
||||
CHANGELOG.md
|
||||
VERSION
|
||||
/test/results
|
||||
/dist
|
||||
/snapshot
|
||||
@@ -14,6 +20,7 @@ CHANGELOG.md
|
||||
*.hpi
|
||||
*.zip
|
||||
.idea/
|
||||
*.iml
|
||||
*.log
|
||||
.images
|
||||
.tmp/
|
||||
|
||||
+18
-11
@@ -1,5 +1,7 @@
|
||||
# TODO: enable this when we have coverage on docstring comments
|
||||
#issues:
|
||||
issues:
|
||||
max-same-issues: 25
|
||||
|
||||
# TODO: enable this when we have coverage on docstring comments
|
||||
# # The list of ids of default excludes to include or disable.
|
||||
# include:
|
||||
# - EXC0002 # disable excluding of issues about comments from golint
|
||||
@@ -10,10 +12,9 @@ linters:
|
||||
enable:
|
||||
- asciicheck
|
||||
- bodyclose
|
||||
- depguard
|
||||
- dogsled
|
||||
- dupl
|
||||
- errcheck
|
||||
- errorlint
|
||||
- exportloopref
|
||||
- funlen
|
||||
- gocognit
|
||||
@@ -21,14 +22,14 @@ linters:
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- gofmt
|
||||
- tparallel
|
||||
- importas
|
||||
- goimports
|
||||
- goprintffuncname
|
||||
- gosec
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- misspell
|
||||
- nolintlint
|
||||
- nakedret
|
||||
- revive
|
||||
- staticcheck
|
||||
- stylecheck
|
||||
@@ -37,6 +38,7 @@ linters:
|
||||
- unparam
|
||||
- unused
|
||||
- whitespace
|
||||
|
||||
linters-settings:
|
||||
funlen:
|
||||
# Checks the number of lines in a function.
|
||||
@@ -53,7 +55,8 @@ run:
|
||||
timeout: 10m
|
||||
|
||||
# do not enable...
|
||||
# - dogsled # found to be to niche and ineffective
|
||||
# - deadcode # The owner seems to have abandoned the linter. Replaced by "unused".
|
||||
# - depguard # We don't have a configuration for this yet
|
||||
# - goprintffuncname # does not catch all cases and there are exceptions
|
||||
# - nakedret # does not catch all cases and should not fail a build
|
||||
# - gochecknoglobals
|
||||
@@ -69,7 +72,11 @@ run:
|
||||
# - 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
|
||||
# - nolintlint # as of go1.19 this conflicts with the behavior of gofmt, which is a deal-breaker (lint-fix will still fail when running lint)
|
||||
# - prealloc # following this rule isn't consistently a good idea, as it sometimes forces unnecessary allocations that result in less idiomatic code
|
||||
# - rowserrcheck # not in a repo with sql, so this is not useful
|
||||
# - scopelint # deprecated
|
||||
# - structcheck # The owner seems to have abandoned the linter. Replaced by "unused".
|
||||
# - testpackage
|
||||
# - wsl # this doens't have an auto-fixer yet and is pretty noisy (https://github.com/bombsimon/wsl/issues/90)
|
||||
# - varcheck # The owner seems to have abandoned the linter. Replaced by "unused".
|
||||
# - wsl # this doens't have an auto-fixer yet and is pretty noisy (https://github.com/bombsimon/wsl/issues/90)
|
||||
|
||||
+1
-4
@@ -5,6 +5,7 @@ release:
|
||||
env:
|
||||
# required to support multi architecture docker builds
|
||||
- DOCKER_CLI_EXPERIMENTAL=enabled
|
||||
- CGO_ENABLED=0
|
||||
|
||||
builds:
|
||||
- id: linux-build
|
||||
@@ -19,8 +20,6 @@ builds:
|
||||
- s390x
|
||||
# set the modified timestamp on the output binary to the git timestamp to ensure a reproducible build
|
||||
mod_timestamp: &build-timestamp '{{ .CommitTimestamp }}'
|
||||
env: &build-env
|
||||
- CGO_ENABLED=0
|
||||
ldflags: &build-ldflags |
|
||||
-w
|
||||
-s
|
||||
@@ -39,7 +38,6 @@ builds:
|
||||
- amd64
|
||||
- arm64
|
||||
mod_timestamp: *build-timestamp
|
||||
env: *build-env
|
||||
ldflags: *build-ldflags
|
||||
hooks:
|
||||
post:
|
||||
@@ -55,7 +53,6 @@ builds:
|
||||
goarch:
|
||||
- amd64
|
||||
mod_timestamp: *build-timestamp
|
||||
env: *build-env
|
||||
ldflags: *build-ldflags
|
||||
|
||||
archives:
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
# 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.
|
||||
|
||||
@@ -10,7 +10,7 @@ Before you can contribute to Syft, you need to configure your development enviro
|
||||
|
||||
You will need to install Go. The version on https://go.dev works best, using the system golang doesn't always work the way you might expect.
|
||||
|
||||
At the time of writing, Go 1.19 does not work correctly with Syft. Please use Go 1.18 for now.
|
||||
Refer to the go.mod file in the root of this repo for the recommended version of Go to install.
|
||||
|
||||
You will also need Docker. There's no reason the system packages shouldn't work, but we used the official Docker package. You can find instructions for installing Docker in Debian [here](https://docs.docker.com/engine/install/debian/).
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Syft Contributors
|
||||
|
||||
The following Syft components were contributed by external authors/organizations.
|
||||
|
||||
## GraalVM Native Image
|
||||
|
||||
A cataloger contributed by Oracle Corporation that extracts packages given within GraalVM Native Image SBOMs.
|
||||
|
||||
## Swift Package Manager
|
||||
|
||||
A cataloger contributed by Axis Communications that catalogs packages resolved by Swift Package Manager.
|
||||
+120
-39
@@ -7,6 +7,25 @@ In order to test and develop in this repo you will need the following dependenci
|
||||
- docker
|
||||
- make
|
||||
|
||||
### Docker settings for getting started
|
||||
Make sure you've updated your docker settings so the default docker socket path is available.
|
||||
|
||||
Go to:
|
||||
|
||||
docker -> settings -> advanced
|
||||
|
||||
Make sure:
|
||||
|
||||
```
|
||||
Allow the default Docker socket to be used
|
||||
```
|
||||
|
||||
is checked.
|
||||
|
||||
Also double check that the docker context being used is the default context. If it is not, run:
|
||||
|
||||
`docker context use default`
|
||||
|
||||
After cloning the following step can help you get setup:
|
||||
1. run `make bootstrap` to download go mod dependencies, create the `/.tmp` dir, and download helper utilities.
|
||||
2. run `make` to view the selection of developer commands in the Makefile
|
||||
@@ -15,37 +34,36 @@ After cloning the following step can help you get setup:
|
||||
- this command `go run cmd/syft/main.go alpine:latest` will compile and run syft against `alpine:latest`
|
||||
5. view the README or syft help output for more output options
|
||||
|
||||
#### Make output
|
||||
The main make tasks for common static analysis and testing are `lint`, `lint-fix`, `unit`, `integration`, and `cli`.
|
||||
The main make tasks for common static analysis and testing are `lint`, `format`, `lint-fix`, `unit`, `integration`, and `cli`.
|
||||
|
||||
See `make help` for all the current make tasks.
|
||||
|
||||
### Internal Artifactory Settings
|
||||
|
||||
**Not always applicable**
|
||||
|
||||
Some companies have Artifactory setup internally as a solution for sourcing secure dependencies.
|
||||
If you're seeing an issue where the unit tests won't run because of the below error then this section might be relevant for your use case.
|
||||
|
||||
```
|
||||
all Run all linux-based checks (linting, license check, unit, integration, and linux compare tests)
|
||||
benchmark Run benchmark tests and compare against the baseline (if available)
|
||||
bootstrap Download and install all tooling dependencies (+ prep tooling in the ./tmp dir)
|
||||
build Build release snapshot binaries and packages
|
||||
check-licenses Ensure transitive dependencies are compliant with the current license policy
|
||||
clean-test-image-cache Clean test image cache
|
||||
clean Remove previous builds, result reports, and test cache
|
||||
cli Run CLI tests
|
||||
compare-linux Run compare tests on build snapshot binaries and packages (Linux)
|
||||
compare-mac Run compare tests on build snapshot binaries and packages (Mac)
|
||||
generate-json-schema Generate a new json schema
|
||||
generate-license-list Generate an updated spdx license list
|
||||
help Display this help
|
||||
integration Run integration tests
|
||||
lint-fix Auto-format all source code + run golangci lint fixers
|
||||
lint Run gofmt + golangci lint checks
|
||||
show-test-image-cache Show all docker and image tar cache
|
||||
show-test-snapshots Show all test snapshots
|
||||
snapshot-with-signing Build snapshot release binaries and packages (with dummy signing)
|
||||
test Run all tests (currently unit, integration, linux compare, and cli tests)
|
||||
unit Run unit tests (with coverage)
|
||||
[ERROR] [ERROR] Some problems were encountered while processing the POMs
|
||||
```
|
||||
|
||||
If you're dealing with an issue where the unit tests will not pull/build certain java fixtures check some of these settings:
|
||||
|
||||
- a `settings.xml` file should be available to help you communicate with your internal artifactory deployment
|
||||
- this can be moved to `syft/pkg/cataloger/java/test-fixtures/java-builds/example-jenkins-plugin/` to help build the unit test-fixtures
|
||||
- you'll also want to modify the `build-example-jenkins-plugin.sh` to use `settings.xml`
|
||||
|
||||
For more information on this setup and troubleshooting see [issue 1895](https://github.com/anchore/syft/issues/1895#issuecomment-1610085319)
|
||||
|
||||
|
||||
## Architecture
|
||||
|
||||
Syft is used to generate a Software Bill of Materials (SBOM) from different kinds of input.
|
||||
|
||||
### Code organization for the cmd package
|
||||
|
||||
Syft's entrypoint can be found in the `cmd` package at `cmd/syft/main.go`. `main.go` builds a new syft `cli` via `cli.New()`
|
||||
and then executes the `cli` via `cli.Execute()`. The `cli` package is responsible for parsing command line arguments,
|
||||
setting up the application context and configuration, and executing the application. Each of syft's commands
|
||||
@@ -55,23 +73,24 @@ They are registered in `syft/cli/commands/go`.
|
||||
.
|
||||
└── syft/
|
||||
├── cli/
|
||||
│ ├── attest/
|
||||
│ ├── attest.go
|
||||
│ ├── commands.go
|
||||
│ ├── completion.go
|
||||
│ ├── convert/
|
||||
│ ├── convert.go
|
||||
│ ├── eventloop/
|
||||
│ ├── options/
|
||||
│ ├── packages/
|
||||
│ ├── packages.go
|
||||
│ ├── poweruser/
|
||||
│ ├── poweruser.go
|
||||
│ └── version.go
|
||||
│ ├── attest/
|
||||
│ ├── attest.go
|
||||
│ ├── commands.go
|
||||
│ ├── completion.go
|
||||
│ ├── convert/
|
||||
│ ├── convert.go
|
||||
│ ├── eventloop/
|
||||
│ ├── options/
|
||||
│ ├── packages/
|
||||
│ ├── packages.go
|
||||
│ ├── poweruser/
|
||||
│ ├── poweruser.go
|
||||
│ └── version.go
|
||||
└── main.go
|
||||
```
|
||||
|
||||
#### Execution flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant main as cmd/syft/main
|
||||
@@ -100,19 +119,19 @@ sequenceDiagram
|
||||
Syft's core library (see, exported) functionality is implemented in the `syft` package. The `syft` package is responsible for organizing the core
|
||||
SBOM data model, it's translated output formats, and the core SBOM generation logic.
|
||||
|
||||
#### Organization and design notes for the syft library
|
||||
- analysis creates a static SBOM which can be encoded and decoded
|
||||
- format objects, should strive to not add or enrich data in encoding that could otherwise be done during analysis
|
||||
- package catalogers and their organization can be viewed/added to the `syft/pkg/cataloger` package
|
||||
- file catalogers and their organization can be viewed/added to the `syft/file` package
|
||||
- The source package provides an abstraction to allow a user to loosely define a data source that can be cataloged
|
||||
- Logging Abstraction ...
|
||||
|
||||
#### Code example of syft as a library
|
||||
Here is a gist of using syft as a library to generate a SBOM from a docker image: [link](https://gist.github.com/wagoodman/57ed59a6d57600c23913071b8470175b).
|
||||
|
||||
Here is a gist of using syft as a library to generate a SBOM for a docker image: [link](https://gist.github.com/wagoodman/57ed59a6d57600c23913071b8470175b).
|
||||
The execution flow for the example is detailed below.
|
||||
|
||||
#### Execution flow examples for the syft library
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant source as source.New(ubuntu:latest)
|
||||
@@ -132,6 +151,68 @@ sequenceDiagram
|
||||
```
|
||||
|
||||
|
||||
### Syft Catalogers
|
||||
|
||||
##### Summary
|
||||
|
||||
Catalogers are the way in which syft is able to identify and construct packages given some amount of source metadata.
|
||||
For example, Syft can locate and process `package-lock.json` files when performing filesystem scans.
|
||||
See: [how to specify file globs](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/cataloger/javascript/cataloger.go#L16-L21)
|
||||
and an implementation of the [package-lock.json parser](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/cataloger/javascript/cataloger.go#L16-L21) for a quick review.
|
||||
|
||||
From a high level catalogers have the following properties:
|
||||
|
||||
- They are independent from one another. The java cataloger has no idea of the processes, assumptions, or results of the python cataloger, for example.
|
||||
|
||||
- They do not know what source is being analyzed. Are we analyzing a local directory? an image? if so, the squashed representation or all layers? The catalogers do not know the answers to these questions. Only that there is an interface to query for file paths and contents from an underlying "source" being scanned.
|
||||
|
||||
- Packages created by the cataloger should not be mutated after they are created. There is one exception made for adding CPEs to a package after the cataloging phase, but that will most likely be moved back into the cataloger in the future.
|
||||
|
||||
#### Building a new Cataloger
|
||||
|
||||
Catalogers must fulfill the interface [found here](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/cataloger.go).
|
||||
This means that when building a new cataloger, the new struct must implement both method signatures of `Catalog` and `Name`.
|
||||
|
||||
A top level view of the functions that construct all the catalogers can be found [here](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/cataloger/cataloger.go).
|
||||
When an author has finished writing a new cataloger this is the spot to plug in the new catalog constructor.
|
||||
|
||||
For a top level view of how the catalogers are used see [this function](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/cataloger/catalog.go#L41-L100) as a reference. It ranges over all catalogers passed as an argument and invokes the `Catalog` method:
|
||||
|
||||
Each cataloger has its own `Catalog` method, but this does not mean that they are all vastly different.
|
||||
Take a look at the `apkdb` cataloger for alpine to see how it [constructs a generic.NewCataloger](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/cataloger/apkdb/cataloger.go).
|
||||
|
||||
`generic.NewCataloger` is an abstraction syft uses to make writing common components easier. First, it takes the `catalogerName` to identify the cataloger.
|
||||
On the other side of the call it uses two key pieces which inform the cataloger how to identify and return packages, the `globPatterns` and the `parseFunction`:
|
||||
- The first piece is a `parseByGlob` matching pattern used to identify the files that contain the package metadata.
|
||||
See [here for the APK example](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/apk_metadata.go#L16-L41).
|
||||
- The other is a `parseFunction` which informs the cataloger what to do when it has found one of the above matches files.
|
||||
See this [link for an example](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/cataloger/apkdb/parse_apk_db.go#L22-L102).
|
||||
|
||||
If you're unsure about using the `Generic Cataloger` and think the use case being filled requires something more custom
|
||||
just file an issue or ask in our slack, and we'd be more than happy to help on the design.
|
||||
|
||||
Identified packages share a common struct so be sure that when the new cataloger is constructing a new package it is using the [`Package` struct](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/package.go#L16-L31).
|
||||
|
||||
Metadata Note: Identified packages are also assigned specific metadata that can be unique to their environment.
|
||||
See [this folder](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg) for examples of the different metadata types.
|
||||
These are plugged into the `MetadataType` and `Metadata` fields in the above struct. `MetadataType` informs which type is being used. `Metadata` is an interface converted to that type.
|
||||
|
||||
Finally, here is an example of where the package construction is done in the apk cataloger. The first link is where `newPackage` is called in the `parseFunction`. The second link shows the package construction:
|
||||
- [Call for new package](https://github.com/anchore/syft/blob/v0.70.0/syft/pkg/cataloger/apkdb/parse_apk_db.go#L106)
|
||||
- [APK Package Constructor](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/cataloger/apkdb/package.go#L12-L27)
|
||||
|
||||
If you have more questions about implementing a cataloger or questions about one you might be currently working
|
||||
always feel free to file an issue or reach out to us [on slack](https://anchore.com/slack).
|
||||
|
||||
#### Searching for files
|
||||
|
||||
All catalogers are provided an instance of the [`file.Resolver`](https://github.com/anchore/syft/blob/v0.70.0/syft/source/file_resolver.go#L8) to interface with the image and search for files. The implementations for these
|
||||
abstractions leverage [`stereoscope`](https://github.com/anchore/stereoscope) in order to perform searching. Here is a
|
||||
rough outline how that works:
|
||||
|
||||
1. a stereoscope `file.Index` is searched based on the input given (a path, glob, or MIME type). The index is relatively fast to search, but requires results to be filtered down to the files that exist in the specific layer(s) of interest. This is done automatically by the `filetree.Searcher` abstraction. This abstraction will fallback to searching directly against the raw `filetree.FileTree` if the index does not contain the file(s) of interest. Note: the `filetree.Searcher` is used by the `file.Resolver` abstraction.
|
||||
2. Once the set of files are returned from the `filetree.Searcher` the results are filtered down further to return the most unique file results. For example, you may have requested for files by a glob that returns multiple results. These results are filtered down to deduplicate by real files, so if a result contains two references to the same file, say one accessed via symlink and one accessed via the real path, then the real path reference is returned and the symlink reference is filtered out. If both were accessed by symlink then the first (by lexical order) is returned. This is done automatically by the `file.Resolver` abstraction.
|
||||
3. By the time results reach the `pkg.Cataloger` you are guaranteed to have a set of unique files that exist in the layer(s) of interest (relative to what the resolver supports).
|
||||
|
||||
## Testing
|
||||
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
BIN = syft
|
||||
VERSION=$(shell git describe --dirty --always --tags)
|
||||
TEMPDIR = ./.tmp
|
||||
BIN := syft
|
||||
TEMP_DIR := ./.tmp
|
||||
|
||||
# commands and versions
|
||||
LINTCMD = $(TEMPDIR)/golangci-lint run --tests=false
|
||||
GOIMPORTS_CMD = $(TEMPDIR)/gosimports -local github.com/anchore
|
||||
RELEASE_CMD=$(TEMPDIR)/goreleaser release --rm-dist
|
||||
SNAPSHOT_CMD=$(RELEASE_CMD) --skip-publish --snapshot
|
||||
# Command templates #################################
|
||||
LINT_CMD := $(TEMP_DIR)/golangci-lint run --tests=false
|
||||
GOIMPORTS_CMD := $(TEMP_DIR)/gosimports -local github.com/anchore
|
||||
RELEASE_CMD := $(TEMP_DIR)/goreleaser release --clean
|
||||
SNAPSHOT_CMD := $(RELEASE_CMD) --skip-publish --skip-sign --snapshot
|
||||
CHRONICLE_CMD = $(TEMP_DIR)/chronicle
|
||||
GLOW_CMD = $(TEMP_DIR)/glow
|
||||
|
||||
# tool versions
|
||||
GOLANGCILINT_VERSION = v1.50.1
|
||||
GOSIMPORTS_VERSION = v0.3.4
|
||||
BOUNCER_VERSION = v0.4.0
|
||||
CHRONICLE_VERSION = v0.4.2
|
||||
GORELEASER_VERSION = v1.13.1
|
||||
YAJSV_VERSION = v1.4.1
|
||||
COSIGN_VERSION = v1.13.1
|
||||
QUILL_VERSION = v0.2.0
|
||||
# Tool versions #################################
|
||||
GOLANGCILINT_VERSION := v1.53.3
|
||||
GOSIMPORTS_VERSION := v0.3.8
|
||||
BOUNCER_VERSION := v0.4.0
|
||||
CHRONICLE_VERSION := v0.6.0
|
||||
GORELEASER_VERSION := v1.19.2
|
||||
YAJSV_VERSION := v1.4.1
|
||||
COSIGN_VERSION := v2.1.1
|
||||
QUILL_VERSION := v0.2.0
|
||||
GLOW_VERSION := v1.5.1
|
||||
|
||||
# formatting variables
|
||||
# Formatting variables #################################
|
||||
BOLD := $(shell tput -T linux bold)
|
||||
PURPLE := $(shell tput -T linux setaf 5)
|
||||
GREEN := $(shell tput -T linux setaf 2)
|
||||
@@ -28,55 +30,23 @@ RESET := $(shell tput -T linux sgr0)
|
||||
TITLE := $(BOLD)$(PURPLE)
|
||||
SUCCESS := $(BOLD)$(GREEN)
|
||||
|
||||
# test variables
|
||||
RESULTSDIR = test/results
|
||||
COMPARE_DIR = ./test/compare
|
||||
COMPARE_TEST_IMAGE = centos:8.2.2004
|
||||
COVER_REPORT = $(RESULTSDIR)/unit-coverage-details.txt
|
||||
COVER_TOTAL = $(RESULTSDIR)/unit-coverage-summary.txt
|
||||
# the quality gate lower threshold for unit test total % coverage (by function statements)
|
||||
COVERAGE_THRESHOLD := 62
|
||||
# Test variables #################################
|
||||
COMPARE_DIR := ./test/compare
|
||||
COMPARE_TEST_IMAGE := centos:8.2.2004
|
||||
COVERAGE_THRESHOLD := 62 # the quality gate lower threshold for unit test total % coverage (by function statements)
|
||||
|
||||
# CI cache busting values; change these if you want CI to not use previous stored cache
|
||||
INTEGRATION_CACHE_BUSTER="894d8ca"
|
||||
CLI_CACHE_BUSTER="e5cdfd8"
|
||||
BOOTSTRAP_CACHE="c7afb99ad"
|
||||
|
||||
## Build variables
|
||||
DISTDIR=./dist
|
||||
SNAPSHOTDIR=./snapshot
|
||||
OS=$(shell uname | tr '[:upper:]' '[:lower:]')
|
||||
SNAPSHOT_BIN=$(realpath $(shell pwd)/$(SNAPSHOTDIR)/$(OS)-build_$(OS)_amd64_v1/$(BIN))
|
||||
|
||||
## Variable assertions
|
||||
ifndef TEMPDIR
|
||||
$(error TEMPDIR is not set)
|
||||
endif
|
||||
|
||||
ifndef RESULTSDIR
|
||||
$(error RESULTSDIR is not set)
|
||||
endif
|
||||
|
||||
ifndef COMPARE_DIR
|
||||
$(error COMPARE_DIR is not set)
|
||||
endif
|
||||
|
||||
ifndef DISTDIR
|
||||
$(error DISTDIR is not set)
|
||||
endif
|
||||
|
||||
ifndef SNAPSHOTDIR
|
||||
$(error SNAPSHOTDIR is not set)
|
||||
endif
|
||||
## Build variables #################################
|
||||
VERSION := $(shell git describe --dirty --always --tags)
|
||||
DIST_DIR := ./dist
|
||||
SNAPSHOT_DIR := ./snapshot
|
||||
CHANGELOG := CHANGELOG.md
|
||||
OS := $(shell uname | tr '[:upper:]' '[:lower:]')
|
||||
SNAPSHOT_BIN := $(realpath $(shell pwd)/$(SNAPSHOT_DIR)/$(OS)-build_$(OS)_amd64_v1/$(BIN))
|
||||
|
||||
ifndef VERSION
|
||||
$(error VERSION is not set)
|
||||
endif
|
||||
|
||||
ifndef REF_NAME
|
||||
REF_NAME = $(VERSION)
|
||||
endif
|
||||
|
||||
define title
|
||||
@printf '$(TITLE)$(1)$(RESET)\n'
|
||||
endef
|
||||
@@ -89,309 +59,177 @@ define safe_rm_rf_children
|
||||
bash -c 'test -z "$(1)" && false || rm -rf $(1)/*'
|
||||
endef
|
||||
|
||||
## Default Task
|
||||
.DEFAULT_GOAL:=help
|
||||
|
||||
## Tasks
|
||||
|
||||
.PHONY: all
|
||||
all: clean static-analysis test ## Run all linux-based checks (linting, license check, unit, integration, and linux compare tests)
|
||||
all: static-analysis test ## Run all linux-based checks (linting, license check, unit, integration, and linux compare tests)
|
||||
@printf '$(SUCCESS)All checks pass!$(RESET)\n'
|
||||
|
||||
.PHONY: static-analysis
|
||||
static-analysis: check-go-mod-tidy lint check-licenses check-json-schema-drift ## Run all static analysis checks
|
||||
|
||||
.PHONY: test
|
||||
test: unit validate-cyclonedx-schema integration benchmark compare-linux cli ## Run all tests (currently unit, integration, linux compare, and cli tests)
|
||||
test: unit integration validate-cyclonedx-schema benchmark cli ## Run all tests (currently unit, integration, linux compare, and cli tests)
|
||||
|
||||
.PHONY: ci-bootstrap
|
||||
ci-bootstrap:
|
||||
DEBIAN_FRONTEND=noninteractive sudo apt update && sudo -E apt install -y bc jq libxml2-utils
|
||||
|
||||
.PHONY:
|
||||
ci-bootstrap-mac:
|
||||
github_changelog_generator --version || sudo gem install github_changelog_generator
|
||||
## Bootstrapping targets #################################
|
||||
|
||||
$(RESULTSDIR):
|
||||
mkdir -p $(RESULTSDIR)
|
||||
|
||||
$(TEMPDIR):
|
||||
mkdir -p $(TEMPDIR)
|
||||
.PHONY: bootstrap
|
||||
bootstrap: $(TEMP_DIR) bootstrap-go bootstrap-tools ## Download and install all tooling dependencies (+ prep tooling in the ./tmp dir)
|
||||
$(call title,Bootstrapping dependencies)
|
||||
|
||||
.PHONY: bootstrap-tools
|
||||
bootstrap-tools: $(TEMPDIR)
|
||||
curl -sSfL https://raw.githubusercontent.com/anchore/quill/main/install.sh | sh -s -- -b $(TEMPDIR)/ $(QUILL_VERSION)
|
||||
GO111MODULE=off GOBIN=$(realpath $(TEMPDIR)) go get -u golang.org/x/perf/cmd/benchstat
|
||||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(TEMPDIR)/ $(GOLANGCILINT_VERSION)
|
||||
curl -sSfL https://raw.githubusercontent.com/wagoodman/go-bouncer/master/bouncer.sh | sh -s -- -b $(TEMPDIR)/ $(BOUNCER_VERSION)
|
||||
curl -sSfL https://raw.githubusercontent.com/anchore/chronicle/main/install.sh | sh -s -- -b $(TEMPDIR)/ $(CHRONICLE_VERSION)
|
||||
.github/scripts/goreleaser-install.sh -d -b $(TEMPDIR)/ $(GORELEASER_VERSION)
|
||||
bootstrap-tools: $(TEMP_DIR)
|
||||
curl -sSfL https://raw.githubusercontent.com/anchore/quill/main/install.sh | sh -s -- -b $(TEMP_DIR)/ $(QUILL_VERSION)
|
||||
GO111MODULE=off GOBIN=$(realpath $(TEMP_DIR)) go get -u golang.org/x/perf/cmd/benchstat
|
||||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(TEMP_DIR)/ $(GOLANGCILINT_VERSION)
|
||||
curl -sSfL https://raw.githubusercontent.com/wagoodman/go-bouncer/master/bouncer.sh | sh -s -- -b $(TEMP_DIR)/ $(BOUNCER_VERSION)
|
||||
curl -sSfL https://raw.githubusercontent.com/anchore/chronicle/main/install.sh | sh -s -- -b $(TEMP_DIR)/ $(CHRONICLE_VERSION)
|
||||
.github/scripts/goreleaser-install.sh -d -b $(TEMP_DIR)/ $(GORELEASER_VERSION)
|
||||
# the only difference between goimports and gosimports is that gosimports removes extra whitespace between import blocks (see https://github.com/golang/go/issues/20818)
|
||||
GOBIN="$(realpath $(TEMPDIR))" go install github.com/rinchsan/gosimports/cmd/gosimports@$(GOSIMPORTS_VERSION)
|
||||
GOBIN="$(realpath $(TEMPDIR))" go install github.com/neilpa/yajsv@$(YAJSV_VERSION)
|
||||
GOBIN="$(realpath $(TEMPDIR))" go install github.com/sigstore/cosign/cmd/cosign@$(COSIGN_VERSION)
|
||||
GOBIN="$(realpath $(TEMP_DIR))" go install github.com/rinchsan/gosimports/cmd/gosimports@$(GOSIMPORTS_VERSION)
|
||||
GOBIN="$(realpath $(TEMP_DIR))" go install github.com/neilpa/yajsv@$(YAJSV_VERSION)
|
||||
GOBIN="$(realpath $(TEMP_DIR))" go install github.com/sigstore/cosign/v2/cmd/cosign@$(COSIGN_VERSION)
|
||||
GOBIN="$(realpath $(TEMP_DIR))" go install github.com/charmbracelet/glow@$(GLOW_VERSION)
|
||||
|
||||
.PHONY: bootstrap-go
|
||||
bootstrap-go:
|
||||
go mod download
|
||||
|
||||
.PHONY: bootstrap
|
||||
bootstrap: $(RESULTSDIR) bootstrap-go bootstrap-tools ## Download and install all tooling dependencies (+ prep tooling in the ./tmp dir)
|
||||
$(call title,Bootstrapping dependencies)
|
||||
$(TEMP_DIR):
|
||||
mkdir -p $(TEMP_DIR)
|
||||
|
||||
.PHONY: static-analysis
|
||||
static-analysis: check-go-mod-tidy check-licenses lint
|
||||
|
||||
## Static analysis targets #################################
|
||||
|
||||
.PHONY: lint
|
||||
lint: ## Run gofmt + golangci lint checks
|
||||
lint: ## Run gofmt + golangci lint checks
|
||||
$(call title,Running linters)
|
||||
# ensure there are no go fmt differences
|
||||
@printf "files with gofmt issues: [$(shell gofmt -l -s .)]\n"
|
||||
@test -z "$(shell gofmt -l -s .)"
|
||||
|
||||
# run all golangci-lint rules
|
||||
$(LINTCMD)
|
||||
$(LINT_CMD)
|
||||
@[ -z "$(shell $(GOIMPORTS_CMD) -d .)" ] || (echo "goimports needs to be fixed" && false)
|
||||
|
||||
# go tooling does not play well with certain filename characters, ensure the common cases don't result in future "go get" failures
|
||||
$(eval MALFORMED_FILENAMES := $(shell find . | grep -e ':'))
|
||||
@bash -c "[[ '$(MALFORMED_FILENAMES)' == '' ]] || (printf '\nfound unsupported filename characters:\n$(MALFORMED_FILENAMES)\n\n' && false)"
|
||||
|
||||
.PHONY: lint-fix
|
||||
lint-fix: ## Auto-format all source code + run golangci lint fixers
|
||||
$(call title,Running lint fixers)
|
||||
.PHONY: format
|
||||
format: ## Auto-format all source code
|
||||
$(call title,Running formatters)
|
||||
gofmt -w -s .
|
||||
$(GOIMPORTS_CMD) -w .
|
||||
$(LINTCMD) --fix
|
||||
go mod tidy
|
||||
|
||||
.PHONY: lint-fix
|
||||
lint-fix: format ## Auto-format all source code + run golangci lint fixers
|
||||
$(call title,Running lint fixers)
|
||||
$(LINT_CMD) --fix
|
||||
|
||||
.PHONY: check-licenses
|
||||
check-licenses: ## Ensure transitive dependencies are compliant with the current license policy
|
||||
$(TEMPDIR)/bouncer check ./...
|
||||
check-licenses: ## Ensure transitive dependencies are compliant with the current license policy
|
||||
$(call title,Checking for license compliance)
|
||||
$(TEMP_DIR)/bouncer check ./...
|
||||
|
||||
check-go-mod-tidy:
|
||||
@ .github/scripts/go-mod-tidy-check.sh && echo "go.mod and go.sum are tidy!"
|
||||
|
||||
check-json-schema-drift:
|
||||
$(call title,Ensure there is no drift between the JSON schema and the code)
|
||||
@.github/scripts/json-schema-drift-check.sh
|
||||
|
||||
## Testing targets #################################
|
||||
|
||||
.PHONY: unit
|
||||
unit: $(TEMP_DIR) fixtures ## Run unit tests (with coverage)
|
||||
$(call title,Running unit tests)
|
||||
go test -coverprofile $(TEMP_DIR)/unit-coverage-details.txt $(shell go list ./... | grep -v anchore/syft/test)
|
||||
@.github/scripts/coverage.py $(COVERAGE_THRESHOLD) $(TEMP_DIR)/unit-coverage-details.txt
|
||||
|
||||
.PHONY: integration
|
||||
integration: ## Run integration tests
|
||||
$(call title,Running integration tests)
|
||||
go test -v ./test/integration
|
||||
|
||||
.PHONY: validate-cyclonedx-schema
|
||||
validate-cyclonedx-schema:
|
||||
cd schema/cyclonedx && make
|
||||
|
||||
.PHONY: unit
|
||||
unit: $(RESULTSDIR) fixtures ## Run unit tests (with coverage)
|
||||
$(call title,Running unit tests)
|
||||
go test -coverprofile $(COVER_REPORT) $(shell go list ./... | grep -v anchore/syft/test)
|
||||
@go tool cover -func $(COVER_REPORT) | grep total | awk '{print substr($$3, 1, length($$3)-1)}' > $(COVER_TOTAL)
|
||||
@echo "Coverage: $$(cat $(COVER_TOTAL))"
|
||||
@if [ $$(echo "$$(cat $(COVER_TOTAL)) >= $(COVERAGE_THRESHOLD)" | bc -l) -ne 1 ]; then echo "$(RED)$(BOLD)Failed coverage quality gate (> $(COVERAGE_THRESHOLD)%)$(RESET)" && false; fi
|
||||
.PHONY: cli
|
||||
cli: $(SNAPSHOT_DIR) ## Run CLI tests
|
||||
chmod 755 "$(SNAPSHOT_BIN)"
|
||||
$(SNAPSHOT_BIN) version
|
||||
SYFT_BINARY_LOCATION='$(SNAPSHOT_BIN)' \
|
||||
go test -count=1 -timeout=15m -v ./test/cli
|
||||
|
||||
|
||||
## Benchmark test targets #################################
|
||||
|
||||
.PHONY: benchmark
|
||||
benchmark: $(RESULTSDIR) ## Run benchmark tests and compare against the baseline (if available)
|
||||
benchmark: $(TEMP_DIR) ## Run benchmark tests and compare against the baseline (if available)
|
||||
$(call title,Running benchmark tests)
|
||||
go test -p 1 -run=^Benchmark -bench=. -count=5 -benchmem ./... | tee $(RESULTSDIR)/benchmark-$(REF_NAME).txt
|
||||
(test -s $(RESULTSDIR)/benchmark-main.txt && \
|
||||
$(TEMPDIR)/benchstat $(RESULTSDIR)/benchmark-main.txt $(RESULTSDIR)/benchmark-$(REF_NAME).txt || \
|
||||
$(TEMPDIR)/benchstat $(RESULTSDIR)/benchmark-$(REF_NAME).txt) \
|
||||
| tee $(RESULTSDIR)/benchstat.txt
|
||||
go test -p 1 -run=^Benchmark -bench=. -count=7 -benchmem ./... | tee $(TEMP_DIR)/benchmark-$(VERSION).txt
|
||||
(test -s $(TEMP_DIR)/benchmark-main.txt && \
|
||||
$(TEMP_DIR)/benchstat $(TEMP_DIR)/benchmark-main.txt $(TEMP_DIR)/benchmark-$(VERSION).txt || \
|
||||
$(TEMP_DIR)/benchstat $(TEMP_DIR)/benchmark-$(VERSION).txt) \
|
||||
| tee $(TEMP_DIR)/benchstat.txt
|
||||
|
||||
.PHONY: show-benchstat
|
||||
show-benchstat:
|
||||
@cat $(RESULTSDIR)/benchstat.txt
|
||||
@cat $(TEMP_DIR)/benchstat.txt
|
||||
|
||||
# note: this is used by CI to determine if the install test fixture cache (docker image tars) should be busted
|
||||
install-fingerprint:
|
||||
cd test/install && \
|
||||
|
||||
## Test-fixture-related targets #################################
|
||||
|
||||
# note: this is used by CI to determine if various test fixture cache should be restored or recreated
|
||||
fingerprints:
|
||||
$(call title,Creating all test cache input fingerprints)
|
||||
|
||||
# for IMAGE integration test fixtures
|
||||
cd test/integration/test-fixtures && \
|
||||
make cache.fingerprint
|
||||
|
||||
install-test: $(SNAPSHOTDIR)
|
||||
cd test/install && \
|
||||
make
|
||||
# for BINARY test fixtures
|
||||
cd syft/pkg/cataloger/binary/test-fixtures && \
|
||||
make cache.fingerprint
|
||||
|
||||
install-test-cache-save: $(SNAPSHOTDIR)
|
||||
cd test/install && \
|
||||
make save
|
||||
|
||||
install-test-cache-load: $(SNAPSHOTDIR)
|
||||
cd test/install && \
|
||||
make load
|
||||
|
||||
install-test-ci-mac: $(SNAPSHOTDIR)
|
||||
cd test/install && \
|
||||
make ci-test-mac
|
||||
|
||||
.PHONY: integration
|
||||
integration: ## Run integration tests
|
||||
$(call title,Running integration tests)
|
||||
go test -v ./test/integration
|
||||
|
||||
# note: this is used by CI to determine if the integration test fixture cache (docker image tars) should be busted
|
||||
integration-fingerprint:
|
||||
$(call title,Integration test fixture fingerprint)
|
||||
find test/integration/test-fixtures/image-* -type f -exec md5sum {} + | awk '{print $1}' | sort | tee /dev/stderr | md5sum | tee test/integration/test-fixtures/cache.fingerprint && echo "$(INTEGRATION_CACHE_BUSTER)" >> test/integration/test-fixtures/cache.fingerprint
|
||||
|
||||
.PHONY: java-packages-fingerprint
|
||||
java-packages-fingerprint:
|
||||
$(call title,Java test fixture fingerprint)
|
||||
# for JAVA BUILD test fixtures
|
||||
cd syft/pkg/cataloger/java/test-fixtures/java-builds && \
|
||||
make packages.fingerprint
|
||||
make cache.fingerprint
|
||||
|
||||
.PHONY: go-binaries-fingerprint
|
||||
go-binaries-fingerprint:
|
||||
$(call title,Go binaries test fixture fingerprint)
|
||||
# for GO BINARY test fixtures
|
||||
cd syft/pkg/cataloger/golang/test-fixtures/archs && \
|
||||
make binaries.fingerprint
|
||||
|
||||
.PHONY: rpm-binaries-fingerprint
|
||||
rpm-binaries-fingerprint:
|
||||
$(call title,RPM binary test fixture fingerprint)
|
||||
# for RPM test fixtures
|
||||
cd syft/pkg/cataloger/rpm/test-fixtures && \
|
||||
make rpms.fingerprint
|
||||
|
||||
# for Kernel test fixtures
|
||||
cd syft/pkg/cataloger/kernel/test-fixtures && \
|
||||
make cache.fingerprint
|
||||
|
||||
# for INSTALL integration test fixtures
|
||||
cd test/install && \
|
||||
make cache.fingerprint
|
||||
|
||||
# for CLI test fixtures
|
||||
cd test/cli/test-fixtures && \
|
||||
make cache.fingerprint
|
||||
|
||||
.PHONY: fixtures
|
||||
fixtures:
|
||||
$(call title,Generating test fixtures)
|
||||
cd syft/pkg/cataloger/java/test-fixtures/java-builds && make
|
||||
cd syft/pkg/cataloger/rpm/test-fixtures && make
|
||||
|
||||
.PHONY: generate-json-schema
|
||||
generate-json-schema: ## Generate a new json schema
|
||||
cd schema/json && go run generate.go
|
||||
|
||||
.PHONY: generate-license-list
|
||||
generate-license-list: ## Generate an updated spdx license list
|
||||
go generate ./internal/spdxlicense/...
|
||||
gofmt -s -w ./internal/spdxlicense
|
||||
|
||||
.PHONY: build
|
||||
build: $(SNAPSHOTDIR) ## Build release snapshot binaries and packages
|
||||
|
||||
$(SNAPSHOTDIR): ## Build snapshot release binaries and packages
|
||||
$(call title,Building snapshot artifacts)
|
||||
|
||||
# create a config with the dist dir overridden
|
||||
echo "dist: $(SNAPSHOTDIR)" > $(TEMPDIR)/goreleaser.yaml
|
||||
cat .goreleaser.yaml >> $(TEMPDIR)/goreleaser.yaml
|
||||
|
||||
# build release snapshots
|
||||
bash -c "SKIP_SIGNING=true $(SNAPSHOT_CMD) --skip-sign --config $(TEMPDIR)/goreleaser.yaml"
|
||||
|
||||
.PHONY: snapshot-with-signing
|
||||
snapshot-with-signing: ## Build snapshot release binaries and packages (with dummy signing)
|
||||
$(call title,Building snapshot artifacts (+ signing))
|
||||
|
||||
# create a config with the dist dir overridden
|
||||
echo "dist: $(SNAPSHOTDIR)" > $(TEMPDIR)/goreleaser.yaml
|
||||
cat .goreleaser.yaml >> $(TEMPDIR)/goreleaser.yaml
|
||||
|
||||
# build release snapshots
|
||||
bash -c "\
|
||||
$(SNAPSHOT_CMD) \
|
||||
--config $(TEMPDIR)/goreleaser.yaml \
|
||||
|| (cat /tmp/quill-*.log && false)"
|
||||
|
||||
# note: we cannot clean the snapshot directory since the pipeline builds the snapshot separately
|
||||
.PHONY: compare-mac
|
||||
compare-mac: $(RESULTSDIR) $(SNAPSHOTDIR) ## Run compare tests on build snapshot binaries and packages (Mac)
|
||||
$(call title,Running compare test: Run on Mac)
|
||||
$(COMPARE_DIR)/mac.sh \
|
||||
$(SNAPSHOTDIR) \
|
||||
$(COMPARE_DIR) \
|
||||
$(COMPARE_TEST_IMAGE) \
|
||||
$(RESULTSDIR)
|
||||
|
||||
# note: we cannot clean the snapshot directory since the pipeline builds the snapshot separately
|
||||
.PHONY: compare-linux
|
||||
compare-linux: compare-test-deb-package-install compare-test-rpm-package-install ## Run compare tests on build snapshot binaries and packages (Linux)
|
||||
|
||||
.PHONY: compare-test-deb-package-install
|
||||
compare-test-deb-package-install: $(RESULTSDIR) $(SNAPSHOTDIR)
|
||||
$(call title,Running compare test: DEB install)
|
||||
$(COMPARE_DIR)/deb.sh \
|
||||
$(SNAPSHOTDIR) \
|
||||
$(COMPARE_DIR) \
|
||||
$(COMPARE_TEST_IMAGE) \
|
||||
$(RESULTSDIR)
|
||||
|
||||
.PHONY: compare-test-rpm-package-install
|
||||
compare-test-rpm-package-install: $(RESULTSDIR) $(SNAPSHOTDIR)
|
||||
$(call title,Running compare test: RPM install)
|
||||
$(COMPARE_DIR)/rpm.sh \
|
||||
$(SNAPSHOTDIR) \
|
||||
$(COMPARE_DIR) \
|
||||
$(COMPARE_TEST_IMAGE) \
|
||||
$(RESULTSDIR)
|
||||
|
||||
# note: this is used by CI to determine if the integration test fixture cache (docker image tars) should be busted
|
||||
cli-fingerprint:
|
||||
$(call title,CLI test fixture fingerprint)
|
||||
find test/cli/test-fixtures/image-* -type f -exec md5sum {} + | awk '{print $1}' | sort | md5sum | tee test/cli/test-fixtures/cache.fingerprint && echo "$(CLI_CACHE_BUSTER)" >> test/cli/test-fixtures/cache.fingerprint
|
||||
|
||||
.PHONY: cli
|
||||
cli: $(SNAPSHOTDIR) ## Run CLI tests
|
||||
chmod 755 "$(SNAPSHOT_BIN)"
|
||||
$(SNAPSHOT_BIN) version
|
||||
SYFT_BINARY_LOCATION='$(SNAPSHOT_BIN)' \
|
||||
go test -count=1 -timeout=15m -v ./test/cli
|
||||
|
||||
.PHONY: changelog
|
||||
changelog: clean-changelog CHANGELOG.md
|
||||
@docker run -it --rm \
|
||||
-v $(shell pwd)/CHANGELOG.md:/CHANGELOG.md \
|
||||
rawkode/mdv \
|
||||
-t 748.5989 \
|
||||
/CHANGELOG.md
|
||||
|
||||
CHANGELOG.md:
|
||||
$(TEMPDIR)/chronicle -vv > CHANGELOG.md
|
||||
|
||||
.PHONY: release
|
||||
release: clean-dist CHANGELOG.md
|
||||
$(call title,Publishing release artifacts)
|
||||
|
||||
# create a config with the dist dir overridden
|
||||
echo "dist: $(DISTDIR)" > $(TEMPDIR)/goreleaser.yaml
|
||||
cat .goreleaser.yaml >> $(TEMPDIR)/goreleaser.yaml
|
||||
|
||||
# note: notarization cannot be done in parallel, thus --parallelism 1
|
||||
bash -c "\
|
||||
$(RELEASE_CMD) \
|
||||
--config $(TEMPDIR)/goreleaser.yaml \
|
||||
--parallelism 1 \
|
||||
--release-notes <(cat CHANGELOG.md) \
|
||||
|| (cat /tmp/quill-*.log && false)"
|
||||
|
||||
# TODO: turn this into a post-release hook
|
||||
# upload the version file that supports the application version update check (excluding pre-releases)
|
||||
.github/scripts/update-version-file.sh "$(DISTDIR)" "$(VERSION)"
|
||||
|
||||
.PHONY: clean
|
||||
clean: clean-dist clean-snapshot clean-test-image-cache ## Remove previous builds, result reports, and test cache
|
||||
$(call safe_rm_rf_children,$(RESULTSDIR))
|
||||
|
||||
.PHONY: clean-snapshot
|
||||
clean-snapshot:
|
||||
$(call safe_rm_rf,$(SNAPSHOTDIR))
|
||||
rm -f $(TEMPDIR)/goreleaser.yaml
|
||||
|
||||
.PHONY: clean-dist
|
||||
clean-dist: clean-changelog
|
||||
$(call safe_rm_rf,$(DISTDIR))
|
||||
rm -f $(TEMPDIR)/goreleaser.yaml
|
||||
|
||||
.PHONY: clean-changelog
|
||||
clean-changelog:
|
||||
rm -f CHANGELOG.md
|
||||
|
||||
clean-test-image-cache: clean-test-image-tar-cache clean-test-image-docker-cache ## Clean test image cache
|
||||
|
||||
.PHONY: clear-test-image-tar-cache
|
||||
clean-test-image-tar-cache:
|
||||
## Delete all test cache (built docker image tars)
|
||||
find . -type f -wholename "**/test-fixtures/cache/stereoscope-fixture-*.tar" -delete
|
||||
|
||||
.PHONY: clear-test-image-docker-cache
|
||||
clean-test-image-docker-cache:
|
||||
## Purge all test docker images
|
||||
docker images --format '{{.ID}} {{.Repository}}' | grep stereoscope-fixture- | awk '{print $$1}' | uniq | xargs -r docker rmi --force
|
||||
cd syft/pkg/cataloger/binary/test-fixtures && make
|
||||
|
||||
.PHONY: show-test-image-cache
|
||||
show-test-image-cache: ## Show all docker and image tar cache
|
||||
show-test-image-cache: ## Show all docker and image tar cache
|
||||
$(call title,Docker daemon cache)
|
||||
@docker images --format '{{.ID}} {{.Repository}}:{{.Tag}}' | grep stereoscope-fixture- | sort
|
||||
|
||||
@@ -399,10 +237,165 @@ show-test-image-cache: ## Show all docker and image tar cache
|
||||
@find . -type f -wholename "**/test-fixtures/cache/stereoscope-fixture-*.tar" | sort
|
||||
|
||||
.PHONY: show-test-snapshots
|
||||
show-test-snapshots: ## Show all test snapshots
|
||||
show-test-snapshots: ## Show all test snapshots
|
||||
$(call title,Test snapshots)
|
||||
@find . -type f -wholename "**/test-fixtures/snapshot/*" | sort
|
||||
|
||||
|
||||
## install.sh testing targets #################################
|
||||
|
||||
install-test: $(SNAPSHOT_DIR)
|
||||
cd test/install && \
|
||||
make
|
||||
|
||||
install-test-cache-save: $(SNAPSHOT_DIR)
|
||||
cd test/install && \
|
||||
make save
|
||||
|
||||
install-test-cache-load: $(SNAPSHOT_DIR)
|
||||
cd test/install && \
|
||||
make load
|
||||
|
||||
install-test-ci-mac: $(SNAPSHOT_DIR)
|
||||
cd test/install && \
|
||||
make ci-test-mac
|
||||
|
||||
.PHONY: generate-compare-file
|
||||
generate-compare-file:
|
||||
$(call title,Generating compare test file)
|
||||
go run ./cmd/syft $(COMPARE_TEST_IMAGE) -o json > $(COMPARE_DIR)/test-fixtures/acceptance-centos-8.2.2004.json
|
||||
|
||||
# note: we cannot clean the snapshot directory since the pipeline builds the snapshot separately
|
||||
.PHONY: compare-mac
|
||||
compare-mac: $(TEMP_DIR) $(SNAPSHOT_DIR) ## Run compare tests on build snapshot binaries and packages (Mac)
|
||||
$(call title,Running compare test: Run on Mac)
|
||||
$(COMPARE_DIR)/mac.sh \
|
||||
$(SNAPSHOT_DIR) \
|
||||
$(COMPARE_DIR) \
|
||||
$(COMPARE_TEST_IMAGE) \
|
||||
$(TEMP_DIR)
|
||||
|
||||
# note: we cannot clean the snapshot directory since the pipeline builds the snapshot separately
|
||||
.PHONY: compare-linux
|
||||
compare-linux: compare-test-deb-package-install compare-test-rpm-package-install ## Run compare tests on build snapshot binaries and packages (Linux)
|
||||
|
||||
.PHONY: compare-test-deb-package-install
|
||||
compare-test-deb-package-install: $(TEMP_DIR) $(SNAPSHOT_DIR)
|
||||
$(call title,Running compare test: DEB install)
|
||||
$(COMPARE_DIR)/deb.sh \
|
||||
$(SNAPSHOT_DIR) \
|
||||
$(COMPARE_DIR) \
|
||||
$(COMPARE_TEST_IMAGE) \
|
||||
$(TEMP_DIR)
|
||||
|
||||
.PHONY: compare-test-rpm-package-install
|
||||
compare-test-rpm-package-install: $(TEMP_DIR) $(SNAPSHOT_DIR)
|
||||
$(call title,Running compare test: RPM install)
|
||||
$(COMPARE_DIR)/rpm.sh \
|
||||
$(SNAPSHOT_DIR) \
|
||||
$(COMPARE_DIR) \
|
||||
$(COMPARE_TEST_IMAGE) \
|
||||
$(TEMP_DIR)
|
||||
|
||||
|
||||
## Code and data generation targets #################################
|
||||
|
||||
.PHONY: generate-json-schema
|
||||
generate-json-schema: ## Generate a new json schema
|
||||
cd syft/internal && go generate . && cd jsonschema && go run .
|
||||
|
||||
.PHONY: generate-license-list
|
||||
generate-license-list: ## Generate an updated spdx license list
|
||||
go generate ./internal/spdxlicense/...
|
||||
gofmt -s -w ./internal/spdxlicense
|
||||
|
||||
.PHONY: generate-cpe-dictionary-index
|
||||
generate-cpe-dictionary-index: ## Build the CPE index based off of the latest available CPE dictionary
|
||||
$(call title,Building CPE index)
|
||||
go generate ./syft/pkg/cataloger/common/cpe/dictionary
|
||||
|
||||
|
||||
## Build-related targets #################################
|
||||
|
||||
.PHONY: build
|
||||
build:
|
||||
CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -o $@ ./cmd/syft
|
||||
|
||||
$(SNAPSHOT_DIR): ## Build snapshot release binaries and packages
|
||||
$(call title,Building snapshot artifacts)
|
||||
|
||||
# create a config with the dist dir overridden
|
||||
echo "dist: $(SNAPSHOT_DIR)" > $(TEMP_DIR)/goreleaser.yaml
|
||||
cat .goreleaser.yaml >> $(TEMP_DIR)/goreleaser.yaml
|
||||
|
||||
# build release snapshots
|
||||
$(SNAPSHOT_CMD) --config $(TEMP_DIR)/goreleaser.yaml
|
||||
|
||||
.PHONY: changelog
|
||||
changelog: clean-changelog ## Generate and show the changelog for the current unreleased version
|
||||
$(CHRONICLE_CMD) -vvv -n --version-file VERSION > $(CHANGELOG)
|
||||
@$(GLOW_CMD) $(CHANGELOG)
|
||||
|
||||
$(CHANGELOG):
|
||||
$(CHRONICLE_CMD) -vvv > $(CHANGELOG)
|
||||
|
||||
.PHONY: release
|
||||
release:
|
||||
@.github/scripts/trigger-release.sh
|
||||
|
||||
.PHONY: ci-release
|
||||
ci-release: ci-check clean-dist $(CHANGELOG)
|
||||
$(call title,Publishing release artifacts)
|
||||
|
||||
# create a config with the dist dir overridden
|
||||
echo "dist: $(DIST_DIR)" > $(TEMP_DIR)/goreleaser.yaml
|
||||
cat .goreleaser.yaml >> $(TEMP_DIR)/goreleaser.yaml
|
||||
|
||||
bash -c "\
|
||||
$(RELEASE_CMD) \
|
||||
--config $(TEMP_DIR)/goreleaser.yaml \
|
||||
--release-notes <(cat $(CHANGELOG)) \
|
||||
|| (cat /tmp/quill-*.log && false)"
|
||||
|
||||
# upload the version file that supports the application version update check (excluding pre-releases)
|
||||
.github/scripts/update-version-file.sh "$(DIST_DIR)" "$(VERSION)"
|
||||
|
||||
.PHONY: ci-check
|
||||
ci-check:
|
||||
@.github/scripts/ci-check.sh
|
||||
|
||||
## Cleanup targets #################################
|
||||
|
||||
.PHONY: clean
|
||||
clean: clean-dist clean-snapshot clean-test-image-cache ## Remove previous builds, result reports, and test cache
|
||||
$(call safe_rm_rf_children,$(TEMP_DIR))
|
||||
|
||||
.PHONY: clean-snapshot
|
||||
clean-snapshot:
|
||||
$(call safe_rm_rf,$(SNAPSHOT_DIR))
|
||||
rm -f $(TEMP_DIR)/goreleaser.yaml
|
||||
|
||||
.PHONY: clean-dist
|
||||
clean-dist: clean-changelog
|
||||
$(call safe_rm_rf,$(DIST_DIR))
|
||||
rm -f $(TEMP_DIR)/goreleaser.yaml
|
||||
|
||||
.PHONY: clean-changelog
|
||||
clean-changelog:
|
||||
rm -f $(CHANGELOG) VERSION
|
||||
|
||||
clean-test-image-cache: clean-test-image-tar-cache clean-test-image-docker-cache ## Clean test image cache
|
||||
|
||||
.PHONY: clear-test-image-tar-cache
|
||||
clean-test-image-tar-cache: ## Delete all test cache (built docker image tars)
|
||||
find . -type f -wholename "**/test-fixtures/cache/stereoscope-fixture-*.tar" -delete
|
||||
|
||||
.PHONY: clear-test-image-docker-cache
|
||||
clean-test-image-docker-cache: ## Purge all test docker images
|
||||
docker images --format '{{.ID}} {{.Repository}}' | grep stereoscope-fixture- | awk '{print $$1}' | uniq | xargs -r docker rmi --force
|
||||
|
||||
## Halp! #################################
|
||||
|
||||
.PHONY: help
|
||||
help: ## Display this help
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "$(BOLD)$(CYAN)%-25s$(RESET)%s\n", $$1, $$2}'
|
||||
|
||||
@@ -38,17 +38,22 @@ For commercial support options with Syft or Grype, please [contact Anchore](http
|
||||
- Debian (dpkg)
|
||||
- Dotnet (deps.json)
|
||||
- Objective-C (cocoapods)
|
||||
- Elixir (mix)
|
||||
- Erlang (rebar3)
|
||||
- Go (go.mod, Go binaries)
|
||||
- Haskell (cabal, stack)
|
||||
- Java (jar, ear, war, par, sar)
|
||||
- Java (jar, ear, war, par, sar, nar, native-image)
|
||||
- JavaScript (npm, yarn)
|
||||
- Jenkins Plugins (jpi, hpi)
|
||||
- Linux kernel archives (vmlinz)
|
||||
- Linux kernel modules (ko)
|
||||
- Nix (outputs in /nix/store)
|
||||
- PHP (composer)
|
||||
- Python (wheel, egg, poetry, requirements.txt)
|
||||
- Red Hat (rpm)
|
||||
- Ruby (gem)
|
||||
- Rust (cargo.lock)
|
||||
- Swift (cocoapods)
|
||||
- Swift (cocoapods, swift-package-manager)
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -73,15 +78,20 @@ The chocolatey distribution of syft is community maintained and not distributed
|
||||
choco install syft -y
|
||||
```
|
||||
|
||||
### Scoop
|
||||
|
||||
```powershell
|
||||
scoop install syft
|
||||
```
|
||||
|
||||
### Homebrew
|
||||
```bash
|
||||
brew tap anchore/syft
|
||||
brew install syft
|
||||
```
|
||||
|
||||
### Nix
|
||||
|
||||
**Note**: Nix packaging of Syft is [community maintained](https://github.com/NixOS/nixpkgs/blob/master/pkgs/tools/admin/syft/default.nix). Syft is currently only in the [unstable channel](https://nixos.wiki/wiki/Nix_channels#The_official_channels) awaiting the `22.05` release
|
||||
**Note**: Nix packaging of Syft is [community maintained](https://github.com/NixOS/nixpkgs/blob/master/pkgs/tools/admin/syft/default.nix). Syft is available in the [stable channel](https://nixos.wiki/wiki/Nix_channels#The_official_channels) since NixOS `22.05`.
|
||||
|
||||
```bash
|
||||
nix-env -i syft
|
||||
@@ -109,9 +119,7 @@ The above output includes only software that is visible in the container (i.e.,
|
||||
syft <image> --scope all-layers
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Supported sources
|
||||
### Supported sources
|
||||
|
||||
Syft can generate a SBOM from a variety of sources:
|
||||
|
||||
@@ -140,45 +148,72 @@ file:path/to/yourproject/file read directly from a path on disk (any
|
||||
registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required)
|
||||
```
|
||||
|
||||
#### Default Cataloger Configuration by scan type
|
||||
If an image source is not provided and cannot be detected from the given reference it is assumed the image should be pulled from the Docker daemon.
|
||||
If docker is not present, then the Podman daemon is attempted next, followed by reaching out directly to the image registry last.
|
||||
|
||||
|
||||
This default behavior can be overridden with the `default-image-pull-source` configuration option (See [Configuration](https://github.com/anchore/syft#configuration) for more details).
|
||||
|
||||
### Default Cataloger Configuration by scan type
|
||||
|
||||
Syft uses different default sets of catalogers depending on what it is scanning: a container image or a directory on disk. The default catalogers for an image scan assumes that package installation steps have already been completed. For example, Syft will identify Python packages that have egg or wheel metadata files under a site-packages directory, since this indicates software actually installed on an image.
|
||||
|
||||
However, if you are scanning a directory, Syft doesn't assume that all relevant software is installed, and will use catalogers that can identify declared dependencies that may not yet be installed on the final system: for example, dependencies listed in a Python requirements.txt.
|
||||
|
||||
You can override the list of enabled/disabled catalogers by using the "catalogers" keyword in the [Syft configuration file](https://github.com/anchore/syft#configuration).
|
||||
|
||||
##### Image Scanning:
|
||||
- alpmdb
|
||||
- rpmdb
|
||||
- dpkgdb
|
||||
- apkdb
|
||||
- portage
|
||||
- ruby-gemspec
|
||||
- python-package
|
||||
- php-composer-installed Cataloger
|
||||
- javascript-package
|
||||
- java
|
||||
- go-module-binary
|
||||
- binary
|
||||
- dotnet-deps
|
||||
- dpkgdb
|
||||
- go-module-binary
|
||||
- graalvm-native-image
|
||||
- java
|
||||
- javascript-package
|
||||
- linux-kernel
|
||||
- nix-store
|
||||
- php-composer-installed
|
||||
- portage
|
||||
- python-package
|
||||
- rpm-db
|
||||
- ruby-gemspec
|
||||
- sbom
|
||||
|
||||
##### Directory Scanning:
|
||||
- alpmdb
|
||||
- apkdb
|
||||
- dpkgdb
|
||||
- portage
|
||||
- rpmdb
|
||||
- ruby-gemfile
|
||||
- python-index
|
||||
- python-package
|
||||
- php-composer-lock
|
||||
- javascript-lock
|
||||
- java
|
||||
- java-pom
|
||||
- go-module-binary
|
||||
- go-mod-file
|
||||
- rust-cargo-lock
|
||||
- dartlang-lock
|
||||
- dotnet-deps
|
||||
- binary
|
||||
- cocoapods
|
||||
- conan
|
||||
- hackage
|
||||
- dartlang-lock
|
||||
- dotnet-deps
|
||||
- dpkgdb
|
||||
- elixir-mix-lock
|
||||
- erlang-rebar-lock
|
||||
- go-mod-file
|
||||
- go-module-binary
|
||||
- graalvm-native-image
|
||||
- haskell
|
||||
- java
|
||||
- java-gradle-lockfile
|
||||
- java-pom
|
||||
- javascript-lock
|
||||
- linux-kernel
|
||||
- nix-store
|
||||
- php-composer-lock
|
||||
- portage
|
||||
- python-index
|
||||
- python-package
|
||||
- rpm-db
|
||||
- rpm-file
|
||||
- ruby-gemfile
|
||||
- rust-cargo-lock
|
||||
- sbom
|
||||
- swift-package-manager
|
||||
|
||||
#### Non Default:
|
||||
##### Non Default:
|
||||
- cargo-auditable-binary
|
||||
|
||||
### Excluding file paths
|
||||
@@ -212,8 +247,10 @@ Where the `formats` available are:
|
||||
- `text`: A row-oriented, human-and-machine-friendly output.
|
||||
- `cyclonedx-xml`: A XML report conforming to the [CycloneDX 1.4 specification](https://cyclonedx.org/specification/overview/).
|
||||
- `cyclonedx-json`: A JSON report conforming to the [CycloneDX 1.4 specification](https://cyclonedx.org/specification/overview/).
|
||||
- `spdx-tag-value`: A tag-value formatted report conforming to the [SPDX 2.2 specification](https://spdx.github.io/spdx-spec/).
|
||||
- `spdx-json`: A JSON report conforming to the [SPDX 2.2 JSON Schema](https://github.com/spdx/spdx-spec/blob/v2.2/schemas/spdx-schema.json).
|
||||
- `spdx-tag-value`: A tag-value formatted report conforming to the [SPDX 2.3 specification](https://spdx.github.io/spdx-spec/v2.3/).
|
||||
- `spdx-tag-value@2.2`: A tag-value formatted report conforming to the [SPDX 2.2 specification](https://spdx.github.io/spdx-spec/v2.2.2/).
|
||||
- `spdx-json`: A JSON report conforming to the [SPDX 2.3 JSON Schema](https://github.com/spdx/spdx-spec/blob/v2.3/schemas/spdx-schema.json).
|
||||
- `spdx-json@2.2`: A JSON report conforming to the [SPDX 2.2 JSON Schema](https://github.com/spdx/spdx-spec/blob/v2.2/schemas/spdx-schema.json).
|
||||
- `github`: A JSON report conforming to GitHub's dependency snapshot format.
|
||||
- `table`: A columnar summary (default).
|
||||
- `template`: Lets the user specify the output format. See ["Using templates"](#using-templates) below.
|
||||
@@ -251,6 +288,8 @@ Which would produce output like:
|
||||
|
||||
Syft also includes a vast array of utility templating functions from [sprig](http://masterminds.github.io/sprig/) apart from the default Golang [text/template](https://pkg.go.dev/text/template#hdr-Functions) to allow users to customize the output format.
|
||||
|
||||
Lastly, Syft has custom templating functions defined in `./syft/format/template/encoder.go` to help parse the passed-in JSON structs.
|
||||
|
||||
## Multiple outputs
|
||||
|
||||
Syft can also output _multiple_ files in differing formats by appending
|
||||
@@ -366,6 +405,8 @@ syft convert sbom.syft.json -o cyclonedx-json=sbom.cdx.json # convert it to Cyc
|
||||
### Keyless support
|
||||
Syft supports generating attestations using cosign's [keyless](https://github.com/sigstore/cosign/blob/main/KEYLESS.md) signatures.
|
||||
|
||||
Note: users need to have >= v1.12.0 of cosign installed for this command to function
|
||||
|
||||
To use this feature with a format like CycloneDX json simply run:
|
||||
```
|
||||
syft attest --output cyclonedx-json <IMAGE WITH OCI WRITE ACCESS>
|
||||
@@ -388,7 +429,7 @@ Certificate subject: test.email@testdomain.com
|
||||
Certificate issuer URL: https://accounts.google.com
|
||||
```
|
||||
|
||||
#### Local private key support
|
||||
### Local private key support
|
||||
|
||||
To generate an SBOM attestation for a container image using a local private key:
|
||||
```
|
||||
@@ -431,6 +472,10 @@ file: ""
|
||||
# same as SYFT_CHECK_FOR_APP_UPDATE env var
|
||||
check-for-app-update: true
|
||||
|
||||
# allows users to specify which image source should be used to generate the sbom
|
||||
# valid values are: registry, docker, podman
|
||||
default-image-pull-source: ""
|
||||
|
||||
# a list of globs to exclude from scanning. same as --exclude ; for example:
|
||||
# exclude:
|
||||
# - "/etc/**"
|
||||
@@ -444,26 +489,40 @@ platform: ""
|
||||
# set the list of package catalogers to use when generating the SBOM
|
||||
# default = empty (cataloger set determined automatically by the source type [image or file/directory])
|
||||
# catalogers:
|
||||
# - ruby-gemfile
|
||||
# - ruby-gemspec
|
||||
# - python-index
|
||||
# - python-package
|
||||
# - javascript-lock
|
||||
# - javascript-package
|
||||
# - php-composer-installed
|
||||
# - php-composer-lock
|
||||
# - alpmdb
|
||||
# - dpkgdb
|
||||
# - rpmdb
|
||||
# - java
|
||||
# - apkdb
|
||||
# - go-module-binary
|
||||
# - go-mod-file
|
||||
# - dartlang-lock
|
||||
# - rust
|
||||
# - dotnet-deps
|
||||
# rust-audit-binary scans Rust binaries built with https://github.com/Shnatsel/rust-audit
|
||||
# - rust-audit-binary
|
||||
# - alpmdb-cataloger
|
||||
# - apkdb-cataloger
|
||||
# - binary-cataloger
|
||||
# - cargo-auditable-binary-cataloger
|
||||
# - cocoapods-cataloger
|
||||
# - conan-cataloger
|
||||
# - dartlang-lock-cataloger
|
||||
# - dotnet-deps-cataloger
|
||||
# - dpkgdb-cataloger
|
||||
# - elixir-mix-lock-cataloger
|
||||
# - erlang-rebar-lock-cataloger
|
||||
# - go-mod-file-cataloger
|
||||
# - go-module-binary-cataloger
|
||||
# - graalvm-native-image-cataloger
|
||||
# - haskell-cataloger
|
||||
# - java-cataloger
|
||||
# - java-gradle-lockfile-cataloger
|
||||
# - java-pom-cataloger
|
||||
# - javascript-lock-cataloger
|
||||
# - javascript-package-cataloger
|
||||
# - linux-kernel-cataloger
|
||||
# - nix-store-cataloger
|
||||
# - php-composer-installed-cataloger
|
||||
# - php-composer-lock-cataloger
|
||||
# - portage-cataloger
|
||||
# - python-index-cataloger
|
||||
# - python-package-cataloger
|
||||
# - rpm-db-cataloger
|
||||
# - rpm-file-cataloger
|
||||
# - ruby-gemfile-cataloger
|
||||
# - ruby-gemspec-cataloger
|
||||
# - rust-cargo-lock-cataloger
|
||||
# - sbom-cataloger
|
||||
# - spm-cataloger
|
||||
catalogers:
|
||||
|
||||
# cataloging packages is exposed through the packages and power-user subcommands
|
||||
@@ -489,16 +548,41 @@ package:
|
||||
# same as -s ; SYFT_PACKAGE_CATALOGER_SCOPE env var
|
||||
scope: "squashed"
|
||||
|
||||
# cataloging file classifications is exposed through the power-user subcommand
|
||||
file-classification:
|
||||
cataloger:
|
||||
# enable/disable cataloging of file classifications
|
||||
# SYFT_FILE_CLASSIFICATION_CATALOGER_ENABLED env var
|
||||
enabled: true
|
||||
golang:
|
||||
# search for go package licences in the GOPATH of the system running Syft, note that this is outside the
|
||||
# container filesystem and potentially outside the root of a local directory scan
|
||||
# SYFT_GOLANG_SEARCH_LOCAL_MOD_CACHE_LICENSES env var
|
||||
search-local-mod-cache-licenses: false
|
||||
|
||||
# specify an explicit go mod cache directory, if unset this defaults to $GOPATH/pkg/mod or $HOME/go/pkg/mod
|
||||
# SYFT_GOLANG_LOCAL_MOD_CACHE_DIR env var
|
||||
local-mod-cache-dir: ""
|
||||
|
||||
# the search space to look for file classifications (options: all-layers, squashed)
|
||||
# SYFT_FILE_CLASSIFICATION_CATALOGER_SCOPE env var
|
||||
scope: "squashed"
|
||||
# search for go package licences by retrieving the package from a network proxy
|
||||
# SYFT_GOLANG_SEARCH_REMOTE_LICENSES env var
|
||||
search-remote-licenses: false
|
||||
|
||||
# remote proxy to use when retrieving go packages from the network,
|
||||
# if unset this defaults to $GOPROXY followed by https://proxy.golang.org
|
||||
# SYFT_GOLANG_PROXY env var
|
||||
proxy: ""
|
||||
|
||||
# specifies packages which should not be fetched by proxy
|
||||
# if unset this defaults to $GONOPROXY
|
||||
# SYFT_GOLANG_NOPROXY env var
|
||||
no-proxy: ""
|
||||
|
||||
linux-kernel:
|
||||
# whether to catalog linux kernel modules found within lib/modules/** directories
|
||||
# SYFT_LINUX_KERNEL_CATALOG_MODULES env var
|
||||
catalog-modules: true
|
||||
|
||||
python:
|
||||
# when running across entries in requirements.txt that do not specify a specific version
|
||||
# (e.g. "sqlalchemy >= 1.0.0, <= 2.0.0, != 3.0.0, <= 3.0.0"), attempt to guess what the version could
|
||||
# be based on the version requirements specified (e.g. "1.0.0"). When enabled the lowest expressible version
|
||||
# when given an arbitrary constraint will be used (even if that version may not be available/published).
|
||||
guess-unpinned-requirements: false
|
||||
|
||||
# cataloging file contents is exposed through the power-user subcommand
|
||||
file-contents:
|
||||
@@ -530,10 +614,13 @@ file-metadata:
|
||||
# SYFT_FILE_METADATA_CATALOGER_SCOPE env var
|
||||
scope: "squashed"
|
||||
|
||||
# the file digest algorithms to use when cataloging files (options: "sha256", "md5", "sha1")
|
||||
# the file digest algorithms to use when cataloging files (options: "md5", "sha1", "sha224", "sha256", "sha384", "sha512")
|
||||
# SYFT_FILE_METADATA_DIGESTS env var
|
||||
digests: ["sha256"]
|
||||
|
||||
# maximum number of workers used to process the list of package catalogers in parallel
|
||||
parallelism: 1
|
||||
|
||||
# cataloging secrets is exposed through the power-user subcommand
|
||||
secrets:
|
||||
cataloger:
|
||||
@@ -565,11 +652,27 @@ secrets:
|
||||
# SYFT_SECRETS_EXCLUDE_PATTERN_NAMES env var
|
||||
exclude-pattern-names: []
|
||||
|
||||
# options that apply to all scan sources
|
||||
source:
|
||||
# alias name for the source
|
||||
# SYFT_SOURCE_NAME env var; --source-name flag
|
||||
name: ""
|
||||
|
||||
# alias version for the source
|
||||
# SYFT_SOURCE_VERSION env var; --source-version flag
|
||||
version: ""
|
||||
|
||||
# options affecting the file source type
|
||||
file:
|
||||
# the file digest algorithms to use on the scanned file (options: "md5", "sha1", "sha224", "sha256", "sha384", "sha512")
|
||||
digests: ["sha256"]
|
||||
|
||||
# options when pulling directly from a registry via the "registry:" scheme
|
||||
registry:
|
||||
# skip TLS verification when communicating with the registry
|
||||
# SYFT_REGISTRY_INSECURE_SKIP_TLS_VERIFY env var
|
||||
insecure-skip-tls-verify: false
|
||||
|
||||
# use http instead of https when connecting to the registry
|
||||
# SYFT_REGISTRY_INSECURE_USE_HTTP env var
|
||||
insecure-use-http: false
|
||||
|
||||
+51
-43
@@ -1,14 +1,49 @@
|
||||
# Release Process
|
||||
# Release
|
||||
|
||||
## Creating a release
|
||||
|
||||
This release process itself should be as automated as possible, and has only a few steps:
|
||||
|
||||
1. **Trigger a new release with `make release`**. At this point you'll see a preview
|
||||
changelog in the terminal. If you're happy with the changelog, press `y` to continue, otherwise
|
||||
you can abort and adjust the labels on the PRs and issues to be included in the release and
|
||||
re-run the release trigger command.
|
||||
|
||||
1. A release admin must approve the release on the GitHub Actions release pipeline run page.
|
||||
Once approved, the release pipeline will generate all assets and publish a GitHub Release.
|
||||
|
||||
1. If there is a release Milestone, close it.
|
||||
|
||||
Ideally releasing should be done often with small increments when possible. Unless a
|
||||
breaking change is blocking the release, or no fixes/features have been merged, a good
|
||||
target release cadence is between every 1 or 2 weeks.
|
||||
|
||||
|
||||
## Retracting a release
|
||||
|
||||
If a release is found to be problematic, it can be retracted with the following steps:
|
||||
|
||||
- Deleting the GitHub Release
|
||||
- Untag the docker images in the `ghcr.io` and `docker.io` registries
|
||||
- Revert the brew formula in [`anchore/homebrew-syft`](https://github.com/anchore/homebrew-syft) to point to the previous release
|
||||
- Add a new `retract` entry in the go.mod for the versioned release
|
||||
|
||||
**Note**: do not delete release tags from the git repository since there may already be references to the release
|
||||
in the go proxy, which will cause confusion when trying to reuse the tag later (the H1 hash will not match and there
|
||||
will be a warning when users try to pull the new release).
|
||||
|
||||
|
||||
## Background
|
||||
|
||||
A good release process has the following qualities:
|
||||
|
||||
1. There is a way to plan what should be in a release
|
||||
1. There is a way to see what is actually in a release
|
||||
1. Allow for different kinds of releases (major breaking vs backwards compatible
|
||||
enhancements vs patch updates)
|
||||
1. Allow for different kinds of releases (major breaking vs backwards compatible enhancements vs patch updates)
|
||||
1. Specify a repeatable way to build and publish software artifacts
|
||||
|
||||
## Planning a release
|
||||
|
||||
### Planning a release
|
||||
|
||||
To indicate a set of features to be released together add each issue to an in-repository
|
||||
Milestone named with major-minor version to be released (e.g. `v0.1`). It is OK for other
|
||||
@@ -21,15 +56,13 @@ completed, would allow the release to be considered complete. A Milestone is onl
|
||||
|
||||
Not all releases need to be planned. For instance, patch releases for fixes should be
|
||||
released when they are ready and when releasing would not interfere with another current
|
||||
release (where some partial or breaking features have already been merged). Beta releases
|
||||
and release candidates should not be independently planned from the non-beta release. That
|
||||
is, the features for a `v0.1-beta.1` release should be planned under the `v0.1` Milestone,
|
||||
not under a separate `v0.1-beta.1` Milestone.
|
||||
release (where some partial or breaking features have already been merged).
|
||||
|
||||
Unless necessary, feature releases should be small and frequent, which may obviate the
|
||||
need for regular release planning under a Milestone.
|
||||
|
||||
## What is in a release
|
||||
|
||||
### What is in a release
|
||||
|
||||
Milestones are specifically for planning a release, not necessarily tracking all changes
|
||||
that a release may bring (and more importantly, not all releases are necessarily planned
|
||||
@@ -42,9 +75,9 @@ Changelog line. Furthermore, there should be a place to see all released version
|
||||
release date for each release, the semantic version of the release, and the set of changes
|
||||
for each release.
|
||||
|
||||
This project auto-generates the Changelog contents for each current release and posts the
|
||||
generated contents to the GitHub Release page. Leveraging the GitHub Releases feature
|
||||
allows GitHub to manage the Changelog on each release outside of the git repository while
|
||||
**This project auto-generates the Changelog contents for each current release and posts the
|
||||
generated contents to the GitHub Release page**. Leveraging the GitHub Releases feature
|
||||
allows GitHub to manage the Changelog on each release outside of the git source tree while
|
||||
still being hosted with the released assets.
|
||||
|
||||
The Changelog is generated from the metadata from in-repository issues and PRs, using
|
||||
@@ -60,45 +93,20 @@ The above suggestions imply that we should:
|
||||
- The appropriate label is applied to PRs and/or issues to drive specific change type
|
||||
sections (deprecated, breaking, security, bug, etc)
|
||||
|
||||
With this approach as we cultivate good organization of PRs and issues we automatically
|
||||
get an equally good Changelog.
|
||||
**With this approach as we cultivate good organization of PRs and issues we automatically
|
||||
get an equally good Changelog.**
|
||||
|
||||
## Major, minor, and patch releases
|
||||
|
||||
### Major, minor, and patch releases
|
||||
|
||||
The latest version of the tool is the only supported version, which implies that multiple
|
||||
parallel release branches will not be a regular process (if ever). Multiple releases can
|
||||
be planned in parallel, however, only one can be actively developed at a time. That is, if
|
||||
PRs attached to a release Milestone have been merged into the main branch, that release is
|
||||
now the "next" release. This implies that the source of truth for release lies with the
|
||||
git log and Changelog, not with the release Milestones (which are purely for planning and
|
||||
now the "next" release. **This implies that the source of truth for release lies with the
|
||||
git log and Changelog, not with the release Milestones** (which are purely for planning and
|
||||
tracking).
|
||||
|
||||
Semantic versioning should be used to indicate breaking changes, new features, and fixes.
|
||||
The exception to this is `< 1.0`, where the major version is not bumped for breaking changes,
|
||||
instead the minor version indicates both new features and breaking changes.
|
||||
|
||||
## Cutting a release
|
||||
|
||||
Ideally releasing should be done often with small increments when possible. Unless a
|
||||
breaking change is blocking the release, or no fixes/features have been merged, a good
|
||||
target release cadence is between every 2 or 4 weeks.
|
||||
|
||||
This release process itself should be as automated as possible, and have only a few steps:
|
||||
|
||||
1. Tag the main branch with a full semantic-version, prefixed with a `v`. If there is a
|
||||
milestone with a partial version, the full version should be used for the git tag (e.g.
|
||||
with a Milestone of `v0.1` the tag should be `v0.1.0`). You can determine the changes going
|
||||
into a release by running `make changelog-unreleased`. Use this change list to determine the
|
||||
release increment. After determining the release increment (major, minor, patch), create the tag.
|
||||
Given the above example the command to create the tag would be `git tag v0.1.0`.
|
||||
|
||||
1. Push the tag. Given the above example the command to push the tag would be `git push origin v0.1.0`.
|
||||
|
||||
1. A release admin must approve the release on the GitHub Actions release pipeline run page.
|
||||
Once approved, the release pipeline will generate all assets and draft a GitHub Release.
|
||||
|
||||
1. Navigate to the GitHub Release draft page to review the final changelog and publish the
|
||||
release. Once published, a release-follow-up pipeline will publish derivative artifacts
|
||||
(docker image to DockerHub, brew formula to the external homebrew git repo, etc).
|
||||
|
||||
1. If there is a release Milestone, close it.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# See documentation here: https://github.com/artifacthub/hub/blob/v1.6.0/docs/metadata/artifacthub-repo.yml
|
||||
|
||||
repositoryID: eced152f-b15d-4879-8b3b-1175397192ba
|
||||
owners:
|
||||
- name: wagoodman
|
||||
email: wagoodman@gmail.com
|
||||
|
||||
+18
-32
@@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
sigopts "github.com/sigstore/cosign/cmd/cosign/cli/options"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
@@ -15,33 +14,26 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
attestExample = ` {{.appName}} {{.command}} --output [FORMAT] --key [KEY] alpine:latest
|
||||
Supports the following image sources:
|
||||
{{.appName}} {{.command}} --key [KEY] yourrepo/yourimage:tag defaults to using images from a Docker daemon. If Docker is not present, the image is pulled directly from the registry.
|
||||
{{.appName}} {{.command}} --key [KEY] path/to/a/file/or/dir only for OCI tar or OCI directory
|
||||
attestExample = ` {{.appName}} {{.command}} --output [FORMAT] alpine:latest defaults to using images from a Docker daemon. If Docker is not present, the image is pulled directly from the registry
|
||||
`
|
||||
attestSchemeHelp = "\n" + indent + schemeHelpHeader + "\n" + imageSchemeHelp
|
||||
|
||||
attestHelp = attestExample + attestSchemeHelp
|
||||
attestHelp = attestExample + attestSchemeHelp
|
||||
)
|
||||
|
||||
func Attest(v *viper.Viper, app *config.Application, ro *options.RootOptions) *cobra.Command {
|
||||
ao := options.AttestOptions{}
|
||||
func Attest(v *viper.Viper, app *config.Application, ro *options.RootOptions, po *options.PackagesOptions, ao *options.AttestOptions) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "attest --output [FORMAT] --key [KEY] [SOURCE]",
|
||||
Short: "Generate a package SBOM as an attestation for the given [SOURCE] container image",
|
||||
Long: "Generate a packaged-based Software Bill Of Materials (SBOM) from a container image as the predicate of an in-toto attestation",
|
||||
Use: "attest --output [FORMAT] <IMAGE>",
|
||||
Short: "Generate an SBOM as an attestation for the given [SOURCE] container image",
|
||||
Long: "Generate a packaged-based Software Bill Of Materials (SBOM) from a container image as the predicate of an in-toto attestation that will be uploaded to the image registry",
|
||||
Example: internal.Tprintf(attestHelp, map[string]interface{}{
|
||||
"appName": internal.ApplicationName,
|
||||
"command": "attest",
|
||||
}),
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
// run to unmarshal viper object onto app config
|
||||
// the viper object correctly
|
||||
if err := app.LoadAllValues(v, ro.Config); err != nil {
|
||||
return fmt.Errorf("invalid application config: %w", err)
|
||||
return fmt.Errorf("unable to load configuration: %w", err)
|
||||
}
|
||||
// configure logging for command
|
||||
|
||||
newLogWrapper(app)
|
||||
logApplicationConfig(app)
|
||||
return validateArgs(cmd, args)
|
||||
@@ -49,29 +41,23 @@ func Attest(v *viper.Viper, app *config.Application, ro *options.RootOptions) *c
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// this MUST be called first to make sure app config decodes
|
||||
// the viper object correctly
|
||||
if app.CheckForAppUpdate {
|
||||
checkForApplicationUpdate()
|
||||
// TODO: this is broke, the bus isn't available yet
|
||||
}
|
||||
|
||||
// build cosign key options for attestation
|
||||
ko := sigopts.KeyOpts{
|
||||
KeyRef: app.Attest.KeyRef,
|
||||
FulcioURL: app.Attest.FulcioURL,
|
||||
IDToken: app.Attest.FulcioIdentityToken,
|
||||
InsecureSkipFulcioVerify: app.Attest.InsecureSkipFulcioVerify,
|
||||
RekorURL: app.Attest.RekorURL,
|
||||
OIDCIssuer: app.Attest.OIDCIssuer,
|
||||
OIDCClientID: app.Attest.OIDCClientID,
|
||||
OIDCRedirectURL: app.Attest.OIDCRedirectURL,
|
||||
}
|
||||
|
||||
return attest.Run(cmd.Context(), app, ko, args)
|
||||
return attest.Run(cmd.Context(), app, args)
|
||||
},
|
||||
}
|
||||
|
||||
err := ao.AddFlags(cmd, v)
|
||||
// syft attest is an enhancement of the packages command, so it should have the same flags
|
||||
err := po.AddFlags(cmd, v)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// syft attest has its own options not included as part of the packages command
|
||||
err = ao.AddFlags(cmd, v)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
+170
-307
@@ -1,113 +1,58 @@
|
||||
package attest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/in-toto/in-toto-golang/in_toto"
|
||||
sigopts "github.com/sigstore/cosign/cmd/cosign/cli/options"
|
||||
"github.com/sigstore/cosign/cmd/cosign/cli/rekor"
|
||||
"github.com/sigstore/cosign/cmd/cosign/cli/sign"
|
||||
"github.com/sigstore/cosign/pkg/cosign"
|
||||
"github.com/sigstore/cosign/pkg/cosign/attestation"
|
||||
cbundle "github.com/sigstore/cosign/pkg/cosign/bundle"
|
||||
"github.com/sigstore/cosign/pkg/oci/mutate"
|
||||
ociremote "github.com/sigstore/cosign/pkg/oci/remote"
|
||||
"github.com/sigstore/cosign/pkg/oci/static"
|
||||
sigs "github.com/sigstore/cosign/pkg/signature"
|
||||
"github.com/sigstore/cosign/pkg/types"
|
||||
"github.com/sigstore/rekor/pkg/generated/client"
|
||||
"github.com/sigstore/rekor/pkg/generated/models"
|
||||
"github.com/sigstore/sigstore/pkg/signature/dsse"
|
||||
signatureoptions "github.com/sigstore/sigstore/pkg/signature/options"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"github.com/anchore/stereoscope"
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
"github.com/anchore/syft/cmd/syft/cli/eventloop"
|
||||
"github.com/anchore/syft/cmd/syft/cli/options"
|
||||
"github.com/anchore/syft/cmd/syft/cli/packages"
|
||||
"github.com/anchore/syft/cmd/syft/internal/ui"
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/config"
|
||||
"github.com/anchore/syft/internal/file"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/ui"
|
||||
"github.com/anchore/syft/syft"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/syft/formats/cyclonedxjson"
|
||||
"github.com/anchore/syft/syft/formats/spdxjson"
|
||||
"github.com/anchore/syft/syft/event/monitor"
|
||||
"github.com/anchore/syft/syft/formats/syftjson"
|
||||
"github.com/anchore/syft/syft/formats/table"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
var (
|
||||
allowedAttestFormats = []sbom.FormatID{
|
||||
syftjson.ID,
|
||||
spdxjson.ID,
|
||||
cyclonedxjson.ID,
|
||||
func Run(_ context.Context, app *config.Application, args []string) error {
|
||||
err := ValidateOutputOptions(app)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
intotoJSONDsseType = `application/vnd.in-toto+json`
|
||||
)
|
||||
|
||||
func Run(ctx context.Context, app *config.Application, ko sigopts.KeyOpts, args []string) error {
|
||||
// We cannot generate an attestation for more than one output
|
||||
if len(app.Outputs) > 1 {
|
||||
return fmt.Errorf("unable to generate attestation for more than one output")
|
||||
}
|
||||
|
||||
// can only be an image for attestation or OCI DIR
|
||||
// note: must be a container image
|
||||
userInput := args[0]
|
||||
si, err := parseImageSource(userInput, app)
|
||||
|
||||
_, err = exec.LookPath("cosign")
|
||||
if err != nil {
|
||||
return err
|
||||
// when cosign is not installed the error will be rendered like so:
|
||||
// 2023/06/30 08:31:52 error during command execution: 'syft attest' requires cosign to be installed: exec: "cosign": executable file not found in $PATH
|
||||
return fmt.Errorf("'syft attest' requires cosign to be installed: %w", err)
|
||||
}
|
||||
|
||||
output := parseAttestationOutput(app.Outputs)
|
||||
|
||||
format := syft.FormatByName(output)
|
||||
|
||||
// user typo or unknown outputs provided
|
||||
if format == nil {
|
||||
format = syft.FormatByID(syftjson.ID) // default attestation format
|
||||
}
|
||||
predicateType := formatPredicateType(format)
|
||||
if predicateType == "" {
|
||||
return fmt.Errorf(
|
||||
"could not produce attestation predicate for given format: %q. Available formats: %+v",
|
||||
options.FormatAliases(format.ID()),
|
||||
options.FormatAliases(allowedAttestFormats...),
|
||||
)
|
||||
}
|
||||
|
||||
if app.Attest.KeyRef != "" {
|
||||
passFunc, err := selectPassFunc(app.Attest.KeyRef, app.Attest.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ko.PassFunc = passFunc
|
||||
}
|
||||
|
||||
sv, err := sign.SignerFromKeyOpts(ctx, "", "", ko)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer sv.Close()
|
||||
|
||||
eventBus := partybus.NewBus()
|
||||
stereoscope.SetBus(eventBus)
|
||||
syft.SetBus(eventBus)
|
||||
subscription := eventBus.Subscribe()
|
||||
|
||||
return eventloop.EventLoop(
|
||||
execWorker(app, *si, format, predicateType, sv),
|
||||
execWorker(app, userInput),
|
||||
eventloop.SetupSignals(),
|
||||
subscription,
|
||||
stereoscope.Cleanup,
|
||||
@@ -115,284 +60,202 @@ func Run(ctx context.Context, app *config.Application, ko sigopts.KeyOpts, args
|
||||
)
|
||||
}
|
||||
|
||||
func parseAttestationOutput(outputs []string) (format string) {
|
||||
if len(outputs) == 0 {
|
||||
outputs = append(outputs, string(syftjson.ID))
|
||||
func buildSBOM(app *config.Application, userInput string, errs chan error) (*sbom.SBOM, error) {
|
||||
cfg := source.DetectConfig{
|
||||
DefaultImageSource: app.DefaultImagePullSource,
|
||||
}
|
||||
|
||||
return outputs[0]
|
||||
}
|
||||
|
||||
func parseImageSource(userInput string, app *config.Application) (s *source.Input, err error) {
|
||||
si, err := source.ParseInputWithName(userInput, app.Platform, false, app.Name)
|
||||
detection, err := source.Detect(userInput, cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not generate source input for attest command: %w", err)
|
||||
return nil, fmt.Errorf("could not deteremine source: %w", err)
|
||||
}
|
||||
|
||||
switch si.Scheme {
|
||||
case source.ImageScheme, source.UnknownScheme:
|
||||
// at this point we know that it cannot be dir: or file: schemes;
|
||||
// we will assume that the unknown scheme could represent an image;
|
||||
si.Scheme = source.ImageScheme
|
||||
default:
|
||||
return nil, fmt.Errorf("attest command can only be used with image sources but discovered %q when given %q", si.Scheme, userInput)
|
||||
if detection.IsContainerImage() {
|
||||
return nil, fmt.Errorf("attestations are only supported for oci images at this time")
|
||||
}
|
||||
|
||||
// if the original detection was from the local daemon we want to short circuit
|
||||
// that and attempt to generate the image source from its current registry source instead
|
||||
switch si.ImageSource {
|
||||
case image.UnknownSource, image.OciRegistrySource:
|
||||
si.ImageSource = image.OciRegistrySource
|
||||
case image.SingularitySource:
|
||||
default:
|
||||
return nil, fmt.Errorf("attest command can only be used with image sources fetch directly from the registry, but discovered an image source of %q when given %q", si.ImageSource, userInput)
|
||||
var platform *image.Platform
|
||||
|
||||
if app.Platform != "" {
|
||||
platform, err = image.NewPlatform(app.Platform)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid platform: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return si, nil
|
||||
hashers, err := file.Hashers(app.Source.File.Digests...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid hash: %w", err)
|
||||
}
|
||||
|
||||
src, err := detection.NewSource(
|
||||
source.DetectionSourceConfig{
|
||||
Alias: source.Alias{
|
||||
Name: app.Source.Name,
|
||||
Version: app.Source.Version,
|
||||
},
|
||||
RegistryOptions: app.Registry.ToOptions(),
|
||||
Platform: platform,
|
||||
Exclude: source.ExcludeConfig{
|
||||
Paths: app.Exclusions,
|
||||
},
|
||||
DigestAlgorithms: hashers,
|
||||
BasePath: app.BasePath,
|
||||
},
|
||||
)
|
||||
|
||||
if src != nil {
|
||||
defer src.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to construct source from user input %q: %w", userInput, err)
|
||||
}
|
||||
|
||||
s, err := packages.GenerateSBOM(src, errs, app)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if s == nil {
|
||||
return nil, fmt.Errorf("no SBOM produced for %q", userInput)
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func execWorker(app *config.Application, sourceInput source.Input, format sbom.Format, predicateType string, sv *sign.SignerVerifier) <-chan error {
|
||||
//nolint:funlen
|
||||
func execWorker(app *config.Application, userInput string) <-chan error {
|
||||
errs := make(chan error)
|
||||
go func() {
|
||||
defer close(errs)
|
||||
defer bus.Exit()
|
||||
|
||||
src, cleanup, err := source.NewFromRegistry(sourceInput, app.Registry.ToOptions(), app.Exclusions)
|
||||
if cleanup != nil {
|
||||
defer cleanup()
|
||||
}
|
||||
s, err := buildSBOM(app, userInput, errs)
|
||||
if err != nil {
|
||||
errs <- fmt.Errorf("failed to construct source from user input %q: %w", sourceInput.UserInput, err)
|
||||
errs <- fmt.Errorf("unable to build SBOM: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
s, err := packages.GenerateSBOM(src, errs, app)
|
||||
// note: ValidateOutputOptions ensures that there is no more than one output type
|
||||
o := app.Outputs[0]
|
||||
|
||||
f, err := os.CreateTemp("", o)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
errs <- fmt.Errorf("unable to create temp file: %w", err)
|
||||
return
|
||||
}
|
||||
defer os.Remove(f.Name())
|
||||
|
||||
writer, err := options.MakeSBOMWriter(app.Outputs, f.Name(), app.OutputTemplatePath)
|
||||
if err != nil {
|
||||
errs <- fmt.Errorf("unable to create SBOM writer: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
sbomBytes, err := syft.Encode(*s, format)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
if err := writer.Write(*s); err != nil {
|
||||
errs <- fmt.Errorf("unable to write SBOM to temp file: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
signedPayload, err := generateAttestation(sourceInput, sbomBytes, src, sv, predicateType)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
// TODO: what other validation here besides binary name?
|
||||
cmd := "cosign"
|
||||
if !commandExists(cmd) {
|
||||
errs <- fmt.Errorf("unable to find cosign in PATH; make sure you have it installed")
|
||||
return
|
||||
}
|
||||
|
||||
err = publishAttestation(app, signedPayload, src, sv)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return
|
||||
// Select Cosign predicate type based on defined output type
|
||||
// As orientation, check: https://github.com/sigstore/cosign/blob/main/pkg/cosign/attestation/attestation.go
|
||||
var predicateType string
|
||||
switch strings.ToLower(o) {
|
||||
case "cyclonedx-json":
|
||||
predicateType = "cyclonedx"
|
||||
case "spdx-tag-value", "spdx-tv":
|
||||
predicateType = "spdx"
|
||||
case "spdx-json", "json":
|
||||
predicateType = "spdxjson"
|
||||
default:
|
||||
predicateType = "custom"
|
||||
}
|
||||
|
||||
bus.Publish(partybus.Event{
|
||||
Type: event.Exit,
|
||||
Value: func() error {
|
||||
return nil
|
||||
args := []string{"attest", userInput, "--predicate", f.Name(), "--type", predicateType}
|
||||
if app.Attest.Key != "" {
|
||||
args = append(args, "--key", app.Attest.Key)
|
||||
}
|
||||
|
||||
execCmd := exec.Command(cmd, args...)
|
||||
execCmd.Env = os.Environ()
|
||||
if app.Attest.Key != "" {
|
||||
execCmd.Env = append(execCmd.Env, fmt.Sprintf("COSIGN_PASSWORD=%s", app.Attest.Password))
|
||||
} else {
|
||||
// no key provided, use cosign's keyless mode
|
||||
execCmd.Env = append(execCmd.Env, "COSIGN_EXPERIMENTAL=1")
|
||||
}
|
||||
|
||||
log.WithFields("cmd", strings.Join(execCmd.Args, " ")).Trace("creating attestation")
|
||||
|
||||
// bus adapter for ui to hook into stdout via an os pipe
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
errs <- fmt.Errorf("unable to create os pipe: %w", err)
|
||||
return
|
||||
}
|
||||
defer w.Close()
|
||||
|
||||
mon := progress.NewManual(-1)
|
||||
|
||||
bus.Publish(
|
||||
partybus.Event{
|
||||
Type: event.AttestationStarted,
|
||||
Source: monitor.GenericTask{
|
||||
Title: monitor.Title{
|
||||
Default: "Create attestation",
|
||||
WhileRunning: "Creating attestation",
|
||||
OnSuccess: "Created attestation",
|
||||
},
|
||||
Context: "cosign",
|
||||
},
|
||||
Value: &monitor.ShellProgress{
|
||||
Reader: r,
|
||||
Progressable: mon,
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
execCmd.Stdout = w
|
||||
execCmd.Stderr = w
|
||||
|
||||
// attest the SBOM
|
||||
err = execCmd.Run()
|
||||
if err != nil {
|
||||
mon.SetError(err)
|
||||
errs <- fmt.Errorf("unable to attest SBOM: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
mon.SetCompleted()
|
||||
}()
|
||||
return errs
|
||||
}
|
||||
|
||||
func generateAttestation(si source.Input, predicate []byte, src *source.Source, sv *sign.SignerVerifier, predicateType string) ([]byte, error) {
|
||||
var h v1.Hash
|
||||
|
||||
switch si.ImageSource {
|
||||
case image.OciRegistrySource:
|
||||
switch len(src.Image.Metadata.RepoDigests) {
|
||||
case 0:
|
||||
return nil, fmt.Errorf("cannot generate attestation since no repo digests were found; make sure you're passing an OCI registry source for the attest command")
|
||||
case 1:
|
||||
d, err := name.NewDigest(src.Image.Metadata.RepoDigests[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
h, err = v1.NewHash(d.Identifier())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("cannot generate attestation since multiple repo digests were found for the image: %+v", src.Image.Metadata.RepoDigests)
|
||||
}
|
||||
|
||||
case image.SingularitySource:
|
||||
var err error
|
||||
h, err = v1.NewHash(src.Image.Metadata.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
sh, err := attestation.GenerateStatement(attestation.GenerateOpts{
|
||||
Predicate: bytes.NewBuffer(predicate),
|
||||
Type: predicateType,
|
||||
Digest: h.Hex,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(sh)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
wrapped := dsse.WrapSigner(sv, intotoJSONDsseType)
|
||||
return wrapped.SignMessage(bytes.NewReader(payload), signatureoptions.WithContext(context.Background()))
|
||||
}
|
||||
|
||||
// publishAttestation publishes signedPayload to the location specified by the user.
|
||||
func publishAttestation(app *config.Application, signedPayload []byte, src *source.Source, sv *sign.SignerVerifier) error {
|
||||
switch {
|
||||
// We want to give the option to not upload the generated attestation
|
||||
// if passed or if the user is using local PKI
|
||||
case app.Attest.NoUpload || app.Attest.KeyRef != "":
|
||||
if app.File != "" {
|
||||
return os.WriteFile(app.File, signedPayload, 0600)
|
||||
}
|
||||
|
||||
_, err := os.Stdout.Write(signedPayload)
|
||||
return err
|
||||
|
||||
default:
|
||||
ref, err := name.ParseReference(src.Metadata.ImageMetadata.UserInput)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
digest, err := ociremote.ResolveDigest(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return uploadAttestation(app, signedPayload, digest, sv)
|
||||
}
|
||||
}
|
||||
|
||||
func trackUploadAttestation() (*progress.Stage, *progress.Manual) {
|
||||
stage := &progress.Stage{}
|
||||
prog := &progress.Manual{}
|
||||
|
||||
bus.Publish(partybus.Event{
|
||||
Type: event.UploadAttestation,
|
||||
Value: progress.StagedProgressable(&struct {
|
||||
progress.Stager
|
||||
progress.Progressable
|
||||
}{
|
||||
Stager: stage,
|
||||
Progressable: prog,
|
||||
}),
|
||||
})
|
||||
|
||||
return stage, prog
|
||||
}
|
||||
|
||||
// uploads signed SBOM payload to Rekor transparency log along with key information;
|
||||
// returns a bundle for attestation annotations
|
||||
// rekor bundle includes a signed payload and rekor timestamp;
|
||||
// the bundle is then wrapped onto an OCI signed entity and uploaded to
|
||||
// the user's image's OCI registry repository as *.att
|
||||
func uploadAttestation(app *config.Application, signedPayload []byte, digest name.Digest, sv *sign.SignerVerifier) error {
|
||||
// add application/vnd.dsse.envelope.v1+json as media type for other applications to decode attestation
|
||||
opts := []static.Option{static.WithLayerMediaType(types.DssePayloadType)}
|
||||
if sv.Cert != nil {
|
||||
opts = append(opts, static.WithCertChain(sv.Cert, sv.Chain))
|
||||
}
|
||||
|
||||
stage, prog := trackUploadAttestation()
|
||||
defer prog.SetCompleted() // just in case we return early
|
||||
|
||||
prog.Total = 2
|
||||
stage.Current = "uploading signing information to transparency log"
|
||||
|
||||
// uploads payload to Rekor transparency log along with key information;
|
||||
// returns bundle for attesation annotations
|
||||
// rekor bundle includes a signed payload and rekor timestamp;
|
||||
// the bundle is then wrapped onto an OCI signed entity and uploaded to
|
||||
// the user's image's OCI registry repository as *.att
|
||||
bundle, err := uploadToTlog(context.TODO(), sv, app.Attest.RekorURL, func(r *client.Rekor, b []byte) (*models.LogEntryAnon, error) {
|
||||
return cosign.TLogUploadInTotoAttestation(context.TODO(), r, signedPayload, b)
|
||||
})
|
||||
func ValidateOutputOptions(app *config.Application) error {
|
||||
err := packages.ValidateOutputOptions(app)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
prog.N = 1
|
||||
stage.Current = "uploading attestation to OCI registry"
|
||||
|
||||
// add bundle OCI attestation that is uploaded to
|
||||
opts = append(opts, static.WithBundle(bundle))
|
||||
sig, err := static.NewAttestation(signedPayload, opts...)
|
||||
if err != nil {
|
||||
return err
|
||||
if len(app.Outputs) > 1 {
|
||||
return fmt.Errorf("multiple SBOM format is not supported for attest at this time")
|
||||
}
|
||||
|
||||
se, err := ociremote.SignedEntity(digest)
|
||||
if err != nil {
|
||||
return err
|
||||
// cannot use table as default output format when using template output
|
||||
if slices.Contains(app.Outputs, table.ID.String()) {
|
||||
app.Outputs = []string{syftjson.ID.String()}
|
||||
}
|
||||
|
||||
newSE, err := mutate.AttachAttestationToEntity(se, sig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Publish the attestations associated with this entity
|
||||
err = ociremote.WriteAttestations(digest.Repository, newSE)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
prog.SetCompleted()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatPredicateType(format sbom.Format) string {
|
||||
switch format.ID() {
|
||||
case spdxjson.ID:
|
||||
return in_toto.PredicateSPDX
|
||||
case cyclonedxjson.ID:
|
||||
return in_toto.PredicateCycloneDX
|
||||
case syftjson.ID:
|
||||
return "https://syft.dev/bom"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
type tlogUploadFn func(*client.Rekor, []byte) (*models.LogEntryAnon, error)
|
||||
|
||||
func uploadToTlog(ctx context.Context, sv *sign.SignerVerifier, rekorURL string, upload tlogUploadFn) (*cbundle.RekorBundle, error) {
|
||||
var rekorBytes []byte
|
||||
// Upload the cert or the public key, depending on what we have
|
||||
if sv.Cert != nil {
|
||||
rekorBytes = sv.Cert
|
||||
} else {
|
||||
pemBytes, err := sigs.PublicKeyPem(sv, signatureoptions.WithContext(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rekorBytes = pemBytes
|
||||
}
|
||||
|
||||
rekorClient, err := rekor.NewClient(rekorURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entry, err := upload(rekorClient, rekorBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if entry.LogIndex != nil {
|
||||
log.Debugf("transparency log entry created with index: %v", *entry.LogIndex)
|
||||
}
|
||||
return cbundle.EntryToBundle(entry), nil
|
||||
func commandExists(cmd string) bool {
|
||||
_, err := exec.LookPath(cmd)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
package attest
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/sigstore/cosign/pkg/cosign"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
)
|
||||
|
||||
func selectPassFunc(keypath, password string) (cosign.PassFunc, error) {
|
||||
keyContents, err := os.ReadFile(keypath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var fn cosign.PassFunc = func(bool) (b []byte, err error) { return nil, nil }
|
||||
|
||||
_, err = cosign.LoadPrivateKey(keyContents, nil)
|
||||
if err != nil {
|
||||
fn = func(bool) (b []byte, err error) {
|
||||
return fetchPassword(password)
|
||||
}
|
||||
}
|
||||
|
||||
return fn, nil
|
||||
}
|
||||
|
||||
func fetchPassword(password string) (b []byte, err error) {
|
||||
potentiallyPipedInput, err := internal.IsPipedInput()
|
||||
if err != nil {
|
||||
log.Warnf("unable to determine if there is piped input: %+v", err)
|
||||
}
|
||||
|
||||
switch {
|
||||
case password != "":
|
||||
return []byte(password), nil
|
||||
case potentiallyPipedInput:
|
||||
// handle piped in passwords
|
||||
pwBytes, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get password from stdin: %w", err)
|
||||
}
|
||||
// be resilient to input that may have newline characters (in case someone is using echo without -n)
|
||||
cleanPw := strings.TrimRight(string(pwBytes), "\n")
|
||||
return []byte(cleanPw), nil
|
||||
case internal.IsTerminal():
|
||||
return cosign.GetPassFromTerm(false)
|
||||
}
|
||||
|
||||
return nil, errors.New("no method available to fetch password")
|
||||
}
|
||||
+11
-13
@@ -26,8 +26,10 @@ import (
|
||||
const indent = " "
|
||||
|
||||
// New constructs the `syft packages` command, aliases the root command to `syft packages`,
|
||||
// and constructs the `syft power-user` and `syft attest` commands. It is also responsible for
|
||||
// and constructs the `syft power-user` command. It is also responsible for
|
||||
// organizing flag usage and injecting the application config for each command.
|
||||
// It also constructs the syft attest command and the syft version command.
|
||||
|
||||
// Because of how the `cobra` library behaves, the application's configuration is initialized
|
||||
// at this level. Values from the config should only be used after `app.LoadAllValues` has been called.
|
||||
// Cobra does not have knowledge of the user provided flags until the `RunE` block of each command.
|
||||
@@ -43,15 +45,17 @@ func New() (*cobra.Command, error) {
|
||||
// we also need the command to have information about the `root` options because of this alias
|
||||
ro := &options.RootOptions{}
|
||||
po := &options.PackagesOptions{}
|
||||
ao := &options.AttestOptions{}
|
||||
packagesCmd := Packages(v, app, ro, po)
|
||||
|
||||
// root options are also passed to the attestCmd so that a user provided config location can be discovered
|
||||
attestCmd := Attest(v, app, ro)
|
||||
poweruserCmd := PowerUser(v, app, ro)
|
||||
convertCmd := Convert(v, app, ro, po)
|
||||
attestCmd := Attest(v, app, ro, po, ao)
|
||||
|
||||
// rootCmd is currently an alias for the packages command
|
||||
rootCmd := &cobra.Command{
|
||||
Use: fmt.Sprintf("%s [SOURCE]", internal.ApplicationName),
|
||||
Short: packagesCmd.Short,
|
||||
Long: packagesCmd.Long,
|
||||
Args: packagesCmd.Args,
|
||||
@@ -73,11 +77,7 @@ func New() (*cobra.Command, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// attest also uses flags from the packagesCmd since it generates an sbom
|
||||
err = po.AddFlags(attestCmd, v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// poweruser also uses the packagesCmd flags since it is a specialized version of the command
|
||||
err = po.AddFlags(poweruserCmd, v)
|
||||
if err != nil {
|
||||
@@ -87,13 +87,11 @@ func New() (*cobra.Command, error) {
|
||||
// commands to add to root
|
||||
cmds := []*cobra.Command{
|
||||
packagesCmd,
|
||||
attestCmd,
|
||||
poweruserCmd,
|
||||
convertCmd,
|
||||
poweruserCmd,
|
||||
poweruserCmd,
|
||||
Completion(),
|
||||
attestCmd,
|
||||
Version(v, app),
|
||||
cranecmd.NewCmdAuthLogin("syft"),
|
||||
cranecmd.NewCmdAuthLogin("syft"), // syft login uses the same command as crane
|
||||
}
|
||||
|
||||
// Add sub-commands.
|
||||
@@ -127,7 +125,7 @@ func checkForApplicationUpdate() {
|
||||
log.Infof("new version of %s is available: %s (current version is %s)", internal.ApplicationName, newVersion, version.FromBuild().Version)
|
||||
|
||||
bus.Publish(partybus.Event{
|
||||
Type: event.AppUpdateAvailable,
|
||||
Type: event.CLIAppUpdateAvailable,
|
||||
Value: newVersion,
|
||||
})
|
||||
} else {
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func Completion() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "completion [bash|zsh|fish]",
|
||||
Short: "Generate a shell completion for Syft (listing local docker images)",
|
||||
Long: `To load completions (docker image list):
|
||||
Bash:
|
||||
$ source <(syft completion bash)
|
||||
# To load completions for each session, execute once:
|
||||
Linux:
|
||||
$ syft completion bash > /etc/bash_completion.d/syft
|
||||
MacOS:
|
||||
$ syft completion bash > /usr/local/etc/bash_completion.d/syft
|
||||
Zsh:
|
||||
# If shell completion is not already enabled in your environment you will need
|
||||
# to enable it. You can execute the following once:
|
||||
$ echo "autoload -U compinit; compinit" >> ~/.zshrc
|
||||
# To load completions for each session, execute once:
|
||||
$ syft completion zsh > "${fpath[1]}/_syft"
|
||||
# You will need to start a new shell for this setup to take effect.
|
||||
Fish:
|
||||
$ syft completion fish | source
|
||||
# To load completions for each session, execute once:
|
||||
$ syft completion fish > ~/.config/fish/completions/syft.fish
|
||||
`,
|
||||
DisableFlagsInUseLine: true,
|
||||
ValidArgs: []string{"bash", "zsh", "fish"},
|
||||
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
var err error
|
||||
switch args[0] {
|
||||
case "bash":
|
||||
err = cmd.Root().GenBashCompletion(os.Stdout)
|
||||
case "zsh":
|
||||
err = cmd.Root().GenZshCompletion(os.Stdout)
|
||||
case "fish":
|
||||
err = cmd.Root().GenFishCompletion(os.Stdout, true)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -14,8 +14,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
convertExample = ` {{.appName}} {{.command}} img.syft.json -o spdx-json convert a syft SBOM to spdx-json, output goes to stdout in table format, by default
|
||||
{{.appName}} {{.command}} img.syft.json -o cyclonedx-json=img.cdx.json convert a syft SBOM to CycloneDX, output goes to a file named img.cdx.json
|
||||
convertExample = ` {{.appName}} {{.command}} img.syft.json -o spdx-json convert a syft SBOM to spdx-json, output goes to stdout
|
||||
{{.appName}} {{.command}} img.syft.json -o cyclonedx-json=img.cdx.json convert a syft SBOM to CycloneDX, output is written to the file "img.cdx.json""
|
||||
{{.appName}} {{.command}} - -o spdx-json convert an SBOM from STDIN to spdx-json
|
||||
`
|
||||
)
|
||||
|
||||
@@ -42,6 +43,7 @@ func Convert(v *viper.Viper, app *config.Application, ro *options.RootOptions, p
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if app.CheckForAppUpdate {
|
||||
checkForApplicationUpdate()
|
||||
// TODO: this is broke, the bus isn't available yet
|
||||
}
|
||||
return convert.Run(cmd.Context(), app, args)
|
||||
},
|
||||
|
||||
@@ -3,39 +3,83 @@ package convert
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/wagoodman/go-partybus"
|
||||
|
||||
"github.com/anchore/stereoscope"
|
||||
"github.com/anchore/syft/cmd/syft/cli/eventloop"
|
||||
"github.com/anchore/syft/cmd/syft/cli/options"
|
||||
"github.com/anchore/syft/cmd/syft/internal/ui"
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/config"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft"
|
||||
"github.com/anchore/syft/syft/formats"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
func Run(ctx context.Context, app *config.Application, args []string) error {
|
||||
func Run(_ context.Context, app *config.Application, args []string) error {
|
||||
log.Warn("convert is an experimental feature, run `syft convert -h` for help")
|
||||
writer, err := options.MakeWriter(app.Outputs, app.File, "")
|
||||
|
||||
writer, err := options.MakeSBOMWriter(app.Outputs, app.File, app.OutputTemplatePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := writer.Close(); err != nil {
|
||||
log.Warnf("unable to write to report destination: %w", err)
|
||||
// could be an image or a directory, with or without a scheme
|
||||
userInput := args[0]
|
||||
|
||||
var reader io.ReadCloser
|
||||
|
||||
if userInput == "-" {
|
||||
reader = os.Stdin
|
||||
} else {
|
||||
f, err := os.Open(userInput)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open SBOM file: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = f.Close()
|
||||
}()
|
||||
reader = f
|
||||
}
|
||||
|
||||
eventBus := partybus.NewBus()
|
||||
stereoscope.SetBus(eventBus)
|
||||
syft.SetBus(eventBus)
|
||||
subscription := eventBus.Subscribe()
|
||||
|
||||
return eventloop.EventLoop(
|
||||
execWorker(reader, writer),
|
||||
eventloop.SetupSignals(),
|
||||
subscription,
|
||||
stereoscope.Cleanup,
|
||||
ui.Select(options.IsVerbose(app), app.Quiet)...,
|
||||
)
|
||||
}
|
||||
|
||||
func execWorker(reader io.Reader, writer sbom.Writer) <-chan error {
|
||||
errs := make(chan error)
|
||||
go func() {
|
||||
defer close(errs)
|
||||
defer bus.Exit()
|
||||
|
||||
s, _, err := formats.Decode(reader)
|
||||
if err != nil {
|
||||
errs <- fmt.Errorf("failed to decode SBOM: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
if s == nil {
|
||||
errs <- fmt.Errorf("no SBOM produced")
|
||||
return
|
||||
}
|
||||
|
||||
if err := writer.Write(*s); err != nil {
|
||||
errs <- fmt.Errorf("failed to write SBOM: %w", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// this can only be a SBOM file
|
||||
userInput := args[0]
|
||||
f, err := os.Open(userInput)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open SBOM file: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
sbom, _, err := syft.Decode(f)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode SBOM: %w", err)
|
||||
}
|
||||
|
||||
return writer.Write(*sbom)
|
||||
return errs
|
||||
}
|
||||
|
||||
@@ -8,20 +8,20 @@ import (
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/ui"
|
||||
)
|
||||
|
||||
// eventLoop listens to worker errors (from execution path), worker events (from a partybus subscription), and
|
||||
// EventLoop listens to worker errors (from execution path), worker events (from a partybus subscription), and
|
||||
// signal interrupts. Is responsible for handling each event relative to a given UI an to coordinate eventing until
|
||||
// an eventual graceful exit.
|
||||
func EventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription *partybus.Subscription, cleanupFn func(), uxs ...ui.UI) error {
|
||||
func EventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription *partybus.Subscription, cleanupFn func(), uxs ...clio.UI) error {
|
||||
defer cleanupFn()
|
||||
events := subscription.Events()
|
||||
var err error
|
||||
var ux ui.UI
|
||||
var ux clio.UI
|
||||
|
||||
if ux, err = setupUI(subscription.Unsubscribe, uxs...); err != nil {
|
||||
if ux, err = setupUI(subscription, uxs...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -85,9 +85,9 @@ func EventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription *
|
||||
// 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) {
|
||||
func setupUI(subscription *partybus.Subscription, uis ...clio.UI) (clio.UI, error) {
|
||||
for _, ux := range uis {
|
||||
if err := ux.Setup(unsubscribe); err != nil {
|
||||
if err := ux.Setup(subscription); err != nil {
|
||||
log.Warnf("unable to setup given UI, falling back to alternative UI: %+v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -11,34 +11,37 @@ import (
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
|
||||
"github.com/anchore/syft/internal/ui"
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
)
|
||||
|
||||
var _ ui.UI = (*uiMock)(nil)
|
||||
var _ clio.UI = (*uiMock)(nil)
|
||||
|
||||
type uiMock struct {
|
||||
t *testing.T
|
||||
finalEvent partybus.Event
|
||||
unsubscribe func() error
|
||||
t *testing.T
|
||||
finalEvent partybus.Event
|
||||
subscription partybus.Unsubscribable
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (u *uiMock) Setup(unsubscribe func() error) error {
|
||||
func (u *uiMock) Setup(unsubscribe partybus.Unsubscribable) error {
|
||||
u.t.Helper()
|
||||
u.t.Logf("UI Setup called")
|
||||
u.unsubscribe = unsubscribe
|
||||
return u.Called(unsubscribe).Error(0)
|
||||
u.subscription = unsubscribe
|
||||
return u.Called(unsubscribe.Unsubscribe).Error(0)
|
||||
}
|
||||
|
||||
func (u *uiMock) Handle(event partybus.Event) error {
|
||||
u.t.Helper()
|
||||
u.t.Logf("UI Handle called: %+v", event.Type)
|
||||
if event == u.finalEvent {
|
||||
assert.NoError(u.t, u.unsubscribe())
|
||||
assert.NoError(u.t, u.subscription.Unsubscribe())
|
||||
}
|
||||
return u.Called(event).Error(0)
|
||||
}
|
||||
|
||||
func (u *uiMock) Teardown(_ bool) error {
|
||||
u.t.Helper()
|
||||
u.t.Logf("UI Teardown called")
|
||||
return u.Called().Error(0)
|
||||
}
|
||||
@@ -51,7 +54,7 @@ func Test_EventLoop_gracefulExit(t *testing.T) {
|
||||
t.Cleanup(testBus.Close)
|
||||
|
||||
finalEvent := partybus.Event{
|
||||
Type: event.Exit,
|
||||
Type: event.CLIExit,
|
||||
}
|
||||
|
||||
worker := func() <-chan error {
|
||||
@@ -183,7 +186,7 @@ func Test_EventLoop_unsubscribeError(t *testing.T) {
|
||||
t.Cleanup(testBus.Close)
|
||||
|
||||
finalEvent := partybus.Event{
|
||||
Type: event.Exit,
|
||||
Type: event.CLIExit,
|
||||
}
|
||||
|
||||
worker := func() <-chan error {
|
||||
@@ -252,7 +255,7 @@ func Test_EventLoop_handlerError(t *testing.T) {
|
||||
t.Cleanup(testBus.Close)
|
||||
|
||||
finalEvent := partybus.Event{
|
||||
Type: event.Exit,
|
||||
Type: event.CLIExit,
|
||||
Error: fmt.Errorf("an exit error occured"),
|
||||
}
|
||||
|
||||
@@ -377,7 +380,7 @@ func Test_EventLoop_uiTeardownError(t *testing.T) {
|
||||
t.Cleanup(testBus.Close)
|
||||
|
||||
finalEvent := partybus.Event{
|
||||
Type: event.Exit,
|
||||
Type: event.CLIExit,
|
||||
}
|
||||
|
||||
worker := func() <-chan error {
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
package eventloop
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"fmt"
|
||||
|
||||
"github.com/anchore/syft/internal/config"
|
||||
"github.com/anchore/syft/internal/file"
|
||||
"github.com/anchore/syft/syft"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/file/cataloger/filecontent"
|
||||
"github.com/anchore/syft/syft/file/cataloger/filedigest"
|
||||
"github.com/anchore/syft/syft/file/cataloger/filemetadata"
|
||||
"github.com/anchore/syft/syft/file/cataloger/secrets"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
type Task func(*sbom.Artifacts, *source.Source) ([]artifact.Relationship, error)
|
||||
type Task func(*sbom.Artifacts, source.Source) ([]artifact.Relationship, error)
|
||||
|
||||
func Tasks(app *config.Application) ([]Task, error) {
|
||||
var tasks []Task
|
||||
@@ -44,16 +45,13 @@ func generateCatalogPackagesTask(app *config.Application) (Task, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
|
||||
task := func(results *sbom.Artifacts, src source.Source) ([]artifact.Relationship, error) {
|
||||
packageCatalog, relationships, theDistro, err := syft.CatalogPackages(src, app.ToCatalogerConfig())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
results.PackageCatalog = packageCatalog
|
||||
results.Packages = packageCatalog
|
||||
results.LinuxDistribution = theDistro
|
||||
|
||||
return relationships, nil
|
||||
return relationships, err
|
||||
}
|
||||
|
||||
return task, nil
|
||||
@@ -64,9 +62,9 @@ func generateCatalogFileMetadataTask(app *config.Application) (Task, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
metadataCataloger := file.NewMetadataCataloger()
|
||||
metadataCataloger := filemetadata.NewCataloger()
|
||||
|
||||
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
|
||||
task := func(results *sbom.Artifacts, src source.Source) ([]artifact.Relationship, error) {
|
||||
resolver, err := src.FileResolver(app.FileMetadata.Cataloger.ScopeOpt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -88,31 +86,14 @@ func generateCatalogFileDigestsTask(app *config.Application) (Task, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
supportedHashAlgorithms := make(map[string]crypto.Hash)
|
||||
for _, h := range []crypto.Hash{
|
||||
crypto.MD5,
|
||||
crypto.SHA1,
|
||||
crypto.SHA256,
|
||||
} {
|
||||
supportedHashAlgorithms[file.DigestAlgorithmName(h)] = h
|
||||
}
|
||||
|
||||
var hashes []crypto.Hash
|
||||
for _, hashStr := range app.FileMetadata.Digests {
|
||||
name := file.CleanDigestAlgorithmName(hashStr)
|
||||
hashObj, ok := supportedHashAlgorithms[name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unsupported hash algorithm: %s", hashStr)
|
||||
}
|
||||
hashes = append(hashes, hashObj)
|
||||
}
|
||||
|
||||
digestsCataloger, err := file.NewDigestsCataloger(hashes)
|
||||
hashes, err := file.Hashers(app.FileMetadata.Digests...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
|
||||
digestsCataloger := filedigest.NewCataloger(hashes)
|
||||
|
||||
task := func(results *sbom.Artifacts, src source.Source) ([]artifact.Relationship, error) {
|
||||
resolver, err := src.FileResolver(app.FileMetadata.Cataloger.ScopeOpt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -134,17 +115,17 @@ func generateCatalogSecretsTask(app *config.Application) (Task, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
patterns, err := file.GenerateSearchPatterns(file.DefaultSecretsPatterns, app.Secrets.AdditionalPatterns, app.Secrets.ExcludePatternNames)
|
||||
patterns, err := secrets.GenerateSearchPatterns(secrets.DefaultSecretsPatterns, app.Secrets.AdditionalPatterns, app.Secrets.ExcludePatternNames)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
secretsCataloger, err := file.NewSecretsCataloger(patterns, app.Secrets.RevealValues, app.Secrets.SkipFilesAboveSize)
|
||||
secretsCataloger, err := secrets.NewCataloger(patterns, app.Secrets.RevealValues, app.Secrets.SkipFilesAboveSize) //nolint:staticcheck
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
|
||||
task := func(results *sbom.Artifacts, src source.Source) ([]artifact.Relationship, error) {
|
||||
resolver, err := src.FileResolver(app.Secrets.Cataloger.ScopeOpt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -166,12 +147,12 @@ func generateCatalogContentsTask(app *config.Application) (Task, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
contentsCataloger, err := file.NewContentsCataloger(app.FileContents.Globs, app.FileContents.SkipFilesAboveSize)
|
||||
contentsCataloger, err := filecontent.NewCataloger(app.FileContents.Globs, app.FileContents.SkipFilesAboveSize) //nolint:staticcheck
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
|
||||
task := func(results *sbom.Artifacts, src source.Source) ([]artifact.Relationship, error) {
|
||||
resolver, err := src.FileResolver(app.FileContents.Cataloger.ScopeOpt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -188,7 +169,7 @@ func generateCatalogContentsTask(app *config.Application) (Task, error) {
|
||||
return task, nil
|
||||
}
|
||||
|
||||
func RunTask(t Task, a *sbom.Artifacts, src *source.Source, c chan<- artifact.Relationship, errs chan<- error) {
|
||||
func RunTask(t Task, a *sbom.Artifacts, src source.Source, c chan<- artifact.Relationship, errs chan<- error) {
|
||||
defer close(c)
|
||||
|
||||
relationships, err := t(a, src)
|
||||
|
||||
@@ -6,72 +6,21 @@ import (
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
const defaultKeyFileName = "cosign.key"
|
||||
|
||||
type AttestOptions struct {
|
||||
Key string
|
||||
Cert string
|
||||
CertChain string
|
||||
NoUpload bool
|
||||
Force bool
|
||||
Recursive bool
|
||||
|
||||
Rekor RekorOptions
|
||||
Fulcio FulcioOptions
|
||||
OIDC OIDCOptions
|
||||
Key string
|
||||
}
|
||||
|
||||
var _ Interface = (*AttestOptions)(nil)
|
||||
|
||||
func (o *AttestOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error {
|
||||
if err := o.Rekor.AddFlags(cmd, v); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := o.Fulcio.AddFlags(cmd, v); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := o.OIDC.AddFlags(cmd, v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&o.Key, "key", "", defaultKeyFileName,
|
||||
"path to the private key file to use for attestation")
|
||||
|
||||
cmd.Flags().StringVarP(&o.Cert, "cert", "", "",
|
||||
"path to the x.509 certificate in PEM format to include in the OCI Signature")
|
||||
|
||||
cmd.Flags().BoolVarP(&o.NoUpload, "no-upload", "", false,
|
||||
"do not upload the generated attestation")
|
||||
|
||||
cmd.Flags().BoolVarP(&o.Force, "force", "", false,
|
||||
"skip warnings and confirmations")
|
||||
|
||||
cmd.Flags().BoolVarP(&o.Recursive, "recursive", "", false,
|
||||
"if a multi-arch image is specified, additionally sign each discrete image")
|
||||
|
||||
return bindAttestationConfigOptions(cmd.Flags(), v)
|
||||
func (o AttestOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error {
|
||||
cmd.Flags().StringVarP(&o.Key, "key", "k", "", "the key to use for the attestation")
|
||||
return bindAttestConfigOptions(cmd.Flags(), v)
|
||||
}
|
||||
|
||||
func bindAttestationConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error {
|
||||
//nolint:revive
|
||||
func bindAttestConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error {
|
||||
if err := v.BindPFlag("attest.key", flags.Lookup("key")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := v.BindPFlag("attest.cert", flags.Lookup("cert")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := v.BindPFlag("attest.no-upload", flags.Lookup("no-upload")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := v.BindPFlag("attest.force", flags.Lookup("force")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := v.BindPFlag("attest.recursive", flags.Lookup("recursive")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/syft/formats/cyclonedxjson"
|
||||
"github.com/anchore/syft/syft/formats/cyclonedxxml"
|
||||
"github.com/anchore/syft/syft/formats/github"
|
||||
"github.com/anchore/syft/syft/formats/spdxjson"
|
||||
"github.com/anchore/syft/syft/formats/spdxtagvalue"
|
||||
"github.com/anchore/syft/syft/formats/syftjson"
|
||||
"github.com/anchore/syft/syft/formats/table"
|
||||
"github.com/anchore/syft/syft/formats/text"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
func FormatAliases(ids ...sbom.FormatID) (aliases []string) {
|
||||
for _, id := range ids {
|
||||
switch id {
|
||||
case syftjson.ID:
|
||||
aliases = append(aliases, "syft-json")
|
||||
case text.ID:
|
||||
aliases = append(aliases, "text")
|
||||
case table.ID:
|
||||
aliases = append(aliases, "table")
|
||||
case spdxjson.ID:
|
||||
aliases = append(aliases, "spdx-json")
|
||||
case spdxtagvalue.ID:
|
||||
aliases = append(aliases, "spdx-tag-value")
|
||||
case cyclonedxxml.ID:
|
||||
aliases = append(aliases, "cyclonedx-xml")
|
||||
case cyclonedxjson.ID:
|
||||
aliases = append(aliases, "cyclonedx-json")
|
||||
case github.ID:
|
||||
aliases = append(aliases, "github", "github-json")
|
||||
default:
|
||||
aliases = append(aliases, string(id))
|
||||
}
|
||||
}
|
||||
return aliases
|
||||
}
|
||||
@@ -31,6 +31,7 @@ func (o *FulcioOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error {
|
||||
return bindFulcioConfigOptions(cmd.Flags(), v)
|
||||
}
|
||||
|
||||
//nolint:revive
|
||||
func bindFulcioConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error {
|
||||
if err := v.BindPFlag("attest.fulcio-url", flags.Lookup("fulcio-url")); err != nil {
|
||||
return err
|
||||
|
||||
@@ -31,6 +31,7 @@ func (o *OIDCOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error {
|
||||
return bindOIDCConfigOptions(cmd.Flags(), v)
|
||||
}
|
||||
|
||||
//nolint:revive
|
||||
func bindOIDCConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error {
|
||||
if err := v.BindPFlag("attest.oidc-issuer", flags.Lookup("oidc-issuer")); err != nil {
|
||||
return err
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/anchore/syft/syft"
|
||||
"github.com/anchore/syft/syft/formats"
|
||||
"github.com/anchore/syft/syft/formats/table"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
@@ -21,7 +21,9 @@ type PackagesOptions struct {
|
||||
Platform string
|
||||
Exclude []string
|
||||
Catalogers []string
|
||||
Name string
|
||||
SourceName string
|
||||
SourceVersion string
|
||||
BasePath string
|
||||
}
|
||||
|
||||
var _ Interface = (*PackagesOptions)(nil)
|
||||
@@ -30,8 +32,8 @@ func (o *PackagesOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error {
|
||||
cmd.Flags().StringVarP(&o.Scope, "scope", "s", cataloger.DefaultSearchConfig().Scope.String(),
|
||||
fmt.Sprintf("selection of layers to catalog, options=%v", source.AllScopes))
|
||||
|
||||
cmd.Flags().StringArrayVarP(&o.Output, "output", "o", FormatAliases(table.ID),
|
||||
fmt.Sprintf("report output format, options=%v", FormatAliases(syft.FormatIDs()...)))
|
||||
cmd.Flags().StringArrayVarP(&o.Output, "output", "o", []string{string(table.ID)},
|
||||
fmt.Sprintf("report output format, options=%v", formats.AllIDs()))
|
||||
|
||||
cmd.Flags().StringVarP(&o.File, "file", "", "",
|
||||
"file to write the default report output to (default is STDOUT)")
|
||||
@@ -48,12 +50,23 @@ func (o *PackagesOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error {
|
||||
cmd.Flags().StringArrayVarP(&o.Catalogers, "catalogers", "", nil,
|
||||
"enable one or more package catalogers")
|
||||
|
||||
cmd.Flags().StringVarP(&o.Name, "name", "", "",
|
||||
cmd.Flags().StringVarP(&o.SourceName, "name", "", "",
|
||||
"set the name of the target being analyzed")
|
||||
cmd.Flags().Lookup("name").Deprecated = "use: source-name"
|
||||
|
||||
cmd.Flags().StringVarP(&o.SourceName, "source-name", "", "",
|
||||
"set the name of the target being analyzed")
|
||||
|
||||
cmd.Flags().StringVarP(&o.SourceVersion, "source-version", "", "",
|
||||
"set the name of the target being analyzed")
|
||||
|
||||
cmd.Flags().StringVarP(&o.BasePath, "base-path", "", "",
|
||||
"base directory for scanning, no links will be followed above this directory, and all paths will be reported relative to this directory")
|
||||
|
||||
return bindPackageConfigOptions(cmd.Flags(), v)
|
||||
}
|
||||
|
||||
//nolint:revive
|
||||
func bindPackageConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error {
|
||||
// Formatting & Input options //////////////////////////////////////////////
|
||||
|
||||
@@ -77,6 +90,14 @@ func bindPackageConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := v.BindPFlag("source.name", flags.Lookup("source-name")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := v.BindPFlag("source.version", flags.Lookup("source-version")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := v.BindPFlag("output", flags.Lookup("output")); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -89,5 +110,9 @@ func bindPackageConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := v.BindPFlag("base-path", flags.Lookup("base-path")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ func (o *RekorOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error {
|
||||
return bindRekorConfigOptions(cmd.Flags(), v)
|
||||
}
|
||||
|
||||
//nolint:revive
|
||||
func bindRekorConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error {
|
||||
// TODO: config re-design
|
||||
if err := v.BindPFlag("attest.rekor-url", flags.Lookup("rekor-url")); err != nil {
|
||||
|
||||
@@ -22,6 +22,7 @@ func (o *RootOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error {
|
||||
return bindRootConfigOptions(cmd.PersistentFlags(), v)
|
||||
}
|
||||
|
||||
//nolint:revive
|
||||
func bindRootConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error {
|
||||
if err := v.BindPFlag("config", flags.Lookup("config")); err != nil {
|
||||
return err
|
||||
|
||||
@@ -9,9 +9,9 @@ type VersionOptions struct {
|
||||
Output string
|
||||
}
|
||||
|
||||
var _ Interface = (*PackagesOptions)(nil)
|
||||
var _ Interface = (*VersionOptions)(nil)
|
||||
|
||||
func (o *VersionOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error {
|
||||
func (o *VersionOptions) AddFlags(cmd *cobra.Command, _ *viper.Viper) error {
|
||||
cmd.Flags().StringVarP(&o.Output, "output", "o", "text", "format to show version information (available=[text, json])")
|
||||
return nil
|
||||
}
|
||||
|
||||
+148
-11
@@ -1,26 +1,40 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
|
||||
"github.com/anchore/syft/syft"
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/formats"
|
||||
"github.com/anchore/syft/syft/formats/table"
|
||||
"github.com/anchore/syft/syft/formats/template"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
// makeWriter creates a sbom.Writer for output or returns an error. this will either return a valid writer
|
||||
var _ sbom.Writer = (*sbomMultiWriter)(nil)
|
||||
|
||||
var _ interface {
|
||||
io.Closer
|
||||
sbom.Writer
|
||||
} = (*sbomStreamWriter)(nil)
|
||||
|
||||
// MakeSBOMWriter creates a sbom.Writer for output or returns an error. this will either return a valid writer
|
||||
// or an error but neither both and if there is no error, sbom.Writer.Close() should be called
|
||||
func MakeWriter(outputs []string, defaultFile, templateFilePath string) (sbom.Writer, error) {
|
||||
outputOptions, err := parseOutputs(outputs, defaultFile, templateFilePath)
|
||||
func MakeSBOMWriter(outputs []string, defaultFile, templateFilePath string) (sbom.Writer, error) {
|
||||
outputOptions, err := parseSBOMOutputFlags(outputs, defaultFile, templateFilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
writer, err := sbom.NewWriter(outputOptions...)
|
||||
writer, err := newSBOMMultiWriter(outputOptions...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -28,11 +42,21 @@ func MakeWriter(outputs []string, defaultFile, templateFilePath string) (sbom.Wr
|
||||
return writer, nil
|
||||
}
|
||||
|
||||
// parseOptions utility to parse command-line option strings and retain the existing behavior of default format and file
|
||||
func parseOutputs(outputs []string, defaultFile, templateFilePath string) (out []sbom.WriterOption, errs error) {
|
||||
// MakeSBOMWriterForFormat creates a sbom.Writer for for the given format or returns an error.
|
||||
func MakeSBOMWriterForFormat(format sbom.Format, path string) (sbom.Writer, error) {
|
||||
writer, err := newSBOMMultiWriter(newSBOMWriterDescription(format, path))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return writer, nil
|
||||
}
|
||||
|
||||
// parseSBOMOutputFlags utility to parse command-line option strings and retain the existing behavior of default format and file
|
||||
func parseSBOMOutputFlags(outputs []string, defaultFile, templateFilePath string) (out []sbomWriterDescription, errs error) {
|
||||
// always should have one option -- we generally get the default of "table", but just make sure
|
||||
if len(outputs) == 0 {
|
||||
outputs = append(outputs, string(table.ID))
|
||||
outputs = append(outputs, table.ID.String())
|
||||
}
|
||||
|
||||
for _, name := range outputs {
|
||||
@@ -52,9 +76,9 @@ func parseOutputs(outputs []string, defaultFile, templateFilePath string) (out [
|
||||
file = parts[1]
|
||||
}
|
||||
|
||||
format := syft.FormatByName(name)
|
||||
format := formats.ByName(name)
|
||||
if format == nil {
|
||||
errs = multierror.Append(errs, fmt.Errorf(`unsupported output format "%s", supported formats are: %+v`, name, FormatAliases(syft.FormatIDs()...)))
|
||||
errs = multierror.Append(errs, fmt.Errorf(`unsupported output format "%s", supported formats are: %+v`, name, formats.AllIDs()))
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -63,7 +87,120 @@ func parseOutputs(outputs []string, defaultFile, templateFilePath string) (out [
|
||||
format = tmpl
|
||||
}
|
||||
|
||||
out = append(out, sbom.NewWriterOption(format, file))
|
||||
out = append(out, newSBOMWriterDescription(format, file))
|
||||
}
|
||||
return out, errs
|
||||
}
|
||||
|
||||
// sbomWriterDescription Format and path strings used to create sbom.Writer
|
||||
type sbomWriterDescription struct {
|
||||
Format sbom.Format
|
||||
Path string
|
||||
}
|
||||
|
||||
func newSBOMWriterDescription(f sbom.Format, p string) sbomWriterDescription {
|
||||
expandedPath, err := homedir.Expand(p)
|
||||
if err != nil {
|
||||
log.Warnf("could not expand given writer output path=%q: %w", p, err)
|
||||
// ignore errors
|
||||
expandedPath = p
|
||||
}
|
||||
return sbomWriterDescription{
|
||||
Format: f,
|
||||
Path: expandedPath,
|
||||
}
|
||||
}
|
||||
|
||||
// sbomMultiWriter holds a list of child sbom.Writers to apply all Write and Close operations to
|
||||
type sbomMultiWriter struct {
|
||||
writers []sbom.Writer
|
||||
}
|
||||
|
||||
// newSBOMMultiWriter create all report writers from input options; if a file is not specified the given defaultWriter is used
|
||||
func newSBOMMultiWriter(options ...sbomWriterDescription) (_ *sbomMultiWriter, err error) {
|
||||
if len(options) == 0 {
|
||||
return nil, fmt.Errorf("no output options provided")
|
||||
}
|
||||
|
||||
out := &sbomMultiWriter{}
|
||||
|
||||
for _, option := range options {
|
||||
switch len(option.Path) {
|
||||
case 0:
|
||||
out.writers = append(out.writers, &sbomPublisher{
|
||||
format: option.Format,
|
||||
})
|
||||
default:
|
||||
// create any missing subdirectories
|
||||
dir := path.Dir(option.Path)
|
||||
if dir != "" {
|
||||
s, err := os.Stat(dir)
|
||||
if err != nil {
|
||||
err = os.MkdirAll(dir, 0755) // maybe should be os.ModePerm ?
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if !s.IsDir() {
|
||||
return nil, fmt.Errorf("output path does not contain a valid directory: %s", option.Path)
|
||||
}
|
||||
}
|
||||
fileOut, err := os.OpenFile(option.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create report file: %w", err)
|
||||
}
|
||||
out.writers = append(out.writers, &sbomStreamWriter{
|
||||
format: option.Format,
|
||||
out: fileOut,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Write writes the SBOM to all writers
|
||||
func (m *sbomMultiWriter) Write(s sbom.SBOM) (errs error) {
|
||||
for _, w := range m.writers {
|
||||
err := w.Write(s)
|
||||
if err != nil {
|
||||
errs = multierror.Append(errs, fmt.Errorf("unable to write SBOM: %w", err))
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// sbomStreamWriter implements sbom.Writer for a given format and io.Writer, also providing a close function for cleanup
|
||||
type sbomStreamWriter struct {
|
||||
format sbom.Format
|
||||
out io.Writer
|
||||
}
|
||||
|
||||
// Write the provided SBOM to the data stream
|
||||
func (w *sbomStreamWriter) Write(s sbom.SBOM) error {
|
||||
defer w.Close()
|
||||
return w.format.Encode(w.out, s)
|
||||
}
|
||||
|
||||
// Close any resources, such as open files
|
||||
func (w *sbomStreamWriter) Close() error {
|
||||
if closer, ok := w.out.(io.Closer); ok {
|
||||
return closer.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// sbomPublisher implements sbom.Writer that publishes results to the event bus
|
||||
type sbomPublisher struct {
|
||||
format sbom.Format
|
||||
}
|
||||
|
||||
// Write the provided SBOM to the data stream
|
||||
func (w *sbomPublisher) Write(s sbom.SBOM) error {
|
||||
buf := &bytes.Buffer{}
|
||||
if err := w.format.Encode(buf, s); err != nil {
|
||||
return fmt.Errorf("unable to encode SBOM: %w", err)
|
||||
}
|
||||
|
||||
bus.Report(buf.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/pkg/homedir"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
func TestIsSupportedFormat(t *testing.T) {
|
||||
func Test_MakeSBOMWriter(t *testing.T) {
|
||||
tests := []struct {
|
||||
outputs []string
|
||||
wantErr assert.ErrorAssertionFunc
|
||||
@@ -28,7 +34,200 @@ func TestIsSupportedFormat(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
_, err := MakeWriter(tt.outputs, "", "")
|
||||
_, err := MakeSBOMWriter(tt.outputs, "", "")
|
||||
tt.wantErr(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func dummyEncoder(io.Writer, sbom.SBOM) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func dummyFormat(name string) sbom.Format {
|
||||
return sbom.NewFormat(sbom.AnyVersion, dummyEncoder, nil, nil, sbom.FormatID(name))
|
||||
}
|
||||
|
||||
func Test_newSBOMMultiWriter(t *testing.T) {
|
||||
type writerConfig struct {
|
||||
format string
|
||||
file string
|
||||
}
|
||||
|
||||
tmp := t.TempDir()
|
||||
|
||||
testName := func(options []sbomWriterDescription, err bool) string {
|
||||
var out []string
|
||||
for _, opt := range options {
|
||||
out = append(out, string(opt.Format.ID())+"="+opt.Path)
|
||||
}
|
||||
errs := ""
|
||||
if err {
|
||||
errs = "(err)"
|
||||
}
|
||||
return strings.Join(out, ", ") + errs
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
outputs []sbomWriterDescription
|
||||
err bool
|
||||
expected []writerConfig
|
||||
}{
|
||||
{
|
||||
outputs: []sbomWriterDescription{},
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
outputs: []sbomWriterDescription{
|
||||
{
|
||||
Format: dummyFormat("table"),
|
||||
Path: "",
|
||||
},
|
||||
},
|
||||
expected: []writerConfig{
|
||||
{
|
||||
format: "table",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
outputs: []sbomWriterDescription{
|
||||
{
|
||||
Format: dummyFormat("json"),
|
||||
},
|
||||
},
|
||||
expected: []writerConfig{
|
||||
{
|
||||
format: "json",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
outputs: []sbomWriterDescription{
|
||||
{
|
||||
Format: dummyFormat("json"),
|
||||
Path: "test-2.json",
|
||||
},
|
||||
},
|
||||
expected: []writerConfig{
|
||||
{
|
||||
format: "json",
|
||||
file: "test-2.json",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
outputs: []sbomWriterDescription{
|
||||
{
|
||||
Format: dummyFormat("json"),
|
||||
Path: "test-3/1.json",
|
||||
},
|
||||
{
|
||||
Format: dummyFormat("spdx-json"),
|
||||
Path: "test-3/2.json",
|
||||
},
|
||||
},
|
||||
expected: []writerConfig{
|
||||
{
|
||||
format: "json",
|
||||
file: "test-3/1.json",
|
||||
},
|
||||
{
|
||||
format: "spdx-json",
|
||||
file: "test-3/2.json",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
outputs: []sbomWriterDescription{
|
||||
{
|
||||
Format: dummyFormat("text"),
|
||||
},
|
||||
{
|
||||
Format: dummyFormat("spdx-json"),
|
||||
Path: "test-4.json",
|
||||
},
|
||||
},
|
||||
expected: []writerConfig{
|
||||
{
|
||||
format: "text",
|
||||
},
|
||||
{
|
||||
format: "spdx-json",
|
||||
file: "test-4.json",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(testName(test.outputs, test.err), func(t *testing.T) {
|
||||
outputs := test.outputs
|
||||
for i := range outputs {
|
||||
if outputs[i].Path != "" {
|
||||
outputs[i].Path = tmp + outputs[i].Path
|
||||
}
|
||||
}
|
||||
|
||||
mw, err := newSBOMMultiWriter(outputs...)
|
||||
|
||||
if test.err {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
assert.Len(t, mw.writers, len(test.expected))
|
||||
|
||||
for i, e := range test.expected {
|
||||
switch w := mw.writers[i].(type) {
|
||||
case *sbomStreamWriter:
|
||||
assert.Equal(t, string(w.format.ID()), e.format)
|
||||
if e.file != "" {
|
||||
assert.NotNil(t, w.out)
|
||||
} else {
|
||||
assert.NotNil(t, w.out)
|
||||
}
|
||||
if e.file != "" {
|
||||
assert.FileExists(t, tmp+e.file)
|
||||
}
|
||||
case *sbomPublisher:
|
||||
assert.Equal(t, string(w.format.ID()), e.format)
|
||||
default:
|
||||
t.Fatalf("unknown writer type: %T", w)
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_newSBOMWriterDescription(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "expand home dir",
|
||||
path: "~/place.txt",
|
||||
expected: filepath.Join(homedir.Get(), "place.txt"),
|
||||
},
|
||||
{
|
||||
name: "passthrough other paths",
|
||||
path: "/other/place.txt",
|
||||
expected: "/other/place.txt",
|
||||
},
|
||||
{
|
||||
name: "no path",
|
||||
path: "",
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
o := newSBOMWriterDescription(dummyFormat("table"), tt.path)
|
||||
assert.Equal(t, tt.expected, o.Path)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,10 @@ const (
|
||||
{{.appName}} {{.command}} alpine:latest -o json show all possible cataloging details
|
||||
{{.appName}} {{.command}} alpine:latest -o cyclonedx show a CycloneDX formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -o cyclonedx-json show a CycloneDX JSON formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -o spdx show a SPDX 2.2 Tag-Value formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -o spdx-json show a SPDX 2.2 JSON formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -o spdx show a SPDX 2.3 Tag-Value formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -o spdx@2.2 show a SPDX 2.2 Tag-Value formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -o spdx-json show a SPDX 2.3 JSON formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -o spdx-json@2.2 show a SPDX 2.2 JSON formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -vv show verbose debug information
|
||||
{{.appName}} {{.command}} alpine:latest -o template -t my_format.tmpl show a SBOM formatted according to given template file
|
||||
|
||||
@@ -68,6 +70,7 @@ func Packages(v *viper.Viper, app *config.Application, ro *options.RootOptions,
|
||||
SilenceErrors: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if app.CheckForAppUpdate {
|
||||
// TODO: this is broke, the bus isn't available yet
|
||||
checkForApplicationUpdate()
|
||||
}
|
||||
return packages.Run(cmd.Context(), app, args)
|
||||
|
||||
@@ -7,45 +7,36 @@ import (
|
||||
"github.com/wagoodman/go-partybus"
|
||||
|
||||
"github.com/anchore/stereoscope"
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
"github.com/anchore/syft/cmd/syft/cli/eventloop"
|
||||
"github.com/anchore/syft/cmd/syft/cli/options"
|
||||
"github.com/anchore/syft/cmd/syft/internal/ui"
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/config"
|
||||
"github.com/anchore/syft/internal/file"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/ui"
|
||||
"github.com/anchore/syft/internal/version"
|
||||
"github.com/anchore/syft/syft"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/syft/formats/template"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
func Run(ctx context.Context, app *config.Application, args []string) error {
|
||||
err := validateOutputOptions(app)
|
||||
func Run(_ context.Context, app *config.Application, args []string) error {
|
||||
err := ValidateOutputOptions(app)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
writer, err := options.MakeWriter(app.Outputs, app.File, app.OutputTemplatePath)
|
||||
writer, err := options.MakeSBOMWriter(app.Outputs, app.File, app.OutputTemplatePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := writer.Close(); err != nil {
|
||||
log.Warnf("unable to write to report destination: %w", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// could be an image or a directory, with or without a scheme
|
||||
userInput := args[0]
|
||||
si, err := source.ParseInputWithName(userInput, app.Platform, true, app.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not generate source input for packages command: %w", err)
|
||||
}
|
||||
|
||||
eventBus := partybus.NewBus()
|
||||
stereoscope.SetBus(eventBus)
|
||||
@@ -53,7 +44,7 @@ func Run(ctx context.Context, app *config.Application, args []string) error {
|
||||
subscription := eventBus.Subscribe()
|
||||
|
||||
return eventloop.EventLoop(
|
||||
execWorker(app, *si, writer),
|
||||
execWorker(app, userInput, writer),
|
||||
eventloop.SetupSignals(),
|
||||
subscription,
|
||||
stereoscope.Cleanup,
|
||||
@@ -61,20 +52,69 @@ func Run(ctx context.Context, app *config.Application, args []string) error {
|
||||
)
|
||||
}
|
||||
|
||||
func execWorker(app *config.Application, si source.Input, writer sbom.Writer) <-chan error {
|
||||
// nolint:funlen
|
||||
func execWorker(app *config.Application, userInput string, writer sbom.Writer) <-chan error {
|
||||
errs := make(chan error)
|
||||
go func() {
|
||||
defer close(errs)
|
||||
defer bus.Exit()
|
||||
|
||||
src, cleanup, err := source.New(si, app.Registry.ToOptions(), app.Exclusions)
|
||||
if cleanup != nil {
|
||||
defer cleanup()
|
||||
}
|
||||
detection, err := source.Detect(
|
||||
userInput,
|
||||
source.DetectConfig{
|
||||
DefaultImageSource: app.DefaultImagePullSource,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
errs <- fmt.Errorf("failed to construct source from user input %q: %w", si.UserInput, err)
|
||||
errs <- fmt.Errorf("could not deteremine source: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
var platform *image.Platform
|
||||
|
||||
if app.Platform != "" {
|
||||
platform, err = image.NewPlatform(app.Platform)
|
||||
if err != nil {
|
||||
errs <- fmt.Errorf("invalid platform: %w", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
hashers, err := file.Hashers(app.Source.File.Digests...)
|
||||
if err != nil {
|
||||
errs <- fmt.Errorf("invalid hash: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
src, err := detection.NewSource(
|
||||
source.DetectionSourceConfig{
|
||||
Alias: source.Alias{
|
||||
Name: app.Source.Name,
|
||||
Version: app.Source.Version,
|
||||
},
|
||||
RegistryOptions: app.Registry.ToOptions(),
|
||||
Platform: platform,
|
||||
Exclude: source.ExcludeConfig{
|
||||
Paths: app.Exclusions,
|
||||
},
|
||||
DigestAlgorithms: hashers,
|
||||
BasePath: app.BasePath,
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
errs <- fmt.Errorf("failed to construct source from user input %q: %w", userInput, err)
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if src != nil {
|
||||
if err := src.Close(); err != nil {
|
||||
log.Tracef("unable to close source: %+v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
s, err := GenerateSBOM(src, errs, app)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
@@ -82,25 +122,26 @@ func execWorker(app *config.Application, si source.Input, writer sbom.Writer) <-
|
||||
}
|
||||
|
||||
if s == nil {
|
||||
errs <- fmt.Errorf("no SBOM produced for %q", si.UserInput)
|
||||
errs <- fmt.Errorf("no SBOM produced for %q", userInput)
|
||||
return
|
||||
}
|
||||
|
||||
bus.Publish(partybus.Event{
|
||||
Type: event.Exit,
|
||||
Value: func() error { return writer.Write(*s) },
|
||||
})
|
||||
if err := writer.Write(*s); err != nil {
|
||||
errs <- fmt.Errorf("failed to write SBOM: %w", err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
return errs
|
||||
}
|
||||
|
||||
func GenerateSBOM(src *source.Source, errs chan error, app *config.Application) (*sbom.SBOM, error) {
|
||||
func GenerateSBOM(src source.Source, errs chan error, app *config.Application) (*sbom.SBOM, error) {
|
||||
tasks, err := eventloop.Tasks(app)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s := sbom.SBOM{
|
||||
Source: src.Metadata,
|
||||
Source: src.Describe(),
|
||||
Descriptor: sbom.Descriptor{
|
||||
Name: internal.ApplicationName,
|
||||
Version: version.FromBuild().Version,
|
||||
@@ -113,7 +154,7 @@ func GenerateSBOM(src *source.Source, errs chan error, app *config.Application)
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
func buildRelationships(s *sbom.SBOM, src *source.Source, tasks []eventloop.Task, errs chan error) {
|
||||
func buildRelationships(s *sbom.SBOM, src source.Source, tasks []eventloop.Task, errs chan error) {
|
||||
var relationships []<-chan artifact.Relationship
|
||||
for _, task := range tasks {
|
||||
c := make(chan artifact.Relationship)
|
||||
@@ -134,7 +175,7 @@ func MergeRelationships(cs ...<-chan artifact.Relationship) (relationships []art
|
||||
return relationships
|
||||
}
|
||||
|
||||
func validateOutputOptions(app *config.Application) error {
|
||||
func ValidateOutputOptions(app *config.Application) error {
|
||||
var usesTemplateOutput bool
|
||||
for _, o := range app.Outputs {
|
||||
if o == template.ID.String() {
|
||||
|
||||
@@ -41,6 +41,7 @@ func PowerUser(v *viper.Viper, app *config.Application, ro *options.RootOptions)
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if app.CheckForAppUpdate {
|
||||
checkForApplicationUpdate()
|
||||
// TODO: this is broke, the bus isn't available yet
|
||||
}
|
||||
return poweruser.Run(cmd.Context(), app, args)
|
||||
},
|
||||
|
||||
@@ -9,47 +9,35 @@ import (
|
||||
"github.com/wagoodman/go-partybus"
|
||||
|
||||
"github.com/anchore/stereoscope"
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
"github.com/anchore/syft/cmd/syft/cli/eventloop"
|
||||
"github.com/anchore/syft/cmd/syft/cli/options"
|
||||
"github.com/anchore/syft/cmd/syft/cli/packages"
|
||||
"github.com/anchore/syft/cmd/syft/internal/ui"
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/config"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/ui"
|
||||
"github.com/anchore/syft/internal/version"
|
||||
"github.com/anchore/syft/syft"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/syft/formats/syftjson"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
func Run(ctx context.Context, app *config.Application, args []string) error {
|
||||
writer, err := sbom.NewWriter(sbom.WriterOption{
|
||||
Format: syftjson.Format(),
|
||||
Path: app.File,
|
||||
})
|
||||
func Run(_ context.Context, app *config.Application, args []string) error {
|
||||
f := syftjson.Format()
|
||||
writer, err := options.MakeSBOMWriterForFormat(f, app.File)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := writer.Close(); err != nil {
|
||||
log.Warnf("unable to write to report destination: %+v", err)
|
||||
}
|
||||
|
||||
// inform user at end of run that command will be removed
|
||||
deprecated := color.Style{color.Red, color.OpBold}.Sprint("DEPRECATED: This command will be removed in v1.0.0")
|
||||
fmt.Fprintln(os.Stderr, deprecated)
|
||||
}()
|
||||
|
||||
userInput := args[0]
|
||||
si, err := source.ParseInputWithName(userInput, app.Platform, true, app.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not generate source input for packages command: %w", err)
|
||||
}
|
||||
|
||||
eventBus := partybus.NewBus()
|
||||
stereoscope.SetBus(eventBus)
|
||||
@@ -57,7 +45,7 @@ func Run(ctx context.Context, app *config.Application, args []string) error {
|
||||
subscription := eventBus.Subscribe()
|
||||
|
||||
return eventloop.EventLoop(
|
||||
execWorker(app, *si, writer),
|
||||
execWorker(app, userInput, writer),
|
||||
eventloop.SetupSignals(),
|
||||
subscription,
|
||||
stereoscope.Cleanup,
|
||||
@@ -65,10 +53,12 @@ func Run(ctx context.Context, app *config.Application, args []string) error {
|
||||
)
|
||||
}
|
||||
|
||||
func execWorker(app *config.Application, si source.Input, writer sbom.Writer) <-chan error {
|
||||
//nolint:funlen
|
||||
func execWorker(app *config.Application, userInput string, writer sbom.Writer) <-chan error {
|
||||
errs := make(chan error)
|
||||
go func() {
|
||||
defer close(errs)
|
||||
defer bus.Exit()
|
||||
|
||||
app.Secrets.Cataloger.Enabled = true
|
||||
app.FileMetadata.Cataloger.Enabled = true
|
||||
@@ -80,17 +70,53 @@ func execWorker(app *config.Application, si source.Input, writer sbom.Writer) <-
|
||||
return
|
||||
}
|
||||
|
||||
src, cleanup, err := source.New(si, app.Registry.ToOptions(), app.Exclusions)
|
||||
detection, err := source.Detect(
|
||||
userInput,
|
||||
source.DetectConfig{
|
||||
DefaultImageSource: app.DefaultImagePullSource,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
errs <- fmt.Errorf("could not deteremine source: %w", err)
|
||||
return
|
||||
}
|
||||
if cleanup != nil {
|
||||
defer cleanup()
|
||||
|
||||
var platform *image.Platform
|
||||
|
||||
if app.Platform != "" {
|
||||
platform, err = image.NewPlatform(app.Platform)
|
||||
if err != nil {
|
||||
errs <- fmt.Errorf("invalid platform: %w", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
src, err := detection.NewSource(
|
||||
source.DetectionSourceConfig{
|
||||
Alias: source.Alias{
|
||||
Name: app.Source.Name,
|
||||
Version: app.Source.Version,
|
||||
},
|
||||
RegistryOptions: app.Registry.ToOptions(),
|
||||
Platform: platform,
|
||||
Exclude: source.ExcludeConfig{
|
||||
Paths: app.Exclusions,
|
||||
},
|
||||
DigestAlgorithms: nil,
|
||||
BasePath: app.BasePath,
|
||||
},
|
||||
)
|
||||
|
||||
if src != nil {
|
||||
defer src.Close()
|
||||
}
|
||||
if err != nil {
|
||||
errs <- fmt.Errorf("failed to construct source from user input %q: %w", userInput, err)
|
||||
return
|
||||
}
|
||||
|
||||
s := sbom.SBOM{
|
||||
Source: src.Metadata,
|
||||
Source: src.Describe(),
|
||||
Descriptor: sbom.Descriptor{
|
||||
Name: internal.ApplicationName,
|
||||
Version: version.FromBuild().Version,
|
||||
@@ -108,10 +134,10 @@ func execWorker(app *config.Application, si source.Input, writer sbom.Writer) <-
|
||||
|
||||
s.Relationships = append(s.Relationships, packages.MergeRelationships(relationships...)...)
|
||||
|
||||
bus.Publish(partybus.Event{
|
||||
Type: event.Exit,
|
||||
Value: func() error { return writer.Write(s) },
|
||||
})
|
||||
if err := writer.Write(s); err != nil {
|
||||
errs <- fmt.Errorf("failed to write sbom: %w", err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
return errs
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
|
||||
[TestHandler_handleAttestationStarted/attesting_in_progress/task_line - 1]
|
||||
⠋ Creating a thing running a thing
|
||||
---
|
||||
|
||||
[TestHandler_handleAttestationStarted/attesting_in_progress/log - 1]
|
||||
░░ contents
|
||||
░░ of
|
||||
░░ stuff!
|
||||
|
||||
---
|
||||
|
||||
[TestHandler_handleAttestationStarted/attesting_complete/task_line - 1]
|
||||
✔ Created a thing running a thing
|
||||
---
|
||||
|
||||
[TestHandler_handleAttestationStarted/attesting_complete/log - 1]
|
||||
|
||||
---
|
||||
@@ -0,0 +1,16 @@
|
||||
|
||||
[TestHandler_handleCatalogerTaskStarted/cataloging_task_in_progress - 1]
|
||||
some task title [some value]
|
||||
---
|
||||
|
||||
[TestHandler_handleCatalogerTaskStarted/cataloging_sub_task_in_progress - 1]
|
||||
└── some task title [some value]
|
||||
---
|
||||
|
||||
[TestHandler_handleCatalogerTaskStarted/cataloging_sub_task_complete - 1]
|
||||
✔ └── some task done [some value]
|
||||
---
|
||||
|
||||
[TestHandler_handleCatalogerTaskStarted/cataloging_sub_task_complete_with_removal - 1]
|
||||
|
||||
---
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
[TestHandler_handleFetchImage/fetch_image_in_progress - 1]
|
||||
⠋ Loading image ━━━━━━━━━━━━━━━━━━━━ [current] the-image
|
||||
---
|
||||
|
||||
[TestHandler_handleFetchImage/fetch_image_complete - 1]
|
||||
✔ Loaded image the-image
|
||||
---
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
[TestHandler_handleFileDigestsCatalogerStarted/cataloging_in_progress - 1]
|
||||
⠋ Cataloging file digests ━━━━━━━━━━━━━━━━━━━━ [current]
|
||||
---
|
||||
|
||||
[TestHandler_handleFileDigestsCatalogerStarted/cataloging_complete - 1]
|
||||
✔ Cataloged file digests
|
||||
---
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
[TestHandler_handleFileIndexingStarted/cataloging_in_progress - 1]
|
||||
⠋ Indexing file system ━━━━━━━━━━━━━━━━━━━━ [current] /some/path
|
||||
---
|
||||
|
||||
[TestHandler_handleFileIndexingStarted/cataloging_complete - 1]
|
||||
✔ Indexed file system /some/path
|
||||
---
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
[TestHandler_handleFileMetadataCatalogerStarted/cataloging_in_progress - 1]
|
||||
⠋ Cataloging file metadata ━━━━━━━━━━━━━━━━━━━━ [current]
|
||||
---
|
||||
|
||||
[TestHandler_handleFileMetadataCatalogerStarted/cataloging_complete - 1]
|
||||
✔ Cataloged file metadata
|
||||
---
|
||||
@@ -0,0 +1,16 @@
|
||||
|
||||
[TestHandler_handlePackageCatalogerStarted/cataloging_in_progress - 1]
|
||||
⠋ Cataloging packages [50 packages]
|
||||
---
|
||||
|
||||
[TestHandler_handlePackageCatalogerStarted/cataloging_only_files_complete - 1]
|
||||
⠋ Cataloging packages [50 packages]
|
||||
---
|
||||
|
||||
[TestHandler_handlePackageCatalogerStarted/cataloging_only_packages_complete - 1]
|
||||
⠋ Cataloging packages [100 packages]
|
||||
---
|
||||
|
||||
[TestHandler_handlePackageCatalogerStarted/cataloging_complete - 1]
|
||||
✔ Cataloged packages [100 packages]
|
||||
---
|
||||
@@ -0,0 +1,12 @@
|
||||
|
||||
[Test_dockerPullStatusFormatter_Render/pulling - 1]
|
||||
3 Layers▕▅▃ ▏[12 B / 30 B]
|
||||
---
|
||||
|
||||
[Test_dockerPullStatusFormatter_Render/download_complete - 1]
|
||||
3 Layers▕█▃ ▏[30 B] Extracting...
|
||||
---
|
||||
|
||||
[Test_dockerPullStatusFormatter_Render/complete - 1]
|
||||
3 Layers▕███▏[30 B] Extracting...
|
||||
---
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
[TestHandler_handleReadImage/read_image_in_progress - 1]
|
||||
⠋ Parsing image ━━━━━━━━━━━━━━━━━━━━ id
|
||||
---
|
||||
|
||||
[TestHandler_handleReadImage/read_image_complete - 1]
|
||||
✔ Parsed image id
|
||||
---
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
[TestHandler_handleSecretsCatalogerStarted/cataloging_in_progress - 1]
|
||||
⠋ Cataloging secrets ━━━━━━━━━━━━━━━━━━━━ [64 secrets]
|
||||
---
|
||||
|
||||
[TestHandler_handleSecretsCatalogerStarted/cataloging_complete - 1]
|
||||
✔ Cataloged secrets [64 secrets]
|
||||
---
|
||||
@@ -0,0 +1,255 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/google/uuid"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
"github.com/zyedidia/generic/queue"
|
||||
|
||||
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
|
||||
)
|
||||
|
||||
var (
|
||||
_ tea.Model = (*attestLogFrame)(nil)
|
||||
)
|
||||
|
||||
type attestLogFrame struct {
|
||||
reader *backgroundLineReader
|
||||
prog progress.Progressable
|
||||
lines []string
|
||||
completed bool
|
||||
failed bool
|
||||
windowSize tea.WindowSizeMsg
|
||||
|
||||
id uint32
|
||||
sequence int
|
||||
|
||||
updateDuration time.Duration
|
||||
borderStype lipgloss.Style
|
||||
}
|
||||
|
||||
// attestLogFrameTickMsg indicates that the timer has ticked and we should render a frame.
|
||||
type attestLogFrameTickMsg struct {
|
||||
Time time.Time
|
||||
Sequence int
|
||||
ID uint32
|
||||
}
|
||||
|
||||
type backgroundLineReader struct {
|
||||
limit int
|
||||
lines *queue.Queue[string]
|
||||
lock *sync.RWMutex
|
||||
|
||||
// This is added specifically for tests to assert when the background reader is done.
|
||||
// The main UI uses the global ui wait group from the handler to otherwise block
|
||||
// Shared concerns among multiple model made it difficult to test using the global wait group
|
||||
// so this is added to allow tests to assert when the background reader is done.
|
||||
running *sync.WaitGroup
|
||||
}
|
||||
|
||||
func (m *Handler) handleAttestationStarted(e partybus.Event) []tea.Model {
|
||||
reader, prog, taskInfo, err := syftEventParsers.ParseAttestationStartedEvent(e)
|
||||
if err != nil {
|
||||
log.WithFields("error", err).Warn("unable to parse event")
|
||||
return nil
|
||||
}
|
||||
|
||||
stage := progress.Stage{}
|
||||
|
||||
tsk := m.newTaskProgress(
|
||||
taskprogress.Title{
|
||||
Default: taskInfo.Title.Default,
|
||||
Running: taskInfo.Title.WhileRunning,
|
||||
Success: taskInfo.Title.OnSuccess,
|
||||
},
|
||||
taskprogress.WithStagedProgressable(
|
||||
struct {
|
||||
progress.Progressable
|
||||
progress.Stager
|
||||
}{
|
||||
Progressable: prog,
|
||||
Stager: &stage,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
tsk.HideStageOnSuccess = false
|
||||
|
||||
if taskInfo.Context != "" {
|
||||
tsk.Context = []string{taskInfo.Context}
|
||||
}
|
||||
|
||||
borderStyle := tsk.HintStyle
|
||||
|
||||
return []tea.Model{
|
||||
tsk,
|
||||
newLogFrame(newBackgroundLineReader(m.Running, reader, &stage), prog, borderStyle),
|
||||
}
|
||||
}
|
||||
|
||||
func newLogFrame(reader *backgroundLineReader, prog progress.Progressable, borderStyle lipgloss.Style) attestLogFrame {
|
||||
return attestLogFrame{
|
||||
reader: reader,
|
||||
prog: prog,
|
||||
id: uuid.Must(uuid.NewUUID()).ID(),
|
||||
updateDuration: 250 * time.Millisecond,
|
||||
borderStype: borderStyle,
|
||||
}
|
||||
}
|
||||
|
||||
func newBackgroundLineReader(wg *sync.WaitGroup, reader io.Reader, stage *progress.Stage) *backgroundLineReader {
|
||||
r := &backgroundLineReader{
|
||||
limit: 7,
|
||||
lock: &sync.RWMutex{},
|
||||
lines: queue.New[string](),
|
||||
running: &sync.WaitGroup{},
|
||||
}
|
||||
|
||||
// tracks the background reader for the global handler wait group
|
||||
wg.Add(1)
|
||||
|
||||
// tracks the background reader for the local wait group (used in tests to decouple from the global handler wait group)
|
||||
r.running.Add(1)
|
||||
|
||||
go func() {
|
||||
r.read(reader, stage)
|
||||
wg.Done()
|
||||
r.running.Done()
|
||||
}()
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (l *backgroundLineReader) read(reader io.Reader, stage *progress.Stage) {
|
||||
s := bufio.NewScanner(reader)
|
||||
|
||||
for s.Scan() {
|
||||
l.lock.Lock()
|
||||
|
||||
text := s.Text()
|
||||
l.lines.Enqueue(text)
|
||||
|
||||
if strings.Contains(text, "tlog entry created with index") {
|
||||
fields := strings.SplitN(text, ":", 2)
|
||||
present := text
|
||||
if len(fields) == 2 {
|
||||
present = fmt.Sprintf("transparency log index: %s", fields[1])
|
||||
}
|
||||
stage.Current = present
|
||||
} else if strings.Contains(text, "WARNING: skipping transparency log upload") {
|
||||
stage.Current = "transparency log upload skipped"
|
||||
}
|
||||
|
||||
// only show the last X lines of the shell output
|
||||
for l.lines.Len() > l.limit {
|
||||
l.lines.Dequeue()
|
||||
}
|
||||
|
||||
l.lock.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (l backgroundLineReader) Lines() []string {
|
||||
l.lock.RLock()
|
||||
defer l.lock.RUnlock()
|
||||
|
||||
var lines []string
|
||||
|
||||
l.lines.Each(func(line string) {
|
||||
lines = append(lines, line)
|
||||
})
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
func (l attestLogFrame) Init() tea.Cmd {
|
||||
// this is the periodic update of state information
|
||||
return func() tea.Msg {
|
||||
return attestLogFrameTickMsg{
|
||||
// The time at which the tick occurred.
|
||||
Time: time.Now(),
|
||||
|
||||
// The ID of the log frame that this message belongs to. This can be
|
||||
// helpful when routing messages, however bear in mind that log frames
|
||||
// will ignore messages that don't contain ID by default.
|
||||
ID: l.id,
|
||||
|
||||
Sequence: l.sequence,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l attestLogFrame) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
l.windowSize = msg
|
||||
return l, nil
|
||||
|
||||
case attestLogFrameTickMsg:
|
||||
l.lines = l.reader.Lines()
|
||||
|
||||
l.completed = progress.IsCompleted(l.prog)
|
||||
err := l.prog.Error()
|
||||
l.failed = err != nil && !progress.IsErrCompleted(err)
|
||||
|
||||
tickCmd := l.handleTick(msg)
|
||||
|
||||
return l, tickCmd
|
||||
}
|
||||
|
||||
return l, nil
|
||||
}
|
||||
|
||||
func (l attestLogFrame) View() string {
|
||||
if l.completed && !l.failed {
|
||||
return ""
|
||||
}
|
||||
|
||||
sb := strings.Builder{}
|
||||
|
||||
for _, line := range l.lines {
|
||||
sb.WriteString(fmt.Sprintf(" %s %s\n", l.borderStype.Render("░░"), line))
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (l attestLogFrame) queueNextTick() tea.Cmd {
|
||||
return tea.Tick(l.updateDuration, func(t time.Time) tea.Msg {
|
||||
return attestLogFrameTickMsg{
|
||||
Time: t,
|
||||
ID: l.id,
|
||||
Sequence: l.sequence,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (l *attestLogFrame) handleTick(msg attestLogFrameTickMsg) tea.Cmd {
|
||||
// If an ID is set, and the ID doesn't belong to this log frame, reject the message.
|
||||
if msg.ID > 0 && msg.ID != l.id {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If a sequence is set, and it's not the one we expect, reject the message.
|
||||
// This prevents the log frame from receiving too many messages and
|
||||
// thus updating too frequently.
|
||||
if msg.Sequence > 0 && msg.Sequence != l.sequence {
|
||||
return nil
|
||||
}
|
||||
|
||||
l.sequence++
|
||||
|
||||
// note: even if the log is completed we should still respond to stage changes and window size events
|
||||
return l.queueNextTick()
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/gkampitakis/go-snaps/snaps"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||
syftEvent "github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/syft/event/monitor"
|
||||
)
|
||||
|
||||
func TestHandler_handleAttestationStarted(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
eventFn func(*testing.T) partybus.Event
|
||||
iterations int
|
||||
}{
|
||||
{
|
||||
name: "attesting in progress",
|
||||
// note: this model depends on a background reader. Multiple iterations ensures that the
|
||||
// reader has time to at least start and process the test fixture before the runModel
|
||||
// test harness completes (which is a fake event loop anyway).
|
||||
iterations: 1,
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
reader := strings.NewReader("contents\nof\nstuff!")
|
||||
|
||||
src := monitor.GenericTask{
|
||||
Title: monitor.Title{
|
||||
Default: "Create a thing",
|
||||
WhileRunning: "Creating a thing",
|
||||
OnSuccess: "Created a thing",
|
||||
},
|
||||
Context: "running a thing",
|
||||
}
|
||||
|
||||
mon := progress.NewManual(-1)
|
||||
mon.Set(50)
|
||||
|
||||
value := &monitor.ShellProgress{
|
||||
Reader: reader,
|
||||
Progressable: mon,
|
||||
}
|
||||
|
||||
return partybus.Event{
|
||||
Type: syftEvent.AttestationStarted,
|
||||
Source: src,
|
||||
Value: value,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "attesting complete",
|
||||
// note: this model depends on a background reader. Multiple iterations ensures that the
|
||||
// reader has time to at least start and process the test fixture before the runModel
|
||||
// test harness completes (which is a fake event loop anyway).
|
||||
iterations: 1,
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
reader := strings.NewReader("contents\nof\nstuff!")
|
||||
|
||||
src := monitor.GenericTask{
|
||||
Title: monitor.Title{
|
||||
Default: "Create a thing",
|
||||
WhileRunning: "Creating a thing",
|
||||
OnSuccess: "Created a thing",
|
||||
},
|
||||
Context: "running a thing",
|
||||
}
|
||||
|
||||
mon := progress.NewManual(-1)
|
||||
mon.Set(50)
|
||||
mon.SetCompleted()
|
||||
|
||||
value := &monitor.ShellProgress{
|
||||
Reader: reader,
|
||||
Progressable: mon,
|
||||
}
|
||||
|
||||
return partybus.Event{
|
||||
Type: syftEvent.AttestationStarted,
|
||||
Source: src,
|
||||
Value: value,
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
event := tt.eventFn(t)
|
||||
handler := New(DefaultHandlerConfig())
|
||||
handler.WindowSize = tea.WindowSizeMsg{
|
||||
Width: 100,
|
||||
Height: 80,
|
||||
}
|
||||
|
||||
models := handler.Handle(event)
|
||||
require.Len(t, models, 2)
|
||||
|
||||
t.Run("task line", func(t *testing.T) {
|
||||
tsk, ok := models[0].(taskprogress.Model)
|
||||
require.True(t, ok)
|
||||
|
||||
got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
|
||||
Time: time.Now(),
|
||||
Sequence: tsk.Sequence(),
|
||||
ID: tsk.ID(),
|
||||
})
|
||||
t.Log(got)
|
||||
snaps.MatchSnapshot(t, got)
|
||||
})
|
||||
|
||||
t.Run("log", func(t *testing.T) {
|
||||
log, ok := models[1].(attestLogFrame)
|
||||
require.True(t, ok)
|
||||
got := runModel(t, log, tt.iterations, attestLogFrameTickMsg{
|
||||
Time: time.Now(),
|
||||
Sequence: log.sequence,
|
||||
ID: log.id,
|
||||
}, log.reader.running)
|
||||
t.Log(got)
|
||||
snaps.MatchSnapshot(t, got)
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/event/monitor"
|
||||
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
|
||||
)
|
||||
|
||||
var _ progress.Stager = (*catalogerTaskStageAdapter)(nil)
|
||||
|
||||
type catalogerTaskStageAdapter struct {
|
||||
mon *monitor.CatalogerTask
|
||||
}
|
||||
|
||||
func newCatalogerTaskStageAdapter(mon *monitor.CatalogerTask) *catalogerTaskStageAdapter {
|
||||
return &catalogerTaskStageAdapter{
|
||||
mon: mon,
|
||||
}
|
||||
}
|
||||
|
||||
func (c catalogerTaskStageAdapter) Stage() string {
|
||||
return c.mon.GetValue()
|
||||
}
|
||||
|
||||
func (m *Handler) handleCatalogerTaskStarted(e partybus.Event) []tea.Model {
|
||||
mon, err := syftEventParsers.ParseCatalogerTaskStarted(e)
|
||||
if err != nil {
|
||||
log.WithFields("error", err).Warn("unable to parse event")
|
||||
return nil
|
||||
}
|
||||
|
||||
var prefix string
|
||||
if mon.SubStatus {
|
||||
// TODO: support list of sub-statuses, not just a single leaf
|
||||
prefix = "└── "
|
||||
}
|
||||
|
||||
tsk := m.newTaskProgress(
|
||||
taskprogress.Title{
|
||||
// TODO: prefix should not be part of the title, but instead a separate field that is aware of the tree structure
|
||||
Default: prefix + mon.Title,
|
||||
Running: prefix + mon.Title,
|
||||
Success: prefix + mon.TitleOnCompletion,
|
||||
},
|
||||
taskprogress.WithStagedProgressable(
|
||||
struct {
|
||||
progress.Stager
|
||||
progress.Progressable
|
||||
}{
|
||||
Progressable: mon.GetMonitor(),
|
||||
Stager: newCatalogerTaskStageAdapter(mon),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
// TODO: this isn't ideal since the model stays around after it is no longer needed, but it works for now
|
||||
tsk.HideOnSuccess = mon.RemoveOnCompletion
|
||||
tsk.HideStageOnSuccess = false
|
||||
tsk.HideProgressOnSuccess = false
|
||||
|
||||
tsk.TitleStyle = lipgloss.NewStyle()
|
||||
// TODO: this is a hack to get the spinner to not show up, but ideally the component would support making the spinner optional
|
||||
tsk.Spinner.Spinner.Frames = []string{" "}
|
||||
|
||||
return []tea.Model{tsk}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/gkampitakis/go-snaps/snaps"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
|
||||
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||
syftEvent "github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/syft/event/monitor"
|
||||
)
|
||||
|
||||
func TestHandler_handleCatalogerTaskStarted(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
eventFn func(*testing.T) partybus.Event
|
||||
iterations int
|
||||
}{
|
||||
{
|
||||
name: "cataloging task in progress",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
src := &monitor.CatalogerTask{
|
||||
SubStatus: false,
|
||||
RemoveOnCompletion: false,
|
||||
Title: "some task title",
|
||||
TitleOnCompletion: "some task done",
|
||||
}
|
||||
|
||||
src.SetValue("some value")
|
||||
|
||||
return partybus.Event{
|
||||
Type: syftEvent.CatalogerTaskStarted,
|
||||
Source: src,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cataloging sub task in progress",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
src := &monitor.CatalogerTask{
|
||||
SubStatus: true,
|
||||
RemoveOnCompletion: false,
|
||||
Title: "some task title",
|
||||
TitleOnCompletion: "some task done",
|
||||
}
|
||||
|
||||
src.SetValue("some value")
|
||||
|
||||
return partybus.Event{
|
||||
Type: syftEvent.CatalogerTaskStarted,
|
||||
Source: src,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cataloging sub task complete",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
src := &monitor.CatalogerTask{
|
||||
SubStatus: true,
|
||||
RemoveOnCompletion: false,
|
||||
Title: "some task title",
|
||||
TitleOnCompletion: "some task done",
|
||||
}
|
||||
|
||||
src.SetValue("some value")
|
||||
src.SetCompleted()
|
||||
|
||||
return partybus.Event{
|
||||
Type: syftEvent.CatalogerTaskStarted,
|
||||
Source: src,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cataloging sub task complete with removal",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
src := &monitor.CatalogerTask{
|
||||
SubStatus: true,
|
||||
RemoveOnCompletion: true,
|
||||
Title: "some task title",
|
||||
TitleOnCompletion: "some task done",
|
||||
}
|
||||
|
||||
src.SetValue("some value")
|
||||
src.SetCompleted()
|
||||
|
||||
return partybus.Event{
|
||||
Type: syftEvent.CatalogerTaskStarted,
|
||||
Source: src,
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
event := tt.eventFn(t)
|
||||
handler := New(DefaultHandlerConfig())
|
||||
handler.WindowSize = tea.WindowSizeMsg{
|
||||
Width: 100,
|
||||
Height: 80,
|
||||
}
|
||||
|
||||
models := handler.Handle(event)
|
||||
require.Len(t, models, 1)
|
||||
model := models[0]
|
||||
|
||||
tsk, ok := model.(taskprogress.Model)
|
||||
require.True(t, ok)
|
||||
|
||||
got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
|
||||
Time: time.Now(),
|
||||
Sequence: tsk.Sequence(),
|
||||
ID: tsk.ID(),
|
||||
})
|
||||
t.Log(got)
|
||||
snaps.MatchSnapshot(t, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
|
||||
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||
stereoEventParsers "github.com/anchore/stereoscope/pkg/event/parsers"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
)
|
||||
|
||||
func (m *Handler) handleFetchImage(e partybus.Event) []tea.Model {
|
||||
imgName, prog, err := stereoEventParsers.ParseFetchImage(e)
|
||||
if err != nil {
|
||||
log.WithFields("error", err).Warn("unable to parse event")
|
||||
return nil
|
||||
}
|
||||
|
||||
tsk := m.newTaskProgress(
|
||||
taskprogress.Title{
|
||||
Default: "Load image",
|
||||
Running: "Loading image",
|
||||
Success: "Loaded image",
|
||||
},
|
||||
taskprogress.WithStagedProgressable(prog),
|
||||
)
|
||||
if imgName != "" {
|
||||
tsk.Context = []string{imgName}
|
||||
}
|
||||
|
||||
return []tea.Model{tsk}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/gkampitakis/go-snaps/snaps"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||
stereoscopeEvent "github.com/anchore/stereoscope/pkg/event"
|
||||
)
|
||||
|
||||
func TestHandler_handleFetchImage(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
eventFn func(*testing.T) partybus.Event
|
||||
iterations int
|
||||
}{
|
||||
{
|
||||
name: "fetch image in progress",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
prog := &progress.Manual{}
|
||||
prog.SetTotal(100)
|
||||
prog.Set(50)
|
||||
|
||||
mon := struct {
|
||||
progress.Progressable
|
||||
progress.Stager
|
||||
}{
|
||||
Progressable: prog,
|
||||
Stager: &progress.Stage{
|
||||
Current: "current",
|
||||
},
|
||||
}
|
||||
|
||||
return partybus.Event{
|
||||
Type: stereoscopeEvent.FetchImage,
|
||||
Source: "the-image",
|
||||
Value: mon,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "fetch image complete",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
prog := &progress.Manual{}
|
||||
prog.SetTotal(100)
|
||||
prog.Set(100)
|
||||
prog.SetCompleted()
|
||||
|
||||
mon := struct {
|
||||
progress.Progressable
|
||||
progress.Stager
|
||||
}{
|
||||
Progressable: prog,
|
||||
Stager: &progress.Stage{
|
||||
Current: "current",
|
||||
},
|
||||
}
|
||||
|
||||
return partybus.Event{
|
||||
Type: stereoscopeEvent.FetchImage,
|
||||
Source: "the-image",
|
||||
Value: mon,
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
event := tt.eventFn(t)
|
||||
handler := New(DefaultHandlerConfig())
|
||||
handler.WindowSize = tea.WindowSizeMsg{
|
||||
Width: 100,
|
||||
Height: 80,
|
||||
}
|
||||
|
||||
models := handler.Handle(event)
|
||||
require.Len(t, models, 1)
|
||||
model := models[0]
|
||||
|
||||
tsk, ok := model.(taskprogress.Model)
|
||||
require.True(t, ok)
|
||||
|
||||
got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
|
||||
Time: time.Now(),
|
||||
Sequence: tsk.Sequence(),
|
||||
ID: tsk.ID(),
|
||||
})
|
||||
t.Log(got)
|
||||
snaps.MatchSnapshot(t, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
|
||||
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
|
||||
)
|
||||
|
||||
func (m *Handler) handleFileDigestsCatalogerStarted(e partybus.Event) []tea.Model {
|
||||
prog, err := syftEventParsers.ParseFileDigestsCatalogingStarted(e)
|
||||
if err != nil {
|
||||
log.WithFields("error", err).Warn("unable to parse event")
|
||||
return nil
|
||||
}
|
||||
|
||||
tsk := m.newTaskProgress(
|
||||
taskprogress.Title{
|
||||
Default: "Catalog file digests",
|
||||
Running: "Cataloging file digests",
|
||||
Success: "Cataloged file digests",
|
||||
}, taskprogress.WithStagedProgressable(prog),
|
||||
)
|
||||
|
||||
return []tea.Model{tsk}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/gkampitakis/go-snaps/snaps"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||
syftEvent "github.com/anchore/syft/syft/event"
|
||||
)
|
||||
|
||||
func TestHandler_handleFileDigestsCatalogerStarted(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
eventFn func(*testing.T) partybus.Event
|
||||
iterations int
|
||||
}{
|
||||
{
|
||||
name: "cataloging in progress",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
prog := &progress.Manual{}
|
||||
prog.SetTotal(100)
|
||||
prog.Set(50)
|
||||
|
||||
mon := struct {
|
||||
progress.Progressable
|
||||
progress.Stager
|
||||
}{
|
||||
Progressable: prog,
|
||||
Stager: &progress.Stage{
|
||||
Current: "current",
|
||||
},
|
||||
}
|
||||
|
||||
return partybus.Event{
|
||||
Type: syftEvent.FileDigestsCatalogerStarted,
|
||||
Value: mon,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cataloging complete",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
prog := &progress.Manual{}
|
||||
prog.SetTotal(100)
|
||||
prog.Set(100)
|
||||
prog.SetCompleted()
|
||||
|
||||
mon := struct {
|
||||
progress.Progressable
|
||||
progress.Stager
|
||||
}{
|
||||
Progressable: prog,
|
||||
Stager: &progress.Stage{
|
||||
Current: "current",
|
||||
},
|
||||
}
|
||||
|
||||
return partybus.Event{
|
||||
Type: syftEvent.FileDigestsCatalogerStarted,
|
||||
Value: mon,
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
event := tt.eventFn(t)
|
||||
handler := New(DefaultHandlerConfig())
|
||||
handler.WindowSize = tea.WindowSizeMsg{
|
||||
Width: 100,
|
||||
Height: 80,
|
||||
}
|
||||
|
||||
models := handler.Handle(event)
|
||||
require.Len(t, models, 1)
|
||||
model := models[0]
|
||||
|
||||
tsk, ok := model.(taskprogress.Model)
|
||||
require.True(t, ok)
|
||||
|
||||
got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
|
||||
Time: time.Now(),
|
||||
Sequence: tsk.Sequence(),
|
||||
ID: tsk.ID(),
|
||||
})
|
||||
t.Log(got)
|
||||
snaps.MatchSnapshot(t, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
|
||||
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
|
||||
)
|
||||
|
||||
func (m *Handler) handleFileIndexingStarted(e partybus.Event) []tea.Model {
|
||||
path, prog, err := syftEventParsers.ParseFileIndexingStarted(e)
|
||||
if err != nil {
|
||||
log.WithFields("error", err).Warn("unable to parse event")
|
||||
return nil
|
||||
}
|
||||
|
||||
tsk := m.newTaskProgress(
|
||||
taskprogress.Title{
|
||||
Default: "Index files system",
|
||||
Running: "Indexing file system",
|
||||
Success: "Indexed file system",
|
||||
},
|
||||
taskprogress.WithStagedProgressable(prog),
|
||||
)
|
||||
|
||||
tsk.Context = []string{path}
|
||||
|
||||
return []tea.Model{tsk}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/gkampitakis/go-snaps/snaps"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||
syftEvent "github.com/anchore/syft/syft/event"
|
||||
)
|
||||
|
||||
func TestHandler_handleFileIndexingStarted(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
eventFn func(*testing.T) partybus.Event
|
||||
iterations int
|
||||
}{
|
||||
{
|
||||
name: "cataloging in progress",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
prog := &progress.Manual{}
|
||||
prog.SetTotal(100)
|
||||
prog.Set(50)
|
||||
|
||||
mon := struct {
|
||||
progress.Progressable
|
||||
progress.Stager
|
||||
}{
|
||||
Progressable: prog,
|
||||
Stager: &progress.Stage{
|
||||
Current: "current",
|
||||
},
|
||||
}
|
||||
|
||||
return partybus.Event{
|
||||
Type: syftEvent.FileIndexingStarted,
|
||||
Source: "/some/path",
|
||||
Value: mon,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cataloging complete",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
prog := &progress.Manual{}
|
||||
prog.SetTotal(100)
|
||||
prog.Set(100)
|
||||
prog.SetCompleted()
|
||||
|
||||
mon := struct {
|
||||
progress.Progressable
|
||||
progress.Stager
|
||||
}{
|
||||
Progressable: prog,
|
||||
Stager: &progress.Stage{
|
||||
Current: "current",
|
||||
},
|
||||
}
|
||||
|
||||
return partybus.Event{
|
||||
Type: syftEvent.FileIndexingStarted,
|
||||
Source: "/some/path",
|
||||
Value: mon,
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
event := tt.eventFn(t)
|
||||
handler := New(DefaultHandlerConfig())
|
||||
handler.WindowSize = tea.WindowSizeMsg{
|
||||
Width: 100,
|
||||
Height: 80,
|
||||
}
|
||||
|
||||
models := handler.Handle(event)
|
||||
require.Len(t, models, 1)
|
||||
model := models[0]
|
||||
|
||||
tsk, ok := model.(taskprogress.Model)
|
||||
require.True(t, ok)
|
||||
|
||||
got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
|
||||
Time: time.Now(),
|
||||
Sequence: tsk.Sequence(),
|
||||
ID: tsk.ID(),
|
||||
})
|
||||
t.Log(got)
|
||||
snaps.MatchSnapshot(t, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
|
||||
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
|
||||
)
|
||||
|
||||
func (m *Handler) handleFileMetadataCatalogerStarted(e partybus.Event) []tea.Model {
|
||||
prog, err := syftEventParsers.ParseFileMetadataCatalogingStarted(e)
|
||||
if err != nil {
|
||||
log.WithFields("error", err).Warn("unable to parse event")
|
||||
return nil
|
||||
}
|
||||
|
||||
tsk := m.newTaskProgress(
|
||||
taskprogress.Title{
|
||||
Default: "Catalog file metadata",
|
||||
Running: "Cataloging file metadata",
|
||||
Success: "Cataloged file metadata",
|
||||
},
|
||||
taskprogress.WithStagedProgressable(prog),
|
||||
)
|
||||
|
||||
return []tea.Model{tsk}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/gkampitakis/go-snaps/snaps"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||
syftEvent "github.com/anchore/syft/syft/event"
|
||||
)
|
||||
|
||||
func TestHandler_handleFileMetadataCatalogerStarted(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
eventFn func(*testing.T) partybus.Event
|
||||
iterations int
|
||||
}{
|
||||
{
|
||||
name: "cataloging in progress",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
prog := &progress.Manual{}
|
||||
prog.SetTotal(100)
|
||||
prog.Set(50)
|
||||
|
||||
mon := struct {
|
||||
progress.Progressable
|
||||
progress.Stager
|
||||
}{
|
||||
Progressable: prog,
|
||||
Stager: &progress.Stage{
|
||||
Current: "current",
|
||||
},
|
||||
}
|
||||
|
||||
return partybus.Event{
|
||||
Type: syftEvent.FileMetadataCatalogerStarted,
|
||||
Value: mon,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cataloging complete",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
prog := &progress.Manual{}
|
||||
prog.SetTotal(100)
|
||||
prog.Set(100)
|
||||
prog.SetCompleted()
|
||||
|
||||
mon := struct {
|
||||
progress.Progressable
|
||||
progress.Stager
|
||||
}{
|
||||
Progressable: prog,
|
||||
Stager: &progress.Stage{
|
||||
Current: "current",
|
||||
},
|
||||
}
|
||||
|
||||
return partybus.Event{
|
||||
Type: syftEvent.FileMetadataCatalogerStarted,
|
||||
Value: mon,
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
event := tt.eventFn(t)
|
||||
handler := New(DefaultHandlerConfig())
|
||||
handler.WindowSize = tea.WindowSizeMsg{
|
||||
Width: 100,
|
||||
Height: 80,
|
||||
}
|
||||
|
||||
models := handler.Handle(event)
|
||||
require.Len(t, models, 1)
|
||||
model := models[0]
|
||||
|
||||
tsk, ok := model.(taskprogress.Model)
|
||||
require.True(t, ok)
|
||||
|
||||
got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
|
||||
Time: time.Now(),
|
||||
Sequence: tsk.Sequence(),
|
||||
ID: tsk.ID(),
|
||||
})
|
||||
t.Log(got)
|
||||
snaps.MatchSnapshot(t, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger"
|
||||
)
|
||||
|
||||
var _ progress.StagedProgressable = (*packageCatalogerProgressAdapter)(nil)
|
||||
|
||||
type packageCatalogerProgressAdapter struct {
|
||||
monitor *cataloger.Monitor
|
||||
monitors []progress.Monitorable
|
||||
}
|
||||
|
||||
func newPackageCatalogerProgressAdapter(monitor *cataloger.Monitor) packageCatalogerProgressAdapter {
|
||||
return packageCatalogerProgressAdapter{
|
||||
monitor: monitor,
|
||||
monitors: []progress.Monitorable{
|
||||
monitor.FilesProcessed,
|
||||
monitor.PackagesDiscovered,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p packageCatalogerProgressAdapter) Stage() string {
|
||||
return fmt.Sprintf("%d packages", p.monitor.PackagesDiscovered.Current())
|
||||
}
|
||||
|
||||
func (p packageCatalogerProgressAdapter) Current() int64 {
|
||||
return p.monitor.PackagesDiscovered.Current()
|
||||
}
|
||||
|
||||
func (p packageCatalogerProgressAdapter) Error() error {
|
||||
completedMonitors := 0
|
||||
for _, monitor := range p.monitors {
|
||||
err := monitor.Error()
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
if progress.IsErrCompleted(err) {
|
||||
completedMonitors++
|
||||
continue
|
||||
}
|
||||
// something went wrong
|
||||
return err
|
||||
}
|
||||
if completedMonitors == len(p.monitors) && len(p.monitors) > 0 {
|
||||
return p.monitors[0].Error()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p packageCatalogerProgressAdapter) Size() int64 {
|
||||
// this is an inherently unknown value (indeterminate total number of packages to discover)
|
||||
return -1
|
||||
}
|
||||
|
||||
func (m *Handler) handlePackageCatalogerStarted(e partybus.Event) []tea.Model {
|
||||
monitor, err := syftEventParsers.ParsePackageCatalogerStarted(e)
|
||||
if err != nil {
|
||||
log.WithFields("error", err).Warn("unable to parse event")
|
||||
return nil
|
||||
}
|
||||
|
||||
tsk := m.newTaskProgress(
|
||||
taskprogress.Title{
|
||||
Default: "Catalog packages",
|
||||
Running: "Cataloging packages",
|
||||
Success: "Cataloged packages",
|
||||
},
|
||||
taskprogress.WithStagedProgressable(
|
||||
newPackageCatalogerProgressAdapter(monitor),
|
||||
),
|
||||
)
|
||||
|
||||
tsk.HideStageOnSuccess = false
|
||||
|
||||
return []tea.Model{tsk}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/gkampitakis/go-snaps/snaps"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||
syftEvent "github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger"
|
||||
)
|
||||
|
||||
func TestHandler_handlePackageCatalogerStarted(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
eventFn func(*testing.T) partybus.Event
|
||||
iterations int
|
||||
}{
|
||||
{
|
||||
name: "cataloging in progress",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
prog := &progress.Manual{}
|
||||
prog.SetTotal(100)
|
||||
prog.Set(50)
|
||||
|
||||
mon := cataloger.Monitor{
|
||||
FilesProcessed: progress.NewManual(-1),
|
||||
PackagesDiscovered: prog,
|
||||
}
|
||||
|
||||
return partybus.Event{
|
||||
Type: syftEvent.PackageCatalogerStarted,
|
||||
Value: mon,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cataloging only files complete",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
prog := &progress.Manual{}
|
||||
prog.SetTotal(100)
|
||||
prog.Set(50)
|
||||
|
||||
files := progress.NewManual(-1)
|
||||
files.SetCompleted()
|
||||
|
||||
mon := cataloger.Monitor{
|
||||
FilesProcessed: files,
|
||||
PackagesDiscovered: prog,
|
||||
}
|
||||
|
||||
return partybus.Event{
|
||||
Type: syftEvent.PackageCatalogerStarted,
|
||||
Value: mon,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cataloging only packages complete",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
prog := &progress.Manual{}
|
||||
prog.SetTotal(100)
|
||||
prog.Set(100)
|
||||
prog.SetCompleted()
|
||||
|
||||
files := progress.NewManual(-1)
|
||||
|
||||
mon := cataloger.Monitor{
|
||||
FilesProcessed: files,
|
||||
PackagesDiscovered: prog,
|
||||
}
|
||||
|
||||
return partybus.Event{
|
||||
Type: syftEvent.PackageCatalogerStarted,
|
||||
Value: mon,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cataloging complete",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
prog := &progress.Manual{}
|
||||
prog.SetTotal(100)
|
||||
prog.Set(100)
|
||||
prog.SetCompleted()
|
||||
|
||||
files := progress.NewManual(-1)
|
||||
files.SetCompleted()
|
||||
|
||||
mon := cataloger.Monitor{
|
||||
FilesProcessed: files,
|
||||
PackagesDiscovered: prog,
|
||||
}
|
||||
|
||||
return partybus.Event{
|
||||
Type: syftEvent.PackageCatalogerStarted,
|
||||
Value: mon,
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
event := tt.eventFn(t)
|
||||
handler := New(DefaultHandlerConfig())
|
||||
handler.WindowSize = tea.WindowSizeMsg{
|
||||
Width: 100,
|
||||
Height: 80,
|
||||
}
|
||||
|
||||
models := handler.Handle(event)
|
||||
require.Len(t, models, 1)
|
||||
model := models[0]
|
||||
|
||||
tsk, ok := model.(taskprogress.Model)
|
||||
require.True(t, ok)
|
||||
|
||||
got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
|
||||
Time: time.Now(),
|
||||
Sequence: tsk.Sequence(),
|
||||
ID: tsk.ID(),
|
||||
})
|
||||
t.Log(got)
|
||||
snaps.MatchSnapshot(t, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||
stereoscopeParsers "github.com/anchore/stereoscope/pkg/event/parsers"
|
||||
"github.com/anchore/stereoscope/pkg/image/docker"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
)
|
||||
|
||||
var _ interface {
|
||||
progress.Stager
|
||||
progress.Progressable
|
||||
} = (*dockerPullProgressAdapter)(nil)
|
||||
|
||||
type dockerPullStatus interface {
|
||||
Complete() bool
|
||||
Layers() []docker.LayerID
|
||||
Current(docker.LayerID) docker.LayerState
|
||||
}
|
||||
|
||||
type dockerPullProgressAdapter struct {
|
||||
status dockerPullStatus
|
||||
formatter dockerPullStatusFormatter
|
||||
}
|
||||
|
||||
type dockerPullStatusFormatter struct {
|
||||
auxInfoStyle lipgloss.Style
|
||||
dockerPullCompletedStyle lipgloss.Style
|
||||
dockerPullDownloadStyle lipgloss.Style
|
||||
dockerPullExtractStyle lipgloss.Style
|
||||
dockerPullStageChars []string
|
||||
layerCaps []string
|
||||
}
|
||||
|
||||
func (m *Handler) handlePullDockerImage(e partybus.Event) []tea.Model {
|
||||
_, pullStatus, err := stereoscopeParsers.ParsePullDockerImage(e)
|
||||
if err != nil {
|
||||
log.WithFields("error", err).Warn("unable to parse event")
|
||||
return nil
|
||||
}
|
||||
|
||||
tsk := m.newTaskProgress(
|
||||
taskprogress.Title{
|
||||
Default: "Pull image",
|
||||
Running: "Pulling image",
|
||||
Success: "Pulled image",
|
||||
},
|
||||
taskprogress.WithStagedProgressable(
|
||||
newDockerPullProgressAdapter(pullStatus),
|
||||
),
|
||||
)
|
||||
|
||||
tsk.HintStyle = lipgloss.NewStyle()
|
||||
tsk.HintEndCaps = nil
|
||||
|
||||
return []tea.Model{tsk}
|
||||
}
|
||||
|
||||
func newDockerPullProgressAdapter(status dockerPullStatus) *dockerPullProgressAdapter {
|
||||
return &dockerPullProgressAdapter{
|
||||
status: status,
|
||||
formatter: newDockerPullStatusFormatter(),
|
||||
}
|
||||
}
|
||||
|
||||
func newDockerPullStatusFormatter() dockerPullStatusFormatter {
|
||||
return dockerPullStatusFormatter{
|
||||
auxInfoStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("#777777")),
|
||||
dockerPullCompletedStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("#fcba03")),
|
||||
dockerPullDownloadStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("#777777")),
|
||||
dockerPullExtractStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("#ffffff")),
|
||||
dockerPullStageChars: strings.Split("▁▃▄▅▆▇█", ""),
|
||||
layerCaps: strings.Split("▕▏", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func (d dockerPullProgressAdapter) Size() int64 {
|
||||
return -1
|
||||
}
|
||||
|
||||
func (d dockerPullProgressAdapter) Current() int64 {
|
||||
return 1
|
||||
}
|
||||
|
||||
func (d dockerPullProgressAdapter) Error() error {
|
||||
if d.status.Complete() {
|
||||
return progress.ErrCompleted
|
||||
}
|
||||
// TODO: return intermediate error indications
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d dockerPullProgressAdapter) Stage() string {
|
||||
return d.formatter.Render(d.status)
|
||||
}
|
||||
|
||||
// Render crafts the given docker image pull status summarized into a single line.
|
||||
func (f dockerPullStatusFormatter) Render(pullStatus dockerPullStatus) string {
|
||||
var size, current uint64
|
||||
|
||||
layers := pullStatus.Layers()
|
||||
status := make(map[docker.LayerID]docker.LayerState)
|
||||
completed := make([]string, len(layers))
|
||||
|
||||
// fetch the current state
|
||||
for idx, layer := range layers {
|
||||
completed[idx] = " "
|
||||
status[layer] = pullStatus.Current(layer)
|
||||
}
|
||||
|
||||
numCompleted := 0
|
||||
for idx, layer := range layers {
|
||||
prog := status[layer].PhaseProgress
|
||||
curN := prog.Current()
|
||||
curSize := prog.Size()
|
||||
|
||||
if progress.IsCompleted(prog) {
|
||||
input := f.dockerPullStageChars[len(f.dockerPullStageChars)-1]
|
||||
completed[idx] = f.formatDockerPullPhase(status[layer].Phase, input)
|
||||
} else if curN != 0 {
|
||||
var ratio float64
|
||||
switch {
|
||||
case curN == 0 || curSize < 0:
|
||||
ratio = 0
|
||||
case curN >= curSize:
|
||||
ratio = 1
|
||||
default:
|
||||
ratio = float64(curN) / float64(curSize)
|
||||
}
|
||||
|
||||
i := int(ratio * float64(len(f.dockerPullStageChars)-1))
|
||||
input := f.dockerPullStageChars[i]
|
||||
completed[idx] = f.formatDockerPullPhase(status[layer].Phase, input)
|
||||
}
|
||||
|
||||
if progress.IsErrCompleted(status[layer].DownloadProgress.Error()) {
|
||||
numCompleted++
|
||||
}
|
||||
}
|
||||
|
||||
for _, layer := range layers {
|
||||
prog := status[layer].DownloadProgress
|
||||
size += uint64(prog.Size())
|
||||
current += uint64(prog.Current())
|
||||
}
|
||||
|
||||
var progStr, auxInfo string
|
||||
if len(layers) > 0 {
|
||||
render := strings.Join(completed, "")
|
||||
prefix := f.dockerPullCompletedStyle.Render(fmt.Sprintf("%d Layers", len(layers)))
|
||||
auxInfo = f.auxInfoStyle.Render(fmt.Sprintf("[%s / %s]", humanize.Bytes(current), humanize.Bytes(size)))
|
||||
if len(layers) == numCompleted {
|
||||
auxInfo = f.auxInfoStyle.Render(fmt.Sprintf("[%s] Extracting...", humanize.Bytes(size)))
|
||||
}
|
||||
|
||||
progStr = fmt.Sprintf("%s%s%s%s", prefix, f.layerCap(false), render, f.layerCap(true))
|
||||
}
|
||||
|
||||
return progStr + auxInfo
|
||||
}
|
||||
|
||||
// formatDockerPullPhase returns a single character that represents the status of a layer pull.
|
||||
func (f dockerPullStatusFormatter) formatDockerPullPhase(phase docker.PullPhase, inputStr string) string {
|
||||
switch phase {
|
||||
case docker.WaitingPhase:
|
||||
// ignore any progress related to waiting
|
||||
return " "
|
||||
case docker.PullingFsPhase, docker.DownloadingPhase:
|
||||
return f.dockerPullDownloadStyle.Render(inputStr)
|
||||
case docker.DownloadCompletePhase:
|
||||
return f.dockerPullDownloadStyle.Render(f.dockerPullStageChars[len(f.dockerPullStageChars)-1])
|
||||
case docker.ExtractingPhase:
|
||||
return f.dockerPullExtractStyle.Render(inputStr)
|
||||
case docker.VerifyingChecksumPhase, docker.PullCompletePhase:
|
||||
return f.dockerPullCompletedStyle.Render(inputStr)
|
||||
case docker.AlreadyExistsPhase:
|
||||
return f.dockerPullCompletedStyle.Render(f.dockerPullStageChars[len(f.dockerPullStageChars)-1])
|
||||
default:
|
||||
return inputStr
|
||||
}
|
||||
}
|
||||
|
||||
func (f dockerPullStatusFormatter) layerCap(end bool) string {
|
||||
l := len(f.layerCaps)
|
||||
if l == 0 {
|
||||
return ""
|
||||
}
|
||||
if end {
|
||||
return f.layerCaps[l-1]
|
||||
}
|
||||
return f.layerCaps[0]
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gkampitakis/go-snaps/snaps"
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/image/docker"
|
||||
)
|
||||
|
||||
var _ dockerPullStatus = (*mockDockerPullStatus)(nil)
|
||||
|
||||
type mockDockerPullStatus struct {
|
||||
complete bool
|
||||
layers []docker.LayerID
|
||||
current map[docker.LayerID]docker.LayerState
|
||||
}
|
||||
|
||||
func (m mockDockerPullStatus) Complete() bool {
|
||||
return m.complete
|
||||
}
|
||||
|
||||
func (m mockDockerPullStatus) Layers() []docker.LayerID {
|
||||
return m.layers
|
||||
}
|
||||
|
||||
func (m mockDockerPullStatus) Current(id docker.LayerID) docker.LayerState {
|
||||
return m.current[id]
|
||||
}
|
||||
|
||||
func Test_dockerPullStatusFormatter_Render(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
status dockerPullStatus
|
||||
}{
|
||||
{
|
||||
name: "pulling",
|
||||
status: func() dockerPullStatus {
|
||||
complete := progress.NewManual(10)
|
||||
complete.Set(10)
|
||||
complete.SetCompleted()
|
||||
|
||||
quarter := progress.NewManual(10)
|
||||
quarter.Set(2)
|
||||
|
||||
half := progress.NewManual(10)
|
||||
half.Set(6)
|
||||
|
||||
empty := progress.NewManual(10)
|
||||
|
||||
return mockDockerPullStatus{
|
||||
complete: false,
|
||||
layers: []docker.LayerID{
|
||||
"sha256:1",
|
||||
"sha256:2",
|
||||
"sha256:3",
|
||||
},
|
||||
current: map[docker.LayerID]docker.LayerState{
|
||||
"sha256:1": {
|
||||
Phase: docker.ExtractingPhase,
|
||||
PhaseProgress: half,
|
||||
DownloadProgress: complete,
|
||||
},
|
||||
"sha256:2": {
|
||||
Phase: docker.DownloadingPhase,
|
||||
PhaseProgress: quarter,
|
||||
DownloadProgress: quarter,
|
||||
},
|
||||
"sha256:3": {
|
||||
Phase: docker.WaitingPhase,
|
||||
PhaseProgress: empty,
|
||||
DownloadProgress: empty,
|
||||
},
|
||||
},
|
||||
}
|
||||
}(),
|
||||
},
|
||||
{
|
||||
name: "download complete",
|
||||
status: func() dockerPullStatus {
|
||||
complete := progress.NewManual(10)
|
||||
complete.Set(10)
|
||||
complete.SetCompleted()
|
||||
|
||||
quarter := progress.NewManual(10)
|
||||
quarter.Set(2)
|
||||
|
||||
half := progress.NewManual(10)
|
||||
half.Set(6)
|
||||
|
||||
empty := progress.NewManual(10)
|
||||
|
||||
return mockDockerPullStatus{
|
||||
complete: false,
|
||||
layers: []docker.LayerID{
|
||||
"sha256:1",
|
||||
"sha256:2",
|
||||
"sha256:3",
|
||||
},
|
||||
current: map[docker.LayerID]docker.LayerState{
|
||||
"sha256:1": {
|
||||
Phase: docker.ExtractingPhase,
|
||||
PhaseProgress: complete,
|
||||
DownloadProgress: complete,
|
||||
},
|
||||
"sha256:2": {
|
||||
Phase: docker.ExtractingPhase,
|
||||
PhaseProgress: quarter,
|
||||
DownloadProgress: complete,
|
||||
},
|
||||
"sha256:3": {
|
||||
Phase: docker.ExtractingPhase,
|
||||
PhaseProgress: empty,
|
||||
DownloadProgress: complete,
|
||||
},
|
||||
},
|
||||
}
|
||||
}(),
|
||||
},
|
||||
{
|
||||
name: "complete",
|
||||
status: func() dockerPullStatus {
|
||||
complete := progress.NewManual(10)
|
||||
complete.Set(10)
|
||||
complete.SetCompleted()
|
||||
|
||||
return mockDockerPullStatus{
|
||||
complete: true,
|
||||
layers: []docker.LayerID{
|
||||
"sha256:1",
|
||||
"sha256:2",
|
||||
"sha256:3",
|
||||
},
|
||||
current: map[docker.LayerID]docker.LayerState{
|
||||
"sha256:1": {
|
||||
Phase: docker.PullCompletePhase,
|
||||
PhaseProgress: complete,
|
||||
DownloadProgress: complete,
|
||||
},
|
||||
"sha256:2": {
|
||||
Phase: docker.PullCompletePhase,
|
||||
PhaseProgress: complete,
|
||||
DownloadProgress: complete,
|
||||
},
|
||||
"sha256:3": {
|
||||
Phase: docker.PullCompletePhase,
|
||||
PhaseProgress: complete,
|
||||
DownloadProgress: complete,
|
||||
},
|
||||
},
|
||||
}
|
||||
}(),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
f := newDockerPullStatusFormatter()
|
||||
snaps.MatchSnapshot(t, f.Render(tt.status))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
|
||||
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||
stereoEventParsers "github.com/anchore/stereoscope/pkg/event/parsers"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
)
|
||||
|
||||
func (m *Handler) handleReadImage(e partybus.Event) []tea.Model {
|
||||
imgMetadata, prog, err := stereoEventParsers.ParseReadImage(e)
|
||||
if err != nil {
|
||||
log.WithFields("error", err).Warn("unable to parse event")
|
||||
return nil
|
||||
}
|
||||
|
||||
tsk := m.newTaskProgress(
|
||||
taskprogress.Title{
|
||||
Default: "Parse image",
|
||||
Running: "Parsing image",
|
||||
Success: "Parsed image",
|
||||
},
|
||||
taskprogress.WithProgress(prog),
|
||||
)
|
||||
|
||||
if imgMetadata != nil {
|
||||
tsk.Context = []string{imgMetadata.ID}
|
||||
}
|
||||
|
||||
return []tea.Model{tsk}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/gkampitakis/go-snaps/snaps"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||
stereoscopeEvent "github.com/anchore/stereoscope/pkg/event"
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
)
|
||||
|
||||
func TestHandler_handleReadImage(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
eventFn func(*testing.T) partybus.Event
|
||||
iterations int
|
||||
}{
|
||||
{
|
||||
name: "read image in progress",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
prog := &progress.Manual{}
|
||||
prog.SetTotal(100)
|
||||
prog.Set(50)
|
||||
|
||||
src := image.Metadata{
|
||||
ID: "id",
|
||||
Size: 42,
|
||||
Config: v1.ConfigFile{
|
||||
Architecture: "arch",
|
||||
Author: "auth",
|
||||
Container: "cont",
|
||||
OS: "os",
|
||||
OSVersion: "os-ver",
|
||||
Variant: "vari",
|
||||
},
|
||||
MediaType: "media",
|
||||
ManifestDigest: "digest",
|
||||
Architecture: "arch",
|
||||
Variant: "var",
|
||||
OS: "os",
|
||||
}
|
||||
|
||||
return partybus.Event{
|
||||
Type: stereoscopeEvent.ReadImage,
|
||||
Source: src,
|
||||
Value: prog,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "read image complete",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
prog := &progress.Manual{}
|
||||
prog.SetTotal(100)
|
||||
prog.Set(100)
|
||||
prog.SetCompleted()
|
||||
|
||||
src := image.Metadata{
|
||||
ID: "id",
|
||||
Size: 42,
|
||||
Config: v1.ConfigFile{
|
||||
Architecture: "arch",
|
||||
Author: "auth",
|
||||
Container: "cont",
|
||||
OS: "os",
|
||||
OSVersion: "os-ver",
|
||||
Variant: "vari",
|
||||
},
|
||||
MediaType: "media",
|
||||
ManifestDigest: "digest",
|
||||
Architecture: "arch",
|
||||
Variant: "var",
|
||||
OS: "os",
|
||||
}
|
||||
|
||||
return partybus.Event{
|
||||
Type: stereoscopeEvent.ReadImage,
|
||||
Source: src,
|
||||
Value: prog,
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
event := tt.eventFn(t)
|
||||
handler := New(DefaultHandlerConfig())
|
||||
handler.WindowSize = tea.WindowSizeMsg{
|
||||
Width: 100,
|
||||
Height: 80,
|
||||
}
|
||||
|
||||
models := handler.Handle(event)
|
||||
require.Len(t, models, 1)
|
||||
model := models[0]
|
||||
|
||||
tsk, ok := model.(taskprogress.Model)
|
||||
require.True(t, ok)
|
||||
|
||||
got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
|
||||
Time: time.Now(),
|
||||
Sequence: tsk.Sequence(),
|
||||
ID: tsk.ID(),
|
||||
})
|
||||
t.Log(got)
|
||||
snaps.MatchSnapshot(t, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
|
||||
"github.com/anchore/syft/syft/file/cataloger/secrets"
|
||||
)
|
||||
|
||||
var _ progress.StagedProgressable = (*secretsCatalogerProgressAdapter)(nil)
|
||||
|
||||
// Deprecated: will be removed in syft 1.0
|
||||
type secretsCatalogerProgressAdapter struct {
|
||||
*secrets.Monitor
|
||||
}
|
||||
|
||||
// Deprecated: will be removed in syft 1.0
|
||||
func newSecretsCatalogerProgressAdapter(monitor *secrets.Monitor) secretsCatalogerProgressAdapter {
|
||||
return secretsCatalogerProgressAdapter{
|
||||
Monitor: monitor,
|
||||
}
|
||||
}
|
||||
|
||||
func (s secretsCatalogerProgressAdapter) Stage() string {
|
||||
return fmt.Sprintf("%d secrets", s.Monitor.SecretsDiscovered.Current())
|
||||
}
|
||||
|
||||
// Deprecated: will be removed in syft 1.0
|
||||
func (m *Handler) handleSecretsCatalogerStarted(e partybus.Event) []tea.Model {
|
||||
mon, err := syftEventParsers.ParseSecretsCatalogingStarted(e)
|
||||
if err != nil {
|
||||
log.WithFields("error", err).Warn("unable to parse event")
|
||||
return nil
|
||||
}
|
||||
|
||||
tsk := m.newTaskProgress(
|
||||
taskprogress.Title{
|
||||
Default: "Catalog secrets",
|
||||
Running: "Cataloging secrets",
|
||||
Success: "Cataloged secrets",
|
||||
},
|
||||
|
||||
taskprogress.WithStagedProgressable(
|
||||
newSecretsCatalogerProgressAdapter(mon),
|
||||
),
|
||||
)
|
||||
|
||||
tsk.HideStageOnSuccess = false
|
||||
|
||||
return []tea.Model{tsk}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/gkampitakis/go-snaps/snaps"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||
syftEvent "github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/syft/file/cataloger/secrets"
|
||||
)
|
||||
|
||||
func TestHandler_handleSecretsCatalogerStarted(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
eventFn func(*testing.T) partybus.Event
|
||||
iterations int
|
||||
}{
|
||||
{
|
||||
name: "cataloging in progress",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
stage := &progress.Stage{
|
||||
Current: "current",
|
||||
}
|
||||
secretsDiscovered := progress.NewManual(-1)
|
||||
secretsDiscovered.Set(64)
|
||||
prog := progress.NewManual(72)
|
||||
prog.Set(50)
|
||||
|
||||
return partybus.Event{
|
||||
Type: syftEvent.SecretsCatalogerStarted,
|
||||
Source: secretsDiscovered,
|
||||
Value: secrets.Monitor{
|
||||
Stager: progress.Stager(stage),
|
||||
SecretsDiscovered: secretsDiscovered,
|
||||
Progressable: prog,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cataloging complete",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
stage := &progress.Stage{
|
||||
Current: "current",
|
||||
}
|
||||
secretsDiscovered := progress.NewManual(-1)
|
||||
secretsDiscovered.Set(64)
|
||||
prog := progress.NewManual(72)
|
||||
prog.Set(72)
|
||||
prog.SetCompleted()
|
||||
|
||||
return partybus.Event{
|
||||
Type: syftEvent.SecretsCatalogerStarted,
|
||||
Source: secretsDiscovered,
|
||||
Value: secrets.Monitor{
|
||||
Stager: progress.Stager(stage),
|
||||
SecretsDiscovered: secretsDiscovered,
|
||||
Progressable: prog,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
event := tt.eventFn(t)
|
||||
handler := New(DefaultHandlerConfig())
|
||||
handler.WindowSize = tea.WindowSizeMsg{
|
||||
Width: 100,
|
||||
Height: 80,
|
||||
}
|
||||
|
||||
models := handler.Handle(event)
|
||||
require.Len(t, models, 1)
|
||||
model := models[0]
|
||||
|
||||
tsk, ok := model.(taskprogress.Model)
|
||||
require.True(t, ok)
|
||||
|
||||
got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
|
||||
Time: time.Now(),
|
||||
Sequence: tsk.Sequence(),
|
||||
ID: tsk.ID(),
|
||||
})
|
||||
t.Log(got)
|
||||
snaps.MatchSnapshot(t, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
|
||||
"github.com/anchore/bubbly"
|
||||
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||
stereoscopeEvent "github.com/anchore/stereoscope/pkg/event"
|
||||
syftEvent "github.com/anchore/syft/syft/event"
|
||||
)
|
||||
|
||||
var _ interface {
|
||||
bubbly.EventHandler
|
||||
bubbly.MessageListener
|
||||
bubbly.HandleWaiter
|
||||
} = (*Handler)(nil)
|
||||
|
||||
type HandlerConfig struct {
|
||||
TitleWidth int
|
||||
AdjustDefaultTask func(taskprogress.Model) taskprogress.Model
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
WindowSize tea.WindowSizeMsg
|
||||
Running *sync.WaitGroup
|
||||
Config HandlerConfig
|
||||
|
||||
bubbly.EventHandler
|
||||
}
|
||||
|
||||
func DefaultHandlerConfig() HandlerConfig {
|
||||
return HandlerConfig{
|
||||
TitleWidth: 30,
|
||||
}
|
||||
}
|
||||
|
||||
func New(cfg HandlerConfig) *Handler {
|
||||
d := bubbly.NewEventDispatcher()
|
||||
|
||||
h := &Handler{
|
||||
EventHandler: d,
|
||||
Running: &sync.WaitGroup{},
|
||||
Config: cfg,
|
||||
}
|
||||
|
||||
// register all supported event types with the respective handler functions
|
||||
d.AddHandlers(map[partybus.EventType]bubbly.EventHandlerFn{
|
||||
stereoscopeEvent.PullDockerImage: h.handlePullDockerImage,
|
||||
stereoscopeEvent.ReadImage: h.handleReadImage,
|
||||
stereoscopeEvent.FetchImage: h.handleFetchImage,
|
||||
syftEvent.PackageCatalogerStarted: h.handlePackageCatalogerStarted,
|
||||
syftEvent.FileDigestsCatalogerStarted: h.handleFileDigestsCatalogerStarted,
|
||||
syftEvent.FileMetadataCatalogerStarted: h.handleFileMetadataCatalogerStarted,
|
||||
syftEvent.FileIndexingStarted: h.handleFileIndexingStarted,
|
||||
syftEvent.AttestationStarted: h.handleAttestationStarted,
|
||||
syftEvent.CatalogerTaskStarted: h.handleCatalogerTaskStarted,
|
||||
|
||||
// deprecated
|
||||
syftEvent.SecretsCatalogerStarted: h.handleSecretsCatalogerStarted,
|
||||
})
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func (m *Handler) OnMessage(msg tea.Msg) {
|
||||
if msg, ok := msg.(tea.WindowSizeMsg); ok {
|
||||
m.WindowSize = msg
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Handler) Wait() {
|
||||
m.Running.Wait()
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package ui
|
||||
|
||||
import "github.com/anchore/bubbly/bubbles/taskprogress"
|
||||
|
||||
func (m Handler) newTaskProgress(title taskprogress.Title, opts ...taskprogress.Option) taskprogress.Model {
|
||||
tsk := taskprogress.New(m.Running, opts...)
|
||||
|
||||
tsk.HideProgressOnSuccess = true
|
||||
tsk.HideStageOnSuccess = true
|
||||
tsk.WindowSize = m.WindowSize
|
||||
tsk.TitleWidth = m.Config.TitleWidth
|
||||
tsk.TitleOptions = title
|
||||
|
||||
if m.Config.AdjustDefaultTask != nil {
|
||||
tsk = m.Config.AdjustDefaultTask(tsk)
|
||||
}
|
||||
|
||||
return tsk
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
func runModel(t testing.TB, m tea.Model, iterations int, message tea.Msg, h ...*sync.WaitGroup) string {
|
||||
t.Helper()
|
||||
if iterations == 0 {
|
||||
iterations = 1
|
||||
}
|
||||
m.Init()
|
||||
var cmd tea.Cmd = func() tea.Msg {
|
||||
return message
|
||||
}
|
||||
|
||||
for _, each := range h {
|
||||
if each != nil {
|
||||
each.Wait()
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; cmd != nil && i < iterations; i++ {
|
||||
msgs := flatten(cmd())
|
||||
var nextCmds []tea.Cmd
|
||||
var next tea.Cmd
|
||||
for _, msg := range msgs {
|
||||
t.Logf("Message: %+v %+v\n", reflect.TypeOf(msg), msg)
|
||||
m, next = m.Update(msg)
|
||||
nextCmds = append(nextCmds, next)
|
||||
}
|
||||
cmd = tea.Batch(nextCmds...)
|
||||
}
|
||||
|
||||
return m.View()
|
||||
}
|
||||
|
||||
func flatten(p tea.Msg) (msgs []tea.Msg) {
|
||||
if reflect.TypeOf(p).Name() == "batchMsg" {
|
||||
partials := extractBatchMessages(p)
|
||||
for _, m := range partials {
|
||||
msgs = append(msgs, flatten(m)...)
|
||||
}
|
||||
} else {
|
||||
msgs = []tea.Msg{p}
|
||||
}
|
||||
return msgs
|
||||
}
|
||||
|
||||
func extractBatchMessages(m tea.Msg) (ret []tea.Msg) {
|
||||
sliceMsgType := reflect.SliceOf(reflect.TypeOf(tea.Cmd(nil)))
|
||||
value := reflect.ValueOf(m) // note: this is technically unaddressable
|
||||
|
||||
// make our own instance that is addressable
|
||||
valueCopy := reflect.New(value.Type()).Elem()
|
||||
valueCopy.Set(value)
|
||||
|
||||
cmds := reflect.NewAt(sliceMsgType, unsafe.Pointer(valueCopy.UnsafeAddr())).Elem()
|
||||
for i := 0; i < cmds.Len(); i++ {
|
||||
item := cmds.Index(i)
|
||||
r := item.Call(nil)
|
||||
ret = append(ret, r[0].Interface().(tea.Msg))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"github.com/anchore/syft/internal/version"
|
||||
)
|
||||
|
||||
func Version(v *viper.Viper, app *config.Application) *cobra.Command {
|
||||
func Version(v *viper.Viper, _ *config.Application) *cobra.Command {
|
||||
o := &options.VersionOptions{}
|
||||
cmd := &cobra.Command{
|
||||
Use: "version",
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
|
||||
[Test_postUIEventWriter_write/no_events/stdout - 1]
|
||||
|
||||
---
|
||||
|
||||
[Test_postUIEventWriter_write/no_events/stderr - 1]
|
||||
|
||||
---
|
||||
|
||||
[Test_postUIEventWriter_write/all_events/stdout - 1]
|
||||
|
||||
|
||||
<my --
|
||||
-
|
||||
-
|
||||
report 1!!>
|
||||
<report 2>
|
||||
|
||||
---
|
||||
|
||||
[Test_postUIEventWriter_write/all_events/stderr - 1]
|
||||
|
||||
|
||||
<my notification 1!!
|
||||
...still notifying>
|
||||
|
||||
|
||||
<notification 2>
|
||||
<notification 3>
|
||||
|
||||
|
||||
<my app can be updated!!
|
||||
...to this version>
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
[Test_postUIEventWriter_write/quiet_only_shows_report/stdout - 1]
|
||||
<report 1>
|
||||
|
||||
---
|
||||
|
||||
[Test_postUIEventWriter_write/quiet_only_shows_report/stderr - 1]
|
||||
|
||||
---
|
||||
@@ -0,0 +1,44 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/wagoodman/go-partybus"
|
||||
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
)
|
||||
|
||||
var _ clio.UI = (*NoUI)(nil)
|
||||
|
||||
type NoUI struct {
|
||||
finalizeEvents []partybus.Event
|
||||
subscription partybus.Unsubscribable
|
||||
quiet bool
|
||||
}
|
||||
|
||||
func None(quiet bool) *NoUI {
|
||||
return &NoUI{
|
||||
quiet: quiet,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NoUI) Setup(subscription partybus.Unsubscribable) error {
|
||||
n.subscription = subscription
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *NoUI) Handle(e partybus.Event) error {
|
||||
switch e.Type {
|
||||
case event.CLIReport, event.CLINotification:
|
||||
// keep these for when the UI is terminated to show to the screen (or perform other events)
|
||||
n.finalizeEvents = append(n.finalizeEvents, e)
|
||||
case event.CLIExit:
|
||||
return n.subscription.Unsubscribe()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n NoUI) Teardown(_ bool) error {
|
||||
return newPostUIEventWriter(os.Stdout, os.Stderr).write(n.quiet, n.finalizeEvents...)
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/syft/event/parsers"
|
||||
)
|
||||
|
||||
type postUIEventWriter struct {
|
||||
handles []postUIHandle
|
||||
}
|
||||
|
||||
type postUIHandle struct {
|
||||
respectQuiet bool
|
||||
event partybus.EventType
|
||||
writer io.Writer
|
||||
dispatch eventWriter
|
||||
}
|
||||
|
||||
type eventWriter func(io.Writer, ...partybus.Event) error
|
||||
|
||||
func newPostUIEventWriter(stdout, stderr io.Writer) *postUIEventWriter {
|
||||
return &postUIEventWriter{
|
||||
handles: []postUIHandle{
|
||||
{
|
||||
event: event.CLIReport,
|
||||
respectQuiet: false,
|
||||
writer: stdout,
|
||||
dispatch: writeReports,
|
||||
},
|
||||
{
|
||||
event: event.CLINotification,
|
||||
respectQuiet: true,
|
||||
writer: stderr,
|
||||
dispatch: writeNotifications,
|
||||
},
|
||||
{
|
||||
event: event.CLIAppUpdateAvailable,
|
||||
respectQuiet: true,
|
||||
writer: stderr,
|
||||
dispatch: writeAppUpdate,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (w postUIEventWriter) write(quiet bool, events ...partybus.Event) error {
|
||||
var errs error
|
||||
for _, h := range w.handles {
|
||||
if quiet && h.respectQuiet {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, e := range events {
|
||||
if e.Type != h.event {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := h.dispatch(h.writer, e); err != nil {
|
||||
errs = multierror.Append(errs, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func writeReports(writer io.Writer, events ...partybus.Event) error {
|
||||
var reports []string
|
||||
for _, e := range events {
|
||||
_, report, err := parsers.ParseCLIReport(e)
|
||||
if err != nil {
|
||||
log.WithFields("error", err).Warn("failed to gather final report")
|
||||
continue
|
||||
}
|
||||
|
||||
// remove all whitespace padding from the end of the report
|
||||
reports = append(reports, strings.TrimRight(report, "\n ")+"\n")
|
||||
}
|
||||
|
||||
// prevent the double new-line at the end of the report
|
||||
report := strings.Join(reports, "\n")
|
||||
|
||||
if _, err := fmt.Fprint(writer, report); err != nil {
|
||||
return fmt.Errorf("failed to write final report to stdout: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeNotifications(writer io.Writer, events ...partybus.Event) error {
|
||||
// 13 = high intensity magenta (ANSI 16 bit code)
|
||||
style := lipgloss.NewStyle().Foreground(lipgloss.Color("13"))
|
||||
|
||||
for _, e := range events {
|
||||
_, notification, err := parsers.ParseCLINotification(e)
|
||||
if err != nil {
|
||||
log.WithFields("error", err).Warn("failed to parse notification")
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err := fmt.Fprintln(writer, style.Render(notification)); err != nil {
|
||||
// don't let this be fatal
|
||||
log.WithFields("error", err).Warn("failed to write final notifications")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeAppUpdate(writer io.Writer, events ...partybus.Event) error {
|
||||
// 13 = high intensity magenta (ANSI 16 bit code) + italics
|
||||
style := lipgloss.NewStyle().Foreground(lipgloss.Color("13")).Italic(true)
|
||||
|
||||
for _, e := range events {
|
||||
notice, err := parsers.ParseCLIAppUpdateAvailable(e)
|
||||
if err != nil {
|
||||
log.WithFields("error", err).Warn("failed to parse app update notification")
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err := fmt.Fprintln(writer, style.Render(notice)); err != nil {
|
||||
// don't let this be fatal
|
||||
log.WithFields("error", err).Warn("failed to write app update notification")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/gkampitakis/go-snaps/snaps"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
|
||||
"github.com/anchore/syft/syft/event"
|
||||
)
|
||||
|
||||
func Test_postUIEventWriter_write(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
quiet bool
|
||||
events []partybus.Event
|
||||
wantErr require.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "no events",
|
||||
},
|
||||
{
|
||||
name: "all events",
|
||||
events: []partybus.Event{
|
||||
{
|
||||
Type: event.CLINotification,
|
||||
Value: "\n\n<my notification 1!!\n...still notifying>\n\n",
|
||||
},
|
||||
{
|
||||
Type: event.CLINotification,
|
||||
Value: "<notification 2>",
|
||||
},
|
||||
{
|
||||
Type: event.CLIAppUpdateAvailable,
|
||||
Value: "\n\n<my app can be updated!!\n...to this version>\n\n",
|
||||
},
|
||||
{
|
||||
Type: event.CLINotification,
|
||||
Value: "<notification 3>",
|
||||
},
|
||||
{
|
||||
Type: event.CLIReport,
|
||||
Value: "\n\n<my --\n-\n-\nreport 1!!>\n\n",
|
||||
},
|
||||
{
|
||||
Type: event.CLIReport,
|
||||
Value: "<report 2>",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "quiet only shows report",
|
||||
quiet: true,
|
||||
events: []partybus.Event{
|
||||
|
||||
{
|
||||
Type: event.CLINotification,
|
||||
Value: "<notification 1>",
|
||||
},
|
||||
{
|
||||
Type: event.CLIAppUpdateAvailable,
|
||||
Value: "<app update>",
|
||||
},
|
||||
{
|
||||
Type: event.CLIReport,
|
||||
Value: "<report 1>",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.wantErr == nil {
|
||||
tt.wantErr = require.NoError
|
||||
}
|
||||
|
||||
stdout := &bytes.Buffer{}
|
||||
stderr := &bytes.Buffer{}
|
||||
w := newPostUIEventWriter(stdout, stderr)
|
||||
|
||||
tt.wantErr(t, w.write(tt.quiet, tt.events...))
|
||||
|
||||
t.Run("stdout", func(t *testing.T) {
|
||||
snaps.MatchSnapshot(t, stdout.String())
|
||||
})
|
||||
|
||||
t.Run("stderr", func(t *testing.T) {
|
||||
snaps.MatchSnapshot(t, stderr.String())
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
//go:build linux || darwin
|
||||
// +build linux darwin
|
||||
//go:build linux || darwin || netbsd
|
||||
// +build linux darwin netbsd
|
||||
|
||||
package ui
|
||||
|
||||
@@ -8,6 +8,9 @@ import (
|
||||
"runtime"
|
||||
|
||||
"golang.org/x/term"
|
||||
|
||||
"github.com/anchore/clio"
|
||||
handler "github.com/anchore/syft/cmd/syft/cli/ui"
|
||||
)
|
||||
|
||||
// Select is responsible for determining the specific UI function given select user option, the current platform
|
||||
@@ -15,16 +18,18 @@ import (
|
||||
// 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) (uis []UI) {
|
||||
func Select(verbose, quiet bool) (uis []clio.UI) {
|
||||
isStdoutATty := term.IsTerminal(int(os.Stdout.Fd()))
|
||||
isStderrATty := term.IsTerminal(int(os.Stderr.Fd()))
|
||||
notATerminal := !isStderrATty && !isStdoutATty
|
||||
notATerminal := !isStderrATty || !isStdoutATty
|
||||
|
||||
switch {
|
||||
case runtime.GOOS == "windows" || verbose || quiet || notATerminal || !isStderrATty:
|
||||
uis = append(uis, NewLoggerUI())
|
||||
uis = append(uis, None(quiet))
|
||||
default:
|
||||
uis = append(uis, NewEphemeralTerminalUI())
|
||||
// TODO: it may make sense in the future to pass handler options into select
|
||||
h := handler.New(handler.DefaultHandlerConfig())
|
||||
uis = append(uis, New(h, verbose, quiet))
|
||||
}
|
||||
|
||||
return uis
|
||||
@@ -3,11 +3,13 @@
|
||||
|
||||
package ui
|
||||
|
||||
import "github.com/anchore/clio"
|
||||
|
||||
// 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). 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) (uis []UI) {
|
||||
return append(uis, NewLoggerUI())
|
||||
func Select(verbose, quiet bool) (uis []clio.UI) {
|
||||
return append(uis, None(quiet))
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
|
||||
"github.com/anchore/bubbly/bubbles/frame"
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/go-logger"
|
||||
handler "github.com/anchore/syft/cmd/syft/cli/ui"
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
)
|
||||
|
||||
var _ interface {
|
||||
tea.Model
|
||||
partybus.Responder
|
||||
clio.UI
|
||||
} = (*UI)(nil)
|
||||
|
||||
type UI struct {
|
||||
program *tea.Program
|
||||
running *sync.WaitGroup
|
||||
quiet bool
|
||||
subscription partybus.Unsubscribable
|
||||
finalizeEvents []partybus.Event
|
||||
|
||||
handler *handler.Handler
|
||||
frame tea.Model
|
||||
}
|
||||
|
||||
func New(h *handler.Handler, _, quiet bool) *UI {
|
||||
return &UI{
|
||||
handler: h,
|
||||
frame: frame.New(),
|
||||
running: &sync.WaitGroup{},
|
||||
quiet: quiet,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *UI) Setup(subscription partybus.Unsubscribable) error {
|
||||
// we still want to collect log messages, however, we also the logger shouldn't write to the screen directly
|
||||
if logWrapper, ok := log.Get().(logger.Controller); ok {
|
||||
logWrapper.SetOutput(m.frame.(*frame.Frame).Footer())
|
||||
}
|
||||
|
||||
m.subscription = subscription
|
||||
m.program = tea.NewProgram(m, tea.WithOutput(os.Stderr), tea.WithInput(os.Stdin))
|
||||
m.running.Add(1)
|
||||
|
||||
go func() {
|
||||
defer m.running.Done()
|
||||
if _, err := m.program.Run(); err != nil {
|
||||
log.Errorf("unable to start UI: %+v", err)
|
||||
m.exit()
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *UI) exit() {
|
||||
// stop the event loop
|
||||
bus.Exit()
|
||||
}
|
||||
|
||||
func (m *UI) Handle(e partybus.Event) error {
|
||||
if m.program != nil {
|
||||
m.program.Send(e)
|
||||
if e.Type == event.CLIExit {
|
||||
return m.subscription.Unsubscribe()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *UI) Teardown(force bool) error {
|
||||
if !force {
|
||||
m.handler.Running.Wait()
|
||||
m.program.Quit()
|
||||
// typically in all cases we would want to wait for the UI to finish. However there are still error cases
|
||||
// that are not accounted for, resulting in hangs. For now, we'll just wait for the UI to finish in the
|
||||
// happy path only. There will always be an indication of the problem to the user via reporting the error
|
||||
// string from the worker (outside of the UI after teardown).
|
||||
m.running.Wait()
|
||||
} else {
|
||||
m.program.Kill()
|
||||
}
|
||||
|
||||
// TODO: allow for writing out the full log output to the screen (only a partial log is shown currently)
|
||||
// this needs coordination to know what the last frame event is to change the state accordingly (which isn't possible now)
|
||||
|
||||
return newPostUIEventWriter(os.Stdout, os.Stderr).write(m.quiet, m.finalizeEvents...)
|
||||
}
|
||||
|
||||
// bubbletea.Model functions
|
||||
|
||||
func (m UI) Init() tea.Cmd {
|
||||
return m.frame.Init()
|
||||
}
|
||||
|
||||
func (m UI) RespondsTo() []partybus.EventType {
|
||||
return append([]partybus.EventType{
|
||||
event.CLIReport,
|
||||
event.CLINotification,
|
||||
event.CLIExit,
|
||||
event.CLIAppUpdateAvailable,
|
||||
}, m.handler.RespondsTo()...)
|
||||
}
|
||||
|
||||
func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
// note: we need a pointer receiver such that the same instance of UI used in Teardown is referenced (to keep finalize events)
|
||||
|
||||
var cmds []tea.Cmd
|
||||
|
||||
// allow for non-partybus UI updates (such as window size events). Note: these must not affect existing models,
|
||||
// that is the responsibility of the frame object on this UI object. The handler is a factory of models
|
||||
// which the frame is responsible for the lifecycle of. This update allows for injecting the initial state
|
||||
// of the world when creating those models.
|
||||
m.handler.OnMessage(msg)
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "esc", "ctrl+c":
|
||||
m.exit()
|
||||
return m, tea.Quit
|
||||
}
|
||||
|
||||
case partybus.Event:
|
||||
log.WithFields("component", "ui").Tracef("event: %q", msg.Type)
|
||||
|
||||
switch msg.Type {
|
||||
case event.CLIReport, event.CLINotification, event.CLIExit, event.CLIAppUpdateAvailable:
|
||||
// keep these for when the UI is terminated to show to the screen (or perform other events)
|
||||
m.finalizeEvents = append(m.finalizeEvents, msg)
|
||||
|
||||
// why not return tea.Quit here for exit events? because there may be UI components that still need the update-render loop.
|
||||
// for this reason we'll let the syft event loop call Teardown() which will explicitly wait for these components
|
||||
return m, nil
|
||||
}
|
||||
|
||||
for _, newModel := range m.handler.Handle(msg) {
|
||||
if newModel == nil {
|
||||
continue
|
||||
}
|
||||
cmds = append(cmds, newModel.Init())
|
||||
m.frame.(*frame.Frame).AppendModel(newModel)
|
||||
}
|
||||
// intentionally fallthrough to update the frame model
|
||||
}
|
||||
|
||||
frameModel, cmd := m.frame.Update(msg)
|
||||
m.frame = frameModel
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m UI) View() string {
|
||||
return m.frame.View()
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package main
|
||||
import (
|
||||
"log"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
|
||||
"github.com/anchore/syft/cmd/syft/cli"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
module github.com/anchore/syft
|
||||
|
||||
go 1.18
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/CycloneDX/cyclonedx-go v0.5.2
|
||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
|
||||
github.com/acobaugh/osrelease v0.1.0
|
||||
github.com/adrg/xdg v0.3.3
|
||||
github.com/adrg/xdg v0.4.0
|
||||
github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb
|
||||
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.1.1-0.20220428202044-a072fa3cb6d7
|
||||
github.com/bmatcuk/doublestar/v4 v4.0.2
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/facebookincubator/nvdtools v0.1.4
|
||||
github.com/go-test/deep v1.0.8
|
||||
github.com/anchore/packageurl-go v0.1.1-0.20230104203445-02e0a6721501
|
||||
github.com/bmatcuk/doublestar/v4 v4.6.0
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/facebookincubator/nvdtools v0.1.5
|
||||
github.com/go-test/deep v1.1.0
|
||||
github.com/google/go-cmp v0.5.9
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/gookit/color v1.4.2
|
||||
github.com/gookit/color v1.5.4
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/jinzhu/copier v0.3.2
|
||||
github.com/jinzhu/copier v0.3.5
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/mholt/archiver/v3 v3.5.1
|
||||
github.com/microsoft/go-rustaudit v0.0.0-20220730194248-4b17361d90a5
|
||||
@@ -30,302 +29,174 @@ require (
|
||||
github.com/pelletier/go-toml v1.9.5
|
||||
// pinned to pull in 386 arch fix: https://github.com/scylladb/go-set/commit/cc7b2070d91ebf40d233207b633e28f5bd8f03a5
|
||||
github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e
|
||||
github.com/sergi/go-diff v1.2.0
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
github.com/spdx/tools-golang v0.3.1-0.20221108182156-8a01147e6342
|
||||
github.com/spf13/afero v1.8.2
|
||||
github.com/spf13/cobra v1.6.0
|
||||
github.com/sergi/go-diff v1.3.1
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spdx/tools-golang v0.5.3
|
||||
github.com/spf13/afero v1.9.5
|
||||
github.com/spf13/cobra v1.7.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/spf13/viper v1.13.0
|
||||
github.com/stretchr/testify v1.8.0
|
||||
github.com/vifraa/gopom v0.1.0
|
||||
github.com/wagoodman/go-partybus v0.0.0-20210627031916-db1f5573bbc5
|
||||
github.com/wagoodman/go-progress v0.0.0-20200731105512-1020f39e6240
|
||||
github.com/wagoodman/jotframe v0.0.0-20211129225309-56b0d0a4aebb
|
||||
github.com/spf13/viper v1.16.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/vifraa/gopom v0.2.2
|
||||
github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651
|
||||
github.com/wagoodman/go-progress v0.0.0-20230301185719-21920a456ad5
|
||||
github.com/xeipuuv/gojsonschema v1.2.0
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4
|
||||
golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458
|
||||
golang.org/x/term v0.0.0-20220919170432-7a66f970e087
|
||||
golang.org/x/mod v0.12.0
|
||||
golang.org/x/net v0.12.0
|
||||
golang.org/x/term v0.10.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Masterminds/sprig/v3 v3.2.2
|
||||
github.com/anchore/go-logger v0.0.0-20220728155337-03b66a5207d8
|
||||
github.com/anchore/stereoscope v0.0.0-20221130153459-3b80d983223f
|
||||
github.com/docker/docker v20.10.17+incompatible
|
||||
github.com/google/go-containerregistry v0.11.0
|
||||
github.com/in-toto/in-toto-golang v0.4.1-0.20221018183522-731d0640b65f
|
||||
github.com/CycloneDX/cyclonedx-go v0.7.1
|
||||
github.com/Masterminds/semver v1.5.0
|
||||
github.com/Masterminds/sprig/v3 v3.2.3
|
||||
github.com/anchore/bubbly v0.0.0-20230712165553-812110ab0a10
|
||||
github.com/anchore/clio v0.0.0-20230602170917-e747e60c4aa0
|
||||
github.com/anchore/go-logger v0.0.0-20230531193951-db5ae83e7dbe
|
||||
github.com/anchore/stereoscope v0.0.0-20230724160817-d515761c6ca2
|
||||
github.com/aquasecurity/go-pep440-version v0.0.0-20210121094942-22b2f8951d46
|
||||
github.com/charmbracelet/bubbletea v0.24.2
|
||||
github.com/charmbracelet/lipgloss v0.7.1
|
||||
github.com/dave/jennifer v1.6.1
|
||||
github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da
|
||||
github.com/docker/distribution v2.8.2+incompatible
|
||||
github.com/docker/docker v24.0.5+incompatible
|
||||
github.com/github/go-spdx/v2 v2.1.2
|
||||
github.com/gkampitakis/go-snaps v0.4.7
|
||||
github.com/go-git/go-billy/v5 v5.4.1
|
||||
github.com/go-git/go-git/v5 v5.8.1
|
||||
github.com/google/go-containerregistry v0.15.2
|
||||
github.com/google/licensecheck v0.3.1
|
||||
github.com/invopop/jsonschema v0.7.0
|
||||
github.com/knqyf263/go-rpmdb v0.0.0-20221030135625-4082a22221ce
|
||||
github.com/knqyf263/go-rpmdb v0.0.0-20230301153543-ba94b245509b
|
||||
github.com/opencontainers/go-digest v1.0.0
|
||||
github.com/saferwall/pe v1.4.4
|
||||
github.com/sassoftware/go-rpmutils v0.2.0
|
||||
github.com/sigstore/cosign v1.13.1
|
||||
github.com/sigstore/rekor v0.12.1-0.20220915152154-4bb6f441c1b2
|
||||
github.com/sigstore/sigstore v1.4.4
|
||||
github.com/vbatts/go-mtree v0.5.0
|
||||
github.com/vbatts/go-mtree v0.5.3
|
||||
github.com/zyedidia/generic v1.2.2-0.20230320175451-4410d2372cb1
|
||||
golang.org/x/exp v0.0.0-20230202163644-54bba9f4231b
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
modernc.org/sqlite v1.24.0
|
||||
)
|
||||
|
||||
require (
|
||||
bitbucket.org/creachadair/shell v0.0.7 // indirect
|
||||
cloud.google.com/go/compute v1.10.0 // indirect
|
||||
github.com/AliyunContainerService/ack-ram-tool/pkg/credentials/alibabacloudsdkgo/helper v0.2.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go v66.0.0+incompatible // indirect
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
|
||||
github.com/Azure/go-autorest/autorest v0.11.28 // indirect
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.21 // indirect
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 // indirect
|
||||
github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect
|
||||
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
|
||||
github.com/Azure/go-autorest/logger v0.2.1 // indirect
|
||||
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
github.com/DataDog/zstd v1.4.5 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.1.1 // indirect
|
||||
github.com/Microsoft/go-winio v0.5.2 // indirect
|
||||
github.com/ThalesIgnite/crypto11 v1.2.5 // indirect
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 // indirect
|
||||
github.com/alibabacloud-go/cr-20160607 v1.0.1 // indirect
|
||||
github.com/alibabacloud-go/cr-20181201 v1.0.10 // indirect
|
||||
github.com/alibabacloud-go/darabonba-openapi v0.1.18 // indirect
|
||||
github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68 // indirect
|
||||
github.com/alibabacloud-go/endpoint-util v1.1.1 // indirect
|
||||
github.com/alibabacloud-go/openapi-util v0.0.11 // indirect
|
||||
github.com/alibabacloud-go/tea v1.1.18 // indirect
|
||||
github.com/alibabacloud-go/tea-utils v1.4.4 // indirect
|
||||
github.com/alibabacloud-go/tea-xml v1.1.2 // indirect
|
||||
github.com/aliyun/credentials-go v1.2.3 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.16.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.17.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.12.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.23 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.24 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ecr v1.15.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.12.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.11.23 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.16.19 // indirect
|
||||
github.com/aws/smithy-go v1.13.3 // indirect
|
||||
github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20220517224237-e6f29200ae04 // indirect
|
||||
github.com/benbjohnson/clock v1.1.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bgentry/speakeasy v0.1.0 // indirect
|
||||
github.com/blang/semver v3.5.1+incompatible // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
|
||||
github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/chrismellard/docker-credential-acr-env v0.0.0-20220119192733-fe33c00cee21 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.5.6 // indirect
|
||||
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490 // indirect
|
||||
github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be // indirect
|
||||
github.com/containerd/containerd v1.6.8 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.12.0 // indirect
|
||||
github.com/coreos/go-oidc/v3 v3.4.0 // indirect
|
||||
github.com/coreos/go-semver v0.3.0 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/cyberphone/json-canonicalization v0.0.0-20210823021906-dc406ceaf94b // indirect
|
||||
github.com/Masterminds/semver/v3 v3.2.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect
|
||||
github.com/acomagu/bufpipe v1.0.4 // indirect
|
||||
github.com/anchore/fangs v0.0.0-20230531202914-48a718c6b4ba // indirect
|
||||
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 // indirect
|
||||
github.com/aquasecurity/go-version v0.0.0-20210121072130-637058cfe492 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/becheran/wildmatch-go v1.0.0 // indirect
|
||||
github.com/charmbracelet/bubbles v0.16.1 // indirect
|
||||
github.com/charmbracelet/harmonica v0.2.0 // indirect
|
||||
github.com/cloudflare/circl v1.3.3 // indirect
|
||||
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
|
||||
github.com/containerd/containerd v1.7.0 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dimchansky/utfbom v1.1.1 // indirect
|
||||
github.com/docker/cli v20.10.17+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.1+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.6.4 // indirect
|
||||
github.com/docker/cli v23.0.5+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.7.0 // indirect
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/docker/go-units v0.4.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
|
||||
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v0.6.2 // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.4 // indirect
|
||||
github.com/fullstorydev/grpcurl v1.8.7 // indirect
|
||||
github.com/edsrzf/mmap-go v1.1.0 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/felixge/fgprof v0.9.3 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.0 // indirect
|
||||
github.com/go-chi/chi v4.1.2+incompatible // indirect
|
||||
github.com/go-logr/logr v1.2.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/analysis v0.21.4 // indirect
|
||||
github.com/go-openapi/errors v0.20.3 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.0 // indirect
|
||||
github.com/go-openapi/loads v0.21.2 // indirect
|
||||
github.com/go-openapi/runtime v0.24.2 // indirect
|
||||
github.com/go-openapi/spec v0.20.7 // indirect
|
||||
github.com/go-openapi/strfmt v0.21.3 // indirect
|
||||
github.com/go-openapi/swag v0.22.3 // indirect
|
||||
github.com/go-openapi/validate v0.22.0 // indirect
|
||||
github.com/go-piv/piv-go v1.10.0 // indirect
|
||||
github.com/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.11.0 // indirect
|
||||
github.com/gkampitakis/ciinfo v0.2.4 // indirect
|
||||
github.com/gkampitakis/go-diff v1.3.2 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-restruct/restruct v1.2.0-alpha // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.4.2 // indirect
|
||||
github.com/golang/glog v1.0.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/mock v1.6.0 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/btree v1.1.2 // indirect
|
||||
github.com/google/certificate-transparency-go v1.1.3 // indirect
|
||||
github.com/google/go-github/v45 v45.2.0 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/trillian v1.5.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.0 // indirect
|
||||
github.com/googleapis/gnostic v0.5.5 // indirect
|
||||
github.com/gorilla/websocket v1.4.2 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3 // indirect
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.1 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/huandu/xstrings v1.3.2 // indirect
|
||||
github.com/huandu/xstrings v1.3.3 // indirect
|
||||
github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 // indirect
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
||||
github.com/jedisct1/go-minisign v0.0.0-20211028175153-1c139d1cc84b // indirect
|
||||
github.com/jhump/protoreflect v1.13.0 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/jonboulle/clockwork v0.3.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/imdario/mergo v0.3.15 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/klauspost/compress v1.15.9 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/klauspost/compress v1.16.5 // indirect
|
||||
github.com/klauspost/pgzip v1.2.5 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/letsencrypt/boulder v0.0.0-20220929215747-76583552c2be // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 // indirect
|
||||
github.com/magiconair/properties v1.8.6 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
|
||||
github.com/miekg/pkcs11 v1.1.1 // indirect
|
||||
github.com/mattn/go-isatty v0.0.18 // indirect
|
||||
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/mozillazg/docker-credential-acr-helper v0.3.0 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/reflow v0.3.0 // indirect
|
||||
github.com/muesli/termenv v0.15.1 // indirect
|
||||
github.com/nwaples/rardecode v1.1.0 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0-rc3 // indirect
|
||||
github.com/pborman/indent v1.2.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.15 // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||
github.com/pkg/profile v1.7.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_golang v1.13.0 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.37.0 // indirect
|
||||
github.com/prometheus/procfs v0.8.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/sassoftware/relic v0.0.0-20210427151427-dfb082b79b74 // indirect
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.4.0 // indirect
|
||||
github.com/segmentio/ksuid v1.0.4 // indirect
|
||||
github.com/shibumi/go-pathspec v1.3.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.9.0 // indirect
|
||||
github.com/shopspring/decimal v1.2.0 // indirect
|
||||
github.com/sigstore/fulcio v0.6.0 // indirect
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect
|
||||
github.com/soheilhy/cmux v0.1.5 // indirect
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
github.com/skeema/knownhosts v1.2.0 // indirect
|
||||
github.com/spf13/cast v1.5.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spiffe/go-spiffe/v2 v2.1.1 // indirect
|
||||
github.com/stretchr/objx v0.4.0 // indirect
|
||||
github.com/subosito/gotenv v1.4.1 // indirect
|
||||
github.com/sylabs/sif/v2 v2.8.1 // indirect
|
||||
github.com/stretchr/objx v0.5.0 // indirect
|
||||
github.com/subosito/gotenv v1.4.2 // indirect
|
||||
github.com/sylabs/sif/v2 v2.11.5 // indirect
|
||||
github.com/sylabs/squashfs v0.6.1 // indirect
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect
|
||||
github.com/tent/canonical-json-go v0.0.0-20130607151641-96e4ba3a7613 // indirect
|
||||
github.com/thales-e-security/pool v0.0.2 // indirect
|
||||
github.com/therootcompany/xz v1.0.1 // indirect
|
||||
github.com/theupdateframework/go-tuf v0.5.2-0.20220930112810-3890c1e7ace4 // indirect
|
||||
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect
|
||||
github.com/tjfoc/gmsm v1.3.2 // indirect
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect
|
||||
github.com/transparency-dev/merkle v0.0.1 // indirect
|
||||
github.com/tidwall/gjson v1.14.4 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/ulikunitz/xz v0.5.10 // indirect
|
||||
github.com/urfave/cli v1.22.7 // indirect
|
||||
github.com/vbatts/tar-split v0.11.2 // indirect
|
||||
github.com/xanzy/go-gitlab v0.73.1 // indirect
|
||||
github.com/vbatts/tar-split v0.11.3 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
|
||||
github.com/zeebo/errs v1.2.2 // indirect
|
||||
go.etcd.io/bbolt v1.3.6 // indirect
|
||||
go.etcd.io/etcd/api/v3 v3.6.0-alpha.0 // indirect
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.0-alpha.0 // indirect
|
||||
go.etcd.io/etcd/client/v2 v2.306.0-alpha.0 // indirect
|
||||
go.etcd.io/etcd/client/v3 v3.6.0-alpha.0 // indirect
|
||||
go.etcd.io/etcd/etcdctl/v3 v3.6.0-alpha.0 // indirect
|
||||
go.etcd.io/etcd/etcdutl/v3 v3.6.0-alpha.0 // indirect
|
||||
go.etcd.io/etcd/pkg/v3 v3.6.0-alpha.0 // indirect
|
||||
go.etcd.io/etcd/raft/v3 v3.6.0-alpha.0 // indirect
|
||||
go.etcd.io/etcd/server/v3 v3.6.0-alpha.0 // indirect
|
||||
go.etcd.io/etcd/tests/v3 v3.6.0-alpha.0 // indirect
|
||||
go.etcd.io/etcd/v3 v3.6.0-alpha.0 // indirect
|
||||
go.mongodb.org/mongo-driver v1.10.0 // indirect
|
||||
go.opencensus.io v0.23.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.28.0 // indirect
|
||||
go.opentelemetry.io/otel v1.7.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.7.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.7.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.7.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.7.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.7.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v0.16.0 // indirect
|
||||
go.uber.org/atomic v1.10.0 // indirect
|
||||
go.uber.org/multierr v1.8.0 // indirect
|
||||
go.uber.org/zap v1.23.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20220823124025-807a23277127 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1 // indirect
|
||||
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0 // indirect
|
||||
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect
|
||||
golang.org/x/text v0.3.8-0.20211004125949-5bd84dd9b33b // indirect
|
||||
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af // indirect
|
||||
golang.org/x/tools v0.1.12 // indirect
|
||||
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sys v0.10.0 // indirect
|
||||
golang.org/x/text v0.11.0 // indirect
|
||||
golang.org/x/tools v0.8.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
google.golang.org/api v0.99.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e // indirect
|
||||
google.golang.org/grpc v1.50.1 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
gopkg.in/cheggaaa/pb.v1 v1.0.28 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
|
||||
google.golang.org/grpc v1.55.0 // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||
k8s.io/api v0.23.5 // indirect
|
||||
k8s.io/apimachinery v0.23.5 // indirect
|
||||
k8s.io/client-go v0.23.5 // indirect
|
||||
k8s.io/klog/v2 v2.60.1-0.20220317184644-43cc75f9ae89 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20220124234850-424119656bbf // indirect
|
||||
k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect
|
||||
lukechampine.com/uint128 v1.1.1 // indirect
|
||||
modernc.org/cc/v3 v3.36.0 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.6 // indirect
|
||||
modernc.org/libc v1.16.7 // indirect
|
||||
modernc.org/mathutil v1.4.1 // indirect
|
||||
modernc.org/memory v1.1.1 // indirect
|
||||
modernc.org/opt v0.1.1 // indirect
|
||||
modernc.org/sqlite v1.17.3 // indirect
|
||||
modernc.org/strutil v1.1.1 // indirect
|
||||
modernc.org/token v1.0.0 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect
|
||||
sigs.k8s.io/release-utils v0.7.3 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect
|
||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
lukechampine.com/uint128 v1.2.0 // indirect
|
||||
modernc.org/cc/v3 v3.40.0 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.13 // indirect
|
||||
modernc.org/libc v1.22.5 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.5.0 // indirect
|
||||
modernc.org/opt v0.1.3 // indirect
|
||||
modernc.org/strutil v1.1.3 // indirect
|
||||
modernc.org/token v1.0.1 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -333,7 +204,7 @@ require (
|
||||
// go: warning: github.com/andybalholm/brotli@v1.0.1: retracted by module author: occasional panics and data corruption
|
||||
github.com/andybalholm/brotli v1.0.4 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be // indirect
|
||||
golang.org/x/crypto v0.11.0 // indirect
|
||||
)
|
||||
|
||||
retract (
|
||||
|
||||
+1
-1
@@ -694,6 +694,6 @@ main() (
|
||||
set +u
|
||||
if [ -z "${TEST_INSTALL_SH}" ]; then
|
||||
set -u
|
||||
main "$@"
|
||||
exit $(main "$@")
|
||||
fi
|
||||
set -u
|
||||
+9
-9
@@ -16,20 +16,20 @@ package bus
|
||||
import "github.com/wagoodman/go-partybus"
|
||||
|
||||
var publisher partybus.Publisher
|
||||
var active bool
|
||||
|
||||
// SetPublisher sets the singleton event bus publisher. This is optional; if no bus is provided, the library will
|
||||
// Set sets the singleton event bus publisher. This is optional; if no bus is provided, the library will
|
||||
// behave no differently than if a bus had been provided.
|
||||
func SetPublisher(p partybus.Publisher) {
|
||||
func Set(p partybus.Publisher) {
|
||||
publisher = p
|
||||
if p != nil {
|
||||
active = true
|
||||
}
|
||||
}
|
||||
|
||||
func Get() partybus.Publisher {
|
||||
return publisher
|
||||
}
|
||||
|
||||
// Publish an event onto the bus. If there is no bus set by the calling application, this does nothing.
|
||||
func Publish(event partybus.Event) {
|
||||
if active {
|
||||
publisher.Publish(event)
|
||||
func Publish(e partybus.Event) {
|
||||
if publisher != nil {
|
||||
publisher.Publish(e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package bus
|
||||
|
||||
import (
|
||||
"github.com/wagoodman/go-partybus"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
)
|
||||
|
||||
func Exit() {
|
||||
Publish(partybus.Event{
|
||||
Type: event.CLIExit,
|
||||
})
|
||||
}
|
||||
|
||||
func Report(report string) {
|
||||
if len(report) == 0 {
|
||||
return
|
||||
}
|
||||
report = log.Redactor.RedactString(report)
|
||||
Publish(partybus.Event{
|
||||
Type: event.CLIReport,
|
||||
Value: report,
|
||||
})
|
||||
}
|
||||
|
||||
func Notify(message string) {
|
||||
Publish(partybus.Event{
|
||||
Type: event.CLINotification,
|
||||
Value: message,
|
||||
})
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package config
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
"sort"
|
||||
@@ -17,6 +18,9 @@ import (
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger"
|
||||
golangCataloger "github.com/anchore/syft/syft/pkg/cataloger/golang"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/kernel"
|
||||
pythonCataloger "github.com/anchore/syft/syft/pkg/cataloger/python"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -38,24 +42,31 @@ type Application struct {
|
||||
ConfigPath string `yaml:"configPath,omitempty" json:"configPath" mapstructure:"config"`
|
||||
Verbosity uint `yaml:"verbosity,omitempty" json:"verbosity" mapstructure:"verbosity"`
|
||||
// -q, indicates to not show any status output to stderr (ETUI or logging UI)
|
||||
Quiet bool `yaml:"quiet" json:"quiet" mapstructure:"quiet"`
|
||||
Outputs []string `yaml:"output" json:"output" mapstructure:"output"` // -o, the format to use for output
|
||||
OutputTemplatePath string `yaml:"output-template-path" json:"output-template-path" mapstructure:"output-template-path"` // -t template file to use for output
|
||||
File string `yaml:"file" json:"file" mapstructure:"file"` // --file, the file to write report output to
|
||||
CheckForAppUpdate bool `yaml:"check-for-app-update" json:"check-for-app-update" mapstructure:"check-for-app-update"` // whether to check for an application update on start up or not
|
||||
Dev development `yaml:"dev" json:"dev" mapstructure:"dev"`
|
||||
Log logging `yaml:"log" json:"log" mapstructure:"log"` // all logging-related options
|
||||
Catalogers []string `yaml:"catalogers" json:"catalogers" mapstructure:"catalogers"`
|
||||
Package pkg `yaml:"package" json:"package" mapstructure:"package"`
|
||||
FileMetadata FileMetadata `yaml:"file-metadata" json:"file-metadata" mapstructure:"file-metadata"`
|
||||
FileClassification fileClassification `yaml:"file-classification" json:"file-classification" mapstructure:"file-classification"`
|
||||
FileContents fileContents `yaml:"file-contents" json:"file-contents" mapstructure:"file-contents"`
|
||||
Secrets secrets `yaml:"secrets" json:"secrets" mapstructure:"secrets"`
|
||||
Registry registry `yaml:"registry" json:"registry" mapstructure:"registry"`
|
||||
Exclusions []string `yaml:"exclude" json:"exclude" mapstructure:"exclude"`
|
||||
Attest attest `yaml:"attest" json:"attest" mapstructure:"attest"`
|
||||
Platform string `yaml:"platform" json:"platform" mapstructure:"platform"`
|
||||
Name string `yaml:"name" json:"name" mapstructure:"name"`
|
||||
Quiet bool `yaml:"quiet" json:"quiet" mapstructure:"quiet"`
|
||||
Outputs []string `yaml:"output" json:"output" mapstructure:"output"` // -o, the format to use for output
|
||||
OutputTemplatePath string `yaml:"output-template-path" json:"output-template-path" mapstructure:"output-template-path"` // -t template file to use for output
|
||||
File string `yaml:"file" json:"file" mapstructure:"file"` // --file, the file to write report output to
|
||||
CheckForAppUpdate bool `yaml:"check-for-app-update" json:"check-for-app-update" mapstructure:"check-for-app-update"` // whether to check for an application update on start up or not
|
||||
Dev development `yaml:"dev" json:"dev" mapstructure:"dev"`
|
||||
Log logging `yaml:"log" json:"log" mapstructure:"log"` // all logging-related options
|
||||
Catalogers []string `yaml:"catalogers" json:"catalogers" mapstructure:"catalogers"`
|
||||
Package pkg `yaml:"package" json:"package" mapstructure:"package"`
|
||||
Golang golang `yaml:"golang" json:"golang" mapstructure:"golang"`
|
||||
LinuxKernel linuxKernel `yaml:"linux-kernel" json:"linux-kernel" mapstructure:"linux-kernel"`
|
||||
Python python `yaml:"python" json:"python" mapstructure:"python"`
|
||||
Attest attest `yaml:"attest" json:"attest" mapstructure:"attest"`
|
||||
FileMetadata FileMetadata `yaml:"file-metadata" json:"file-metadata" mapstructure:"file-metadata"`
|
||||
FileClassification fileClassification `yaml:"file-classification" json:"file-classification" mapstructure:"file-classification"`
|
||||
FileContents fileContents `yaml:"file-contents" json:"file-contents" mapstructure:"file-contents"`
|
||||
Secrets secrets `yaml:"secrets" json:"secrets" mapstructure:"secrets"`
|
||||
Registry registry `yaml:"registry" json:"registry" mapstructure:"registry"`
|
||||
Exclusions []string `yaml:"exclude" json:"exclude" mapstructure:"exclude"`
|
||||
Platform string `yaml:"platform" json:"platform" mapstructure:"platform"`
|
||||
Name string `yaml:"name" json:"name" mapstructure:"name"`
|
||||
Source sourceCfg `yaml:"source" json:"source" mapstructure:"source"`
|
||||
Parallelism int `yaml:"parallelism" json:"parallelism" mapstructure:"parallelism"` // the number of catalog workers to run in parallel
|
||||
DefaultImagePullSource string `yaml:"default-image-pull-source" json:"default-image-pull-source" mapstructure:"default-image-pull-source"` // specify default image pull source
|
||||
BasePath string `yaml:"base-path" json:"base-path" mapstructure:"base-path"` // specify base path for all file paths
|
||||
}
|
||||
|
||||
func (cfg Application) ToCatalogerConfig() cataloger.Config {
|
||||
@@ -65,7 +76,20 @@ func (cfg Application) ToCatalogerConfig() cataloger.Config {
|
||||
IncludeUnindexedArchives: cfg.Package.SearchUnindexedArchives,
|
||||
Scope: cfg.Package.Cataloger.ScopeOpt,
|
||||
},
|
||||
Catalogers: cfg.Catalogers,
|
||||
Catalogers: cfg.Catalogers,
|
||||
Parallelism: cfg.Parallelism,
|
||||
Golang: golangCataloger.NewGoCatalogerOpts().
|
||||
WithSearchLocalModCacheLicenses(cfg.Golang.SearchLocalModCacheLicenses).
|
||||
WithLocalModCacheDir(cfg.Golang.LocalModCacheDir).
|
||||
WithSearchRemoteLicenses(cfg.Golang.SearchRemoteLicenses).
|
||||
WithProxy(cfg.Golang.Proxy).
|
||||
WithNoProxy(cfg.Golang.NoProxy),
|
||||
LinuxKernel: kernel.LinuxCatalogerConfig{
|
||||
CatalogModules: cfg.LinuxKernel.CatalogModules,
|
||||
},
|
||||
Python: pythonCataloger.CatalogerConfig{
|
||||
GuessUnpinnedRequirements: cfg.Python.GuessUnpinnedRequirements,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,6 +145,19 @@ func (cfg *Application) parseConfigValues() error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := checkDefaultSourceValues(cfg.DefaultImagePullSource); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cfg.Name != "" {
|
||||
log.Warnf("name parameter is deprecated. please use: source-name. name will be removed in a future version")
|
||||
if cfg.Source.Name == "" {
|
||||
cfg.Source.Name = cfg.Name
|
||||
}
|
||||
}
|
||||
|
||||
// check for valid default source options
|
||||
// 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)
|
||||
@@ -182,6 +219,8 @@ func loadDefaultValues(v *viper.Viper) {
|
||||
v.SetDefault("quiet", false)
|
||||
v.SetDefault("check-for-app-update", true)
|
||||
v.SetDefault("catalogers", nil)
|
||||
v.SetDefault("parallelism", 1)
|
||||
v.SetDefault("default-image-pull-source", "")
|
||||
|
||||
// for each field in the configuration struct, see if the field implements the defaultValueLoader interface and invoke it if it does
|
||||
value := reflect.ValueOf(Application{})
|
||||
@@ -205,6 +244,7 @@ func (cfg Application) String() string {
|
||||
return string(appaStr)
|
||||
}
|
||||
|
||||
// nolint:funlen
|
||||
func loadConfig(v *viper.Viper, configPath string) error {
|
||||
var err error
|
||||
// use explicitly the given user config
|
||||
@@ -220,13 +260,26 @@ func loadConfig(v *viper.Viper, configPath string) error {
|
||||
|
||||
// start searching for valid configs in order...
|
||||
// 1. look for .<appname>.yaml (in the current directory)
|
||||
confFilePath := "." + internal.ApplicationName
|
||||
|
||||
// TODO: Remove this before v1.0.0
|
||||
// See syft #1634
|
||||
v.AddConfigPath(".")
|
||||
v.SetConfigName("." + internal.ApplicationName)
|
||||
if err = v.ReadInConfig(); err == nil {
|
||||
v.Set("config", v.ConfigFileUsed())
|
||||
return nil
|
||||
} else if !errors.As(err, &viper.ConfigFileNotFoundError{}) {
|
||||
return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err)
|
||||
v.SetConfigName(confFilePath)
|
||||
|
||||
// check if config.yaml exists in the current directory
|
||||
// DEPRECATED: this will be removed in v1.0.0
|
||||
if _, err := os.Stat("config.yaml"); err == nil {
|
||||
log.Warn("DEPRECATED: ./config.yaml as a configuration file is deprecated and will be removed as an option in v1.0.0, please rename to .syft.yaml")
|
||||
}
|
||||
|
||||
if _, err := os.Stat(confFilePath + ".yaml"); err == nil {
|
||||
if err = v.ReadInConfig(); err == nil {
|
||||
v.Set("config", v.ConfigFileUsed())
|
||||
return nil
|
||||
} else if !errors.As(err, &viper.ConfigFileNotFoundError{}) {
|
||||
return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. look for .<appname>/config.yaml (in the current directory)
|
||||
@@ -253,11 +306,12 @@ func loadConfig(v *viper.Viper, configPath string) error {
|
||||
}
|
||||
|
||||
// 4. look for <appname>/config.yaml in xdg locations (starting with xdg home config dir, then moving upwards)
|
||||
v.AddConfigPath(path.Join(xdg.ConfigHome, internal.ApplicationName))
|
||||
v.SetConfigName("config")
|
||||
configPath = path.Join(xdg.ConfigHome, internal.ApplicationName)
|
||||
v.AddConfigPath(configPath)
|
||||
for _, dir := range xdg.ConfigDirs {
|
||||
v.AddConfigPath(path.Join(dir, internal.ApplicationName))
|
||||
}
|
||||
v.SetConfigName("config")
|
||||
if err = v.ReadInConfig(); err == nil {
|
||||
v.Set("config", v.ConfigFileUsed())
|
||||
return nil
|
||||
@@ -266,3 +320,15 @@ func loadConfig(v *viper.Viper, configPath string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var validDefaultSourceValues = []string{"registry", "docker", "podman", ""}
|
||||
|
||||
func checkDefaultSourceValues(source string) error {
|
||||
validValues := internal.NewStringSet(validDefaultSourceValues...)
|
||||
if !validValues.Contains(source) {
|
||||
validValuesString := strings.Join(validDefaultSourceValues, ", ")
|
||||
return fmt.Errorf("%s is not a valid default source; please use one of the following: %s''", source, validValuesString)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/adrg/xdg"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TODO: set negative case when config.yaml is no longer a valid option
|
||||
func TestApplicationConfig(t *testing.T) {
|
||||
// disable homedir package cache for testing
|
||||
originalCacheOpt := homedir.DisableCache
|
||||
homedir.DisableCache = true
|
||||
t.Cleanup(func() {
|
||||
homedir.DisableCache = originalCacheOpt
|
||||
})
|
||||
|
||||
// ensure we have no side effects for xdg package for future tests
|
||||
originalXDG := os.Getenv("XDG_CONFIG_HOME")
|
||||
t.Cleanup(func() {
|
||||
// note: we're not using t.Setenv since the effect we're trying to eliminate is within the xdg package
|
||||
require.NoError(t, os.Setenv("XDG_CONFIG_HOME", originalXDG))
|
||||
xdg.Reload()
|
||||
})
|
||||
|
||||
// config is picked up at desired configuration paths
|
||||
// VALID: .syft.yaml, .syft/config.yaml, ~/.syft.yaml, <XDG_CONFIG_HOME>/syft/config.yaml
|
||||
// DEPRECATED: config.yaml is currently supported by
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(t *testing.T) string
|
||||
assertions func(t *testing.T, app *Application)
|
||||
cleanup func()
|
||||
}{
|
||||
{
|
||||
name: "explicit config",
|
||||
setup: func(t *testing.T) string {
|
||||
return "./test-fixtures/.syft.yaml"
|
||||
}, // no-op for explicit config
|
||||
assertions: func(t *testing.T, app *Application) {
|
||||
assert.Equal(t, "test-explicit-config", app.File)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "current working directory named config",
|
||||
setup: func(t *testing.T) string {
|
||||
err := os.Chdir("./test-fixtures/config-wd-file") // change application cwd to test-fixtures
|
||||
require.NoError(t, err)
|
||||
return ""
|
||||
},
|
||||
assertions: func(t *testing.T, app *Application) {
|
||||
assert.Equal(t, "test-wd-named-config", app.File)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "current working directory syft dir config",
|
||||
setup: func(t *testing.T) string {
|
||||
err := os.Chdir("./test-fixtures/config-dir-test") // change application cwd to test-fixtures
|
||||
require.NoError(t, err)
|
||||
return ""
|
||||
},
|
||||
assertions: func(t *testing.T, app *Application) {
|
||||
assert.Equal(t, "test-dir-config", app.File)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "home directory file config",
|
||||
setup: func(t *testing.T) string {
|
||||
// Because Setenv affects the whole process, it cannot be used in parallel tests or
|
||||
// tests with parallel ancestors: see separate XDG test for consequence of this
|
||||
t.Setenv("HOME", "./test-fixtures/config-home-test/config-file")
|
||||
return ""
|
||||
},
|
||||
assertions: func(t *testing.T, app *Application) {
|
||||
assert.Equal(t, "test-home-config", app.File)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "XDG file config",
|
||||
setup: func(t *testing.T) string {
|
||||
wd, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
configDir := path.Join(wd, "./test-fixtures/config-home-test") // set HOME to testdata
|
||||
// note: this explicitly has multiple XDG paths, make certain we use the first VALID one (not the first one)
|
||||
t.Setenv("XDG_CONFIG_DIRS", fmt.Sprintf("/another/foo/bar:%s", configDir))
|
||||
xdg.Reload()
|
||||
return ""
|
||||
},
|
||||
assertions: func(t *testing.T, app *Application) {
|
||||
assert.Equal(t, "test-home-XDG-config", app.File)
|
||||
},
|
||||
cleanup: func() {
|
||||
require.NoError(t, os.Unsetenv("XDG_CONFIG_DIRS"))
|
||||
xdg.Reload()
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if test.cleanup != nil {
|
||||
t.Cleanup(test.cleanup)
|
||||
}
|
||||
wd, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
|
||||
defer os.Chdir(wd) // reset working directory after test
|
||||
application := &Application{}
|
||||
viperInstance := viper.New()
|
||||
|
||||
// this will override home in case you are running this test locally and DO have a syft config
|
||||
// in your home directory... now it will be ignored. Same for XDG_CONFIG_DIRS.
|
||||
t.Setenv("HOME", "/foo/bar")
|
||||
t.Setenv("XDG_CONFIG_DIRS", "/foo/bar")
|
||||
xdg.Reload()
|
||||
|
||||
configPath := test.setup(t)
|
||||
err = application.LoadAllValues(viperInstance, configPath)
|
||||
require.NoError(t, err)
|
||||
test.assertions(t, application)
|
||||
})
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user