add manager for binary cataloger test fixtures

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Alex Goodman 2023-12-21 18:06:14 -05:00
parent bab4142881
commit e516eb4967
53 changed files with 2655 additions and 711 deletions

9
go.mod
View File

@ -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
View File

@ -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

View File

@ -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"),

View File

@ -1 +1,2 @@
classifiers/dynamic
classifiers/bin

View File

@ -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}'

View 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.

View File

@ -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"

View File

@ -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

View File

@ -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

View 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

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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, "-", "/")
}

View File

@ -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",
}
}

View File

@ -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)
}

View File

@ -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))
}

View File

@ -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))
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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")
}

View File

@ -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()
}

View File

@ -0,0 +1,7 @@
package ui
import "fmt"
func RenderError(err error) string {
return fmt.Sprintf("%s%v%s", red, err, reset)
}

View File

@ -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)
}

View File

@ -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))
}

View 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)
}

View File

@ -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")
}