mirror of
https://github.com/anchore/syft.git
synced 2025-11-18 17:03:17 +01:00
add manager for binary cataloger test fixtures
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
parent
bab4142881
commit
e516eb4967
9
go.mod
9
go.mod
@ -76,6 +76,12 @@ require (
|
||||
modernc.org/sqlite v1.28.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
|
||||
github.com/charmbracelet/bubbles v0.16.1
|
||||
github.com/jedib0t/go-pretty/v6 v6.4.9
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
|
||||
@ -91,9 +97,9 @@ require (
|
||||
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 // indirect
|
||||
github.com/andybalholm/brotli v1.0.4 // indirect
|
||||
github.com/aquasecurity/go-version v0.0.0-20210121072130-637058cfe492 // indirect
|
||||
github.com/atotto/clipboard v0.1.4 // 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/cgroups v1.1.0 // indirect
|
||||
@ -179,6 +185,7 @@ require (
|
||||
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.3.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/sahilm/fuzzy v0.1.0 // indirect
|
||||
github.com/shopspring/decimal v1.2.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/skeema/knownhosts v1.2.1 // indirect
|
||||
|
||||
11
go.sum
11
go.sum
@ -127,6 +127,8 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj
|
||||
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/becheran/wildmatch-go v1.0.0 h1:mE3dGGkTmpKtT4Z+88t8RStG40yN9T+kFEGj2PZFSzA=
|
||||
@ -462,6 +464,8 @@ github.com/invopop/jsonschema v0.7.0 h1:2vgQcBz1n256N+FpX3Jq7Y17AjYt46Ig3zIWyy77
|
||||
github.com/invopop/jsonschema v0.7.0/go.mod h1:O9uiLokuu0+MGFlyiaqtWxwqJm41/+8Nj0lD7A36YH0=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jedib0t/go-pretty/v6 v6.4.9 h1:vZ6bjGg2eBSrJn365qlxGcaWu09Id+LHtrfDWlB2Usc=
|
||||
github.com/jedib0t/go-pretty/v6 v6.4.9/go.mod h1:Ndk3ase2CkQbXLLNf5QDHoYb6J9WtVfmHZu9n8rk2xs=
|
||||
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
|
||||
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
@ -501,6 +505,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 h1:bqDmpDG49ZRnB5PcgP0RXtQvnMSgIF14M7CBd2shtXs=
|
||||
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
@ -531,6 +537,7 @@ github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZ
|
||||
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
@ -627,6 +634,7 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18=
|
||||
github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
|
||||
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
|
||||
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
|
||||
@ -671,6 +679,8 @@ github.com/sagikazarmark/locafero v0.3.0 h1:zT7VEGWC2DTflmccN/5T1etyKvxSxpHsjb9c
|
||||
github.com/sagikazarmark/locafero v0.3.0/go.mod h1:w+v7UsPNFwzF1cHuOajOOzoq4U7v/ig1mpRjqV+Bu1U=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
|
||||
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA=
|
||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
|
||||
github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo=
|
||||
@ -736,6 +746,7 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -233,8 +233,6 @@ var defaultClassifiers = []classifier{
|
||||
Class: "mysql-binary",
|
||||
FileGlob: "**/mysql",
|
||||
EvidenceMatcher: fileContentsVersionMatcher(
|
||||
// ../../mysql-8.0.34
|
||||
// /mysql-5.6.51/bld/client
|
||||
`(?m).*/mysql-(?P<version>[0-9]+(\.[0-9]+)?(\.[0-9]+)?(alpha[0-9]|beta[0-9]|rc[0-9])?)`),
|
||||
Package: "mysql",
|
||||
PURL: mustPURL("pkg:generic/mysql@version"),
|
||||
@ -286,9 +284,15 @@ var defaultClassifiers = []classifier{
|
||||
{
|
||||
Class: "erlang-binary",
|
||||
FileGlob: "**/erlexec",
|
||||
EvidenceMatcher: fileContentsVersionMatcher(
|
||||
// <artificial>[NUL]/usr/local/src/otp-25.3.2.7/erts/
|
||||
`(?m)/usr/local/src/otp-(?P<version>[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+?)/erts/`,
|
||||
EvidenceMatcher: evidenceMatchers(
|
||||
fileContentsVersionMatcher(
|
||||
// <artificial>[NUL]/usr/src/otp_src_25.3.2.6/erts/
|
||||
`(?m)/src/otp_src_(?P<version>[0-9]+\.[0-9]+(\.[0-9]+\.[0-9]+)?)/erts/`,
|
||||
),
|
||||
fileContentsVersionMatcher(
|
||||
// <artificial>[NUL]/usr/local/src/otp-25.3.2.7/erts/
|
||||
`(?m)/usr/local/src/otp-(?P<version>[0-9]+\.[0-9]+(\.[0-9]+\.[0-9]+)?)/erts/`,
|
||||
),
|
||||
),
|
||||
Package: "erlang",
|
||||
PURL: mustPURL("pkg:generic/erlang@version"),
|
||||
|
||||
@ -1 +1,2 @@
|
||||
classifiers/dynamic
|
||||
classifiers/bin
|
||||
@ -1,110 +1,24 @@
|
||||
.PHONY: all
|
||||
all: \
|
||||
classifiers/dynamic/python-binary-shared-lib-3.11 \
|
||||
classifiers/dynamic/python-binary-shared-lib-redhat-3.9 \
|
||||
classifiers/dynamic/python-binary-with-version-3.9 \
|
||||
classifiers/dynamic/python-binary-3.4-alpine \
|
||||
classifiers/dynamic/ruby-library-3.2.1 \
|
||||
classifiers/dynamic/ruby-library-2.7.7 \
|
||||
classifiers/dynamic/ruby-library-2.6.10 \
|
||||
classifiers/dynamic/helm-3.11.1 \
|
||||
classifiers/dynamic/helm-3.10.3 \
|
||||
classifiers/dynamic/consul-1.15.2
|
||||
.PHONY: default list download download-all fingerprint
|
||||
|
||||
.DEFAULT_GOAL := default
|
||||
|
||||
default: download
|
||||
|
||||
list: ## list all managed binaries and snippets
|
||||
go run ./manager list
|
||||
|
||||
download: ## download only binaries that are not covered by a snippet
|
||||
go run ./manager download $(name) --skip-if-covered-by-snippet
|
||||
|
||||
download-all: ## download all managed binaries
|
||||
go run ./manager download
|
||||
|
||||
fingerprint: ## prints the sha256sum of the any input to the download command (to determine if there is a cache miss)
|
||||
@cat ./config.yaml | sha256sum | awk '{print $$1}'
|
||||
|
||||
|
||||
## Halp! #################################
|
||||
|
||||
classifiers/dynamic/python-binary-shared-lib-3.11:
|
||||
$(eval $@_image := "python:3.11-slim@sha256:0b106e1d2bf485c2a41474bc9cd5103e9eea4e179f40f10741b53b127059221e")
|
||||
./get-image-file.sh $($@_image) \
|
||||
/usr/local/bin/python3.11 \
|
||||
$@/python3
|
||||
./get-image-file.sh $($@_image) \
|
||||
/usr/local/lib/libpython3.11.so.1.0 \
|
||||
$@/libpython3.11.so.1.0
|
||||
|
||||
classifiers/dynamic/python-binary-shared-lib-redhat-3.9:
|
||||
$(eval $@_image := "registry.access.redhat.com/ubi8/python-39@sha256:f3cf958b96ce016b63e3e163e488f52e42891304dafef5a0811563f22e3cbad0")
|
||||
./get-image-file.sh $($@_image) \
|
||||
/usr/bin/python3.9 \
|
||||
$@/python3.9
|
||||
./get-image-file.sh $($@_image) \
|
||||
/usr/lib64/libpython3.9.so.1.0 \
|
||||
$@/libpython3.9.so.1.0
|
||||
|
||||
classifiers/dynamic/python-binary-with-version-3.9:
|
||||
$(eval $@_image := "python:3.9.16-bullseye@sha256:93fb93c461a2e47a2176706fad1f39eaacd5dd40e19c0b018699a28c03eb2e2a")
|
||||
./get-image-file.sh $($@_image) \
|
||||
/usr/bin/python3.9 \
|
||||
$@/python3.9
|
||||
|
||||
classifiers/dynamic/python-binary-3.4-alpine:
|
||||
$(eval $@_image := "python:3.4-alpine@sha256:c210b660e2ea553a7afa23b41a6ed112f85dbce25cbcb567c75dfe05342a4c4b")
|
||||
./get-image-file.sh $($@_image) \
|
||||
/usr/local/bin/python3.4 \
|
||||
$@/python3.4
|
||||
./get-image-file.sh $($@_image) \
|
||||
/usr/local/lib/libpython3.4m.so.1.0 \
|
||||
$@/libpython3.4m.so.1.0
|
||||
|
||||
classifiers/dynamic/ruby-library-3.2.1:
|
||||
$(eval $@_image := "ruby:3.2.1-bullseye@sha256:b4a140656b0c5d26c0a80559b228b4d343f3fdbf56682fcbe88f6db1fa9afa6b")
|
||||
./get-image-file.sh $($@_image) \
|
||||
/usr/local/bin/ruby \
|
||||
$@/ruby
|
||||
./get-image-file.sh $($@_image) \
|
||||
/usr/local/lib/libruby.so.3.2.1 \
|
||||
$@/libruby.so.3.2.1
|
||||
./get-image-file.sh $($@_image) \
|
||||
/usr/local/lib/libruby.so.3.2 \
|
||||
$@/libruby.so.3.2
|
||||
|
||||
classifiers/dynamic/ruby-library-2.7.7:
|
||||
$(eval $@_image := "ruby:2.7.7-bullseye@sha256:055191740a063f33fef1f09423e5ed8f91143aae62a3772a90910118464c5120")
|
||||
./get-image-file.sh $($@_image) \
|
||||
/usr/local/bin/ruby \
|
||||
$@/ruby
|
||||
./get-image-file.sh $($@_image) \
|
||||
/usr/local/lib/libruby.so.2.7.7 \
|
||||
$@/libruby.so.2.7.7
|
||||
./get-image-file.sh $($@_image) \
|
||||
/usr/local/lib/libruby.so.2.7 \
|
||||
$@/libruby.so.2.7
|
||||
|
||||
classifiers/dynamic/ruby-library-2.6.10:
|
||||
$(eval $@_image := "ruby:2.6.10@sha256:771a810704167e55da8a19970c5dfa6eb795dfee32547adffa29ea72703f7243")
|
||||
./get-image-file.sh $($@_image) \
|
||||
/usr/local/bin/ruby \
|
||||
$@/ruby
|
||||
./get-image-file.sh $($@_image) \
|
||||
/usr/local/lib/libruby.so.2.6.10 \
|
||||
$@/libruby.so.2.6.10
|
||||
./get-image-file.sh $($@_image) \
|
||||
/usr/local/lib/libruby.so.2.6 \
|
||||
$@/libruby.so.2.6
|
||||
|
||||
classifiers/dynamic/helm-3.11.1:
|
||||
$(eval $@_image := "alpine/helm:3.11.1@sha256:8628e3695fb743a8b9de89626f1b7a221280c2152c0e288c2504e59b68233e8b")
|
||||
./get-image-file.sh $($@_image) \
|
||||
/usr/bin/helm \
|
||||
$@/helm
|
||||
|
||||
classifiers/dynamic/helm-3.10.3:
|
||||
$(eval $@_image := "argoproj/argocd:v2.6.4@sha256:61fcbba187ff53c00696cb580edf70cada59c45cf399d8477631acf43cf522ee")
|
||||
./get-image-file.sh $($@_image) \
|
||||
/usr/local/bin/helm \
|
||||
$@/helm
|
||||
|
||||
classifiers/dynamic/consul-1.15.2:
|
||||
$(eval $@_image := "hashicorp/consul:1.15.2@sha256:c2169f3bb18dd947ae8eb5f6766896695c71fb439f050a3343e0007d895615b8")
|
||||
./get-image-file.sh $($@_image) \
|
||||
/bin/consul \
|
||||
$@/consul
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf classifiers/dynamic
|
||||
|
||||
.PHONY: cache.fingerprint
|
||||
cache.fingerprint: # for CI
|
||||
$(title,Install test fixture fingerprint)
|
||||
@find ./classifiers/dynamic/* -type f -exec md5sum {} + | awk '{print $1}' | sort | tee /dev/stderr | md5sum | tee cache.fingerprint >> cache.fingerprint
|
||||
.PHONY: help
|
||||
help:
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "$(BOLD)$(CYAN)%-25s$(RESET)%s\n", $$1, $$2}'
|
||||
78
syft/pkg/cataloger/binary/test-fixtures/README.md
Normal file
78
syft/pkg/cataloger/binary/test-fixtures/README.md
Normal file
@ -0,0 +1,78 @@
|
||||
# Binary cataloger test fixtures
|
||||
|
||||
To test the binary cataloger we run it against a set of files ("test fixtures"). There are two kinds of test fixtures:
|
||||
|
||||
- **Full binaries**: files downloaded and cached at test runtime
|
||||
- **Snippets**: ~100 byte files checked into the repo
|
||||
|
||||
The upside with snippets is that they live with the test, don't necessarily require network access or hosting concerns, and are easy to add. The downside is that they are not the entire real binary so modifications may require recreating the snippet entirely.
|
||||
|
||||
The upside with full binaries is that they are the "Real McCoy" and allows the business logic to change without needing to update the fixture. The downside is that they require network access and take up a lot of space. For instance, downloading all binaries for testing today requires downloading ~15GB of container images and ends up being ~500MB of disk space.
|
||||
|
||||
You can find the test fixtures at the following locations:
|
||||
```
|
||||
syft/pkg/cataloger/binary/test-fixtures/
|
||||
└── classifiers/
|
||||
├── bin/ # full binaries
|
||||
├── ...
|
||||
└── snippets/ # snippets
|
||||
```
|
||||
|
||||
And use tooling to list and manage the fixtures:
|
||||
|
||||
- `make list` - list all fixtures
|
||||
- `make download` - download binaries that are not covered by a snippet
|
||||
- `make download-all` - download all binaries
|
||||
- `go run ./manager add-snippet` - add a new snippet based off of a configured binary
|
||||
- `capture-snippet.sh` - add a new snippet based off of a binary on your local machine (not recommended, but allowed)
|
||||
|
||||
There is a `config.yaml` that tracks all binaries that the tests can use. This makes it possible to download it at any time from a hosted source. Today the only method allowed is to download a container image and extract files out.
|
||||
|
||||
## Testing
|
||||
|
||||
The test cases have been setup to allow testing against full binaries or a mix of both (default).
|
||||
To force running only against full binaries run with:
|
||||
|
||||
```bash
|
||||
go test -must-use-full-binaries ./syft/pkg/cataloger/binary/test-fixtures/...
|
||||
```
|
||||
|
||||
## Adding a new test fixture
|
||||
|
||||
### Adding a full binary
|
||||
|
||||
1. Add a new entry to `config.yaml` with the following fields
|
||||
- if you are adding a single binary, the `name` field does not need to be specified
|
||||
- the `name` field is useful for distinguishing a quality about the binary (e.g. `java` vs `java-jre-ibm`)
|
||||
|
||||
2. Run `make download` and ensure your new binary is downloaded
|
||||
|
||||
|
||||
### Adding a snippet
|
||||
|
||||
Even if you are adding a snippet, it is best practice to:
|
||||
|
||||
- create that snippet from a full binary (not craft a snippet by hand)
|
||||
- track where the binary is from and how to download it in `config.yaml`
|
||||
|
||||
1. Follow the steps above to [add a full binary](#adding-a-full-binary)
|
||||
|
||||
2. Run `go run ./manager add-snippet` and follow the prompts to create a new snippet
|
||||
- you should see your binary in the list of binaries to choose from. If not, check step 2
|
||||
- if the search results in no matching snippets, you can specify your own search with `--search-for <grep-pattern>`
|
||||
- you should see a new snippet file created in `snippets/`
|
||||
|
||||
3. Write a test that references your new snippet by `<name>/<version>/<architecture>`
|
||||
- `<name>` is the name of the binary (e.g. `curl`) or the name in `config.yaml` if specified
|
||||
- note that your test does not know about if it's running against a snippet or a full binary
|
||||
|
||||
### Adding a custom snippet
|
||||
|
||||
If you need to add a snippet that is not based off of a full binary, you can use the `capture-snippet.sh` script.
|
||||
|
||||
```bash
|
||||
./capture-snippet.sh <binary-path> <version> [--search-for <pattern>] [--length <length>] [--prefix-length <prefix_length>] [--group <name>]
|
||||
```
|
||||
|
||||
|
||||
This is **not** recommended because it is not reproducible and does not allow for the test to be run against a full binary.
|
||||
@ -4,10 +4,11 @@
|
||||
LENGTH=100
|
||||
PREFIX_LENGTH=10
|
||||
SEARCH_FOR=''
|
||||
GROUP_NAME=''
|
||||
|
||||
# Function to show usage
|
||||
usage() {
|
||||
echo "Usage: $0 <path-to-binary> <version> [--search-for <pattern>] [--length <length>] [--prefix-length <prefix_length>]"
|
||||
echo "Usage: $0 <path-to-binary> <version> [--search-for <pattern>] [--length <length>] [--prefix-length <prefix_length>] [--group <name>]"
|
||||
exit 1
|
||||
}
|
||||
|
||||
@ -26,6 +27,11 @@ while [[ $# -gt 0 ]]; do
|
||||
shift # past argument
|
||||
shift # past value
|
||||
;;
|
||||
--group)
|
||||
GROUP_NAME="$2"
|
||||
shift # past argument
|
||||
shift # past value
|
||||
;;
|
||||
--prefix-length)
|
||||
PREFIX_LENGTH="$2"
|
||||
shift # past argument
|
||||
@ -50,6 +56,11 @@ if [ -z "$BINARY_FILE" ] || [ -z "$VERSION" ]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
# if group name is empty use the binary filename
|
||||
if [ -z "$GROUP_NAME" ]; then
|
||||
GROUP_NAME=$(basename "$BINARY_FILE")
|
||||
fi
|
||||
|
||||
# check if xxd is even installed
|
||||
if ! command -v xxd &> /dev/null; then
|
||||
echo "xxd not found. Please install xxd."
|
||||
@ -59,7 +70,7 @@ fi
|
||||
|
||||
PATTERN=${SEARCH_FOR:-$VERSION}
|
||||
|
||||
PATTERN_RESULTS=$(strings -a -t d "$BINARY_FILE" | grep "$PATTERN")
|
||||
PATTERN_RESULTS=$(strings -t d "$BINARY_FILE" | grep "$PATTERN")
|
||||
|
||||
# if there are multiple matches, prompt the user to select one
|
||||
if [ $(echo "$PATTERN_RESULTS" | wc -l) -gt 1 ]; then
|
||||
@ -116,27 +127,4 @@ if [ "$RESPONSE" != "y" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# generate a text file with metadata and the binary snippet
|
||||
SHA256=$(sha256sum "$BINARY_FILE" | cut -d ' ' -f 1)
|
||||
DATE=$(date)
|
||||
BASE64_PATTERN=$(echo -n "$PATTERN" | base64)
|
||||
FILENAME=$(basename "$BINARY_FILE")
|
||||
INFO=$(file -b "$BINARY_FILE")
|
||||
OUTPUT_DIRECTORY="classifiers/positive/$FILENAME-$VERSION"
|
||||
mkdir "$OUTPUT_DIRECTORY"
|
||||
|
||||
OUTPUT_FILE="$OUTPUT_DIRECTORY/$FILENAME"
|
||||
|
||||
cat > "$OUTPUT_FILE" <<EOF
|
||||
### generated by script $(basename $0) at $DATE ###
|
||||
# filename: $FILENAME
|
||||
# sha256: $SHA256
|
||||
# file info: $INFO
|
||||
# base64(search): $BASE64_PATTERN
|
||||
# start offset: $OFFSET
|
||||
# length: $LENGTH
|
||||
### start of binary snippet ###
|
||||
EOF
|
||||
echo "$SNIPPET" | xxd -r -s -"$OFFSET" >> "$OUTPUT_FILE"
|
||||
|
||||
echo "Snippet written to $OUTPUT_FILE"
|
||||
go run ./manager write-snippet "$BINARY_FILE" --offset "$OFFSET" --length "$LENGTH" --name "$GROUP_NAME" --version "$VERSION"
|
||||
Binary file not shown.
Binary file not shown.
@ -0,0 +1 @@
|
||||
./[
|
||||
@ -0,0 +1,12 @@
|
||||
name: consul
|
||||
offset: 57272433
|
||||
length: 100
|
||||
snippetSha256: a4a955b180d6df471797a9f17f5ebf6f23b92d688d40532712683922e119dac0
|
||||
fileSha256: 5c5fed218247eaf43c3b54008b6a4c5d5cfa1b38539d6e7bfc09ac04623389fc
|
||||
|
||||
### byte snippet to follow ###
|
||||
e%7D" />
|
||||
|
||||
<!-- CONSUL_VERSION: 1.15.2
|
||||
-->
|
||||
<link rel="icon" href="{{.ContentPath}}assets/favicon
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,8 @@
|
||||
name: mysql
|
||||
offset: 1377134
|
||||
length: 100
|
||||
snippetSha256: f18b2e276e188db350b2d5e580085db8f8d5ae28f1eb9413523a8ea5948cb981
|
||||
fileSha256: 38ba0547af106c7ccd78e98233499530d50eaa0c1aaea0f46002b03f28992beb
|
||||
|
||||
### byte snippet to follow ###
|
||||
íÿ`íÿzíÿ/var/lib/pb2/sb_1-11875240-1687434163.06/rpm/BUILD/mysql-8.0.34/mysql-8.0.34/sql-common/cl
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
472
syft/pkg/cataloger/binary/test-fixtures/config.yaml
Normal file
472
syft/pkg/cataloger/binary/test-fixtures/config.yaml
Normal file
@ -0,0 +1,472 @@
|
||||
download-path: classifiers/bin
|
||||
snippet-path: classifiers/snippets
|
||||
|
||||
# this section is for pulling entire binaries out of container images
|
||||
from-images:
|
||||
|
||||
# from the positive snippets...
|
||||
|
||||
- name: busybox
|
||||
version: 1.36.1
|
||||
images:
|
||||
- ref: busybox:1.36.1@sha256:058f0df5310fbbbfea7e81a3a3e2b4bf3452438ec841138d170e170adbbd27a4
|
||||
platform: linux/amd64
|
||||
paths:
|
||||
- /bin/[
|
||||
|
||||
- version: 5.1.16
|
||||
images:
|
||||
- ref: bash:5.1.16@sha256:c7a903a541d8f5fe693cbe7f5ece18a1b6a03734c76092d2b153d7e98a964927
|
||||
platform: linux/amd64
|
||||
paths:
|
||||
- /usr/local/bin/bash
|
||||
|
||||
- version: 25.3.2.6
|
||||
images:
|
||||
- ref: erlang:25.3.2.6@sha256:0d1e530ec0e8047094f0a1d841754515bad9b0554260a3147fb34df31b3064fe
|
||||
platform: linux/amd64
|
||||
paths:
|
||||
- /usr/local/lib/erlang/erts-13.2.2.3/bin/erlexec
|
||||
|
||||
- version: 26.2.0.0
|
||||
images:
|
||||
- ref: erlang:26.2.0.0@sha256:31c3aa505fbe7526ca83c57b64e56ba505e62733e7e6518f4c06219de6e7396e
|
||||
platform: linux/amd64
|
||||
paths:
|
||||
- /usr/local/lib/erlang/erts-14.2/bin/erlexec
|
||||
|
||||
- version: 1.21.3
|
||||
images:
|
||||
- ref: golang:1.21.3@sha256:3ce8313c3513515040870c55e0c041a2b94f3576a58cfd3948633604214aa811
|
||||
platform: linux/amd64
|
||||
paths:
|
||||
- /usr/local/go/bin/go
|
||||
|
||||
- version: 1.5.14
|
||||
images:
|
||||
- ref: haproxy:1.5.14@sha256:3d57e3921cc84e860f764e863ce729dd0765e3d28d444775127bc42d68f98e10
|
||||
platform: linux/amd64
|
||||
paths:
|
||||
- /usr/local/sbin/haproxy
|
||||
|
||||
- version: 1.8.22
|
||||
images:
|
||||
- ref: haproxy:1.8.22@sha256:166ea77f87599b8a679921de4aa847e776801f3f07b4f17ce4e2aec7fb54e3ea
|
||||
platform: linux/amd64
|
||||
paths:
|
||||
- /usr/local/sbin/haproxy
|
||||
|
||||
- version: 2.7.3
|
||||
images:
|
||||
- ref: haproxy:2.7.3@sha256:17d8aa6bf16882a294bdcccc757dd4002045f34b719e9f94dfd4801614801aea
|
||||
platform: linux/amd64
|
||||
paths:
|
||||
- /usr/local/sbin/haproxy
|
||||
|
||||
- version: 2.4.54
|
||||
images:
|
||||
- ref: httpd:2.4.54@sha256:c13feaef62bdb03e65e645f47d9780adea5a080c78eb9e4b3c32e861327262b4
|
||||
platform: linux/amd64
|
||||
paths:
|
||||
- /usr/local/apache2/bin/httpd
|
||||
|
||||
- name: java-jre-ibm
|
||||
version: 1.8.0_391
|
||||
images:
|
||||
- ref: ibmjava:8@sha256:05ef6b0f754aa3a8cebcec36260a70c234a217b21240a998604f33459037bc08
|
||||
platform: linux/amd64
|
||||
paths:
|
||||
- /opt/ibm/java/jre/bin/java
|
||||
|
||||
- version: 10.6.15
|
||||
images:
|
||||
- ref: mariadb:10.6.15@sha256:92d499d9e02e92dc55c8160ef4004aa07f2e835197b18864ed214ca441e0dcfc
|
||||
platform: linux/amd64
|
||||
paths:
|
||||
- /usr/bin/mariadb
|
||||
|
||||
# TODO: add pattern for mariadbd
|
||||
# - version: 10.6.15
|
||||
# images:
|
||||
# - ref: mariadb:10.6.15@sha256:92d499d9e02e92dc55c8160ef4004aa07f2e835197b18864ed214ca441e0dcfc
|
||||
# platform: linux/amd64
|
||||
# paths:
|
||||
# - /usr/sbin/mariadbd
|
||||
|
||||
- version: 1.6.18
|
||||
images:
|
||||
- ref: memcached:1.6.18@sha256:9af8e788d5f7f4dc82fd49cf4a7efff1a0b5b4673085bc88f3ccb8a1c772ab3e
|
||||
platform: linux/amd64
|
||||
paths:
|
||||
- /usr/local/bin/memcached
|
||||
|
||||
- version: 5.6.51
|
||||
images:
|
||||
- ref: mysql:5.6.51@sha256:897086d07d1efa876224b147397ea8d3147e61dd84dce963aace1d5e9dc2802d
|
||||
platform: linux/amd64
|
||||
paths:
|
||||
- /usr/bin/mysql
|
||||
|
||||
- version: 8.0.34
|
||||
images:
|
||||
- ref: mysql:8.0.34@sha256:8b8835a2c32cd7357a5d2ea4b49ad870ff519c8c1d4add362803feddf4a0a973
|
||||
platform: linux/amd64
|
||||
paths:
|
||||
- /usr/bin/mysql
|
||||
|
||||
# TODO: add pattern for mysqld
|
||||
# - version: 5.6.51
|
||||
# images:
|
||||
# - ref: mysql:5.6.51@sha256:897086d07d1efa876224b147397ea8d3147e61dd84dce963aace1d5e9dc2802d
|
||||
# platform: linux/amd64
|
||||
# paths:
|
||||
# - /usr/sbin/mysqld
|
||||
#
|
||||
# - version: 8.0.34
|
||||
# images:
|
||||
# - ref: mysql:8.0.34@sha256:8b8835a2c32cd7357a5d2ea4b49ad870ff519c8c1d4add362803feddf4a0a973
|
||||
# platform: linux/amd64
|
||||
# paths:
|
||||
# - /usr/sbin/mysqld
|
||||
|
||||
- version: 1.25.1
|
||||
images:
|
||||
- ref: nginx:1.25.1@sha256:73e957703f1266530db0aeac1fd6a3f87c1e59943f4c13eb340bb8521c6041d7
|
||||
platform: linux/amd64
|
||||
paths:
|
||||
- /usr/sbin/nginx
|
||||
|
||||
- name: nginx-openresty
|
||||
version: 1.21.4.3
|
||||
images:
|
||||
- ref: openresty/openresty:1.21.4.3-2-alpine-fat@sha256:9f9b9d86f2a0f903b1226c3e8a6790293cbb58e521a186ac0031a030ea42c39b
|
||||
platform: linux/amd64
|
||||
paths:
|
||||
- /usr/local/openresty/nginx/sbin/nginx
|
||||
|
||||
- version: 19.2.0
|
||||
images:
|
||||
- ref: node:19.2.0@sha256:9bf5846b28f63acab0ccb0a39a245fbc414e6b7ecd467282f58016537c06e159
|
||||
platform: linux/amd64
|
||||
paths:
|
||||
- /usr/local/bin/node
|
||||
|
||||
|
||||
# todo (from existing snippets)...
|
||||
|
||||
#
|
||||
# - name: openjdk
|
||||
# version: 1.8.0
|
||||
# images:
|
||||
# - ref:
|
||||
# platform: linux/amd64
|
||||
# paths:
|
||||
# -
|
||||
#
|
||||
# - name: openjdk
|
||||
# version: 11.0.17
|
||||
# images:
|
||||
# - ref:
|
||||
# platform: linux/amd64
|
||||
# paths:
|
||||
# -
|
||||
#
|
||||
# - name: oracle-java
|
||||
# version: 19.0.1
|
||||
# images:
|
||||
# - ref:
|
||||
# platform: linux/amd64
|
||||
# paths:
|
||||
# -
|
||||
#
|
||||
# - name: oracle-java #macos
|
||||
# version: 19.0.1
|
||||
# images:
|
||||
# - ref:
|
||||
# platform: linux/amd64
|
||||
# paths:
|
||||
# -
|
||||
|
||||
- version: 5.12.5
|
||||
images:
|
||||
- ref: perl:5.12.5@sha256:68169b63f0dc2fd481563ef02d4173979d981e43e5d36bb39af56a5959961c5e
|
||||
platform: linux/amd64
|
||||
paths:
|
||||
- /usr/bin/perl
|
||||
|
||||
- version: 5.20.0
|
||||
images:
|
||||
- ref: perl:5.20.0@sha256:f1b8d36e0be0fd426c40e478fc84ea7603d712158001d72d1b3f929f4e1543f3
|
||||
platform: linux/amd64
|
||||
paths:
|
||||
- /usr/bin/perl
|
||||
|
||||
- version: 5.37.8
|
||||
images:
|
||||
- ref: perl:5.37.8@sha256:6a432250d7bf0b736c58772a6a50e2bf9d1485cd70ac3af10eff6cfccde3957b
|
||||
platform: linux/amd64
|
||||
paths:
|
||||
- /usr/bin/perl
|
||||
|
||||
# - name: php-apache
|
||||
# version: 8.2.1
|
||||
# images:
|
||||
# - ref:
|
||||
# platform: linux/amd64
|
||||
# paths:
|
||||
# -
|
||||
#
|
||||
# - name: php-cli
|
||||
# version: 8.2.1
|
||||
# images:
|
||||
# - ref:
|
||||
# platform: linux/amd64
|
||||
# paths:
|
||||
# -
|
||||
#
|
||||
# - name: php-fpm
|
||||
# version: 8.2.1
|
||||
# images:
|
||||
# - ref:
|
||||
# platform: linux/amd64
|
||||
# paths:
|
||||
# -
|
||||
|
||||
- version: 15.1
|
||||
images:
|
||||
- ref: postgres:15.1@sha256:b4140dd3a62f364f16a82c1bd88d28b9887ecb47f07dbe2941237d073574d428
|
||||
platform: linux/amd64
|
||||
paths:
|
||||
- /usr/lib/postgresql/15/bin/postgres
|
||||
|
||||
- version: 15beta4
|
||||
images:
|
||||
- ref: postgres:15beta4@sha256:f2b4d06ac06f0f50236c39289edfd6701eb1313d2d17f3028c8df0c00f2b21db
|
||||
platform: linux/amd64
|
||||
paths:
|
||||
- /usr/lib/postgresql/15/bin/postgres
|
||||
|
||||
# - version: 9.5alpha1 # postgresql
|
||||
# images:
|
||||
# - ref:
|
||||
# platform: linux/amd64
|
||||
# paths:
|
||||
# -
|
||||
|
||||
- version: 9.6.24
|
||||
images:
|
||||
- ref: postgres:9.6.24@sha256:15055f7b681334cbf0212b58e510148b1b23973639e3904260fb41fa0761a103
|
||||
platform: linux/amd64
|
||||
paths:
|
||||
- /usr/lib/postgresql/9.6/bin/postgres
|
||||
|
||||
# - name: python-with-incorrect-match
|
||||
# version: 3.5
|
||||
# images:
|
||||
# - ref:
|
||||
# platform: linux/amd64
|
||||
# paths:
|
||||
# -
|
||||
|
||||
- name: python
|
||||
version: 3.6
|
||||
images:
|
||||
- ref: python:3.6.3@sha256:cdef88d8625cf50ca705b7abfe99e8eb33b889652a9389b017eb46a6d2f1aaf3
|
||||
platform: linux/amd64
|
||||
paths:
|
||||
- /usr/local/bin/python3.6
|
||||
|
||||
# - name: python-lib
|
||||
# version: 3.7
|
||||
# images:
|
||||
# - ref:
|
||||
# platform: linux/amd64
|
||||
# paths:
|
||||
# -
|
||||
#
|
||||
# - version: python-duplicates
|
||||
# images:
|
||||
# - ref:
|
||||
# platform: linux/amd64
|
||||
# paths:
|
||||
# -
|
||||
#
|
||||
# - version: 2.8.23 # redis-server
|
||||
# images:
|
||||
# - ref:
|
||||
# platform: linux/amd64
|
||||
# paths:
|
||||
# -
|
||||
#
|
||||
# - version: 4.0.11 # redis-server
|
||||
# images:
|
||||
# - ref:
|
||||
# platform: linux/amd64
|
||||
# paths:
|
||||
# -
|
||||
#
|
||||
# - version: 5.0.0 # redis-server
|
||||
# images:
|
||||
# - ref:
|
||||
# platform: linux/amd64
|
||||
# paths:
|
||||
# -
|
||||
#
|
||||
# - version: 6.0.16 # redis-server
|
||||
# images:
|
||||
# - ref:
|
||||
# platform: linux/amd64
|
||||
# paths:
|
||||
# -
|
||||
#
|
||||
# - version: 7.0.0 # redis-server
|
||||
# images:
|
||||
# - ref:
|
||||
# platform: linux/amd64
|
||||
# paths:
|
||||
# -
|
||||
#
|
||||
# - version: 7.0.14 # redis-server
|
||||
# images:
|
||||
# - ref:
|
||||
# platform: linux/amd64
|
||||
# paths:
|
||||
# -
|
||||
#
|
||||
# - version: 7.2.3-amd64 # redis-server
|
||||
# images:
|
||||
# - ref:
|
||||
# platform: linux/amd64
|
||||
# paths:
|
||||
# -
|
||||
#
|
||||
# - version: 7.2.3-arm64 # redis-server
|
||||
# images:
|
||||
# - ref:
|
||||
# platform: linux/amd64
|
||||
# paths:
|
||||
# -
|
||||
#
|
||||
# - version: 1.9.3p551 # ruby
|
||||
# images:
|
||||
# - ref:
|
||||
# platform: linux/amd64
|
||||
# paths:
|
||||
# -
|
||||
#
|
||||
# - version: 1.50.0 # ruby
|
||||
# images:
|
||||
# - ref:
|
||||
# platform: linux/amd64
|
||||
# paths:
|
||||
# -
|
||||
#
|
||||
# - version: 1.67.1 # ruby
|
||||
# images:
|
||||
# - ref:
|
||||
# platform: linux/amd64
|
||||
# paths:
|
||||
# -
|
||||
#
|
||||
# - version: 1.7.34 # traefik
|
||||
# images:
|
||||
# - ref:
|
||||
# platform: linux/amd64
|
||||
# paths:
|
||||
# -
|
||||
#
|
||||
# - version: 2.9.6 # traefik
|
||||
# images:
|
||||
# - ref:
|
||||
# platform: linux/amd64
|
||||
# paths:
|
||||
# -
|
||||
|
||||
|
||||
|
||||
# from the original dynamic fixtures...
|
||||
|
||||
- name: python-rhel-shared-libs
|
||||
version: 3.9
|
||||
images:
|
||||
- ref: registry.access.redhat.com/ubi8/python-39@sha256:f3cf958b96ce016b63e3e163e488f52e42891304dafef5a0811563f22e3cbad0
|
||||
platform: linux/amd64
|
||||
paths:
|
||||
- /usr/bin/python3.9
|
||||
- /usr/lib64/libpython3.9.so.1.0
|
||||
|
||||
- name: python-slim-shared-libs
|
||||
version: 3.11
|
||||
images:
|
||||
- ref: python:3.11-slim@sha256:0b106e1d2bf485c2a41474bc9cd5103e9eea4e179f40f10741b53b127059221e
|
||||
platform: linux/amd64
|
||||
paths:
|
||||
- /usr/local/bin/python3.11
|
||||
- /usr/local/lib/libpython3.11.so.1.0
|
||||
|
||||
- version: 3.9.16
|
||||
images:
|
||||
- ref: python:3.9.16-bullseye@sha256:93fb93c461a2e47a2176706fad1f39eaacd5dd40e19c0b018699a28c03eb2e2a
|
||||
platform: linux/amd64
|
||||
paths:
|
||||
- /usr/bin/python3.9
|
||||
|
||||
- name: python-alpine-shared-libs
|
||||
version: 3.4
|
||||
images:
|
||||
- ref: python:3.4-alpine@sha256:c210b660e2ea553a7afa23b41a6ed112f85dbce25cbcb567c75dfe05342a4c4b
|
||||
platform: linux/amd64
|
||||
paths:
|
||||
- /usr/local/bin/python3.4
|
||||
- /usr/local/lib/libpython3.4m.so.1.0
|
||||
|
||||
- name: ruby-bullseye-shared-libs
|
||||
version: 3.2.1
|
||||
images:
|
||||
- ref: ruby:3.2.1-bullseye@sha256:b4a140656b0c5d26c0a80559b228b4d343f3fdbf56682fcbe88f6db1fa9afa6b
|
||||
platform: linux/amd64
|
||||
paths:
|
||||
- /usr/local/bin/ruby
|
||||
- /usr/local/lib/libruby.so.3.2.1
|
||||
- /usr/local/lib/libruby.so.3.2
|
||||
|
||||
- name: ruby-bullseye-shared-libs
|
||||
version: 2.7.7
|
||||
images:
|
||||
- ref: ruby:2.7.7-bullseye@sha256:055191740a063f33fef1f09423e5ed8f91143aae62a3772a90910118464c5120
|
||||
platform: linux/amd64
|
||||
paths:
|
||||
- /usr/local/bin/ruby
|
||||
- /usr/local/lib/libruby.so.2.7.7
|
||||
- /usr/local/lib/libruby.so.2.7
|
||||
|
||||
- name: ruby-shared-libs
|
||||
version: 2.6.10
|
||||
images:
|
||||
- ref: ruby:2.6.10@sha256:771a810704167e55da8a19970c5dfa6eb795dfee32547adffa29ea72703f7243
|
||||
platform: linux/amd64
|
||||
paths:
|
||||
- /usr/local/bin/ruby
|
||||
- /usr/local/lib/libruby.so.2.6.10
|
||||
- /usr/local/lib/libruby.so.2.6
|
||||
|
||||
- version: 3.11.1
|
||||
images:
|
||||
- ref: alpine/helm:3.11.1@sha256:8628e3695fb743a8b9de89626f1b7a221280c2152c0e288c2504e59b68233e8b
|
||||
platform: linux/amd64
|
||||
paths:
|
||||
- /usr/bin/helm
|
||||
|
||||
- version: 3.10.3
|
||||
images:
|
||||
- ref: argoproj/argocd:v2.6.4@sha256:61fcbba187ff53c00696cb580edf70cada59c45cf399d8477631acf43cf522ee
|
||||
platform: linux/amd64
|
||||
paths:
|
||||
- /usr/local/bin/helm
|
||||
|
||||
- version: 1.15.2
|
||||
images:
|
||||
- ref: hashicorp/consul:1.15.2@sha256:c2169f3bb18dd947ae8eb5f6766896695c71fb439f050a3343e0007d895615b8
|
||||
platform: linux/amd64
|
||||
paths:
|
||||
- /bin/consul
|
||||
@ -1,120 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -eu -o pipefail
|
||||
|
||||
DESTINATION_DIR="bin"
|
||||
|
||||
curate_destination() {
|
||||
organization_name=$1
|
||||
binary_name=$2
|
||||
version=$3
|
||||
arch=$4
|
||||
|
||||
# translate all / into -
|
||||
arch=$(echo $arch | tr '/' '-')
|
||||
|
||||
# Create directory and define file path
|
||||
dir_path="${DESTINATION_DIR}/${organization_name}-${version}/${arch}"
|
||||
mkdir -p "$dir_path"
|
||||
file_path="${dir_path}/${binary_name}"
|
||||
|
||||
echo $file_path
|
||||
}
|
||||
|
||||
# function to get a binary from a container
|
||||
docker_copy_binary() {
|
||||
local image=$1
|
||||
local platform=$2
|
||||
local binary_path=$3
|
||||
local binary_name=$4
|
||||
local version=$5
|
||||
local organization_name=${6:-$binary_name}
|
||||
|
||||
|
||||
file_path=$(curate_destination $organization_name $binary_name $version $platform)
|
||||
|
||||
# Check if the file already exists
|
||||
if [ -f "$file_path" ]; then
|
||||
echo "...$file_path already exists (skipping)"
|
||||
return
|
||||
fi
|
||||
|
||||
echo "Pulling $image..."
|
||||
docker pull "$image" --platform $platform -q
|
||||
|
||||
container_id=$(docker create "$image")
|
||||
|
||||
echo " - copying $binary_path to $file_path..."
|
||||
docker cp "$container_id:$binary_path" "$file_path" -q
|
||||
|
||||
docker rm "$container_id"
|
||||
}
|
||||
|
||||
|
||||
# let's download stuff!
|
||||
|
||||
docker_copy_binary \
|
||||
busybox:1.36.1@sha256:058f0df5310fbbbfea7e81a3a3e2b4bf3452438ec841138d170e170adbbd27a4 linux/amd64 /bin/busybox \
|
||||
busybox 1.36.1
|
||||
|
||||
docker_copy_binary \
|
||||
bash:5.1.16@sha256:c7a903a541d8f5fe693cbe7f5ece18a1b6a03734c76092d2b153d7e98a964927 linux/amd64 /usr/local/bin/bash \
|
||||
bash 5.1.16
|
||||
|
||||
docker_copy_binary \
|
||||
erlang:25.3.2.6@sha256:0d1e530ec0e8047094f0a1d841754515bad9b0554260a3147fb34df31b3064fe linux/amd64 /usr/local/lib/erlang/bin/erl \
|
||||
erlang 25.3.2.6
|
||||
|
||||
docker_copy_binary \
|
||||
golang:1.21.3@sha256:3ce8313c3513515040870c55e0c041a2b94f3576a58cfd3948633604214aa811 linux/amd64 /usr/local/go/bin/go \
|
||||
go 1.21.3
|
||||
|
||||
docker_copy_binary \
|
||||
haproxy:1.5.14@sha256:3d57e3921cc84e860f764e863ce729dd0765e3d28d444775127bc42d68f98e10 linux/amd64 /usr/local/sbin/haproxy \
|
||||
haproxy 1.5.14
|
||||
|
||||
docker_copy_binary \
|
||||
haproxy:1.8.22@sha256:acd6d3feb77b3f50e672427756b1375fa479b8aeaf30823051e811d10b98da3f linux/amd64 /usr/local/sbin/haproxy \
|
||||
haproxy 1.8.22
|
||||
|
||||
docker_copy_binary \
|
||||
haproxy:2.7.3@sha256:17d8aa6bf16882a294bdcccc757dd4002045f34b719e9f94dfd4801614801aea linux/amd64 /usr/local/sbin/haproxy \
|
||||
haproxy 2.7.3
|
||||
|
||||
docker_copy_binary \
|
||||
httpd:2.4.54@sha256:c13feaef62bdb03e65e645f47d9780adea5a080c78eb9e4b3c32e861327262b4 linux/amd64 /usr/local/apache2/bin/httpd \
|
||||
httpd 2.4.54
|
||||
|
||||
docker_copy_binary \
|
||||
ibmjava:8@sha256:05ef6b0f754aa3a8cebcec36260a70c234a217b21240a998604f33459037bc08 linux/amd64 /opt/ibm/java/jre/bin/java \
|
||||
java 1.8.0_391 java-jre-ibm
|
||||
|
||||
docker_copy_binary \
|
||||
mariadb:10.6.15@sha256:92d499d9e02e92dc55c8160ef4004aa07f2e835197b18864ed214ca441e0dcfc linux/amd64 /usr/sbin/mariadbd \
|
||||
mariadb 10.6.15
|
||||
|
||||
docker_copy_binary \
|
||||
memcached:1.6.18@sha256:9af8e788d5f7f4dc82fd49cf4a7efff1a0b5b4673085bc88f3ccb8a1c772ab3e linux/amd64 /usr/local/bin/memcached \
|
||||
memcached 1.6.18
|
||||
|
||||
docker_copy_binary \
|
||||
mysql:5.6.51@sha256:897086d07d1efa876224b147397ea8d3147e61dd84dce963aace1d5e9dc2802d linux/amd64 /usr/sbin/mysqld \
|
||||
mysql 5.6.51
|
||||
|
||||
docker_copy_binary \
|
||||
mysql:8.0.34@sha256:8b8835a2c32cd7357a5d2ea4b49ad870ff519c8c1d4add362803feddf4a0a973 linux/amd64 /usr/sbin/mysqld \
|
||||
mysql 8.0.34
|
||||
|
||||
docker_copy_binary \
|
||||
nginx:1.25.1@sha256:73e957703f1266530db0aeac1fd6a3f87c1e59943f4c13eb340bb8521c6041d7 linux/amd64 /usr/sbin/nginx \
|
||||
nginx 1.25.1
|
||||
|
||||
docker_copy_binary \
|
||||
openresty/openresty:1.21.4.3-2-alpine-fat@sha256:9f9b9d86f2a0f903b1226c3e8a6790293cbb58e521a186ac0031a030ea42c39b linux/amd64 /usr/local/openresty/nginx/sbin/nginx \
|
||||
nginx 1.21.4.3 nginx-openresty
|
||||
|
||||
docker_copy_binary \
|
||||
node:19.2.0@sha256:9bf5846b28f63acab0ccb0a39a245fbc414e6b7ecd467282f58016537c06e159 linux/amd64 /usr/local/bin/node \
|
||||
node 19.2.0
|
||||
|
||||
echo "Done!"
|
||||
tree $DESTINATION_DIR
|
||||
@ -1,15 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -uxe
|
||||
|
||||
CTRID=$(docker create $1)
|
||||
|
||||
function cleanup() {
|
||||
docker rm "${CTRID}"
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
set +e
|
||||
|
||||
mkdir -p $(dirname $3)
|
||||
|
||||
docker cp ${CTRID}:$2 $3
|
||||
@ -0,0 +1,39 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/internal/cli/commands"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/internal/config"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// list all managed binaries (in ./bin, organized by 'name-version/platform/binary')
|
||||
// manager list binaries
|
||||
|
||||
// list all managed snippets (in ./snippets, same organization as ./bin: 'name-version/platform/binary' where each bin is a snippet)
|
||||
// manager list snippets
|
||||
|
||||
// download all binaries (to ./bin)
|
||||
// manager download [--name <name>] [--version <version>]
|
||||
|
||||
// capture snippet from a binary identified by offset
|
||||
// manager capture snippet --binary <binary> --offset <offset> --length <length>
|
||||
|
||||
func New() (*cobra.Command, error) {
|
||||
cfgP, err := config.Read()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg := *cfgP
|
||||
|
||||
root := commands.Root(cfg)
|
||||
|
||||
root.AddCommand(
|
||||
commands.List(cfg),
|
||||
commands.Download(cfg),
|
||||
commands.AddSnippet(cfg),
|
||||
commands.WriteSnippet(cfg),
|
||||
)
|
||||
|
||||
return root, nil
|
||||
}
|
||||
@ -0,0 +1,108 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/internal"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/internal/config"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/internal/ui"
|
||||
"github.com/anmitsu/go-shlex"
|
||||
"github.com/spf13/cobra"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func AddSnippet(appConfig config.Application) *cobra.Command {
|
||||
var binaryPath, searchPattern string
|
||||
var length, prefixLength int
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "add-snippet",
|
||||
Short: "capture snippets from binaries",
|
||||
Args: cobra.NoArgs,
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
|
||||
candidates, err := internal.ListAllBinaries(appConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to list binaries: %w", err)
|
||||
}
|
||||
|
||||
// launch the UI to pick a path
|
||||
var binaryPaths []string
|
||||
for _, k := range internal.NewLogicalEntryKeys(candidates) {
|
||||
info := candidates[k]
|
||||
if info.BinaryPath != "" {
|
||||
binaryPaths = append(binaryPaths, info.BinaryPath)
|
||||
}
|
||||
}
|
||||
|
||||
binaryPath, err = ui.PromptSelectBinary(binaryPaths)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
name, version, _, err := inferInfoFromBinaryPath(appConfig, binaryPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to infer name and version from binary path: %w", err)
|
||||
}
|
||||
|
||||
if searchPattern == "" {
|
||||
searchPattern = strings.ReplaceAll(version, ".", `\\.`)
|
||||
}
|
||||
|
||||
return runAddSnippet(binaryPath, name, version, searchPattern, length, prefixLength)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVar(&searchPattern, "search-for", "", "the pattern to search for in the binary (defaults to the version)")
|
||||
cmd.Flags().IntVar(&length, "length", 100, "the length of the snippet to capture")
|
||||
cmd.Flags().IntVar(&prefixLength, "prefix-length", 10, "number of bytes before the search hit to capture")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runAddSnippet(binaryPath, name, version, searchPattern string, length, prefixLength int) error {
|
||||
// invoke ./capture-snippet.sh <path-to-binary> <version> [--search-for <pattern>] [--length <length>] [--prefix-length <prefix_length>]"
|
||||
|
||||
cmd := exec.Command("./capture-snippet.sh", binaryPath, version)
|
||||
|
||||
var args []string
|
||||
if searchPattern != "" {
|
||||
args = append(args, "--search-for", searchPattern)
|
||||
}
|
||||
if name != "" {
|
||||
args = append(args, "--group", name)
|
||||
}
|
||||
if length > 0 {
|
||||
args = append(args, fmt.Sprintf("--length %d", length))
|
||||
}
|
||||
if prefixLength > 0 {
|
||||
args = append(args, fmt.Sprintf("--prefix-length %d", prefixLength))
|
||||
}
|
||||
|
||||
var err error
|
||||
args, err = shlex.Split(strings.Join(args, " "), true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse arguments: %w", err)
|
||||
}
|
||||
cmd.Args = append(cmd.Args, args...)
|
||||
|
||||
fmt.Printf("running: %s\n", strings.Join(cmd.Args, " "))
|
||||
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("failed to start command: %w", err)
|
||||
}
|
||||
|
||||
if err := cmd.Wait(); err != nil {
|
||||
return fmt.Errorf("command execution failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -0,0 +1,78 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/internal"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/internal/config"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func Download(appConfig config.Application) *cobra.Command {
|
||||
var configs []config.BinaryFromImage
|
||||
|
||||
var skipSnippets bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "download",
|
||||
Short: "download binaries [name@version ...]",
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
|
||||
if len(args) > 0 {
|
||||
for _, arg := range args {
|
||||
binaryFromImageCfg := appConfig.GetBinaryFromImage(arg, "")
|
||||
if binaryFromImageCfg == nil {
|
||||
return fmt.Errorf("no config found for %q", arg)
|
||||
}
|
||||
configs = append(configs, *binaryFromImageCfg)
|
||||
}
|
||||
} else {
|
||||
configs = appConfig.FromImages
|
||||
}
|
||||
|
||||
if skipSnippets {
|
||||
var err error
|
||||
configs, err = configsWithoutSnippets(appConfig, configs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
for _, binaryFromImageCfg := range configs {
|
||||
if err := internal.DownloadFromImage(appConfig.DownloadPath, binaryFromImageCfg); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(configs) == 0 {
|
||||
fmt.Println("no binaries to download")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVarP(&skipSnippets, "skip-if-covered-by-snippet", "s", false, "skip downloading entries already covered by snippets")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func configsWithoutSnippets(appConfig config.Application, configs []config.BinaryFromImage) ([]config.BinaryFromImage, error) {
|
||||
entries, err := internal.ListAllEntries(appConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var filtered []config.BinaryFromImage
|
||||
|
||||
for _, cfg := range configs {
|
||||
if entries.BinaryFromImageHasSnippet(cfg) {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, cfg)
|
||||
}
|
||||
|
||||
return filtered, nil
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/internal"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/internal/config"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/spf13/cobra"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func List(appConfig config.Application) *cobra.Command {
|
||||
|
||||
var showPaths bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "list managed binaries and managed/unmanaged snippets",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
|
||||
return runList(appConfig, showPaths)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVarP(&showPaths, "show-paths", "p", false, "show paths to binaries and snippets")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runList(appConfig config.Application, showPaths bool) error {
|
||||
|
||||
material, err := internal.ListAllEntries(appConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
report := renderCatalogerListTable(material, showPaths)
|
||||
|
||||
fmt.Println(report)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func renderCatalogerListTable(material map[internal.LogicalEntryKey]internal.EntryInfo, showPaths bool) string {
|
||||
t := table.NewWriter()
|
||||
t.SetStyle(table.StyleLight)
|
||||
t.AppendHeader(table.Row{"Group", "Version", "Platform", "Name", "Configured?", "Binary", "Snippet"})
|
||||
|
||||
keys := internal.NewLogicalEntryKeys(material)
|
||||
|
||||
for _, k := range keys {
|
||||
info := material[k]
|
||||
|
||||
isConfigured := ""
|
||||
if info.IsConfigured {
|
||||
isConfigured = "yes"
|
||||
}
|
||||
|
||||
bin := ""
|
||||
snippet := ""
|
||||
if showPaths {
|
||||
bin = info.BinaryPath
|
||||
snippet = info.SnippetPath
|
||||
} else {
|
||||
if info.BinaryPath != "" {
|
||||
bin = "yes"
|
||||
}
|
||||
|
||||
if info.SnippetPath != "" {
|
||||
snippet = "yes"
|
||||
}
|
||||
}
|
||||
|
||||
t.AppendRow(table.Row{
|
||||
k.OrgName,
|
||||
k.Version,
|
||||
displayPlatform(k.Platform),
|
||||
k.Filename,
|
||||
isConfigured,
|
||||
bin,
|
||||
snippet,
|
||||
})
|
||||
}
|
||||
|
||||
return t.Render()
|
||||
}
|
||||
|
||||
func displayPlatform(platform string) string {
|
||||
return strings.ReplaceAll(platform, "-", "/")
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/internal/config"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func Root(_ config.Application) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "manager",
|
||||
Short: "manager is a tool for managing binaries and snippets",
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,256 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"debug/elf"
|
||||
"debug/macho"
|
||||
"debug/pe"
|
||||
"fmt"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/internal"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/internal/config"
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/yaml.v3"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func WriteSnippet(appConfig config.Application) *cobra.Command {
|
||||
var offset, length int
|
||||
var name, version string
|
||||
var binaryPath string
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "write-snippet [binary]",
|
||||
Short: "capture snippets from binaries",
|
||||
Args: cobra.ExactArgs(1),
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) == 0 && (name != "" || version != "") {
|
||||
return fmt.Errorf("cannot provide name or version without a binary path")
|
||||
}
|
||||
|
||||
binaryPath = args[0]
|
||||
if _, err := os.Stat(binaryPath); err != nil {
|
||||
return fmt.Errorf("unable to stat %q: %w", binaryPath, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
platform, err := getPlatform(binaryPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get platform: %w", err)
|
||||
}
|
||||
|
||||
snippetPath, err := getSnippetPath(appConfig, binaryPath, name, version, platform)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get snippet path: %w", err)
|
||||
}
|
||||
|
||||
return runWriteSnippet(binaryPath, offset, length, snippetPath)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().IntVar(&offset, "offset", -1, "the offset in the binary to start the snippet")
|
||||
cmd.Flags().IntVar(&length, "length", 100, "the length of the snippet to capture")
|
||||
cmd.Flags().StringVar(&name, "name", "", "the name of the snippet")
|
||||
cmd.Flags().StringVar(&version, "version", "", "the version of the snippet")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runWriteSnippet(binaryPath string, offset, length int, snippetPath string) error {
|
||||
f, err := os.Open(binaryPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to open binary %q: %w", binaryPath, err)
|
||||
}
|
||||
|
||||
n, err := f.Seek(int64(offset), io.SeekStart)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to seek to offset %d: %w", offset, err)
|
||||
}
|
||||
|
||||
if n != int64(offset) {
|
||||
return fmt.Errorf("unexpectd to seek value: %d != %d", offset, n)
|
||||
}
|
||||
|
||||
buf := make([]byte, length)
|
||||
n2, err := f.Read(buf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read %d bytes: %w", length, err)
|
||||
}
|
||||
|
||||
if n2 != length {
|
||||
return fmt.Errorf("unexpected read length: %d != %d", length, n2)
|
||||
}
|
||||
|
||||
fileDigest, err := internal.Sha256SumFile(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
metadata := internal.SnippetMetadata{
|
||||
Name: filepath.Base(binaryPath),
|
||||
Offset: offset,
|
||||
Length: length,
|
||||
SnippetSha256: internal.Sha256SumBytes(buf),
|
||||
FileSha256: fileDigest,
|
||||
}
|
||||
|
||||
metadataBytes, err := yaml.Marshal(metadata)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to marshal metadata: %w", err)
|
||||
}
|
||||
|
||||
splitter := []byte(fmt.Sprintf("\n### byte snippet to follow ###\n"))
|
||||
|
||||
var finalBuf []byte
|
||||
finalBuf = append(finalBuf, metadataBytes...)
|
||||
finalBuf = append(finalBuf, splitter...)
|
||||
finalBuf = append(finalBuf, buf...)
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(snippetPath), 0755); err != nil {
|
||||
return fmt.Errorf("unable to create destination directory: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(snippetPath, finalBuf, 0644); err != nil {
|
||||
return fmt.Errorf("unable to write snippet: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("wrote snippet to %q\n", snippetPath)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getSnippetPath(appConfig config.Application, binaryPath string, name, version, platform string) (string, error) {
|
||||
binFilename := filepath.Base(binaryPath)
|
||||
platform = config.PlatformAsValue(platform)
|
||||
|
||||
// if all values provided, use them
|
||||
if name != "" && version != "" && platform != "" {
|
||||
return filepath.Join(appConfig.SnippetPath, name, version, platform, binFilename), nil
|
||||
}
|
||||
|
||||
// otherwise, try to infer them from the existing binary path
|
||||
name, version, platform, err := inferInfoFromBinaryPath(appConfig, binaryPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return filepath.Join(appConfig.SnippetPath, name, version, platform, binFilename), nil
|
||||
}
|
||||
|
||||
func inferInfoFromBinaryPath(appConfig config.Application, binaryPath string) (string, string, string, error) {
|
||||
relativePath, err := filepath.Rel(appConfig.DownloadPath, binaryPath)
|
||||
if err != nil {
|
||||
return "", "", "", fmt.Errorf("unable to get relative path: %w", err)
|
||||
}
|
||||
|
||||
// otherwise, try to infer them from the existing binary path
|
||||
items := internal.SplitFilepath(relativePath)
|
||||
if len(items) != 4 {
|
||||
return "", "", "", fmt.Errorf("too few fields: %q", binaryPath)
|
||||
}
|
||||
|
||||
name := items[0]
|
||||
version := items[1]
|
||||
platform := items[2]
|
||||
|
||||
return name, version, platform, nil
|
||||
}
|
||||
|
||||
// getPlatform will return <os>-<arch> for the given binary path, where os can be "linux", "darwin", "windows",
|
||||
// and arch can be "amd64", "arm64", "arm", etc.
|
||||
func getPlatform(binaryPath string) (string, error) {
|
||||
f, err := os.Open(binaryPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to open binary %q: %w", binaryPath, err)
|
||||
}
|
||||
|
||||
elfPlatform := getPlatformElf(f)
|
||||
if elfPlatform != "" {
|
||||
return elfPlatform, nil
|
||||
}
|
||||
|
||||
macPlatform := getPlatformMac(f)
|
||||
if macPlatform != "" {
|
||||
return macPlatform, nil
|
||||
}
|
||||
|
||||
winPlatform := getPlatformWindows(f)
|
||||
if winPlatform != "" {
|
||||
return winPlatform, nil
|
||||
}
|
||||
|
||||
// attempt to infer from the path. It is possible to see invalid-looking binaries that are still something
|
||||
// we'd like to detect.
|
||||
items := internal.SplitFilepath(binaryPath)
|
||||
if len(items) > 2 {
|
||||
candidate := items[len(items)-2]
|
||||
if strings.Contains(candidate, "linux") || strings.Contains(candidate, "darwin") || strings.Contains(candidate, "windows") {
|
||||
return candidate, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("unable to determine platform for %q", binaryPath)
|
||||
}
|
||||
|
||||
func getPlatformElf(f *os.File) string {
|
||||
elfFile, err := elf.NewFile(f)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
var arch string
|
||||
switch elfFile.Machine {
|
||||
case elf.EM_X86_64:
|
||||
arch = "amd64"
|
||||
case elf.EM_AARCH64:
|
||||
arch = "arm64"
|
||||
// TODO...
|
||||
default:
|
||||
arch = fmt.Sprintf("unknown-%x", elfFile.Machine)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("linux-%s", arch)
|
||||
}
|
||||
|
||||
func getPlatformMac(f *os.File) string {
|
||||
machoFile, err := macho.NewFile(f)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
var arch string
|
||||
switch machoFile.Cpu {
|
||||
case macho.CpuAmd64:
|
||||
arch = "amd64"
|
||||
case macho.CpuArm64:
|
||||
arch = "arm64"
|
||||
// TODO...
|
||||
default:
|
||||
arch = fmt.Sprintf("unknown-%x", machoFile.Cpu)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("darwin-%s", arch)
|
||||
}
|
||||
|
||||
func getPlatformWindows(f *os.File) string {
|
||||
peFile, err := pe.NewFile(f)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
var arch string
|
||||
switch peFile.Machine {
|
||||
case pe.IMAGE_FILE_MACHINE_AMD64:
|
||||
arch = "amd64"
|
||||
case pe.IMAGE_FILE_MACHINE_ARM64:
|
||||
arch = "arm64"
|
||||
// TODO...
|
||||
default:
|
||||
arch = fmt.Sprintf("unknown-%x", peFile.Machine)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("windows-%s", arch)
|
||||
}
|
||||
@ -0,0 +1,139 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/scylladb/go-set/strset"
|
||||
"gopkg.in/yaml.v3"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const Path = "config.yaml"
|
||||
|
||||
type Application struct {
|
||||
DownloadPath string `yaml:"download-path"`
|
||||
SnippetPath string `yaml:"snippet-path"`
|
||||
FromImages []BinaryFromImage `yaml:"from-images"`
|
||||
}
|
||||
|
||||
func DefaultApplication() Application {
|
||||
return Application{
|
||||
DownloadPath: "bin",
|
||||
}
|
||||
}
|
||||
|
||||
func Read() (*Application, error) {
|
||||
return read(Path)
|
||||
}
|
||||
|
||||
func read(path string) (*Application, error) {
|
||||
appConfig := DefaultApplication()
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = yaml.Unmarshal(data, &appConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := appConfig.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &appConfig, nil
|
||||
}
|
||||
|
||||
func (c Image) Key() string {
|
||||
return fmt.Sprintf("%s:%s", c.Reference, c.Platform)
|
||||
}
|
||||
|
||||
func (c Application) Validate() error {
|
||||
set := strset.New()
|
||||
var err error
|
||||
for i, entry := range c.FromImages {
|
||||
key := entry.Key()
|
||||
|
||||
if set.Has(key) {
|
||||
err = multierror.Append(err, fmt.Errorf("duplicate entry %q", entry))
|
||||
continue
|
||||
}
|
||||
|
||||
set.Add(entry.Key())
|
||||
|
||||
if len(entry.PathsInImage) > 1 && entry.GenericName == "" {
|
||||
err = multierror.Append(err, fmt.Errorf("specified multiple paths but no name for entry %d (%s)", i+1, key))
|
||||
}
|
||||
if entry.Name() == "" {
|
||||
err = multierror.Append(err, fmt.Errorf("missing name for entry %d", i+1))
|
||||
}
|
||||
if entry.Version == "" {
|
||||
err = multierror.Append(err, fmt.Errorf("missing version for entry %d", i+1))
|
||||
}
|
||||
if len(entry.Images) == 0 {
|
||||
err = multierror.Append(err, fmt.Errorf("missing images for entry %d (%s)", i+1, key))
|
||||
}
|
||||
|
||||
var imageSet = strset.New()
|
||||
for j, image := range entry.Images {
|
||||
imgKey := image.Key()
|
||||
if imageSet.Has(imgKey) {
|
||||
err = multierror.Append(err, fmt.Errorf("duplicate image %q for entry %d (%s)", image.Key(), i+1, key))
|
||||
continue
|
||||
}
|
||||
imageSet.Add(imgKey)
|
||||
|
||||
if image.Reference == "" {
|
||||
err = multierror.Append(err, fmt.Errorf("missing ref reference for entry %d (%s) image %d", i+1, key, j+1))
|
||||
}
|
||||
if image.Platform == "" {
|
||||
err = multierror.Append(err, fmt.Errorf("missing platform for entry %d (%s) image %d", i+1, key, j+1))
|
||||
}
|
||||
}
|
||||
if len(entry.PathsInImage) == 0 {
|
||||
err = multierror.Append(err, fmt.Errorf("missing paths for entry %d (%s)", i+1, key))
|
||||
}
|
||||
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (c Application) GetBinaryFromImage(name, version string) *BinaryFromImage {
|
||||
if strings.Contains(name, "@") && version == "" {
|
||||
parts := strings.Split(name, "@")
|
||||
name = parts[0]
|
||||
version = parts[1]
|
||||
}
|
||||
for _, entry := range c.FromImages {
|
||||
if entry.Name() == name && entry.Version == version {
|
||||
return &entry
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Application) GetBinaryFromImageByPath(storePath string) *BinaryFromImage {
|
||||
// each key is the store path except for the root (e.g. bin or snippet)
|
||||
entryByStorePath := make(map[string]BinaryFromImage)
|
||||
|
||||
for _, entry := range c.FromImages {
|
||||
for _, path := range entry.AllStorePaths(c.DownloadPath) {
|
||||
pathWithoutRoot := splitFilepath(path)[1:]
|
||||
entryByStorePath[filepath.Join(pathWithoutRoot...)] = entry
|
||||
}
|
||||
}
|
||||
|
||||
pathWithoutRoot := filepath.Join(splitFilepath(storePath)[1:]...)
|
||||
if entry, ok := entryByStorePath[pathWithoutRoot]; ok {
|
||||
return &entry
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func splitFilepath(path string) []string {
|
||||
return strings.Split(path, string(filepath.Separator))
|
||||
}
|
||||
@ -0,0 +1,79 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"gopkg.in/yaml.v3"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type BinaryFromImage struct {
|
||||
GenericName string `yaml:"name"`
|
||||
Version string `yaml:"version"`
|
||||
|
||||
Images []Image `yaml:"images"`
|
||||
PathsInImage []string `yaml:"paths"`
|
||||
}
|
||||
|
||||
type Image struct {
|
||||
Reference string `yaml:"ref"`
|
||||
Platform string `yaml:"platform"`
|
||||
}
|
||||
|
||||
func (c BinaryFromImage) Key() string {
|
||||
return fmt.Sprintf("%s:%s", c.Name(), c.Version)
|
||||
}
|
||||
|
||||
func (c BinaryFromImage) Name() string {
|
||||
displayName := c.GenericName
|
||||
if displayName == "" {
|
||||
var path string
|
||||
if len(c.PathsInImage) > 0 {
|
||||
path = c.PathsInImage[0]
|
||||
}
|
||||
if path == "" {
|
||||
return ""
|
||||
}
|
||||
return filepath.Base(path)
|
||||
}
|
||||
return displayName
|
||||
}
|
||||
|
||||
func (c BinaryFromImage) AllStorePaths(dest string) []string {
|
||||
var paths []string
|
||||
for _, image := range c.Images {
|
||||
paths = append(paths, c.AllStorePathsForImage(image, dest)...)
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
func (c BinaryFromImage) AllStorePathsForImage(image Image, dest string) []string {
|
||||
var paths []string
|
||||
|
||||
platform := PlatformAsValue(image.Platform)
|
||||
for _, path := range c.PathsInImage {
|
||||
base := filepath.Base(path)
|
||||
if path == "" {
|
||||
base = ""
|
||||
}
|
||||
paths = append(paths, filepath.Join(dest, c.Name(), c.Version, platform, base))
|
||||
}
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
func PlatformAsValue(platform string) string {
|
||||
return strings.ReplaceAll(platform, "/", "-")
|
||||
}
|
||||
|
||||
func (c BinaryFromImage) Fingerprint() string {
|
||||
by, err := yaml.Marshal(c)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
hasher := sha256.New()
|
||||
hasher.Write(by)
|
||||
return fmt.Sprintf("%x", hasher.Sum(nil))
|
||||
}
|
||||
@ -0,0 +1,157 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/internal/config"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/internal/ui"
|
||||
"github.com/google/uuid"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func DownloadFromImage(dest string, config config.BinaryFromImage) error {
|
||||
t := ui.Title{Name: config.Name(), Version: config.Version}
|
||||
t.Start()
|
||||
|
||||
hostPaths := config.AllStorePaths(dest)
|
||||
if allPathsExist(hostPaths) {
|
||||
if !isDownloadStale(config, hostPaths) {
|
||||
t.Skip("already exists")
|
||||
return nil
|
||||
} else {
|
||||
t.Update("stale, updating...")
|
||||
}
|
||||
}
|
||||
|
||||
if err := pullDockerImages(config.Images); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := copyBinariesFromDockerImages(config, dest); err != nil {
|
||||
return fmt.Errorf("failed to copy binary for %s@%s: %v", config.Name(), config.Version, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isDownloadStale(config config.BinaryFromImage, binaryPaths []string) bool {
|
||||
currentFingerprint := config.Fingerprint()
|
||||
|
||||
for _, path := range binaryPaths {
|
||||
fingerprintPath := path + ".fingerprint"
|
||||
if _, err := os.Stat(fingerprintPath); err != nil {
|
||||
// missing a fingerprint file means the download is stale
|
||||
return true
|
||||
}
|
||||
|
||||
writtenFingerprint, err := os.ReadFile(fingerprintPath)
|
||||
if err != nil {
|
||||
// missing a fingerprint file means the download is stale
|
||||
return true
|
||||
}
|
||||
|
||||
if string(writtenFingerprint) != currentFingerprint {
|
||||
// the fingerprint file does not match the current fingerprint, so the download is stale
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func allPathsExist(paths []string) bool {
|
||||
for _, path := range paths {
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func pullDockerImages(images []config.Image) error {
|
||||
for _, image := range images {
|
||||
if err := pullDockerImage(image.Reference, image.Platform); err != nil {
|
||||
return fmt.Errorf("failed to pull image %s for platform %s: %v", image.Reference, image.Platform, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func pullDockerImage(imageReference, platform string) error {
|
||||
a := ui.Action{Msg: fmt.Sprintf("pull image %s (%s)", imageReference, platform)}
|
||||
a.Start()
|
||||
cmd := exec.Command("docker", "image", "inspect", imageReference)
|
||||
if err := cmd.Run(); err == nil {
|
||||
a.Skip(fmt.Sprintf("docker image already exists %q", imageReference))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
cmd = exec.Command("docker", "pull", "--platform", platform, imageReference)
|
||||
err := cmd.Run()
|
||||
|
||||
a.Done(err)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func copyBinariesFromDockerImages(config config.BinaryFromImage, destination string) (err error) {
|
||||
for _, image := range config.Images {
|
||||
if err := copyBinariesFromDockerImage(config, destination, image); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyBinariesFromDockerImage(config config.BinaryFromImage, destination string, image config.Image) (err error) {
|
||||
containerName := fmt.Sprintf("%s-%s-%s", config.Name(), config.Version, uuid.New().String())
|
||||
|
||||
cmd := exec.Command("docker", "create", "--name", containerName, image.Reference)
|
||||
if err = cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
cmd := exec.Command("docker", "rm", containerName)
|
||||
cmd.Run()
|
||||
}()
|
||||
|
||||
for i, destinationPath := range config.AllStorePathsForImage(image, destination) {
|
||||
path := config.PathsInImage[i]
|
||||
if err := copyBinaryFromContainer(containerName, path, destinationPath, config.Fingerprint()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyBinaryFromContainer(containerName, containerPath, destinationPath, fingerprint string) (err error) {
|
||||
|
||||
a := ui.Action{Msg: fmt.Sprintf("extract %s", containerPath)}
|
||||
a.Start()
|
||||
|
||||
defer func() {
|
||||
a.Done(err)
|
||||
}()
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(destinationPath), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := exec.Command("docker", "cp", fmt.Sprintf("%s:%s", containerName, containerPath), destinationPath)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// capture fingerprint file
|
||||
fingerprintPath := destinationPath + ".fingerprint"
|
||||
if err := os.WriteFile(fingerprintPath, []byte(fingerprint), 0644); err != nil {
|
||||
return fmt.Errorf("unable to write fingerprint file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -0,0 +1,197 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/internal/config"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Entries map[LogicalEntryKey]EntryInfo
|
||||
|
||||
type EntryInfo struct {
|
||||
IsConfigured bool
|
||||
BinaryPath string
|
||||
SnippetPath string
|
||||
}
|
||||
|
||||
type LogicalEntryKey struct {
|
||||
OrgName string
|
||||
Version string
|
||||
Platform string
|
||||
Filename string
|
||||
}
|
||||
|
||||
func (k LogicalEntryKey) Path() string {
|
||||
return fmt.Sprintf("%s/%s/%s/%s", k.OrgName, k.Version, k.Platform, k.Filename)
|
||||
}
|
||||
|
||||
type LogicalEntryKeys []LogicalEntryKey
|
||||
|
||||
func (l LogicalEntryKeys) Len() int {
|
||||
return len(l)
|
||||
}
|
||||
|
||||
func (l LogicalEntryKeys) Less(i, j int) bool {
|
||||
if l[i].OrgName == l[j].OrgName {
|
||||
if l[i].Version == l[j].Version {
|
||||
if l[i].Platform == l[j].Platform {
|
||||
return l[i].Filename < l[j].Filename
|
||||
}
|
||||
return l[i].Platform < l[j].Platform
|
||||
}
|
||||
return l[i].Version < l[j].Version
|
||||
}
|
||||
return l[i].OrgName < l[j].OrgName
|
||||
}
|
||||
|
||||
func (l LogicalEntryKeys) Swap(i, j int) {
|
||||
l[i], l[j] = l[j], l[i]
|
||||
}
|
||||
|
||||
func NewLogicalEntryKeys(m map[LogicalEntryKey]EntryInfo) LogicalEntryKeys {
|
||||
var keys LogicalEntryKeys
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Sort(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
func ListAllBinaries(appConfig config.Application) (Entries, error) {
|
||||
binaries, err := allFilePaths(appConfig.DownloadPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to list binaries: %w", err)
|
||||
}
|
||||
|
||||
cases := make(map[LogicalEntryKey]EntryInfo)
|
||||
for _, storePath := range binaries {
|
||||
isConfigured := appConfig.GetBinaryFromImageByPath(storePath) != nil
|
||||
|
||||
relativePath, err := filepath.Rel(appConfig.DownloadPath, storePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get relative path for %q: %w", storePath, err)
|
||||
}
|
||||
|
||||
key, err := getLogicalKey(relativePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get logical key for binary %q: %w", storePath, err)
|
||||
}
|
||||
cases[*key] = EntryInfo{
|
||||
IsConfigured: isConfigured,
|
||||
BinaryPath: storePath,
|
||||
}
|
||||
}
|
||||
|
||||
return cases, nil
|
||||
}
|
||||
|
||||
func ListAllEntries(appConfig config.Application) (Entries, error) {
|
||||
|
||||
snippets, err := allFilePaths(appConfig.SnippetPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to list snippets: %w", err)
|
||||
}
|
||||
|
||||
cases, err := ListAllBinaries(appConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to list binaries: %w", err)
|
||||
}
|
||||
|
||||
// anything configured that isn't in the binaries list?
|
||||
for _, cfg := range appConfig.FromImages {
|
||||
for _, image := range cfg.Images {
|
||||
for _, path := range cfg.AllStorePathsForImage(image, appConfig.DownloadPath) {
|
||||
key := newLogicalEntryForImage(cfg, image, path)
|
||||
if _, ok := cases[key]; ok {
|
||||
continue
|
||||
}
|
||||
cases[key] = EntryInfo{
|
||||
IsConfigured: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// correlate snippets to existing binaries and configurations (and add unmanaged ones)
|
||||
for _, storePath := range snippets {
|
||||
relativePath, err := filepath.Rel(appConfig.SnippetPath, storePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get relative path for %q: %w", storePath, err)
|
||||
}
|
||||
key, err := getLogicalKey(relativePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get logical key for snippet %q: %w", storePath, err)
|
||||
}
|
||||
|
||||
if v, ok := cases[*key]; ok {
|
||||
v.SnippetPath = storePath
|
||||
cases[*key] = v
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
cases[*key] = EntryInfo{
|
||||
IsConfigured: false,
|
||||
SnippetPath: storePath,
|
||||
}
|
||||
}
|
||||
|
||||
return cases, nil
|
||||
}
|
||||
|
||||
func newLogicalEntryForImage(cfg config.BinaryFromImage, image config.Image, storePath string) LogicalEntryKey {
|
||||
return LogicalEntryKey{
|
||||
OrgName: cfg.Name(),
|
||||
Version: cfg.Version,
|
||||
Platform: config.PlatformAsValue(image.Platform),
|
||||
Filename: filepath.Base(storePath),
|
||||
}
|
||||
}
|
||||
|
||||
func getLogicalKey(managedBinaryPath string) (*LogicalEntryKey, error) {
|
||||
// infer the logical key from the path alone: name/version/platform/filename
|
||||
|
||||
items := SplitFilepath(managedBinaryPath)
|
||||
if len(items) != 4 {
|
||||
return nil, fmt.Errorf("invalid managed binary path: %q", managedBinaryPath)
|
||||
}
|
||||
|
||||
return &LogicalEntryKey{
|
||||
OrgName: items[0],
|
||||
Version: items[1],
|
||||
Platform: items[2],
|
||||
Filename: items[3],
|
||||
}, nil
|
||||
}
|
||||
|
||||
func allFilePaths(root string) ([]string, error) {
|
||||
var paths []string
|
||||
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
||||
if info != nil && !info.IsDir() && !strings.HasSuffix(path, ".fingerprint") {
|
||||
paths = append(paths, path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return paths, nil
|
||||
}
|
||||
|
||||
func (e Entries) BinaryFromImageHasSnippet(cfg config.BinaryFromImage) bool {
|
||||
// all paths for all images must have snippets to return true
|
||||
for _, image := range cfg.Images {
|
||||
for _, storePath := range cfg.AllStorePathsForImage(image, "") {
|
||||
key := newLogicalEntryForImage(cfg, image, storePath)
|
||||
if v, ok := e[key]; ok {
|
||||
if v.SnippetPath == "" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gopkg.in/yaml.v3"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type SnippetMetadata struct {
|
||||
Name string `yaml:"name"`
|
||||
Offset int `yaml:"offset"`
|
||||
Length int `yaml:"length"`
|
||||
SnippetSha256 string `yaml:"snippetSha256"`
|
||||
FileSha256 string `yaml:"fileSha256"`
|
||||
}
|
||||
|
||||
func ReadSnippetMetadata(path string) (*SnippetMetadata, error) {
|
||||
if path == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
contents, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fields := strings.Split(string(contents), "\n### byte snippet to follow ###\n")
|
||||
if len(fields) != 2 {
|
||||
return nil, fmt.Errorf("this is not a snippet")
|
||||
}
|
||||
|
||||
var metadata SnippetMetadata
|
||||
if err := yaml.Unmarshal([]byte(fields[0]), &metadata); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &metadata, nil
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Action struct {
|
||||
Msg string
|
||||
}
|
||||
|
||||
func (a Action) Start() {
|
||||
fmt.Printf(" • %s%s%s\n", purple+italic, a.Msg, reset)
|
||||
}
|
||||
|
||||
func (a Action) Skip(newMsg ...string) {
|
||||
if len(newMsg) > 0 {
|
||||
// clear the line
|
||||
goToPreviousLineStart()
|
||||
// add a little extra to account for ansi escape codes (hack)
|
||||
fmt.Printf("%s\n", strings.Repeat(" ", len(a.Msg)+10))
|
||||
a.Msg = newMsg[0]
|
||||
}
|
||||
goToPreviousLineStart()
|
||||
formatSkip(a.Msg)
|
||||
}
|
||||
|
||||
func (a Action) Done(err error) {
|
||||
goToPreviousLineStart()
|
||||
if err != nil {
|
||||
|
||||
fmt.Printf(" %s✗%s %s%s%s\n", red+bold, reset, red, a.Msg, reset)
|
||||
|
||||
var exitError *exec.ExitError
|
||||
if errors.As(err, &exitError) && len(exitError.Stderr) > 0 {
|
||||
fmt.Printf(" %s├──%s %s%s%s\n", grey, reset, red, err, reset)
|
||||
fmt.Printf(" %s└──%s %s%s%s\n", grey, reset, red, "stderr:", reset)
|
||||
fmt.Println(string(exitError.Stderr))
|
||||
} else {
|
||||
fmt.Printf(" %s└──%s %s%s%s\n", grey, reset, red, err, reset)
|
||||
}
|
||||
return
|
||||
}
|
||||
fmt.Printf(" %s✔%s %s\n", green+bold, reset, a.Msg)
|
||||
}
|
||||
|
||||
func formatSkip(msg string) {
|
||||
fmt.Printf(" %s⏭%s %s%s%s\n", bold, reset, grey, msg, reset)
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
package ui
|
||||
|
||||
import "fmt"
|
||||
|
||||
const (
|
||||
grey = "\033[90m"
|
||||
reset = "\033[0m"
|
||||
bold = "\033[1m"
|
||||
red = "\033[31m"
|
||||
italic = "\033[3m"
|
||||
purple = "\033[95m" // hi variant
|
||||
green = "\033[32m"
|
||||
)
|
||||
|
||||
func goToPreviousLineStart() {
|
||||
fmt.Printf("\033[F")
|
||||
}
|
||||
@ -0,0 +1,107 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/charmbracelet/bubbles/list"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"os"
|
||||
)
|
||||
|
||||
var quitTextStyle = lipgloss.NewStyle().Margin(1, 0, 2, 4)
|
||||
|
||||
type item string
|
||||
|
||||
func (i item) Title() string { return string(i) }
|
||||
func (i item) Description() string { return "" }
|
||||
func (i item) FilterValue() string { return string(i) }
|
||||
|
||||
type model struct {
|
||||
list list.Model
|
||||
choice string
|
||||
quitting bool
|
||||
}
|
||||
|
||||
func PromptSelectBinary(binaryPaths []string) (string, error) {
|
||||
var items []list.Item
|
||||
for _, p := range binaryPaths {
|
||||
items = append(items, item(p))
|
||||
}
|
||||
|
||||
d := list.NewDefaultDelegate()
|
||||
d.ShowDescription = false
|
||||
d.Styles.NormalTitle = d.Styles.NormalTitle.PaddingLeft(4)
|
||||
d.Styles.SelectedTitle = d.Styles.SelectedTitle.PaddingLeft(3)
|
||||
d.SetSpacing(0)
|
||||
|
||||
l := list.New(items, d, 80, 80)
|
||||
l.Title = "Select a binary to capture a snippet from:"
|
||||
l.SetShowStatusBar(false)
|
||||
l.SetFilteringEnabled(true)
|
||||
l.Styles.Title = lipgloss.NewStyle().Bold(true).MarginLeft(1)
|
||||
l.Styles.PaginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(4)
|
||||
l.Styles.HelpStyle = list.DefaultStyles().HelpStyle.PaddingLeft(4).PaddingBottom(1)
|
||||
|
||||
m := model{list: l}
|
||||
|
||||
p := tea.NewProgram(m, tea.WithAltScreen())
|
||||
|
||||
fm, err := p.Run()
|
||||
if err != nil {
|
||||
fmt.Println("Error running program:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
m = fm.(model)
|
||||
|
||||
if m.quitting {
|
||||
return "", fmt.Errorf("cancelled")
|
||||
}
|
||||
|
||||
if m.choice == "" {
|
||||
return "", fmt.Errorf("no binary selected")
|
||||
}
|
||||
|
||||
return m.choice, nil
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
m.list.SetWidth(msg.Width)
|
||||
m.list.SetHeight(msg.Height)
|
||||
return m, nil
|
||||
|
||||
case tea.KeyMsg:
|
||||
switch keypress := msg.String(); keypress {
|
||||
case "ctrl+c":
|
||||
m.quitting = true
|
||||
return m, tea.Quit
|
||||
|
||||
case "enter":
|
||||
i, ok := m.list.SelectedItem().(item)
|
||||
if ok {
|
||||
m.choice = string(i)
|
||||
}
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
|
||||
var cmd tea.Cmd
|
||||
m.list, cmd = m.list.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m model) View() string {
|
||||
if m.choice != "" {
|
||||
return quitTextStyle.Render(fmt.Sprintf("Selected %q", m.choice))
|
||||
}
|
||||
if m.quitting {
|
||||
return quitTextStyle.Render("Cancelled")
|
||||
}
|
||||
return "\n" + m.list.View()
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
package ui
|
||||
|
||||
import "fmt"
|
||||
|
||||
func RenderError(err error) string {
|
||||
return fmt.Sprintf("%s%v%s", red, err, reset)
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Title struct {
|
||||
Name, Version string
|
||||
}
|
||||
|
||||
func (t Title) Start() {
|
||||
t.start()
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
func (t Title) start() {
|
||||
fmt.Printf("%s%s@%s%s", bold, t.Name, t.Version, reset)
|
||||
}
|
||||
|
||||
func (t Title) Update(msg string) {
|
||||
goToPreviousLineStart()
|
||||
t.start()
|
||||
fmt.Print(strings.Repeat(" ", 35-(len(t.Name)+len(t.Version))))
|
||||
fmt.Printf(" %s⚠%s %s%s%s\n", bold, reset, italic+grey, msg, reset)
|
||||
}
|
||||
|
||||
func (t Title) Skip(msg string) {
|
||||
goToPreviousLineStart()
|
||||
t.start()
|
||||
fmt.Print(strings.Repeat(" ", 35-(len(t.Name)+len(t.Version))))
|
||||
formatSkip(msg)
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func SplitFilepath(path string) []string {
|
||||
return strings.Split(path, string(filepath.Separator))
|
||||
}
|
||||
|
||||
func Sha256SumFile(f *os.File) (string, error) {
|
||||
_, err := f.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to seek to start of file: %w", err)
|
||||
}
|
||||
hasher := sha256.New()
|
||||
_, err = io.Copy(hasher, f)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to hash file: %w", err)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%x", hasher.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func Sha256SumBytes(buf []byte) string {
|
||||
hasher := sha256.New()
|
||||
hasher.Write(buf)
|
||||
return fmt.Sprintf("%x", hasher.Sum(nil))
|
||||
}
|
||||
26
syft/pkg/cataloger/binary/test-fixtures/manager/main.go
Normal file
26
syft/pkg/cataloger/binary/test-fixtures/manager/main.go
Normal file
@ -0,0 +1,26 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/internal/cli"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/internal/ui"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cmd, err := cli.New()
|
||||
if err != nil {
|
||||
exit(err)
|
||||
}
|
||||
|
||||
if err := cmd.Execute(); err != nil {
|
||||
exit(err)
|
||||
}
|
||||
}
|
||||
|
||||
func exit(err error) {
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", ui.RenderError(err))
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
@ -0,0 +1,100 @@
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/internal"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/internal/config"
|
||||
"github.com/stretchr/testify/require"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// SnippetOrBinary returns the path to either the binary or the snippet for the given logical entry key.
|
||||
// Note: this is intended to be used only within the context of the binary cataloger test fixtures. Any other
|
||||
// use is unsupported. Path should be a logical path relative to the test-fixtures/classifiers directory (but does
|
||||
// not specify the "bin" or "snippets" parent path... this is determined logically [snippets > binary unless told
|
||||
// otherwise]). Path should also be to the directory containing the binary or snippets of interest (not the binaries
|
||||
// or snippets itself).
|
||||
func SnippetOrBinary(t *testing.T, path string, requireBinary bool) string {
|
||||
t.Helper()
|
||||
|
||||
require.Len(t, internal.SplitFilepath(path), 3, "path must be a in the form <name>/<version>/<arch>")
|
||||
|
||||
// cd to test-fixtures directory and load the config
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, os.Chdir("test-fixtures"))
|
||||
defer func() {
|
||||
require.NoError(t, os.Chdir(cwd))
|
||||
}()
|
||||
|
||||
appConfig, err := config.Read()
|
||||
require.NoError(t, err)
|
||||
|
||||
// find the first matching fixture path that matches the given requirements
|
||||
|
||||
entries, err := internal.ListAllEntries(*appConfig)
|
||||
require.NoError(t, err)
|
||||
|
||||
var fixturePath string
|
||||
for k, v := range entries {
|
||||
if filepath.Dir(k.Path()) == path {
|
||||
// prefer the snippet over the binary
|
||||
if !requireBinary {
|
||||
if v.SnippetPath != "" {
|
||||
t.Logf("using snippet for %q", path)
|
||||
validateSnippet(t, v.BinaryPath, v.SnippetPath)
|
||||
fixturePath = v.SnippetPath
|
||||
break
|
||||
}
|
||||
if v.BinaryPath != "" {
|
||||
fixturePath = v.BinaryPath
|
||||
break
|
||||
}
|
||||
t.Fatalf("no binary or snippet found for %q", path)
|
||||
}
|
||||
if v.BinaryPath != "" {
|
||||
t.Logf("forcing the use of the original binary for %q", path)
|
||||
fixturePath = v.BinaryPath
|
||||
break
|
||||
}
|
||||
|
||||
t.Fatalf("no binary found for %q", path)
|
||||
}
|
||||
}
|
||||
|
||||
if fixturePath == "" {
|
||||
t.Fatalf("no fixture found for %q", path)
|
||||
}
|
||||
|
||||
// this should be relative to the tests-fixtures directory and should be the directory containing the binary or
|
||||
// snippet of interest (not the path to the binary or snippet itself)
|
||||
return filepath.Join("test-fixtures", filepath.Dir(fixturePath))
|
||||
}
|
||||
|
||||
func validateSnippet(t *testing.T, binaryPath, snippetPath string) {
|
||||
t.Helper()
|
||||
|
||||
// get a sha256 of the binary
|
||||
if _, err := os.Stat(binaryPath); err != nil {
|
||||
// no binary to validate against (this is ok)
|
||||
return
|
||||
}
|
||||
|
||||
metadata, err := internal.ReadSnippetMetadata(snippetPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
if metadata == nil {
|
||||
return
|
||||
}
|
||||
|
||||
f, err := os.Open(binaryPath)
|
||||
require.NoError(t, err)
|
||||
expected, err := internal.Sha256SumFile(f)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, expected, metadata.FileSha256, "snippet shadows a binary with a different sha256")
|
||||
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user