Compare commits

...
19 Commits
Author SHA1 Message Date
Alex Goodman b0e9b92a25 Merge pull request #483 from anchore/fix-gomod-gosum-validation
Fix go module tidy checks for release pipeline
2021-08-18 11:00:49 -04:00
Alex Goodman d25d3b1fb3 fix go module tidy checks for release pipeline
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-08-18 10:13:56 -04:00
Keith Zantow 93b4bf9379 Add option to enable http registry connections (#482)
Signed-off-by: Keith Zantow <kzantow@gmail.com>
2021-08-17 12:52:51 -04:00
Keith Zantow 58f2be95fd chore: update packageurl-go and add encoding tests for #351 (#481)
Signed-off-by: Keith Zantow <kzantow@gmail.com>
2021-08-13 15:39:52 -04:00
Alex Goodman 6f038e9208 Merge pull request #447 from bureado/main
Minimal changes to use ID_LIKE
2021-08-12 14:20:13 -04:00
Alex Goodman 32a54b94be add unit tests for distro identification based on "like" field
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-08-12 14:09:22 -04:00
Alex Goodman 98d4749f86 Enhance CPE generation (#472)
* adjust CPE specificity sorting to include field length and bias certain fields

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

* remove * vendor values from CPE generation

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

* re-enable generating CPEs for jenkins and jira plugins

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

* improve CPE generation logic based on java artifactID and groupID

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

* add ruby-lang as target software candidate for gems in CPE generation logic

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

* rename filterCpes to filterCPEs

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

* refactor CPE filters and groupID processing (for linting)

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

* use ruby-lang as vendor candidate not target software

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

* address PR comments for CPE generation

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-08-09 19:52:19 +00:00
Alex Goodman 3a5168917e Update SPDX license list from 3.13 to 3.14 (#474)
* update SPDX license list from 3.13 to 3.14

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

* remove license list version from spdx snapshot unit tests

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-08-09 19:47:33 +00:00
Alex Goodman 58b21c03c3 Merge pull request #461 from anchore/update-slack
Update slack channels
2021-07-12 08:28:01 -04:00
Alex Goodman 2093d2a1dc update slack channels
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-07-12 08:04:04 -04:00
Alex Goodman dd56450f92 Revert "Add callout for OSS meetup (#446)" (#459)
This reverts commit 66e71c39e2.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2021-07-02 14:29:27 +00:00
Alex Goodman 6f2b52971d directory resolver indexer should report one progressable (#457)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-07-02 10:18:41 -04:00
Alex Goodman fee74d57b8 Update documentation to show SPDX support (#452)
* add docs for spdx support

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

* copy updates for format options

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-07-01 14:52:57 +00:00
Alex Goodman fee35dd175 redirect cursor hide/show to stderr (#456)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-06-30 13:10:00 -04:00
Alex Goodman ecf4e5546a add help message when no arguments are provided (#455)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-06-30 12:53:12 -04:00
Alex Goodman 2de56c0749 force UI teardown when event is sourced from a signal interrupt (#453)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-06-29 18:16:32 -04:00
Alex Goodman fb0857ff93 Add support for indexing root filesystem (#442)
* change directory resolver to ignore system runtime paths + drive by index

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

* add event/etui support for filesystem indexing (for dir resolver)

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

* add warnings for path indexing problems

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

* add directory resolver index tests

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

* improve testing around directory resolver

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

* renamed p var to path when not conflicting with import

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

* pull docker image in CLI dir scan timeout test

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

* ensure file not exist errors do not stop directory resolver indexing

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-06-29 22:06:47 +00:00
Jose Miguel Parrella c833472b6e If distro name is unknown, try with the ID_LIKE
Signed-off-by: Jose Miguel Parrella <j@bureado.com>
2021-06-25 22:18:40 -07:00
Jose Miguel Parrella c825ae2404 Completing the test for distro version
Signed-off-by: Jose Miguel Parrella <j@bureado.com>
2021-06-25 22:18:40 -07:00
57 changed files with 2137 additions and 383 deletions
+1 -1
View File
@@ -3,4 +3,4 @@ contact_links:
- name: Join the Slack community 💬
# link to our community Slack registration page
url: https://anchore.com/slack
about: 'Be sure to check out the channels #toolbox-help (ask us questions) and #toolbox-dev (collaborate with us on design and development)!'
about: 'Be sure to check out the channels #syft-help (ask us questions) and #syft-dev (collaborate with us on design and development)!'
+15 -11
View File
@@ -1,27 +1,31 @@
#!/usr/bin/env bash
set -u
set -eu
TMP_GOMOD=$(mktemp)
TMP_GOSUM=$(mktemp)
ORIGINAL_STATE_DIR=$(mktemp -d "TEMP-original-state-XXXXXXXXX")
TIDY_STATE_DIR=$(mktemp -d "TEMP-tidy-state-XXXXXXXXX")
trap "rm -f ${TMP_GOSUM} ${TMP_GOMOD}" EXIT
trap "cp -v ${ORIGINAL_STATE_DIR}/* ./ && rm -fR ${ORIGINAL_STATE_DIR} ${TIDY_STATE_DIR}" EXIT
cp go.mod "${TMP_GOMOD}"
cp go.sum "${TMP_GOSUM}"
echo "Capturing original state of files..."
cp -v go.mod go.sum "${ORIGINAL_STATE_DIR}"
echo "Capturing state of go.mod and go.sum after running go mod tidy..."
go mod tidy
cp -v go.mod go.sum "${TIDY_STATE_DIR}"
echo ""
DIFF_MOD=$(diff -u "${TMP_GOMOD}" go.mod)
DIFF_SUM=$(diff -u "${TMP_GOSUM}" go.sum)
set +e
cp "${TMP_GOMOD}" go.mod
cp "${TMP_GOSUM}" go.sum
# Detect difference between the git HEAD state and the go mod tidy state
DIFF_MOD=$(diff -u "${ORIGINAL_STATE_DIR}/go.mod" "${TIDY_STATE_DIR}/go.mod")
DIFF_SUM=$(diff -u "${ORIGINAL_STATE_DIR}/go.sum" "${TIDY_STATE_DIR}/go.sum")
if [[ -n "${DIFF_MOD}" || -n "${DIFF_SUM}" ]]; then
echo "go.mod and/or go.sum are not tidy; please run go mod tidy"
echo "go.mod diff:"
echo "${DIFF_MOD}"
echo "go.sum diff:"
echo "${DIFF_SUM}"
echo ""
printf "FAILED! go.mod and/or go.sum are NOT tidy; please run 'go mod tidy'.\n\n"
exit 1
fi
-1
View File
@@ -108,7 +108,6 @@ bootstrap-tools: $(TEMPDIR)
.PHONY: bootstrap-go
bootstrap-go:
go mod download
go mod tidy # note: it is important that the go.sum is kept in a consistent state at all times (especially during release)
.PHONY: bootstrap
bootstrap: $(RESULTSDIR) bootstrap-go bootstrap-tools ## Download and install all go dependencies (+ prep tooling in the ./tmp dir)
+6 -18
View File
@@ -8,23 +8,6 @@
A CLI tool and go library for generating a Software Bill of Materials (SBOM) from container images and filesystems. Exceptional for vulnerability detection when used with a scanner tool like [Grype](https://github.com/anchore/grype).
## Happening soon: [OSS Virtual Meetup](https://get.anchore.com/anchore-oss-meetup-jun-30-2021/)
[**Register here!**](https://get.anchore.com/anchore-oss-meetup-jun-30-2021/)
**When:** June 30, 11am-noon PT
**What:** 3 fast-paced talks, with time for Q&A
- **Using Syft to Streamline Compliance** (Chet Burgess — Principal Engineer, Cisco)
- **Fast Container Scanning in CI: Three Ways to Use Grype in Your Builds** (Dan Luhring — Manager of Open Source Engineering, Anchore)
- **Building More Optimal Container Images with Dive** (Alex Goodman — Creator of [Dive](https://github.com/wagoodman/dive), Sr Software Engineer for Open Source, Anchore)
![syft-demo](https://user-images.githubusercontent.com/590471/90277200-2a253000-de33-11ea-893f-32c219eea11a.gif)
**Features**
@@ -83,7 +66,9 @@ syft packages <image> -o <format>
Where the `format`s available are:
- `json`: Use this to get as much information out of Syft as possible!
- `text`: A row-oriented, human-and-machine-friendly output.
- `cyclonedx`: A XML report conforming to the [CycloneDX 1.2](https://cyclonedx.org/) specification.
- `cyclonedx`: A XML report conforming to the [CycloneDX 1.2 specification](https://cyclonedx.org/specification/overview/).
- `spdx`: A tag-value formatted report conforming to the [SPDX 2.2 specification](https://spdx.github.io/spdx-spec/).
- `spdx-json`: A JSON report conforming to the [SPDX 2.2 JSON Schema](https://github.com/spdx/spdx-spec/blob/v2.2/schemas/spdx-schema.json).
- `table`: A columnar summary (default).
## Installation
@@ -219,6 +204,9 @@ registry:
# skip TLS verification when communicating with the registry
# SYFT_REGISTRY_INSECURE_SKIP_TLS_VERIFY env var
insecure-skip-tls-verify: false
# use http instead of https when connecting to the registry
# SYFT_REGISTRY_INSECURE_USE_HTTP env var
insecure-use-http: false
# credentials for specific registries
auth:
+4 -2
View File
@@ -13,7 +13,7 @@ import (
// eventLoop listens to worker errors (from execution path), worker events (from a partybus subscription), and
// signal interrupts. Is responsible for handling each event relative to a given UI an to coordinate eventing until
// an eventual graceful exit.
// nolint:gocognit
// nolint:gocognit,funlen
func eventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription *partybus.Subscription, ux ui.UI, cleanupFn func()) error {
defer cleanupFn()
events := subscription.Events()
@@ -23,6 +23,7 @@ func eventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription *
}
var retErr error
var forceTeardown bool
for {
if workerErrs == nil && events == nil {
@@ -66,10 +67,11 @@ func eventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription *
// of processing.
events = nil
workerErrs = nil
forceTeardown = true
}
}
if err := ux.Teardown(); err != nil {
if err := ux.Teardown(forceTeardown); err != nil {
retErr = multierror.Append(retErr, err)
}
+1 -1
View File
@@ -37,7 +37,7 @@ func (u *uiMock) Handle(event partybus.Event) error {
return u.Called(event).Error(0)
}
func (u *uiMock) Teardown() error {
func (u *uiMock) Teardown(_ bool) error {
u.t.Logf("UI Teardown called")
return u.Called().Error(0)
}
+5 -4
View File
@@ -28,7 +28,9 @@ import (
const (
packagesExample = ` {{.appName}} {{.command}} alpine:latest a summary of discovered packages
{{.appName}} {{.command}} alpine:latest -o json show all possible cataloging details
{{.appName}} {{.command}} alpine:latest -o cyclonedx show a CycloneDX SBOM
{{.appName}} {{.command}} alpine:latest -o cyclonedx show a CycloneDX formatted SBOM
{{.appName}} {{.command}} alpine:latest -o spdx show a SPDX 2.2 tag-value formatted SBOM
{{.appName}} {{.command}} alpine:latest -o spdx-json show a SPDX 2.2 JSON formatted SBOM
{{.appName}} {{.command}} alpine:latest -vv show verbose debug information
Supports the following image sources:
@@ -47,7 +49,7 @@ const (
var (
packagesPresenterOpt packages.PresenterOption
packagesArgs = cobra.MinimumNArgs(1)
packagesArgs = cobra.MaximumNArgs(1)
packagesCmd = &cobra.Command{
Use: "packages [SOURCE]",
Short: "Generate a package SBOM",
@@ -65,8 +67,7 @@ var (
if err != nil {
return err
}
// silently exit
return fmt.Errorf("")
return fmt.Errorf("an image/directory argument is required")
}
// set the presenter
+9 -1
View File
@@ -34,11 +34,19 @@ var powerUserCmd = &cobra.Command{
"appName": internal.ApplicationName,
"command": "power-user",
}),
Args: cobra.ExactArgs(1),
Args: cobra.MaximumNArgs(1),
Hidden: true,
SilenceUsage: true,
SilenceErrors: true,
PreRunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
err := cmd.Help()
if err != nil {
return err
}
return fmt.Errorf("an image/directory argument is required")
}
if appConfig.Dev.ProfileCPU && appConfig.Dev.ProfileMem {
return fmt.Errorf("cannot profile CPU and memory simultaneously")
}
+3 -2
View File
@@ -10,7 +10,7 @@ require (
github.com/anchore/go-rpmdb v0.0.0-20210602151223-1f0f707a2894
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
github.com/anchore/stereoscope v0.0.0-20210524175238-3b7662f3a66f
github.com/anchore/stereoscope v0.0.0-20210817160504-0f4abc2a5a5a
github.com/antihax/optional v1.0.0
github.com/bmatcuk/doublestar/v2 v2.0.4
github.com/docker/docker v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible
@@ -21,7 +21,6 @@ require (
github.com/gookit/color v1.2.7
github.com/hashicorp/go-multierror v1.1.0
github.com/hashicorp/go-version v1.2.0
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213
github.com/mitchellh/go-homedir v1.1.0
github.com/mitchellh/mapstructure v1.3.1
github.com/olekukonko/tablewriter v0.0.4
@@ -46,3 +45,5 @@ require (
golang.org/x/mod v0.3.0
gopkg.in/yaml.v2 v2.3.0
)
replace github.com/package-url/packageurl-go v0.1.0 => github.com/anchore/packageurl-go v0.1.0-fixed
+4 -7
View File
@@ -115,13 +115,14 @@ github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 h1:VzprUTpc0v
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04/go.mod h1:6dK64g27Qi1qGQZ67gFmBFvEHScy0/C8qhQhNe5B5pQ=
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZVsCYMrIZBpFxwV26CbsuoEh5muXD5I1Ods=
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E=
github.com/anchore/stereoscope v0.0.0-20210524175238-3b7662f3a66f h1:bFadyOLOkzME3BrZFZ5m8cf/b2hsn3aMSS9s+SKubRk=
github.com/anchore/stereoscope v0.0.0-20210524175238-3b7662f3a66f/go.mod h1:vhh1M99rfWx5ejMvz1lkQiFZUrC5wu32V12R4JXH+ZI=
github.com/anchore/packageurl-go v0.1.0-fixed h1:2QJUTALDF7m2WHPOS1NyHguSX4ciG0xD3idaBQwHJS8=
github.com/anchore/packageurl-go v0.1.0-fixed/go.mod h1:C/ApiuWpmbpni4DIOECf6WCjFUZV7O1Fx7VAzrZHgBw=
github.com/anchore/stereoscope v0.0.0-20210817160504-0f4abc2a5a5a h1:RQb+Gft1MKxjDfJCnHP/f1mwfy0Jz50Kp9QGgSWKQiY=
github.com/anchore/stereoscope v0.0.0-20210817160504-0f4abc2a5a5a/go.mod h1:165DfE5jApgEkHTWwu7Bijeml9fofudrgcpuWaD9+tk=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/apex/log v1.1.4/go.mod h1:AlpoD9aScyQfJDVHmLMEcx4oU6LqzkWp4Mg9GdAcEvQ=
github.com/apex/log v1.3.0 h1:1fyfbPvUwD10nMoh3hY6MXzvZShJQn9/ck7ATgAt5pA=
github.com/apex/log v1.3.0/go.mod h1:jd8Vpsr46WAe3EZSQ/IUMs2qQD/GOycT5rPWCO1yGcs=
github.com/apex/logs v0.0.4/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo=
github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE=
@@ -466,7 +467,6 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 h1:qGQQKEcAR99REcMpsXCp3lJ03zYT1PkRd3kQGPn9GVg=
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
@@ -590,8 +590,6 @@ github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5X
github.com/opencontainers/runc v0.1.1 h1:GlxAyO6x8rfZYN9Tt0Kti5a/cP41iuiO2yYT0IJGY8Y=
github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/package-url/packageurl-go v0.1.0 h1:efWBc98O/dBZRg1pw2xiDzovnlMjCa9NPnfaiBduh8I=
github.com/package-url/packageurl-go v0.1.0/go.mod h1:C/ApiuWpmbpni4DIOECf6WCjFUZV7O1Fx7VAzrZHgBw=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
@@ -723,7 +721,6 @@ github.com/tetafro/godot v0.3.7/go.mod h1:/7NLHhv08H1+8DNj0MElpAACw1ajsCuf3TKNQx
github.com/tetafro/godot v0.4.2/go.mod h1:/7NLHhv08H1+8DNj0MElpAACw1ajsCuf3TKNQxA5S+0=
github.com/timakin/bodyclose v0.0.0-20190930140734-f7f2e9bca95e/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
github.com/timakin/bodyclose v0.0.0-20200424151742-cb6215831a94/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
github.com/tj/assert v0.0.0-20171129193455-018094318fb0 h1:Rw8kxzWo1mr6FSaYXjQELRe88y2KdfynXdnK72rdjtA=
github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0=
github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0=
github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao=
+3
View File
@@ -20,11 +20,13 @@ type RegistryCredentials struct {
type registry struct {
InsecureSkipTLSVerify bool `yaml:"insecure-skip-tls-verify" json:"insecure-skip-tls-verify" mapstructure:"insecure-skip-tls-verify"`
InsecureUseHTTP bool `yaml:"insecure-use-http" json:"insecure-use-http" mapstructure:"insecure-use-http"`
Auth []RegistryCredentials `yaml:"auth" json:"auth" mapstructure:"auth"`
}
func (cfg registry) loadDefaultValues(v *viper.Viper) {
v.SetDefault("registry.insecure-skip-tls-verify", false)
v.SetDefault("registry.insecure-use-http", false)
v.SetDefault("registry.auth", []RegistryCredentials{})
}
@@ -67,6 +69,7 @@ func (cfg *registry) ToOptions() *image.RegistryOptions {
}
return &image.RegistryOptions{
InsecureSkipTLSVerify: cfg.InsecureSkipTLSVerify,
InsecureUseHTTP: cfg.InsecureUseHTTP,
Credentials: auth,
}
}
+55
View File
@@ -2,6 +2,7 @@ package config
import (
"fmt"
"github.com/anchore/stereoscope/pkg/image"
"testing"
"github.com/stretchr/testify/assert"
@@ -54,3 +55,57 @@ func TestHasNonEmptyCredentials(t *testing.T) {
})
}
}
func Test_registry_ToOptions(t *testing.T) {
tests := []struct {
name string
input registry
expected image.RegistryOptions
}{
{
name: "no registry options",
input: registry{},
expected: image.RegistryOptions{
Credentials: []image.RegistryCredentials{},
},
},
{
name: "set InsecureSkipTLSVerify",
input: registry{
InsecureSkipTLSVerify: true,
},
expected: image.RegistryOptions{
InsecureSkipTLSVerify: true,
Credentials: []image.RegistryCredentials{},
},
},
{
name: "set InsecureUseHTTP",
input: registry{
InsecureUseHTTP: true,
},
expected: image.RegistryOptions{
InsecureUseHTTP: true,
Credentials: []image.RegistryCredentials{},
},
},
{
name: "set all bool options",
input: registry{
InsecureSkipTLSVerify: true,
InsecureUseHTTP: true,
},
expected: image.RegistryOptions{
InsecureSkipTLSVerify: true,
InsecureUseHTTP: true,
Credentials: []image.RegistryCredentials{},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, &test.expected, test.input.ToOptions())
})
}
}
@@ -29,5 +29,8 @@ func TestSPDXJSONImagePresenter(t *testing.T) {
}
func spdxJsonRedactor(s []byte) []byte {
return regexp.MustCompile(`"created": .*`).ReplaceAll(s, []byte("redacted"))
// each SBOM reports the time it was generated, which is not useful during snapshot testing
s = regexp.MustCompile(`"created": .*`).ReplaceAll(s, []byte("redacted"))
// the license list will be updated periodically, the value here should not be directly tested in snapshot tests
return regexp.MustCompile(`"licenseListVersion": .*`).ReplaceAll(s, []byte("redacted"))
}
@@ -29,5 +29,8 @@ func TestSPDXTagValueImagePresenter(t *testing.T) {
}
func spdxTagValueRedactor(s []byte) []byte {
return regexp.MustCompile(`Created: .*`).ReplaceAll(s, []byte("redacted"))
// each SBOM reports the time it was generated, which is not useful during snapshot testing
s = regexp.MustCompile(`Created: .*`).ReplaceAll(s, []byte("redacted"))
// the license list will be updated periodically, the value here should not be directly tested in snapshot tests
return regexp.MustCompile(`LicenseListVersion: .*`).ReplaceAll(s, []byte("redacted"))
}
@@ -227,6 +227,7 @@
},
"registry": {
"insecure-skip-tls-verify": false,
"insecure-use-http": false,
"auth": null
}
}
+16 -2
View File
@@ -1,9 +1,9 @@
// Code generated by go generate; DO NOT EDIT.
// This file was generated by robots at 2021-06-23 11:13:41.969613 -0400 EDT m=+0.325660925
// This file was generated by robots at 2021-08-09 15:08:22.639351 -0400 EDT m=+0.318960693
// using data from https://spdx.org/licenses/licenses.json
package spdxlicense
const Version = "3.13"
const Version = "3.14"
var licenseIDs = map[string]string{
"0bsd": "0BSD",
@@ -84,30 +84,40 @@ var licenseIDs = map[string]string{
"cc-by-1.0": "CC-BY-1.0",
"cc-by-2.0": "CC-BY-2.0",
"cc-by-2.5": "CC-BY-2.5",
"cc-by-2.5-au": "CC-BY-2.5-AU",
"cc-by-3.0": "CC-BY-3.0",
"cc-by-3.0-at": "CC-BY-3.0-AT",
"cc-by-3.0-de": "CC-BY-3.0-DE",
"cc-by-3.0-nl": "CC-BY-3.0-NL",
"cc-by-3.0-us": "CC-BY-3.0-US",
"cc-by-4.0": "CC-BY-4.0",
"cc-by-nc-1.0": "CC-BY-NC-1.0",
"cc-by-nc-2.0": "CC-BY-NC-2.0",
"cc-by-nc-2.5": "CC-BY-NC-2.5",
"cc-by-nc-3.0": "CC-BY-NC-3.0",
"cc-by-nc-3.0-de": "CC-BY-NC-3.0-DE",
"cc-by-nc-4.0": "CC-BY-NC-4.0",
"cc-by-nc-nd-1.0": "CC-BY-NC-ND-1.0",
"cc-by-nc-nd-2.0": "CC-BY-NC-ND-2.0",
"cc-by-nc-nd-2.5": "CC-BY-NC-ND-2.5",
"cc-by-nc-nd-3.0": "CC-BY-NC-ND-3.0",
"cc-by-nc-nd-3.0-de": "CC-BY-NC-ND-3.0-DE",
"cc-by-nc-nd-3.0-igo": "CC-BY-NC-ND-3.0-IGO",
"cc-by-nc-nd-4.0": "CC-BY-NC-ND-4.0",
"cc-by-nc-sa-1.0": "CC-BY-NC-SA-1.0",
"cc-by-nc-sa-2.0": "CC-BY-NC-SA-2.0",
"cc-by-nc-sa-2.0-fr": "CC-BY-NC-SA-2.0-FR",
"cc-by-nc-sa-2.0-uk": "CC-BY-NC-SA-2.0-UK",
"cc-by-nc-sa-2.5": "CC-BY-NC-SA-2.5",
"cc-by-nc-sa-3.0": "CC-BY-NC-SA-3.0",
"cc-by-nc-sa-3.0-de": "CC-BY-NC-SA-3.0-DE",
"cc-by-nc-sa-3.0-igo": "CC-BY-NC-SA-3.0-IGO",
"cc-by-nc-sa-4.0": "CC-BY-NC-SA-4.0",
"cc-by-nd-1.0": "CC-BY-ND-1.0",
"cc-by-nd-2.0": "CC-BY-ND-2.0",
"cc-by-nd-2.5": "CC-BY-ND-2.5",
"cc-by-nd-3.0": "CC-BY-ND-3.0",
"cc-by-nd-3.0-de": "CC-BY-ND-3.0-DE",
"cc-by-nd-4.0": "CC-BY-ND-4.0",
"cc-by-sa-1.0": "CC-BY-SA-1.0",
"cc-by-sa-2.0": "CC-BY-SA-2.0",
@@ -116,6 +126,7 @@ var licenseIDs = map[string]string{
"cc-by-sa-2.5": "CC-BY-SA-2.5",
"cc-by-sa-3.0": "CC-BY-SA-3.0",
"cc-by-sa-3.0-at": "CC-BY-SA-3.0-AT",
"cc-by-sa-3.0-de": "CC-BY-SA-3.0-DE",
"cc-by-sa-4.0": "CC-BY-SA-4.0",
"cc-pddc": "CC-PDDC",
"cc0-1.0": "CC0-1.0",
@@ -123,6 +134,7 @@ var licenseIDs = map[string]string{
"cddl-1.1": "CDDL-1.1",
"cdl-1.0": "CDL-1.0",
"cdla-permissive-1.0": "CDLA-Permissive-1.0",
"cdla-permissive-2.0": "CDLA-Permissive-2.0",
"cdla-sharing-1.0": "CDLA-Sharing-1.0",
"cecill-1.0": "CECILL-1.0",
"cecill-1.1": "CECILL-1.1",
@@ -320,6 +332,7 @@ var licenseIDs = map[string]string{
"nist-pd": "NIST-PD",
"nist-pd-fallback": "NIST-PD-fallback",
"nlod-1.0": "NLOD-1.0",
"nlod-2.0": "NLOD-2.0",
"nlpl": "NLPL",
"nokia": "Nokia",
"nosl": "NOSL",
@@ -368,6 +381,7 @@ var licenseIDs = map[string]string{
"oml": "OML",
"openssl": "OpenSSL",
"opl-1.0": "OPL-1.0",
"opubl-1.0": "OPUBL-1.0",
"oset-pl-2.1": "OSET-PL-2.1",
"osl-1.0": "OSL-1.0",
"osl-1.1": "OSL-1.1",
+7
View File
@@ -12,3 +12,10 @@ func HasAnyOfPrefixes(input string, prefixes ...string) bool {
return false
}
func TruncateMiddleEllipsis(input string, maxLen int) string {
if len(input) <= maxLen {
return input
}
return input[:maxLen/2] + "..." + input[len(input)-(maxLen/2):]
}
+41
View File
@@ -1,6 +1,7 @@
package internal
import (
"strconv"
"testing"
"github.com/stretchr/testify/assert"
@@ -63,3 +64,43 @@ func TestHasAnyOfPrefixes(t *testing.T) {
})
}
}
func TestTruncateMiddleEllipsis(t *testing.T) {
tests := []struct {
input string
len int
expected string
}{
{
input: "nobody expects the spanish inquisition",
len: 39,
expected: "nobody expects the spanish inquisition",
},
{
input: "nobody expects the spanish inquisition",
len: 30,
expected: "nobody expects ...ish inquisition",
},
{
input: "nobody expects the spanish inquisition",
len: 38,
expected: "nobody expects the spanish inquisition",
},
{
input: "",
len: 30,
expected: "",
},
{
input: "",
len: 0,
expected: "",
},
}
for _, test := range tests {
t.Run(test.input+":"+strconv.Itoa(test.len), func(t *testing.T) {
assert.Equal(t, test.expected, TruncateMiddleEllipsis(test.input, test.len))
})
}
}
+23 -11
View File
@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"fmt"
"io"
"os"
"sync"
@@ -11,7 +12,6 @@ import (
"github.com/anchore/syft/internal/logger"
syftEvent "github.com/anchore/syft/syft/event"
"github.com/anchore/syft/ui"
"github.com/k0kubun/go-ansi"
"github.com/wagoodman/go-partybus"
"github.com/wagoodman/jotframe/pkg/frame"
)
@@ -38,18 +38,20 @@ type ephemeralTerminalUI struct {
waitGroup *sync.WaitGroup
frame *frame.Frame
logBuffer *bytes.Buffer
output *os.File
}
func NewEphemeralTerminalUI() UI {
return &ephemeralTerminalUI{
handler: ui.NewHandler(),
waitGroup: &sync.WaitGroup{},
output: os.Stderr,
}
}
func (h *ephemeralTerminalUI) Setup(unsubscribe func() error) error {
h.unsubscribe = unsubscribe
ansi.CursorHide()
hideCursor(h.output)
// prep the logger to not clobber the screen from now on (logrus only)
h.logBuffer = bytes.NewBufferString("")
@@ -77,7 +79,7 @@ func (h *ephemeralTerminalUI) Handle(event partybus.Event) error {
case event.Type == syftEvent.PresenterReady:
// we need to close the screen now since signaling the the presenter is ready means that we
// are about to write bytes to stdout, so we should reset the terminal state first
h.closeScreen()
h.closeScreen(false)
if err := handleCatalogerPresenterReady(event); err != nil {
log.Errorf("unable to show %s event: %+v", event.Type, err)
@@ -93,7 +95,7 @@ func (h *ephemeralTerminalUI) openScreen() error {
config := frame.Config{
PositionPolicy: frame.PolicyFloatForward,
// only report output to stderr, reserve report output for stdout
Output: os.Stderr,
Output: h.output,
}
fr, err := frame.New(config)
@@ -105,11 +107,13 @@ func (h *ephemeralTerminalUI) openScreen() error {
return nil
}
func (h *ephemeralTerminalUI) closeScreen() {
func (h *ephemeralTerminalUI) closeScreen(force bool) {
// we may have other background processes still displaying progress, wait for them to
// finish before discontinuing dynamic content and showing the final report
if !h.frame.IsClosed() {
h.waitGroup.Wait()
if !force {
h.waitGroup.Wait()
}
h.frame.Close()
// TODO: there is a race condition within frame.Close() that sometimes leads to an extra blank line being output
frame.Close()
@@ -124,14 +128,22 @@ func (h *ephemeralTerminalUI) flushLog() {
logWrapper, ok := log.Log.(*logger.LogrusLogger)
if ok {
fmt.Fprint(logWrapper.Output, h.logBuffer.String())
logWrapper.Logger.SetOutput(os.Stderr)
logWrapper.Logger.SetOutput(h.output)
} else {
fmt.Fprint(os.Stderr, h.logBuffer.String())
fmt.Fprint(h.output, h.logBuffer.String())
}
}
func (h *ephemeralTerminalUI) Teardown() error {
h.closeScreen()
ansi.CursorShow()
func (h *ephemeralTerminalUI) Teardown(force bool) error {
h.closeScreen(force)
showCursor(h.output)
return nil
}
func hideCursor(output io.Writer) {
fmt.Fprint(output, "\x1b[?25l")
}
func showCursor(output io.Writer) {
fmt.Fprint(output, "\x1b[?25h")
}
+1 -1
View File
@@ -33,6 +33,6 @@ func (l loggerUI) Handle(event partybus.Event) error {
return l.unsubscribe()
}
func (l loggerUI) Teardown() error {
func (l loggerUI) Teardown(_ bool) error {
return nil
}
+1 -1
View File
@@ -7,5 +7,5 @@ import (
type UI interface {
Setup(unsubscribe func() error) error
partybus.Handler
Teardown() error
Teardown(force bool) error
}
+6 -1
View File
@@ -91,10 +91,15 @@ func assemble(name, version, like string) *Distro {
distroType, ok := IDMapping[name]
// Both distro and version must be present
if len(name) == 0 {
if len(name) == 0 && len(version) == 0 {
return nil
}
// If it's an unknown distro, try mapping the ID_LIKE
if !ok && len(like) != 0 {
distroType, ok = IDMapping[like]
}
if ok {
distro, err := NewDistro(distroType, version, like)
if err != nil {
+15 -2
View File
@@ -7,9 +7,8 @@ import (
"testing"
"github.com/anchore/syft/internal"
"github.com/stretchr/testify/assert"
"github.com/anchore/syft/syft/source"
"github.com/stretchr/testify/assert"
)
func TestIdentifyDistro(t *testing.T) {
@@ -85,6 +84,20 @@ func TestIdentifyDistro(t *testing.T) {
fixture: "test-fixtures/os/arch",
Type: ArchLinux,
},
{
fixture: "test-fixtures/partial-fields/missing-id",
Type: Debian,
Version: "8.0.0",
},
{
fixture: "test-fixtures/partial-fields/unknown-id",
Type: Debian,
Version: "8.0.0",
},
{
fixture: "test-fixtures/partial-fields/missing-version",
Type: UnknownDistroType,
},
}
observedDistros := internal.NewStringSet()
@@ -0,0 +1,3 @@
NAME="Debian GNU/Linux"
VERSION_ID="8"
ID_LIKE=debian
@@ -0,0 +1,2 @@
NAME="Debian GNU/Linux"
ID_LIKE=debian
@@ -0,0 +1,4 @@
NAME="Debian GNU/Linux"
VERSION_ID="8"
ID=my-awesome-distro
ID_LIKE=debian
+3
View File
@@ -23,6 +23,9 @@ const (
// FileDigestsCatalogerStarted is a partybus event that occurs when the file digests cataloging has begun
FileDigestsCatalogerStarted partybus.EventType = "syft-file-digests-cataloger-started-event"
// FileIndexingStarted is a partybus event that occurs when the directory resolver begins indexing a filesystem
FileIndexingStarted partybus.EventType = "syft-file-indexing-started-event"
// PresenterReady is a partybus event that occurs when an analysis result is ready for final presentation
PresenterReady partybus.EventType = "syft-presenter-ready-event"
+18
View File
@@ -94,6 +94,24 @@ func ParseFileDigestsCatalogingStarted(e partybus.Event) (progress.StagedProgres
return prog, nil
}
func ParseFileIndexingStarted(e partybus.Event) (string, progress.StagedProgressable, error) {
if err := checkEventType(e.Type, event.FileIndexingStarted); err != nil {
return "", nil, err
}
path, ok := e.Source.(string)
if !ok {
return "", nil, newPayloadErr(e.Type, "Source", e.Source)
}
prog, ok := e.Value.(progress.StagedProgressable)
if !ok {
return "", nil, newPayloadErr(e.Type, "Value", e.Value)
}
return path, prog, nil
}
func ParsePresenterReady(e partybus.Event) (presenter.Presenter, error) {
if err := checkEventType(e.Type, event.PresenterReady); err != nil {
return nil, err
+38
View File
@@ -5,6 +5,7 @@ import (
"testing"
"github.com/go-test/deep"
"github.com/package-url/packageurl-go"
"github.com/sergi/go-diff/diffmatchpatch"
)
@@ -21,6 +22,23 @@ func TestApkMetadata_pURL(t *testing.T) {
},
expected: "pkg:alpine/p@v?arch=a",
},
// verify #351
{
metadata: ApkMetadata{
Package: "g++",
Version: "v84",
Architecture: "am86",
},
expected: "pkg:alpine/g++@v84?arch=am86",
},
{
metadata: ApkMetadata{
Package: "g plus plus",
Version: "v84",
Architecture: "am86",
},
expected: "pkg:alpine/g%20plus%20plus@v84?arch=am86",
},
}
for _, test := range tests {
@@ -31,6 +49,26 @@ func TestApkMetadata_pURL(t *testing.T) {
diffs := dmp.DiffMain(test.expected, actual, true)
t.Errorf("diff: %s", dmp.DiffPrettyText(diffs))
}
// verify packageurl can parse
purl, err := packageurl.FromString(actual)
if err != nil {
t.Errorf("cannot re-parse purl: %s", actual)
}
if purl.Name != test.metadata.Package {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(test.metadata.Package, purl.Name, true)
t.Errorf("invalid purl name: %s", dmp.DiffPrettyText(diffs))
}
if purl.Version != test.metadata.Version {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(test.metadata.Version, purl.Version, true)
t.Errorf("invalid purl version: %s", dmp.DiffPrettyText(diffs))
}
if purl.Qualifiers.Map()["arch"] != test.metadata.Architecture {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(test.metadata.Architecture, purl.Qualifiers.Map()["arch"], true)
t.Errorf("invalid purl architecture: %s", dmp.DiffPrettyText(diffs))
}
})
}
}
+255 -89
View File
@@ -1,16 +1,32 @@
package cataloger
import (
"bufio"
"bytes"
"fmt"
"net/url"
"sort"
"strings"
"github.com/scylladb/go-set/strset"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/pkg"
"github.com/facebookincubator/nvdtools/wfn"
)
var domains = []string{
"com",
"org",
"net",
"io",
}
var (
forbiddenProductGroupIDFields = strset.New("plugin", "plugins", "client")
forbiddenVendorGroupIDFields = strset.New("plugin", "plugins")
)
var productCandidatesByPkgType = candidateStore{
pkg.JavaPkg: {
"springframework": []string{"spring_framework", "springsource_spring_framework"},
@@ -37,30 +53,6 @@ var productCandidatesByPkgType = candidateStore{
},
}
var cpeFilters = []filterFn{
func(cpe pkg.CPE, p pkg.Package) bool {
// jira / atlassian should not apply to clients
if cpe.Product == "jira" && strings.Contains(strings.ToLower(p.Name), "client") {
if cpe.Vendor == wfn.Any || cpe.Vendor == "jira" || cpe.Vendor == "atlassian" {
return true
}
}
return false
},
// nolint: goconst
func(cpe pkg.CPE, p pkg.Package) bool {
// jenkins server should only match against a product with the name jenkins
if cpe.Product == "jenkins" && !strings.Contains(strings.ToLower(p.Name), "jenkins") {
if cpe.Vendor == wfn.Any || cpe.Vendor == "jenkins" || cpe.Vendor == "cloudbees" {
return true
}
}
return false
},
}
type filterFn func(cpe pkg.CPE, p pkg.Package) bool
// this is a static mapping of known package names (keys) to official cpe names for each package
type candidateStore map[pkg.Type]map[string][]string
@@ -87,7 +79,7 @@ func newCPE(product, vendor, version, targetSW string) wfn.Attributes {
return cpe
}
func filterCpes(cpes []pkg.CPE, p pkg.Package, filters ...filterFn) (result []pkg.CPE) {
func filterCPEs(cpes []pkg.CPE, p pkg.Package, filters ...filterFn) (result []pkg.CPE) {
cpeLoop:
for _, cpe := range cpes {
for _, fn := range filters {
@@ -114,7 +106,7 @@ func generatePackageCPEs(p pkg.Package) []pkg.CPE {
keys := internal.NewStringSet()
cpes := make([]pkg.CPE, 0)
for _, product := range products {
for _, vendor := range append([]string{wfn.Any}, vendors...) {
for _, vendor := range vendors {
for _, targetSw := range append([]string{wfn.Any}, targetSws...) {
// prevent duplicate entries...
key := fmt.Sprintf("%s|%s|%s|%s", product, vendor, p.Version, targetSw)
@@ -131,7 +123,7 @@ func generatePackageCPEs(p pkg.Package) []pkg.CPE {
}
// filter out any known combinations that don't accurately represent this package
cpes = filterCpes(cpes, p, cpeFilters...)
cpes = filterCPEs(cpes, p, cpeFilters...)
sort.Sort(ByCPESpecificity(cpes))
@@ -168,52 +160,57 @@ func candidateTargetSoftwareAttrsForJava(p pkg.Package) []string {
func candidateVendors(p pkg.Package) []string {
// TODO: Confirm whether using products as vendors is helpful to the matching process
vendors := candidateProducts(p)
vendors := strset.New(candidateProducts(p)...)
switch p.Language {
case pkg.Ruby:
vendors.Add("ruby-lang")
case pkg.Java:
if p.MetadataType == pkg.JavaMetadataType {
vendors = append(vendors, candidateVendorsForJava(p)...)
vendors.Add(candidateVendorsForJava(p)...)
}
case pkg.Go:
// replace all candidates with only the golang-specific helper
vendors = nil
vendors.Clear()
vendor := candidateVendorForGo(p.Name)
if vendor != "" {
vendors = []string{vendor}
vendors.Add(vendor)
}
}
return vendors
// try swapping hyphens for underscores, vice versa, and removing separators altogether
addSeparatorVariations(vendors)
// generate sub-selections of each candidate based on separators (e.g. jenkins-ci -> [jenkins, jenkins-ci])
return generateAllSubSelections(vendors.List())
}
func candidateProducts(p pkg.Package) []string {
products := []string{p.Name}
products := strset.New(p.Name)
switch p.Language {
case pkg.Python:
switch {
case p.Language == pkg.Python:
if !strings.HasPrefix(p.Name, "python") {
products = append(products, "python-"+p.Name)
products.Add("python-" + p.Name)
}
case pkg.Java:
products = append(products, candidateProductsForJava(p)...)
case pkg.Go:
case p.Language == pkg.Java || p.MetadataType == pkg.JavaMetadataType:
products.Add(candidateProductsForJava(p)...)
case p.Language == pkg.Go:
// replace all candidates with only the golang-specific helper
products = nil
products.Clear()
prod := candidateProductForGo(p.Name)
if prod != "" {
products = []string{prod}
products.Add(prod)
}
}
for _, prod := range products {
if strings.Contains(prod, "-") {
products = append(products, strings.ReplaceAll(prod, "-", "_"))
}
}
// try swapping hyphens for underscores, vice versa, and removing separators altogether
addSeparatorVariations(products)
// return any known product name swaps prepended to the results
return append(productCandidatesByPkgType.getCandidates(p.Type, p.Name), products...)
// prepend any known product name swaps prepended to the results
return append(productCandidatesByPkgType.getCandidates(p.Type, p.Name), products.List()...)
}
// candidateProductForGo attempts to find a single product name in a best-effort attempt. This implementation prefers
@@ -270,50 +267,78 @@ func candidateVendorForGo(name string) string {
}
func candidateProductsForJava(p pkg.Package) []string {
// TODO: we could get group-id-like info from the MANIFEST.MF "Automatic-Module-Name" field
// for more info see pkg:maven/commons-io/commons-io@2.8.0 within cloudbees/cloudbees-core-mm:2.263.4.2
// at /usr/share/jenkins/jenkins.war:WEB-INF/plugins/analysis-model-api.hpi:WEB-INF/lib/commons-io-2.8.0.jar
if product, _ := productAndVendorFromPomPropertiesGroupID(p); product != "" {
// ignore group ID info from a jenkins plugin, as using this info may imply that this package
// CPE belongs to the cloudbees org (or similar) which is wrong.
if p.Type == pkg.JenkinsPluginPkg && strings.ToLower(product) == "jenkins" {
return nil
}
return []string{product}
}
return nil
return productsFromArtifactAndGroupIDs(artifactIDFromJavaPackage(p), groupIDsFromJavaPackage(p))
}
func candidateVendorsForJava(p pkg.Package) []string {
if _, vendor := productAndVendorFromPomPropertiesGroupID(p); vendor != "" {
return []string{vendor}
}
return nil
return vendorsFromGroupIDs(groupIDsFromJavaPackage(p))
}
func productAndVendorFromPomPropertiesGroupID(p pkg.Package) (string, string) {
groupID := groupIDFromPomProperties(p)
if !shouldConsiderGroupID(groupID) {
return "", ""
func vendorsFromGroupIDs(groupIDs []string) []string {
vendors := strset.New()
for _, groupID := range groupIDs {
for i, field := range strings.Split(groupID, ".") {
field = strings.TrimSpace(field)
if len(field) == 0 {
continue
}
if forbiddenVendorGroupIDFields.Has(strings.ToLower(field)) {
continue
}
if i == 0 {
continue
}
// e.g. jenkins-ci -> [jenkins-ci, jenkins]
vendors.Add(generateSubSelections(field)...)
}
}
if !internal.HasAnyOfPrefixes(groupID, "com", "org") {
return "", ""
}
fields := strings.Split(groupID, ".")
if len(fields) < 3 {
return "", ""
}
product := fields[2]
vendor := fields[1]
return product, vendor
return vendors.List()
}
func groupIDFromPomProperties(p pkg.Package) string {
func productsFromArtifactAndGroupIDs(artifactID string, groupIDs []string) []string {
products := strset.New()
if artifactID != "" {
products.Add(artifactID)
}
for _, groupID := range groupIDs {
isPlugin := strings.Contains(artifactID, "plugin") || strings.Contains(groupID, "plugin")
for i, field := range strings.Split(groupID, ".") {
field = strings.TrimSpace(field)
if len(field) == 0 {
continue
}
// don't add this field as a name if the name is implying the package is a plugin or client
if forbiddenProductGroupIDFields.Has(strings.ToLower(field)) {
continue
}
if i <= 1 {
continue
}
// umbrella projects tend to have sub components that either start or end with the project name. We want
// to identify fields that may represent the umbrella project, and not fields that indicate auxiliary
// information about the package.
couldBeProjectName := strings.HasPrefix(artifactID, field) || strings.HasSuffix(artifactID, field)
if artifactID == "" || (couldBeProjectName && !isPlugin) {
products.Add(field)
}
}
}
return products.List()
}
func artifactIDFromJavaPackage(p pkg.Package) string {
metadata, ok := p.Metadata.(pkg.JavaMetadata)
if !ok {
return ""
@@ -323,15 +348,156 @@ func groupIDFromPomProperties(p pkg.Package) string {
return ""
}
return metadata.PomProperties.GroupID
artifactID := strings.TrimSpace(metadata.PomProperties.ArtifactID)
if startsWithDomain(artifactID) && len(strings.Split(artifactID, ".")) > 1 {
// there is a strong indication that the artifact ID is really a group ID, don't use it
return ""
}
return artifactID
}
func shouldConsiderGroupID(groupID string) bool {
if groupID == "" {
return false
func groupIDsFromJavaPackage(p pkg.Package) (groupIDs []string) {
metadata, ok := p.Metadata.(pkg.JavaMetadata)
if !ok {
return nil
}
excludedGroupIDs := append([]string{pkg.JiraPluginPomPropertiesGroupID}, pkg.JenkinsPluginPomPropertiesGroupIDs...)
groupIDs = append(groupIDs, groupIDsFromPomProperties(metadata.PomProperties)...)
groupIDs = append(groupIDs, groupIDsFromJavaManifest(metadata.Manifest)...)
return !internal.HasAnyOfPrefixes(groupID, excludedGroupIDs...)
return groupIDs
}
func groupIDsFromPomProperties(properties *pkg.PomProperties) (groupIDs []string) {
if properties == nil {
return nil
}
if startsWithDomain(properties.GroupID) {
groupIDs = append(groupIDs, strings.TrimSpace(properties.GroupID))
}
// sometimes the publisher puts the group ID in the artifact ID field unintentionally
if startsWithDomain(properties.ArtifactID) && len(strings.Split(properties.ArtifactID, ".")) > 1 {
// there is a strong indication that the artifact ID is really a group ID
groupIDs = append(groupIDs, strings.TrimSpace(properties.ArtifactID))
}
return groupIDs
}
func groupIDsFromJavaManifest(manifest *pkg.JavaManifest) (groupIDs []string) {
if manifest == nil {
return nil
}
// attempt to get group-id-like info from the MANIFEST.MF "Automatic-Module-Name" and "Extension-Name" field.
// for more info see pkg:maven/commons-io/commons-io@2.8.0 within cloudbees/cloudbees-core-mm:2.263.4.2
// at /usr/share/jenkins/jenkins.war:WEB-INF/plugins/analysis-model-api.hpi:WEB-INF/lib/commons-io-2.8.0.jar
// as well as the ant package from cloudbees/cloudbees-core-mm:2.277.2.4-ra.
for name, value := range manifest.Main {
value = strings.TrimSpace(value)
switch name {
case "Extension-Name", "Automatic-Module-Name":
if startsWithDomain(value) {
groupIDs = append(groupIDs, value)
}
}
}
for _, section := range manifest.NamedSections {
for name, value := range section {
value = strings.TrimSpace(value)
switch name {
case "Extension-Name", "Automatic-Module-Name":
if startsWithDomain(value) {
groupIDs = append(groupIDs, value)
}
}
}
}
return groupIDs
}
func startsWithDomain(value string) bool {
return internal.HasAnyOfPrefixes(value, domains...)
}
func generateAllSubSelections(fields []string) (results []string) {
for _, field := range fields {
results = append(results, generateSubSelections(field)...)
}
return results
}
// generateSubSelections attempts to split a field by hyphens and underscores and return a list of sensible sub-selections
// that can be used as product or vendor candidates. E.g. jenkins-ci-tools -> [jenkins-ci-tools, jenkins-ci, jenkins].
func generateSubSelections(field string) (results []string) {
scanner := bufio.NewScanner(strings.NewReader(field))
scanner.Split(scanByHyphenOrUnderscore)
var lastToken uint8
for scanner.Scan() {
rawCandidate := scanner.Text()
if len(rawCandidate) == 0 {
break
}
// trim any number of hyphen or underscore that is prefixed/suffixed on the given candidate. Since
// scanByHyphenOrUnderscore preserves delimiters (hyphens and underscores) they are guaranteed to be at least
// prefixed.
candidate := strings.TrimFunc(rawCandidate, trimHyphenOrUnderscore)
// capture the result (if there is content)
if len(candidate) > 0 {
if len(results) > 0 {
results = append(results, results[len(results)-1]+string(lastToken)+candidate)
} else {
results = append(results, candidate)
}
}
// keep track of the trailing separator for the next loop
lastToken = rawCandidate[len(rawCandidate)-1]
}
return results
}
// trimHyphenOrUnderscore is a character filter function for use with strings.TrimFunc in order to remove any hyphen or underscores.
func trimHyphenOrUnderscore(r rune) bool {
switch r {
case '-', '_':
return true
}
return false
}
// scanByHyphenOrUnderscore splits on hyphen or underscore and includes the separator in the split
func scanByHyphenOrUnderscore(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
if i := bytes.IndexAny(data, "-_"); i >= 0 {
return i + 1, data[0 : i+1], nil
}
if atEOF {
return len(data), data, nil
}
return 0, nil, nil
}
func addSeparatorVariations(fields *strset.Set) {
for _, field := range fields.List() {
hasHyphen := strings.Contains(field, "-")
hasUnderscore := strings.Contains(field, "_")
if hasHyphen {
// provide variations of hyphen candidates with an underscore
fields.Add(strings.ReplaceAll(field, "-", "_"))
}
if hasUnderscore {
// provide variations of underscore candidates with a hyphen
fields.Add(strings.ReplaceAll(field, "_", "-"))
}
}
}
+48
View File
@@ -0,0 +1,48 @@
package cataloger
import (
"strings"
"github.com/anchore/syft/syft/pkg"
"github.com/facebookincubator/nvdtools/wfn"
)
const jenkinsName = "jenkins"
type filterFn func(cpe pkg.CPE, p pkg.Package) bool
var cpeFilters = []filterFn{
jiraClientPackageFilter,
jenkinsPackageNameFilter,
jenkinsPluginFilter,
}
// jenkins plugins should not match against jenkins
func jenkinsPluginFilter(cpe pkg.CPE, p pkg.Package) bool {
if p.Type == pkg.JenkinsPluginPkg && cpe.Product == jenkinsName {
return true
}
return false
}
// filter to account that packages that are not for jenkins but have a CPE generated that will match against jenkins
func jenkinsPackageNameFilter(cpe pkg.CPE, p pkg.Package) bool {
// jenkins server should only match against a product with the name jenkins
if cpe.Product == jenkinsName && !strings.Contains(strings.ToLower(p.Name), jenkinsName) {
if cpe.Vendor == wfn.Any || cpe.Vendor == jenkinsName || cpe.Vendor == "cloudbees" {
return true
}
}
return false
}
// filter to account for packages which are jira client packages but have a CPE that will match against jira
func jiraClientPackageFilter(cpe pkg.CPE, p pkg.Package) bool {
// jira / atlassian should not apply to clients
if cpe.Product == "jira" && strings.Contains(strings.ToLower(p.Name), "client") {
if cpe.Vendor == wfn.Any || cpe.Vendor == "jira" || cpe.Vendor == "atlassian" {
return true
}
}
return false
}
+167
View File
@@ -0,0 +1,167 @@
package cataloger
import (
"testing"
"github.com/anchore/syft/syft/pkg"
"github.com/stretchr/testify/assert"
)
func Test_jenkinsPluginFilter(t *testing.T) {
tests := []struct {
name string
cpe pkg.CPE
pkg pkg.Package
expected bool
}{
{
name: "go case (filter out)",
cpe: mustCPE("cpe:2.3:a:name:jenkins:3.2:*:*:*:*:*:*:*"),
pkg: pkg.Package{
Type: pkg.JenkinsPluginPkg,
},
expected: true,
},
{
name: "ignore jenkins plugins with unique name",
cpe: mustCPE("cpe:2.3:a:name:ci-jenkins:3.2:*:*:*:*:*:*:*"),
pkg: pkg.Package{
Type: pkg.JenkinsPluginPkg,
},
expected: false,
},
{
name: "ignore java packages",
cpe: mustCPE("cpe:2.3:a:name:jenkins:3.2:*:*:*:*:*:*:*"),
pkg: pkg.Package{
Type: pkg.JavaPkg,
},
expected: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, jenkinsPluginFilter(test.cpe, test.pkg))
})
}
}
func Test_jenkinsPackageNameFilter(t *testing.T) {
tests := []struct {
name string
cpe pkg.CPE
pkg pkg.Package
expected bool
}{
{
name: "filter out mismatched name (cloudbees vendor)",
cpe: mustCPE("cpe:2.3:a:cloudbees:jenkins:3.2:*:*:*:*:*:*:*"),
pkg: pkg.Package{
Name: "not-j*nkins",
Type: pkg.JavaPkg,
},
expected: true,
},
{
name: "filter out mismatched name (jenkins vendor)",
cpe: mustCPE("cpe:2.3:a:jenkins:jenkins:3.2:*:*:*:*:*:*:*"),
pkg: pkg.Package{
Name: "not-j*nkins",
Type: pkg.JavaPkg,
},
expected: true,
},
{
name: "filter out mismatched name (any vendor)",
cpe: mustCPE("cpe:2.3:a:*:jenkins:3.2:*:*:*:*:*:*:*"),
pkg: pkg.Package{
Name: "not-j*nkins",
Type: pkg.JavaPkg,
},
expected: true,
},
{
name: "ignore packages with the name jenkins",
cpe: mustCPE("cpe:2.3:a:*:jenkins:3.2:*:*:*:*:*:*:*"),
pkg: pkg.Package{
Name: "jenkins-thing",
Type: pkg.JavaPkg,
},
expected: false,
},
{
name: "ignore product names that are not exactly 'jenkins'",
cpe: mustCPE("cpe:2.3:a:*:jenkins-something-else:3.2:*:*:*:*:*:*:*"),
pkg: pkg.Package{
Name: "not-j*nkins",
Type: pkg.JavaPkg,
},
expected: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, jenkinsPackageNameFilter(test.cpe, test.pkg))
})
}
}
func Test_jiraClientPackageFilter(t *testing.T) {
tests := []struct {
name string
cpe pkg.CPE
pkg pkg.Package
expected bool
}{
{
name: "filter out mismatched name (atlassian vendor)",
cpe: mustCPE("cpe:2.3:a:atlassian:jira:3.2:*:*:*:*:*:*:*"),
pkg: pkg.Package{
Name: "something-client",
Type: pkg.JavaPkg,
},
expected: true,
},
{
name: "filter out mismatched name (jira vendor)",
cpe: mustCPE("cpe:2.3:a:jira:jira:3.2:*:*:*:*:*:*:*"),
pkg: pkg.Package{
Name: "something-client",
Type: pkg.JavaPkg,
},
expected: true,
},
{
name: "filter out mismatched name (any vendor)",
cpe: mustCPE("cpe:2.3:a:*:jira:3.2:*:*:*:*:*:*:*"),
pkg: pkg.Package{
Name: "something-client",
Type: pkg.JavaPkg,
},
expected: true,
},
{
name: "ignore package names that do not have 'client'",
cpe: mustCPE("cpe:2.3:a:*:jira:3.2:*:*:*:*:*:*:*"),
pkg: pkg.Package{
Name: "jira-thing",
Type: pkg.JavaPkg,
},
expected: false,
},
{
name: "ignore product names that are not exactly 'jira'",
cpe: mustCPE("cpe:2.3:a:*:jira-something-else:3.2:*:*:*:*:*:*:*"),
pkg: pkg.Package{
Name: "not-j*ra",
Type: pkg.JavaPkg,
},
expected: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, jiraClientPackageFilter(test.cpe, test.pkg))
})
}
}
+33 -15
View File
@@ -1,31 +1,49 @@
package cataloger
import "github.com/facebookincubator/nvdtools/wfn"
import (
"sort"
"github.com/facebookincubator/nvdtools/wfn"
)
var _ sort.Interface = (*ByCPESpecificity)(nil)
type ByCPESpecificity []wfn.Attributes
// Implementing sort.Interface
func (c ByCPESpecificity) Len() int { return len(c) }
func (c ByCPESpecificity) Len() int { return len(c) }
func (c ByCPESpecificity) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
func (c ByCPESpecificity) Less(i, j int) bool {
return countSpecifiedFields(c[i]) > countSpecifiedFields(c[j])
iScore := weightedCountForSpecifiedFields(c[i])
jScore := weightedCountForSpecifiedFields(c[j])
if iScore == jScore {
return countFieldLength(c[i]) > countFieldLength(c[j])
}
return iScore > jScore
}
func countSpecifiedFields(cpe wfn.Attributes) int {
checksForSpecifiedField := []func(cpe wfn.Attributes) bool{
func(cpe wfn.Attributes) bool { return cpe.Part != "" },
func(cpe wfn.Attributes) bool { return cpe.Product != "" },
func(cpe wfn.Attributes) bool { return cpe.Vendor != "" },
func(cpe wfn.Attributes) bool { return cpe.Version != "" },
func(cpe wfn.Attributes) bool { return cpe.TargetSW != "" },
func countFieldLength(cpe wfn.Attributes) int {
return len(cpe.Part + cpe.Vendor + cpe.Product + cpe.Version + cpe.TargetSW)
}
func weightedCountForSpecifiedFields(cpe wfn.Attributes) int {
checksForSpecifiedField := []func(cpe wfn.Attributes) (bool, int){
func(cpe wfn.Attributes) (bool, int) { return cpe.Part != "", 2 },
func(cpe wfn.Attributes) (bool, int) { return cpe.Vendor != "", 3 },
func(cpe wfn.Attributes) (bool, int) { return cpe.Product != "", 4 },
func(cpe wfn.Attributes) (bool, int) { return cpe.Version != "", 1 },
func(cpe wfn.Attributes) (bool, int) { return cpe.TargetSW != "", 1 },
}
count := 0
weightedCount := 0
for _, fieldIsSpecified := range checksForSpecifiedField {
if fieldIsSpecified(cpe) {
count++
isSpecified, weight := fieldIsSpecified(cpe)
if isSpecified {
weightedCount += weight
}
}
return count
return weightedCount
}
@@ -0,0 +1,92 @@
package cataloger
import (
"sort"
"testing"
"github.com/anchore/syft/syft/pkg"
"github.com/stretchr/testify/assert"
)
func mustCPE(c string) pkg.CPE {
return must(pkg.NewCPE(c))
}
func must(c pkg.CPE, e error) pkg.CPE {
if e != nil {
panic(e)
}
return c
}
func TestCPESpecificity(t *testing.T) {
tests := []struct {
name string
input []pkg.CPE
expected []pkg.CPE
}{
{
name: "sort strictly by wfn *",
input: []pkg.CPE{
mustCPE("cpe:2.3:a:*:package:1:*:*:*:*:*:*:*"),
mustCPE("cpe:2.3:a:some:package:1:*:*:*:*:*:*:*"),
mustCPE("cpe:2.3:a:*:package:1:*:*:*:*:some:*:*"),
mustCPE("cpe:2.3:a:some:package:1:*:*:*:*:some:*:*"),
mustCPE("cpe:2.3:a:some:package:*:*:*:*:*:*:*:*"),
},
expected: []pkg.CPE{
mustCPE("cpe:2.3:a:some:package:1:*:*:*:*:some:*:*"),
mustCPE("cpe:2.3:a:some:package:1:*:*:*:*:*:*:*"),
mustCPE("cpe:2.3:a:some:package:*:*:*:*:*:*:*:*"),
mustCPE("cpe:2.3:a:*:package:1:*:*:*:*:some:*:*"),
mustCPE("cpe:2.3:a:*:package:1:*:*:*:*:*:*:*"),
},
},
{
name: "sort strictly by field length",
input: []pkg.CPE{
mustCPE("cpe:2.3:a:1:22:1:*:*:*:*:1:*:*"),
mustCPE("cpe:2.3:a:55555:1:1:*:*:*:*:1:*:*"),
mustCPE("cpe:2.3:a:1:1:333:*:*:*:*:1:*:*"),
mustCPE("cpe:2.3:a:1:666666:1:*:*:*:*:1:*:*"),
mustCPE("cpe:2.3:a:1:1:1:*:*:*:*:1:*:*"),
mustCPE("cpe:2.3:a:1:1:1:*:*:*:*:4444:*:*"),
},
expected: []pkg.CPE{
mustCPE("cpe:2.3:a:1:666666:1:*:*:*:*:1:*:*"),
mustCPE("cpe:2.3:a:55555:1:1:*:*:*:*:1:*:*"),
mustCPE("cpe:2.3:a:1:1:1:*:*:*:*:4444:*:*"),
mustCPE("cpe:2.3:a:1:1:333:*:*:*:*:1:*:*"),
mustCPE("cpe:2.3:a:1:22:1:*:*:*:*:1:*:*"),
mustCPE("cpe:2.3:a:1:1:1:*:*:*:*:1:*:*"),
},
},
{
name: "sort by mix of field length and specificity",
input: []pkg.CPE{
mustCPE("cpe:2.3:a:1:666666:*:*:*:*:*:1:*:*"),
mustCPE("cpe:2.3:a:*:1:1:*:*:*:*:4444:*:*"),
mustCPE("cpe:2.3:a:1:*:333:*:*:*:*:*:*:*"),
mustCPE("cpe:2.3:a:1:1:1:*:*:*:*:1:*:*"),
mustCPE("cpe:2.3:a:1:22:1:*:*:*:*:1:*:*"),
mustCPE("cpe:2.3:a:55555:1:1:*:*:*:*:1:*:*"),
},
expected: []pkg.CPE{
mustCPE("cpe:2.3:a:55555:1:1:*:*:*:*:1:*:*"),
mustCPE("cpe:2.3:a:1:22:1:*:*:*:*:1:*:*"),
mustCPE("cpe:2.3:a:1:1:1:*:*:*:*:1:*:*"),
mustCPE("cpe:2.3:a:1:666666:*:*:*:*:*:1:*:*"),
mustCPE("cpe:2.3:a:*:1:1:*:*:*:*:4444:*:*"),
mustCPE("cpe:2.3:a:1:*:333:*:*:*:*:*:*:*"),
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
sort.Sort(ByCPESpecificity(test.input))
assert.Equal(t, test.expected, test.input)
})
}
}
+502 -71
View File
@@ -3,6 +3,7 @@ package cataloger
import (
"fmt"
"sort"
"strings"
"testing"
"github.com/anchore/syft/syft/pkg"
@@ -27,14 +28,10 @@ func TestGeneratePackageCPEs(t *testing.T) {
Type: pkg.DebPkg,
},
expected: []string{
"cpe:2.3:a:*:name-part:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:*:name-part:3.2:*:*:*:*:python:*:*",
"cpe:2.3:a:name-part:name-part:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:name-part:name-part:3.2:*:*:*:*:python:*:*",
"cpe:2.3:a:python-name-part:name-part:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:python-name-part:name-part:3.2:*:*:*:*:python:*:*",
"cpe:2.3:a:*:name_part:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:*:name_part:3.2:*:*:*:*:python:*:*",
"cpe:2.3:a:name_part:name_part:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:name_part:name_part:3.2:*:*:*:*:python:*:*",
"cpe:2.3:a:python_name_part:name_part:3.2:*:*:*:*:*:*:*",
@@ -47,10 +44,6 @@ func TestGeneratePackageCPEs(t *testing.T) {
"cpe:2.3:a:python-name-part:name_part:3.2:*:*:*:*:python:*:*",
"cpe:2.3:a:python_name_part:name-part:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:python_name_part:name-part:3.2:*:*:*:*:python:*:*",
"cpe:2.3:a:*:python-name-part:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:*:python-name-part:3.2:*:*:*:*:python:*:*",
"cpe:2.3:a:*:python_name_part:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:*:python_name_part:3.2:*:*:*:*:python:*:*",
"cpe:2.3:a:name-part:python-name-part:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:name-part:python-name-part:3.2:*:*:*:*:python:*:*",
"cpe:2.3:a:name-part:python_name_part:3.2:*:*:*:*:*:*:*",
@@ -67,6 +60,38 @@ func TestGeneratePackageCPEs(t *testing.T) {
"cpe:2.3:a:python_name_part:python-name-part:3.2:*:*:*:*:python:*:*",
"cpe:2.3:a:python_name_part:python_name_part:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:python_name_part:python_name_part:3.2:*:*:*:*:python:*:*",
"cpe:2.3:a:name:name-part:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:name:name-part:3.2:*:*:*:*:python:*:*",
"cpe:2.3:a:name:name_part:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:name:name_part:3.2:*:*:*:*:python:*:*",
"cpe:2.3:a:name:python-name-part:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:name:python-name-part:3.2:*:*:*:*:python:*:*",
"cpe:2.3:a:name:python_name_part:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:name:python_name_part:3.2:*:*:*:*:python:*:*",
"cpe:2.3:a:python-name:name-part:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:python-name:name-part:3.2:*:*:*:*:python:*:*",
"cpe:2.3:a:python-name:name_part:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:python-name:name_part:3.2:*:*:*:*:python:*:*",
"cpe:2.3:a:python-name:python-name-part:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:python-name:python-name-part:3.2:*:*:*:*:python:*:*",
"cpe:2.3:a:python-name:python_name_part:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:python-name:python_name_part:3.2:*:*:*:*:python:*:*",
"cpe:2.3:a:python:name-part:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:python:name-part:3.2:*:*:*:*:python:*:*",
"cpe:2.3:a:python:name_part:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:python:name_part:3.2:*:*:*:*:python:*:*",
"cpe:2.3:a:python:python-name-part:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:python:python-name-part:3.2:*:*:*:*:python:*:*",
"cpe:2.3:a:python:python_name_part:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:python:python_name_part:3.2:*:*:*:*:python:*:*",
"cpe:2.3:a:python_name:name-part:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:python_name:name-part:3.2:*:*:*:*:python:*:*",
"cpe:2.3:a:python_name:name_part:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:python_name:name_part:3.2:*:*:*:*:python:*:*",
"cpe:2.3:a:python_name:python-name-part:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:python_name:python-name-part:3.2:*:*:*:*:python:*:*",
"cpe:2.3:a:python_name:python_name_part:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:python_name:python_name_part:3.2:*:*:*:*:python:*:*",
},
},
{
@@ -79,18 +104,12 @@ func TestGeneratePackageCPEs(t *testing.T) {
Type: pkg.DebPkg,
},
expected: []string{
"cpe:2.3:a:*:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:python:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:python:*:*",
"cpe:2.3:a:python-name:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:python-name:name:3.2:*:*:*:*:python:*:*",
"cpe:2.3:a:python_name:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:python_name:name:3.2:*:*:*:*:python:*:*",
"cpe:2.3:a:*:python-name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:*:python-name:3.2:*:*:*:*:python:*:*",
"cpe:2.3:a:*:python_name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:*:python_name:3.2:*:*:*:*:python:*:*",
"cpe:2.3:a:name:python-name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:name:python-name:3.2:*:*:*:*:python:*:*",
"cpe:2.3:a:name:python_name:3.2:*:*:*:*:*:*:*",
@@ -103,6 +122,12 @@ func TestGeneratePackageCPEs(t *testing.T) {
"cpe:2.3:a:python_name:python-name:3.2:*:*:*:*:python:*:*",
"cpe:2.3:a:python_name:python_name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:python_name:python_name:3.2:*:*:*:*:python:*:*",
"cpe:2.3:a:python:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:python:name:3.2:*:*:*:*:python:*:*",
"cpe:2.3:a:python:python-name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:python:python-name:3.2:*:*:*:*:python:*:*",
"cpe:2.3:a:python:python_name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:python:python_name:3.2:*:*:*:*:python:*:*",
},
},
{
@@ -115,9 +140,6 @@ func TestGeneratePackageCPEs(t *testing.T) {
Type: pkg.DebPkg,
},
expected: []string{
"cpe:2.3:a:*:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:node.js:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:nodejs:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:node.js:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:nodejs:*:*",
@@ -133,12 +155,18 @@ func TestGeneratePackageCPEs(t *testing.T) {
Type: pkg.DebPkg,
},
expected: []string{
"cpe:2.3:a:*:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:ruby:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:rails:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:ruby:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:rails:*:*",
"cpe:2.3:a:ruby-lang:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:ruby-lang:name:3.2:*:*:*:*:rails:*:*",
"cpe:2.3:a:ruby-lang:name:3.2:*:*:*:*:ruby:*:*",
"cpe:2.3:a:ruby:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:ruby:name:3.2:*:*:*:*:rails:*:*",
"cpe:2.3:a:ruby:name:3.2:*:*:*:*:ruby:*:*",
"cpe:2.3:a:ruby_lang:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:ruby_lang:name:3.2:*:*:*:*:rails:*:*",
"cpe:2.3:a:ruby_lang:name:3.2:*:*:*:*:ruby:*:*",
},
},
{
@@ -151,9 +179,6 @@ func TestGeneratePackageCPEs(t *testing.T) {
Type: pkg.DebPkg,
},
expected: []string{
"cpe:2.3:a:*:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:maven:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:maven:*:*",
@@ -175,18 +200,12 @@ func TestGeneratePackageCPEs(t *testing.T) {
},
},
expected: []string{
"cpe:2.3:a:*:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:maven:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:maven:*:*",
"cpe:2.3:a:sonatype:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:sonatype:name:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:sonatype:name:3.2:*:*:*:*:maven:*:*",
"cpe:2.3:a:*:nexus:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:*:nexus:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:*:nexus:3.2:*:*:*:*:maven:*:*",
"cpe:2.3:a:sonatype:nexus:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:sonatype:nexus:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:sonatype:nexus:3.2:*:*:*:*:maven:*:*",
@@ -211,9 +230,6 @@ func TestGeneratePackageCPEs(t *testing.T) {
Type: pkg.JenkinsPluginPkg,
},
expected: []string{
"cpe:2.3:a:*:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:jenkins:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:cloudbees_jenkins:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:jenkins:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:cloudbees_jenkins:*:*",
@@ -234,12 +250,12 @@ func TestGeneratePackageCPEs(t *testing.T) {
},
},
expected: []string{
"cpe:2.3:a:*:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:jenkins:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:cloudbees_jenkins:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:jenkins:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:cloudbees_jenkins:*:*",
"cpe:2.3:a:jenkins:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:jenkins:name:3.2:*:*:*:*:cloudbees_jenkins:*:*",
"cpe:2.3:a:jenkins:name:3.2:*:*:*:*:jenkins:*:*",
},
},
{
@@ -257,12 +273,18 @@ func TestGeneratePackageCPEs(t *testing.T) {
},
},
expected: []string{
"cpe:2.3:a:*:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:jenkins:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:cloudbees_jenkins:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:jenkins:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:cloudbees_jenkins:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:jenkins:*:*",
"cpe:2.3:a:name:something:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:name:something:3.2:*:*:*:*:cloudbees_jenkins:*:*",
"cpe:2.3:a:name:something:3.2:*:*:*:*:jenkins:*:*",
"cpe:2.3:a:something:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:something:name:3.2:*:*:*:*:cloudbees_jenkins:*:*",
"cpe:2.3:a:something:name:3.2:*:*:*:*:jenkins:*:*",
"cpe:2.3:a:something:something:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:something:something:3.2:*:*:*:*:cloudbees_jenkins:*:*",
"cpe:2.3:a:something:something:3.2:*:*:*:*:jenkins:*:*",
},
},
{
@@ -280,9 +302,6 @@ func TestGeneratePackageCPEs(t *testing.T) {
},
},
expected: []string{
"cpe:2.3:a:*:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:jenkins:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:cloudbees_jenkins:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:jenkins:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:cloudbees_jenkins:*:*",
@@ -303,9 +322,6 @@ func TestGeneratePackageCPEs(t *testing.T) {
},
},
expected: []string{
"cpe:2.3:a:*:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:jenkins:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:cloudbees_jenkins:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:jenkins:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:cloudbees_jenkins:*:*",
@@ -326,9 +342,6 @@ func TestGeneratePackageCPEs(t *testing.T) {
},
},
expected: []string{
"cpe:2.3:a:*:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:jenkins:*:*",
"cpe:2.3:a:*:name:3.2:*:*:*:*:cloudbees_jenkins:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:jenkins:*:*",
"cpe:2.3:a:name:name:3.2:*:*:*:*:cloudbees_jenkins:*:*",
@@ -351,9 +364,6 @@ func TestGeneratePackageCPEs(t *testing.T) {
},
},
expected: []string{
"cpe:2.3:a:*:jira_client_core:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:*:jira_client_core:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:*:jira_client_core:3.2:*:*:*:*:maven:*:*",
"cpe:2.3:a:atlassian:jira_client_core:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:atlassian:jira_client_core:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:atlassian:jira_client_core:3.2:*:*:*:*:maven:*:*",
@@ -366,6 +376,42 @@ func TestGeneratePackageCPEs(t *testing.T) {
"cpe:2.3:a:jira_client_core:jira_client_core:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:jira_client_core:jira_client_core:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:jira_client_core:jira_client_core:3.2:*:*:*:*:maven:*:*",
"cpe:2.3:a:atlassian:jira-client-core:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:atlassian:jira-client-core:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:atlassian:jira-client-core:3.2:*:*:*:*:maven:*:*",
"cpe:2.3:a:jira-client-core:jira-client-core:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:jira-client-core:jira-client-core:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:jira-client-core:jira-client-core:3.2:*:*:*:*:maven:*:*",
"cpe:2.3:a:jira-client-core:jira:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:jira-client-core:jira:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:jira-client-core:jira:3.2:*:*:*:*:maven:*:*",
"cpe:2.3:a:jira-client-core:jira_client_core:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:jira-client-core:jira_client_core:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:jira-client-core:jira_client_core:3.2:*:*:*:*:maven:*:*",
"cpe:2.3:a:jira-client:jira-client-core:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:jira-client:jira-client-core:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:jira-client:jira-client-core:3.2:*:*:*:*:maven:*:*",
"cpe:2.3:a:jira-client:jira:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:jira-client:jira:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:jira-client:jira:3.2:*:*:*:*:maven:*:*",
"cpe:2.3:a:jira-client:jira_client_core:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:jira-client:jira_client_core:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:jira-client:jira_client_core:3.2:*:*:*:*:maven:*:*",
"cpe:2.3:a:jira:jira-client-core:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:jira:jira-client-core:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:jira:jira-client-core:3.2:*:*:*:*:maven:*:*",
"cpe:2.3:a:jira_client:jira-client-core:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:jira_client:jira-client-core:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:jira_client:jira-client-core:3.2:*:*:*:*:maven:*:*",
"cpe:2.3:a:jira_client:jira:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:jira_client:jira:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:jira_client:jira:3.2:*:*:*:*:maven:*:*",
"cpe:2.3:a:jira_client:jira_client_core:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:jira_client:jira_client_core:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:jira_client:jira_client_core:3.2:*:*:*:*:maven:*:*",
"cpe:2.3:a:jira_client_core:jira-client-core:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:jira_client_core:jira-client-core:3.2:*:*:*:*:java:*:*",
"cpe:2.3:a:jira_client_core:jira-client-core:3.2:*:*:*:*:maven:*:*",
},
},
{
@@ -385,21 +431,12 @@ func TestGeneratePackageCPEs(t *testing.T) {
},
},
expected: []string{
"cpe:2.3:a:*:cloudbees-installation-manager:2.89.0.33:*:*:*:*:*:*:*",
"cpe:2.3:a:*:cloudbees-installation-manager:2.89.0.33:*:*:*:*:java:*:*",
"cpe:2.3:a:*:cloudbees-installation-manager:2.89.0.33:*:*:*:*:maven:*:*",
"cpe:2.3:a:*:cloudbees_installation_manager:2.89.0.33:*:*:*:*:*:*:*",
"cpe:2.3:a:*:cloudbees_installation_manager:2.89.0.33:*:*:*:*:java:*:*",
"cpe:2.3:a:*:cloudbees_installation_manager:2.89.0.33:*:*:*:*:maven:*:*",
"cpe:2.3:a:cloudbees-installation-manager:cloudbees-installation-manager:2.89.0.33:*:*:*:*:*:*:*",
"cpe:2.3:a:cloudbees-installation-manager:cloudbees-installation-manager:2.89.0.33:*:*:*:*:java:*:*",
"cpe:2.3:a:cloudbees-installation-manager:cloudbees-installation-manager:2.89.0.33:*:*:*:*:maven:*:*",
"cpe:2.3:a:cloudbees-installation-manager:cloudbees_installation_manager:2.89.0.33:*:*:*:*:*:*:*",
"cpe:2.3:a:cloudbees-installation-manager:cloudbees_installation_manager:2.89.0.33:*:*:*:*:java:*:*",
"cpe:2.3:a:cloudbees-installation-manager:cloudbees_installation_manager:2.89.0.33:*:*:*:*:maven:*:*",
"cpe:2.3:a:cloudbees-installation-manager:jenkins:2.89.0.33:*:*:*:*:*:*:*",
"cpe:2.3:a:cloudbees-installation-manager:jenkins:2.89.0.33:*:*:*:*:java:*:*",
"cpe:2.3:a:cloudbees-installation-manager:jenkins:2.89.0.33:*:*:*:*:maven:*:*",
"cpe:2.3:a:cloudbees:cloudbees-installation-manager:2.89.0.33:*:*:*:*:*:*:*",
"cpe:2.3:a:cloudbees:cloudbees-installation-manager:2.89.0.33:*:*:*:*:java:*:*",
"cpe:2.3:a:cloudbees:cloudbees-installation-manager:2.89.0.33:*:*:*:*:maven:*:*",
@@ -412,15 +449,30 @@ func TestGeneratePackageCPEs(t *testing.T) {
"cpe:2.3:a:cloudbees_installation_manager:cloudbees_installation_manager:2.89.0.33:*:*:*:*:*:*:*",
"cpe:2.3:a:cloudbees_installation_manager:cloudbees_installation_manager:2.89.0.33:*:*:*:*:java:*:*",
"cpe:2.3:a:cloudbees_installation_manager:cloudbees_installation_manager:2.89.0.33:*:*:*:*:maven:*:*",
"cpe:2.3:a:cloudbees_installation_manager:jenkins:2.89.0.33:*:*:*:*:*:*:*",
"cpe:2.3:a:cloudbees_installation_manager:jenkins:2.89.0.33:*:*:*:*:java:*:*",
"cpe:2.3:a:cloudbees_installation_manager:jenkins:2.89.0.33:*:*:*:*:maven:*:*",
"cpe:2.3:a:jenkins:cloudbees-installation-manager:2.89.0.33:*:*:*:*:*:*:*",
"cpe:2.3:a:jenkins:cloudbees-installation-manager:2.89.0.33:*:*:*:*:java:*:*",
"cpe:2.3:a:jenkins:cloudbees-installation-manager:2.89.0.33:*:*:*:*:maven:*:*",
"cpe:2.3:a:jenkins:cloudbees_installation_manager:2.89.0.33:*:*:*:*:*:*:*",
"cpe:2.3:a:jenkins:cloudbees_installation_manager:2.89.0.33:*:*:*:*:java:*:*",
"cpe:2.3:a:jenkins:cloudbees_installation_manager:2.89.0.33:*:*:*:*:maven:*:*",
"cpe:2.3:a:cloudbees-installation:cloudbees-installation-manager:2.89.0.33:*:*:*:*:*:*:*",
"cpe:2.3:a:cloudbees-installation:cloudbees-installation-manager:2.89.0.33:*:*:*:*:java:*:*",
"cpe:2.3:a:cloudbees-installation:cloudbees-installation-manager:2.89.0.33:*:*:*:*:maven:*:*",
"cpe:2.3:a:cloudbees-installation:cloudbees_installation_manager:2.89.0.33:*:*:*:*:*:*:*",
"cpe:2.3:a:cloudbees-installation:cloudbees_installation_manager:2.89.0.33:*:*:*:*:java:*:*",
"cpe:2.3:a:cloudbees-installation:cloudbees_installation_manager:2.89.0.33:*:*:*:*:maven:*:*",
"cpe:2.3:a:cloudbees_installation:cloudbees-installation-manager:2.89.0.33:*:*:*:*:*:*:*",
"cpe:2.3:a:cloudbees_installation:cloudbees-installation-manager:2.89.0.33:*:*:*:*:java:*:*",
"cpe:2.3:a:cloudbees_installation:cloudbees-installation-manager:2.89.0.33:*:*:*:*:maven:*:*",
"cpe:2.3:a:cloudbees_installation:cloudbees_installation_manager:2.89.0.33:*:*:*:*:*:*:*",
"cpe:2.3:a:cloudbees_installation:cloudbees_installation_manager:2.89.0.33:*:*:*:*:java:*:*",
"cpe:2.3:a:cloudbees_installation:cloudbees_installation_manager:2.89.0.33:*:*:*:*:maven:*:*",
"cpe:2.3:a:modules:cloudbees-installation-manager:2.89.0.33:*:*:*:*:*:*:*",
"cpe:2.3:a:modules:cloudbees-installation-manager:2.89.0.33:*:*:*:*:java:*:*",
"cpe:2.3:a:modules:cloudbees-installation-manager:2.89.0.33:*:*:*:*:maven:*:*",
"cpe:2.3:a:modules:cloudbees_installation_manager:2.89.0.33:*:*:*:*:*:*:*",
"cpe:2.3:a:modules:cloudbees_installation_manager:2.89.0.33:*:*:*:*:java:*:*",
"cpe:2.3:a:modules:cloudbees_installation_manager:2.89.0.33:*:*:*:*:maven:*:*",
},
},
{
@@ -433,9 +485,6 @@ func TestGeneratePackageCPEs(t *testing.T) {
Type: pkg.GoModulePkg,
},
expected: []string{
"cpe:2.3:a:*:something:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:*:something:3.2:*:*:*:*:go:*:*",
"cpe:2.3:a:*:something:3.2:*:*:*:*:golang:*:*",
"cpe:2.3:a:someone:something:3.2:*:*:*:*:*:*:*",
"cpe:2.3:a:someone:something:3.2:*:*:*:*:go:*:*",
"cpe:2.3:a:someone:something:3.2:*:*:*:*:golang:*:*",
@@ -464,16 +513,22 @@ func TestGeneratePackageCPEs(t *testing.T) {
actualCpeSet.Add(a.BindToFmtString())
}
extra := strset.Difference(actualCpeSet, expectedCpeSet).List()
extra := strset.Difference(expectedCpeSet, actualCpeSet).List()
sort.Strings(extra)
if len(extra) > 0 {
t.Errorf("found extra CPEs:")
}
for _, d := range extra {
t.Errorf("extra CPE: %+v", d)
fmt.Printf(" %q,\n", d)
}
missing := strset.Difference(expectedCpeSet, actualCpeSet).List()
missing := strset.Difference(actualCpeSet, expectedCpeSet).List()
sort.Strings(missing)
if len(missing) > 0 {
t.Errorf("missing CPEs:")
}
for _, d := range missing {
t.Errorf("missing CPE: %+v", d)
fmt.Printf(" %q,\n", d)
}
})
}
@@ -515,7 +570,7 @@ func TestCandidateProducts(t *testing.T) {
},
},
},
expected: []string{"some-jenkins-plugin", "some_jenkins_plugin"},
expected: []string{"some-jenkins-plugin", "some_jenkins_plugin", "jenkins"},
},
{
p: pkg.Package{
@@ -698,3 +753,379 @@ func TestCandidateVendorForGo(t *testing.T) {
})
}
}
func Test_generateSubSelections(t *testing.T) {
tests := []struct {
field string
expected []string
}{
{
field: "jenkins",
expected: []string{"jenkins"},
},
{
field: "jenkins-ci",
expected: []string{"jenkins", "jenkins-ci"},
},
{
field: "jenkins--ci",
expected: []string{"jenkins", "jenkins-ci"},
},
{
field: "jenkins_ci_tools",
expected: []string{"jenkins", "jenkins_ci", "jenkins_ci_tools"},
},
{
field: "-jenkins",
expected: []string{"jenkins"},
},
{
field: "jenkins_",
expected: []string{"jenkins"},
},
{
field: "",
expected: nil,
},
{
field: "-",
expected: nil,
},
{
field: "_",
expected: nil,
},
}
for _, test := range tests {
t.Run(test.field, func(t *testing.T) {
assert.ElementsMatch(t, test.expected, generateSubSelections(test.field))
})
}
}
func Test_addSeparatorVariations(t *testing.T) {
tests := []struct {
input []string
expected []string
}{
{
input: []string{"jenkins-ci"},
expected: []string{"jenkins-ci", "jenkins_ci"}, //, "jenkinsci"},
},
{
input: []string{"jenkins_ci"},
expected: []string{"jenkins_ci", "jenkins-ci"}, //, "jenkinsci"},
},
{
input: []string{"jenkins"},
expected: []string{"jenkins"},
},
{
input: []string{"jenkins-ci", "circle-ci"},
expected: []string{"jenkins-ci", "jenkins_ci", "circle-ci", "circle_ci"}, //, "jenkinsci", "circleci"},
},
}
for _, test := range tests {
t.Run(strings.Join(test.input, ","), func(t *testing.T) {
val := strset.New(test.input...)
addSeparatorVariations(val)
assert.ElementsMatch(t, test.expected, val.List())
})
}
}
func Test_productsFromArtifactAndGroupIDs(t *testing.T) {
tests := []struct {
groupIDs []string
artifactID string
expected []string
}{
{
groupIDs: []string{"org.sonatype.nexus"},
artifactID: "nexus-extender",
expected: []string{"nexus", "nexus-extender"},
},
{
groupIDs: []string{"org.sonatype.nexus"},
expected: []string{"nexus"},
},
{
groupIDs: []string{"org.jenkins-ci.plugins"},
artifactID: "ant",
expected: []string{"ant"},
},
{
groupIDs: []string{"org.jenkins-ci.plugins"},
artifactID: "antisamy-markup-formatter",
expected: []string{"antisamy-markup-formatter"},
},
{
groupIDs: []string{"io.jenkins.plugins"},
artifactID: "aws-global-configuration",
expected: []string{"aws-global-configuration"},
},
{
groupIDs: []string{"com.cloudbees.jenkins.plugins"},
artifactID: "cloudbees-servicenow-jenkins-plugin",
expected: []string{"cloudbees-servicenow-jenkins-plugin"},
},
{
groupIDs: []string{"com.atlassian.confluence.plugins"},
artifactID: "confluence-mobile-plugin",
expected: []string{"confluence-mobile-plugin"},
},
{
groupIDs: []string{"com.atlassian.confluence.plugins"},
artifactID: "confluence-view-file-macro",
expected: []string{"confluence-view-file-macro"},
},
{
groupIDs: []string{"com.google.guava"},
artifactID: "failureaccess",
expected: []string{"failureaccess"},
},
}
for _, test := range tests {
t.Run(strings.Join(test.groupIDs, ",")+":"+test.artifactID, func(t *testing.T) {
actual := productsFromArtifactAndGroupIDs(test.artifactID, test.groupIDs)
assert.ElementsMatch(t, test.expected, actual, "different products")
})
}
}
func Test_candidateProductsForJava(t *testing.T) {
tests := []struct {
name string
pkg pkg.Package
expected []string
}{
{
name: "duplicate groupID in artifactID field",
pkg: pkg.Package{
Metadata: pkg.JavaMetadata{
PomProperties: &pkg.PomProperties{
GroupID: "org.sonatype.nexus",
ArtifactID: "org.sonatype.nexus",
},
},
},
expected: []string{"nexus"},
},
{
name: "detect groupID-like value in artifactID field",
pkg: pkg.Package{
Metadata: pkg.JavaMetadata{
PomProperties: &pkg.PomProperties{
ArtifactID: "org.sonatype.nexus",
},
},
},
expected: []string{"nexus"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actual := candidateProductsForJava(test.pkg)
assert.ElementsMatch(t, test.expected, actual, "different products")
})
}
}
func Test_vendorsFromGroupIDs(t *testing.T) {
tests := []struct {
groupIDs []string
expected []string
}{
{
groupIDs: []string{"org.sonatype.nexus"},
expected: []string{"sonatype", "nexus"},
},
{
groupIDs: []string{"org.sonatype.nexus"},
expected: []string{"sonatype", "nexus"},
},
{
groupIDs: []string{"org.sonatype.nexus"},
expected: []string{"sonatype", "nexus"},
},
{
groupIDs: []string{"org.jenkins-ci.plugins"},
expected: []string{"jenkins-ci", "jenkins"},
},
{
groupIDs: []string{"org.jenkins-ci.plugins"},
expected: []string{"jenkins-ci", "jenkins"},
},
{
groupIDs: []string{"io.jenkins.plugins"},
expected: []string{"jenkins"},
},
{
groupIDs: []string{"com.cloudbees.jenkins.plugins"},
expected: []string{"cloudbees", "jenkins"},
},
{
groupIDs: []string{"com.atlassian.confluence.plugins"},
expected: []string{"atlassian", "confluence"},
},
{
groupIDs: []string{"com.atlassian.confluence.plugins"},
expected: []string{"atlassian", "confluence"},
},
{
groupIDs: []string{"com.google.guava"},
expected: []string{"google", "guava"},
},
}
for _, test := range tests {
t.Run(strings.Join(test.groupIDs, ","), func(t *testing.T) {
actual := vendorsFromGroupIDs(test.groupIDs)
assert.ElementsMatch(t, test.expected, actual, "different vendors")
})
}
}
func Test_groupIDsFromJavaPackage(t *testing.T) {
tests := []struct {
name string
pkg pkg.Package
expects []string
}{
{
name: "go case",
pkg: pkg.Package{
Metadata: pkg.JavaMetadata{
PomProperties: &pkg.PomProperties{
GroupID: "io.jenkins-ci.plugin.thing",
},
},
},
expects: []string{"io.jenkins-ci.plugin.thing"},
},
{
name: "from artifactID",
pkg: pkg.Package{
Metadata: pkg.JavaMetadata{
PomProperties: &pkg.PomProperties{
ArtifactID: "io.jenkins-ci.plugin.thing",
},
},
},
expects: []string{"io.jenkins-ci.plugin.thing"},
},
{
name: "from main Extension-Name field",
pkg: pkg.Package{
Metadata: pkg.JavaMetadata{
Manifest: &pkg.JavaManifest{
Main: map[string]string{
"Extension-Name": "io.jenkins-ci.plugin.thing",
},
},
},
},
expects: []string{"io.jenkins-ci.plugin.thing"},
},
{
name: "from named section Extension-Name field",
pkg: pkg.Package{
Metadata: pkg.JavaMetadata{
Manifest: &pkg.JavaManifest{
NamedSections: map[string]map[string]string{
"section": {
"Extension-Name": "io.jenkins-ci.plugin.thing",
},
},
},
},
},
expects: []string{"io.jenkins-ci.plugin.thing"},
},
{
name: "from main Automatic-Module-Name field",
pkg: pkg.Package{
Metadata: pkg.JavaMetadata{
Manifest: &pkg.JavaManifest{
Main: map[string]string{
"Automatic-Module-Name": "io.jenkins-ci.plugin.thing",
},
},
},
},
expects: []string{"io.jenkins-ci.plugin.thing"},
},
{
name: "from named section Automatic-Module-Name field",
pkg: pkg.Package{
Metadata: pkg.JavaMetadata{
Manifest: &pkg.JavaManifest{
NamedSections: map[string]map[string]string{
"section": {
"Automatic-Module-Name": "io.jenkins-ci.plugin.thing",
},
},
},
},
},
expects: []string{"io.jenkins-ci.plugin.thing"},
},
{
name: "no manifest or pom info",
pkg: pkg.Package{
Metadata: pkg.JavaMetadata{},
},
expects: nil,
},
{
name: "no java info",
pkg: pkg.Package{},
expects: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expects, groupIDsFromJavaPackage(test.pkg))
})
}
}
func Test_artifactIDFromJavaPackage(t *testing.T) {
tests := []struct {
name string
pkg pkg.Package
expects string
}{
{
name: "go case",
pkg: pkg.Package{
Metadata: pkg.JavaMetadata{
PomProperties: &pkg.PomProperties{
ArtifactID: "cloudbees-installation-manager",
},
},
},
expects: "cloudbees-installation-manager",
},
{
name: "ignore groupID-like things",
pkg: pkg.Package{
Metadata: pkg.JavaMetadata{
PomProperties: &pkg.PomProperties{
ArtifactID: "io.jenkins-ci.plugin.thing",
},
},
},
expects: "",
},
{
name: "no java info",
pkg: pkg.Package{},
expects: "",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expects, artifactIDFromJavaPackage(test.pkg))
})
}
}
-2
View File
@@ -7,8 +7,6 @@ import (
"github.com/package-url/packageurl-go"
)
const JiraPluginPomPropertiesGroupID = "com.atlassian.jira.plugins"
var JenkinsPluginPomPropertiesGroupIDs = []string{
"io.jenkins.plugins",
"org.jenkins.plugins",
+238 -55
View File
@@ -1,41 +1,194 @@
package source
import (
"errors"
"fmt"
"io"
"os"
"path"
"path/filepath"
"strings"
"github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/stereoscope/pkg/filetree"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/log"
"github.com/bmatcuk/doublestar/v2"
"github.com/anchore/syft/syft/event"
"github.com/wagoodman/go-partybus"
"github.com/wagoodman/go-progress"
)
var unixSystemRuntimePrefixes = []string{
"/proc",
"/sys",
"/dev",
}
var _ FileResolver = (*directoryResolver)(nil)
type pathFilterFn func(string) bool
// directoryResolver implements path and content access for the directory data source.
type directoryResolver struct {
path string
path string
cwd string
fileTree *filetree.FileTree
infos map[file.ID]os.FileInfo
// TODO: wire up to report these paths in the json report
pathFilterFns []pathFilterFn
errPaths map[string]error
}
func newDirectoryResolver(path string) *directoryResolver {
return &directoryResolver{path: path}
}
func (r directoryResolver) requestPath(userPath string) string {
fullPath := userPath
if filepath.IsAbs(fullPath) {
// a path relative to root should be prefixed with the resolvers directory path, otherwise it should be left as is
fullPath = path.Join(r.path, fullPath)
func newDirectoryResolver(root string, pathFilters ...pathFilterFn) (*directoryResolver, error) {
cwd, err := os.Getwd()
if err != nil {
return nil, fmt.Errorf("could not create directory resolver: %w", err)
}
return fullPath
if pathFilters == nil {
pathFilters = []pathFilterFn{isUnixSystemRuntimePath}
}
resolver := directoryResolver{
path: root,
cwd: cwd,
fileTree: filetree.NewFileTree(),
infos: make(map[file.ID]os.FileInfo),
pathFilterFns: pathFilters,
errPaths: make(map[string]error),
}
return &resolver, indexAllRoots(root, resolver.indexTree)
}
func (r *directoryResolver) indexTree(root string, stager *progress.Stage) ([]string, error) {
log.Infof("indexing filesystem path=%q", root)
var err error
root, err = filepath.Abs(root)
if err != nil {
return nil, err
}
var roots []string
return roots, filepath.Walk(root,
func(path string, info os.FileInfo, err error) error {
stager.Current = path
// ignore any path which a filter function returns true
for _, filterFn := range r.pathFilterFns {
if filterFn(path) {
return nil
}
}
if err = r.handleFileAccessErr(path, err); err != nil {
return err
}
// link cycles could cause a revisit --we should not allow this
if r.fileTree.HasPath(file.Path(path)) {
return nil
}
if info == nil {
// walk may not be able to provide a FileInfo object, don't allow for this to stop indexing; keep track of the paths and continue.
r.errPaths[path] = fmt.Errorf("no file info observable at path=%q", path)
return nil
}
newRoot, err := r.addPathToIndex(path, info)
if err = r.handleFileAccessErr(path, err); err != nil {
return fmt.Errorf("unable to index path: %w", err)
}
if newRoot != "" {
roots = append(roots, newRoot)
}
return nil
})
}
func (r *directoryResolver) handleFileAccessErr(path string, err error) error {
if errors.Is(err, os.ErrPermission) || errors.Is(err, os.ErrNotExist) {
// don't allow for permission errors to stop indexing, keep track of the paths and continue.
log.Warnf("unable to access path=%q: %+v", path, err)
r.errPaths[path] = err
return nil
} else if err != nil {
return fmt.Errorf("unable to access path=%q: %w", path, err)
}
return nil
}
func (r directoryResolver) addPathToIndex(p string, info os.FileInfo) (string, error) {
var ref *file.Reference
var err error
var newRoot string
switch newFileTypeFromMode(info.Mode()) {
case SymbolicLink:
linkTarget, err := os.Readlink(p)
if err != nil {
return "", fmt.Errorf("unable to readlink for path=%q: %w", p, err)
}
ref, err = r.fileTree.AddSymLink(file.Path(p), file.Path(linkTarget))
if err != nil {
return "", err
}
targetAbsPath := linkTarget
if !filepath.IsAbs(targetAbsPath) {
targetAbsPath = filepath.Clean(filepath.Join(path.Dir(p), linkTarget))
}
newRoot = targetAbsPath
case Directory:
ref, err = r.fileTree.AddDir(file.Path(p))
if err != nil {
return "", err
}
default:
ref, err = r.fileTree.AddFile(file.Path(p))
if err != nil {
return "", err
}
}
r.infos[ref.ID()] = info
return newRoot, nil
}
func (r directoryResolver) requestPath(userPath string) (string, error) {
if filepath.IsAbs(userPath) {
// don't allow input to potentially hop above root path
userPath = path.Join(r.path, userPath)
}
var err error
userPath, err = filepath.Abs(userPath)
if err != nil {
return "", err
}
return userPath, nil
}
func (r directoryResolver) responsePath(path string) string {
// always return references relative to the request path (not absolute path)
if filepath.IsAbs(path) {
return strings.TrimPrefix(path, r.cwd+string(filepath.Separator))
}
return path
}
// HasPath indicates if the given path exists in the underlying source.
func (r *directoryResolver) HasPath(userPath string) bool {
_, err := os.Stat(r.requestPath(userPath))
return !os.IsNotExist(err)
requestPath, err := r.requestPath(userPath)
if err != nil {
return false
}
return r.fileTree.HasPath(file.Path(requestPath))
}
// Stringer to represent a directory path data source
@@ -48,12 +201,16 @@ func (r directoryResolver) FilesByPath(userPaths ...string) ([]Location, error)
var references = make([]Location, 0)
for _, userPath := range userPaths {
userStrPath := r.requestPath(userPath)
userStrPath, err := r.requestPath(userPath)
if err != nil {
log.Warnf("unable to get file by path=%q : %+v", userPath, err)
continue
}
fileMeta, err := os.Stat(userStrPath)
if os.IsNotExist(err) {
continue
} else if err != nil {
log.Errorf("path (%r) is not valid: %v", userStrPath, err)
log.Warnf("path (%r) is not valid: %+v", userStrPath, err)
}
// don't consider directories
@@ -61,7 +218,7 @@ func (r directoryResolver) FilesByPath(userPaths ...string) ([]Location, error)
continue
}
references = append(references, NewLocation(userStrPath))
references = append(references, NewLocation(r.responsePath(userStrPath)))
}
return references, nil
@@ -72,23 +229,12 @@ func (r directoryResolver) FilesByGlob(patterns ...string) ([]Location, error) {
result := make([]Location, 0)
for _, pattern := range patterns {
pathPattern := path.Join(r.path, pattern)
pathMatches, err := doublestar.Glob(pathPattern)
globResults, err := r.fileTree.FilesByGlob(pattern)
if err != nil {
return nil, err
}
for _, matchedPath := range pathMatches {
fileMeta, err := os.Stat(matchedPath)
if err != nil {
continue
}
// don't consider directories
if fileMeta.IsDir() {
continue
}
result = append(result, NewLocation(matchedPath))
for _, globResult := range globResults {
result = append(result, NewLocation(r.responsePath(string(globResult.MatchPath))))
}
}
@@ -120,41 +266,78 @@ func (r *directoryResolver) AllLocations() <-chan Location {
results := make(chan Location)
go func() {
defer close(results)
err := filepath.Walk(r.path,
func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
results <- NewLocation(path)
return nil
})
if err != nil {
log.Errorf("unable to walk path=%q : %+v", r.path, err)
for _, ref := range r.fileTree.AllFiles() {
results <- NewLocation(r.responsePath(string(ref.RealPath)))
}
}()
return results
}
func (r *directoryResolver) FileMetadataByLocation(location Location) (FileMetadata, error) {
fi, err := os.Stat(location.RealPath)
if err != nil {
return FileMetadata{}, err
}
// best effort
ty := UnknownFileType
switch {
case fi.Mode().IsDir():
ty = Directory
case fi.Mode().IsRegular():
ty = RegularFile
info, exists := r.infos[location.ref.ID()]
if !exists {
return FileMetadata{}, fmt.Errorf("location: %+v : %w", location, os.ErrExist)
}
return FileMetadata{
Mode: fi.Mode(),
Type: ty,
Mode: info.Mode(),
Type: newFileTypeFromMode(info.Mode()),
// unsupported across platforms
UserID: -1,
GroupID: -1,
}, nil
}
func isUnixSystemRuntimePath(path string) bool {
return internal.HasAnyOfPrefixes(path, unixSystemRuntimePrefixes...)
}
func indexAllRoots(root string, indexer func(string, *progress.Stage) ([]string, error)) error {
// why account for multiple roots? To cover cases when there is a symlink that references above the root path,
// in which case we need to additionally index where the link resolves to. it's for this reason why the filetree
// must be relative to the root of the filesystem (and not just relative to the given path).
pathsToIndex := []string{root}
stager, prog := indexingProgress(root)
defer prog.SetCompleted()
loop:
for {
var currentPath string
switch len(pathsToIndex) {
case 0:
break loop
case 1:
currentPath, pathsToIndex = pathsToIndex[0], nil
default:
currentPath, pathsToIndex = pathsToIndex[0], pathsToIndex[1:]
}
additionalRoots, err := indexer(currentPath, stager)
if err != nil {
return fmt.Errorf("unable to index filesystem path=%q: %w", currentPath, err)
}
pathsToIndex = append(pathsToIndex, additionalRoots...)
}
return nil
}
func indexingProgress(path string) (*progress.Stage, *progress.Manual) {
stage := &progress.Stage{}
prog := &progress.Manual{
Total: -1,
}
bus.Publish(partybus.Event{
Type: event.FileIndexingStarted,
Source: path,
Value: struct {
progress.Stager
progress.Progressable
}{
Stager: progress.Stager(stage),
Progressable: prog,
},
})
return stage, prog
}
+289 -43
View File
@@ -1,7 +1,15 @@
package source
import (
"os"
"path"
"reflect"
"strings"
"testing"
"github.com/anchore/stereoscope/pkg/file"
"github.com/stretchr/testify/assert"
"github.com/wagoodman/go-progress"
)
func TestDirectoryResolver_FilesByPath(t *testing.T) {
@@ -57,7 +65,8 @@ func TestDirectoryResolver_FilesByPath(t *testing.T) {
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
resolver := directoryResolver{c.root}
resolver, err := newDirectoryResolver(c.root)
assert.NoError(t, err)
hasPath := resolver.HasPath(c.input)
if !c.forcePositiveHasPath {
@@ -112,12 +121,10 @@ func TestDirectoryResolver_MultipleFilesByPath(t *testing.T) {
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
resolver := directoryResolver{"test-fixtures"}
resolver, err := newDirectoryResolver("./test-fixtures")
assert.NoError(t, err)
refs, err := resolver.FilesByPath(c.input...)
if err != nil {
t.Fatalf("could not use resolver: %+v, %+v", err, refs)
}
assert.NoError(t, err)
if len(refs) != c.refCount {
t.Errorf("unexpected number of refs: %d != %d", len(refs), c.refCount)
@@ -127,51 +134,290 @@ func TestDirectoryResolver_MultipleFilesByPath(t *testing.T) {
}
func TestDirectoryResolver_FilesByGlobMultiple(t *testing.T) {
t.Run("finds multiple matching files", func(t *testing.T) {
resolver := directoryResolver{"test-fixtures"}
refs, err := resolver.FilesByGlob("image-symlinks/file*")
resolver, err := newDirectoryResolver("./test-fixtures")
assert.NoError(t, err)
refs, err := resolver.FilesByGlob("**/image-symlinks/file*")
assert.NoError(t, err)
if err != nil {
t.Fatalf("could not use resolver: %+v, %+v", err, refs)
}
expected := 2
if len(refs) != expected {
t.Errorf("unexpected number of refs: %d != %d", len(refs), expected)
}
})
assert.Len(t, refs, 2)
}
func TestDirectoryResolver_FilesByGlobRecursive(t *testing.T) {
t.Run("finds multiple matching files", func(t *testing.T) {
resolver := directoryResolver{"test-fixtures/image-symlinks"}
refs, err := resolver.FilesByGlob("**/*.txt")
if err != nil {
t.Fatalf("could not use resolver: %+v, %+v", err, refs)
}
expected := 6
if len(refs) != expected {
t.Errorf("unexpected number of refs: %d != %d", len(refs), expected)
}
})
resolver, err := newDirectoryResolver("./test-fixtures/image-symlinks")
assert.NoError(t, err)
refs, err := resolver.FilesByGlob("**/*.txt")
assert.NoError(t, err)
assert.Len(t, refs, 6)
}
func TestDirectoryResolver_FilesByGlobSingle(t *testing.T) {
t.Run("finds multiple matching files", func(t *testing.T) {
resolver := directoryResolver{"test-fixtures"}
refs, err := resolver.FilesByGlob("image-symlinks/*1.txt")
if err != nil {
t.Fatalf("could not use resolver: %+v, %+v", err, refs)
}
resolver, err := newDirectoryResolver("./test-fixtures")
assert.NoError(t, err)
refs, err := resolver.FilesByGlob("**/image-symlinks/*1.txt")
assert.NoError(t, err)
expected := 1
if len(refs) != expected {
t.Errorf("unexpected number of refs: %d != %d", len(refs), expected)
}
assert.Len(t, refs, 1)
assert.Equal(t, "test-fixtures/image-symlinks/file-1.txt", refs[0].RealPath)
}
func TestDirectoryResolverDoesNotIgnoreRelativeSystemPaths(t *testing.T) {
// let's make certain that "dev/place" is not ignored, since it is not "/dev/place"
resolver, err := newDirectoryResolver("test-fixtures/system_paths/target")
assert.NoError(t, err)
// ensure the correct filter function is wired up by default
expectedFn := reflect.ValueOf(isUnixSystemRuntimePath)
actualFn := reflect.ValueOf(resolver.pathFilterFns[0])
assert.Equal(t, expectedFn.Pointer(), actualFn.Pointer())
// all paths should be found (non filtering matches a path)
refs, err := resolver.FilesByGlob("**/place")
assert.NoError(t, err)
// 4: within target/
// 1: target/link --> relative path to "place"
// 1: outside_root/link_target/place
assert.Len(t, refs, 6)
// ensure that symlink indexing outside of root worked
assert.Contains(t, refs, Location{
RealPath: "test-fixtures/system_paths/outside_root/link_target/place",
})
}
func TestDirectoryResolverUsesPathFilterFunction(t *testing.T) {
// let's make certain that the index honors the filter function
filter := func(s string) bool {
// a dummy function that works for testing purposes
return strings.Contains(s, "dev/place") || strings.Contains(s, "proc/place") || strings.Contains(s, "sys/place")
}
resolver, err := newDirectoryResolver("test-fixtures/system_paths/target", filter)
assert.NoError(t, err)
// ensure the correct filter function is wired up by default
expectedFn := reflect.ValueOf(filter)
actualFn := reflect.ValueOf(resolver.pathFilterFns[0])
assert.Equal(t, expectedFn.Pointer(), actualFn.Pointer())
assert.Len(t, resolver.pathFilterFns, 1)
refs, err := resolver.FilesByGlob("**/place")
assert.NoError(t, err)
// target/home/place + target/link/.../place + outside_root/.../place
assert.Len(t, refs, 3)
}
func Test_isUnixSystemRuntimePath(t *testing.T) {
tests := []struct {
path string
expected bool
}{
{
path: "proc/place",
expected: false,
},
{
path: "/proc/place",
expected: true,
},
{
path: "/proc",
expected: true,
},
{
path: "/pro/c",
expected: false,
},
{
path: "/pro",
expected: false,
},
{
path: "/dev",
expected: true,
},
{
path: "/sys",
expected: true,
},
{
path: "/something/sys",
expected: false,
},
}
for _, test := range tests {
t.Run(test.path, func(t *testing.T) {
assert.Equal(t, test.expected, isUnixSystemRuntimePath(test.path))
})
}
}
func Test_directoryResolver_index(t *testing.T) {
// note: this test is testing the effects from newDirectoryResolver, indexTree, and addPathToIndex
r, err := newDirectoryResolver("test-fixtures/system_paths/target")
if err != nil {
t.Fatalf("unable to get indexed dir resolver: %+v", err)
}
tests := []struct {
name string
path string
}{
{
name: "has dir",
path: "test-fixtures/system_paths/target/home",
},
{
name: "has path",
path: "test-fixtures/system_paths/target/home/place",
},
{
name: "has symlink",
path: "test-fixtures/system_paths/target/link/a-symlink",
},
{
name: "has symlink target",
path: "test-fixtures/system_paths/outside_root/link_target/place",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
info, err := os.Stat(test.path)
assert.NoError(t, err)
// note: the index uses absolute paths, so assertions MUST keep this in mind
cwd, err := os.Getwd()
if err != nil {
t.Fatalf("could not get working dir: %+v", err)
}
p := file.Path(path.Join(cwd, test.path))
assert.Equal(t, true, r.fileTree.HasPath(p))
exists, ref, err := r.fileTree.File(p)
assert.Equal(t, true, exists)
if assert.NoError(t, err) {
return
}
assert.Equal(t, info, r.infos[ref.ID()])
})
}
}
func Test_handleFileAccessErr(t *testing.T) {
tests := []struct {
name string
input error
expectedErr error
expectedPathTracked bool
}{
{
name: "permission error does not propagate",
input: os.ErrPermission,
expectedPathTracked: true,
expectedErr: nil,
},
{
name: "file does not exist error does not propagate",
input: os.ErrNotExist,
expectedPathTracked: true,
expectedErr: nil,
},
{
name: "non-permission errors propagate",
input: os.ErrInvalid,
expectedPathTracked: false,
expectedErr: os.ErrInvalid,
},
{
name: "non-errors ignored",
input: nil,
expectedPathTracked: false,
expectedErr: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
r := directoryResolver{
errPaths: make(map[string]error),
}
p := "a/place"
assert.ErrorIs(t, r.handleFileAccessErr(p, test.input), test.expectedErr)
_, exists := r.errPaths[p]
assert.Equal(t, test.expectedPathTracked, exists)
})
}
}
type indexerMock struct {
observedRoots []string
additionalRoots map[string][]string
}
func (m *indexerMock) indexer(s string, _ *progress.Stage) ([]string, error) {
m.observedRoots = append(m.observedRoots, s)
return m.additionalRoots[s], nil
}
func Test_indexAllRoots(t *testing.T) {
tests := []struct {
name string
root string
mock indexerMock
expectedRoots []string
}{
{
name: "no additional roots",
root: "a/place",
mock: indexerMock{
additionalRoots: make(map[string][]string),
},
expectedRoots: []string{
"a/place",
},
},
{
name: "additional roots from a single call",
root: "a/place",
mock: indexerMock{
additionalRoots: map[string][]string{
"a/place": {
"another/place",
"yet-another/place",
},
},
},
expectedRoots: []string{
"a/place",
"another/place",
"yet-another/place",
},
},
{
name: "additional roots from a multiple calls",
root: "a/place",
mock: indexerMock{
additionalRoots: map[string][]string{
"a/place": {
"another/place",
"yet-another/place",
},
"yet-another/place": {
"a-quiet-place-2",
"a-final/place",
},
},
},
expectedRoots: []string{
"a/place",
"another/place",
"yet-another/place",
"a-quiet-place-2",
"a-final/place",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.NoError(t, indexAllRoots(test.root, test.mock.indexer))
})
}
}
+16 -1
View File
@@ -1,6 +1,9 @@
package source
import "archive/tar"
import (
"archive/tar"
"os"
)
const (
UnknownFileType FileType = "UnknownFileType"
@@ -34,3 +37,15 @@ func newFileTypeFromTarHeaderTypeFlag(flag byte) FileType {
}
return UnknownFileType
}
// TODO: fill in more types from mod...
func newFileTypeFromMode(mode os.FileMode) FileType {
switch {
case mode&os.ModeSymlink == os.ModeSymlink:
return SymbolicLink
case mode.IsDir():
return Directory
default:
return RegularFile
}
}
+2 -2
View File
@@ -83,7 +83,7 @@ func TestDetectScheme(t *testing.T) {
ref: "latest",
},
expectedScheme: ImageScheme,
// we want to be able to handle this case better, however, I don't see a way to do this
// we expected to be able to handle this case better, however, I don't see a way to do this
// the user will need to provide more explicit input (docker:docker:latest)
expectedLocation: "latest",
},
@@ -95,7 +95,7 @@ func TestDetectScheme(t *testing.T) {
ref: "docker:latest",
},
expectedScheme: ImageScheme,
// we want to be able to handle this case better, however, I don't see a way to do this
// we expected to be able to handle this case better, however, I don't see a way to do this
// the user will need to provide more explicit input (docker:docker:latest)
expectedLocation: "docker:latest",
},
+1 -1
View File
@@ -94,7 +94,7 @@ func NewFromImage(img *image.Image, userImageStr string) (Source, error) {
func (s Source) FileResolver(scope Scope) (FileResolver, error) {
switch s.Metadata.Scheme {
case DirectoryScheme:
return newDirectoryResolver(s.Metadata.Path), nil
return newDirectoryResolver(s.Metadata.Path)
case ImageScheme:
switch scope {
case SquashedScope:
+6 -6
View File
@@ -3,6 +3,8 @@ package source
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/anchore/stereoscope/pkg/image"
)
@@ -41,7 +43,6 @@ func TestNewFromDirectory(t *testing.T) {
desc: "no paths exist",
input: "foobar/",
inputPaths: []string{"/opt/", "/other"},
expRefs: 0,
},
{
desc: "path detected",
@@ -73,9 +74,8 @@ func TestNewFromDirectory(t *testing.T) {
t.Errorf("mismatched stringer: '%s' != '%s'", src.Metadata.Path, test.input)
}
resolver, err := src.FileResolver(SquashedScope)
if err != nil {
t.Errorf("could not get resolver error: %+v", err)
}
assert.NoError(t, err)
refs, err := resolver.FilesByPath(test.inputPaths...)
if err != nil {
t.Errorf("FilesByPath call produced an error: %+v", err)
@@ -141,13 +141,13 @@ func TestFilesByGlob(t *testing.T) {
{
input: "test-fixtures/path-detected",
desc: "a single match",
glob: "*vimrc",
glob: "**/*vimrc",
expected: 1,
},
{
input: "test-fixtures/path-detected",
desc: "multiple matches",
glob: "*",
glob: "**",
expected: 2,
},
}
@@ -0,0 +1 @@
bad
@@ -0,0 +1 @@
good
@@ -0,0 +1 @@
../../outside_root/link_target
@@ -0,0 +1 @@
bad
@@ -0,0 +1 @@
bad
+46
View File
@@ -0,0 +1,46 @@
package cli
import (
"os/exec"
"strings"
"testing"
"time"
)
func TestDirectoryScanCompletesWithinTimeout(t *testing.T) {
image := "alpine:latest"
// we want to pull the image ahead of the test as to not affect the timeout value
pullDockerImage(t, image)
var cmd *exec.Cmd
var stdout, stderr string
done := make(chan struct{})
go func() {
defer close(done)
cmd, stdout, stderr = runSyftInDocker(t, nil, image, "dir:/", "-vv")
}()
select {
case <-done:
break
case <-time.After(5 * time.Second):
t.Fatalf("directory scan is taking too long")
}
assertions := []traitAssertion{
assertTableReport,
assertSuccessfulReturnCode,
}
for _, traitFn := range assertions {
traitFn(t, stdout, stderr, cmd.ProcessState.ExitCode())
}
if t.Failed() {
t.Log("STDOUT:\n", stdout)
t.Log("STDERR:\n", stderr)
t.Log("COMMAND:", strings.Join(cmd.Args, " "))
}
}
+1 -1
View File
@@ -60,7 +60,7 @@ func TestJSONSchema(t *testing.T) {
args = append(args, a)
}
_, stdout, stderr := runSyftCommand(t, nil, args...)
_, stdout, stderr := runSyft(t, nil, args...)
if len(strings.Trim(stdout, "\n ")) < 100 {
t.Fatalf("bad syft run:\noutput: %q\n:error: %q", stdout, stderr)
+21 -2
View File
@@ -16,6 +16,15 @@ func TestPackagesCmdFlags(t *testing.T) {
env map[string]string
assertions []traitAssertion
}{
{
name: "no-args-shows-help",
args: []string{"packages"},
assertions: []traitAssertion{
assertInOutput("an image/directory argument is required"), // specific error that should be shown
assertInOutput("Generate a packaged-based Software Bill Of Materials"), // excerpt from help description
assertFailingReturnCode,
},
},
{
name: "json-output-flag",
args: []string{"packages", "-o", "json", request},
@@ -125,7 +134,7 @@ func TestPackagesCmdFlags(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
cmd, stdout, stderr := runSyftCommand(t, test.env, test.args...)
cmd, stdout, stderr := runSyft(t, test.env, test.args...)
for _, traitFn := range test.assertions {
traitFn(t, stdout, stderr, cmd.ProcessState.ExitCode())
}
@@ -193,11 +202,21 @@ func TestRegistryAuth(t *testing.T) {
assertInOutput(`no registry credentials configured, using the default keychain`),
},
},
{
name: "allows insecure http flag",
args: []string{"packages", "-vv", "registry:localhost:5000/something:latest"},
env: map[string]string{
"SYFT_REGISTRY_INSECURE_USE_HTTP": "true",
},
assertions: []traitAssertion{
assertInOutput("insecure-use-http: true"),
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
cmd, stdout, stderr := runSyftCommand(t, test.env, test.args...)
cmd, stdout, stderr := runSyft(t, test.env, test.args...)
for _, traitAssertionFn := range test.assertions {
traitAssertionFn(t, stdout, stderr, cmd.ProcessState.ExitCode())
}
+10 -1
View File
@@ -12,6 +12,15 @@ func TestPowerUserCmdFlags(t *testing.T) {
env map[string]string
assertions []traitAssertion
}{
{
name: "no-args-shows-help",
args: []string{"power-user"},
assertions: []traitAssertion{
assertInOutput("an image/directory argument is required"), // specific error that should be shown
assertInOutput("Run bulk operations on container images"), // excerpt from help description
assertFailingReturnCode,
},
},
{
name: "json-output-flag-fails",
args: []string{"power-user", "-o", "json", "docker-archive:" + getFixtureImage(t, "image-pkg-coverage")},
@@ -66,7 +75,7 @@ func TestPowerUserCmdFlags(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
cmd, stdout, stderr := runSyftCommand(t, test.env, test.args...)
cmd, stdout, stderr := runSyft(t, test.env, test.args...)
for _, traitFn := range test.assertions {
traitFn(t, stdout, stderr, cmd.ProcessState.ExitCode())
}
+3 -3
View File
@@ -36,12 +36,12 @@ func TestRootCmdAliasesToPackagesSubcommand(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
aliasCmd, aliasStdout, aliasStderr := runSyftCommand(t, test.env, request)
aliasCmd, aliasStdout, aliasStderr := runSyft(t, test.env, request)
for _, traitFn := range test.assertions {
traitFn(t, aliasStdout, aliasStderr, aliasCmd.ProcessState.ExitCode())
}
pkgCmd, pkgsStdout, pkgsStderr := runSyftCommand(t, test.env, "packages", request)
pkgCmd, pkgsStdout, pkgsStderr := runSyft(t, test.env, "packages", request)
for _, traitFn := range test.assertions {
traitFn(t, pkgsStdout, pkgsStderr, pkgCmd.ProcessState.ExitCode())
}
@@ -102,7 +102,7 @@ func TestPersistentFlags(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
cmd, stdout, stderr := runSyftCommand(t, test.env, test.args...)
cmd, stdout, stderr := runSyft(t, test.env, test.args...)
for _, traitFn := range test.assertions {
traitFn(t, stdout, stderr, cmd.ProcessState.ExitCode())
}
+1 -1
View File
@@ -59,7 +59,7 @@ func TestSPDXJSONSchema(t *testing.T) {
args = append(args, a)
}
_, stdout, _ := runSyftCommand(t, nil, args...)
_, stdout, _ := runSyft(t, nil, args...)
if len(strings.Trim(stdout, "\n ")) < 100 {
t.Fatalf("bad syft output: %q", stdout)
+56 -16
View File
@@ -19,8 +19,45 @@ func getFixtureImage(t testing.TB, fixtureImageName string) string {
return imagetest.GetFixtureImageTarPath(t, fixtureImageName)
}
func runSyftCommand(t testing.TB, env map[string]string, args ...string) (*exec.Cmd, string, string) {
func pullDockerImage(t testing.TB, image string) {
cmd := exec.Command("docker", "pull", image)
stdout, stderr := runCommand(cmd, nil)
if cmd.ProcessState.ExitCode() != 0 {
t.Log("STDOUT", stdout)
t.Log("STDERR", stderr)
t.Fatalf("could not pull docker image")
}
}
func runSyftInDocker(t testing.TB, env map[string]string, image string, args ...string) (*exec.Cmd, string, string) {
allArgs := append(
[]string{
"run",
"-t",
"-e",
"SYFT_CHECK_FOR_APP_UPDATE=false",
"-v",
fmt.Sprintf("%s:/syft", getSyftBinaryLocationByOS(t, "linux")),
image,
"/syft",
},
args...,
)
cmd := exec.Command("docker", allArgs...)
stdout, stderr := runCommand(cmd, env)
return cmd, stdout, stderr
}
func runSyft(t testing.TB, env map[string]string, args ...string) (*exec.Cmd, string, string) {
cmd := getSyftCommand(t, args...)
if env != nil {
env["SYFT_CHECK_FOR_APP_UPDATE"] = "false"
}
stdout, stderr := runCommand(cmd, env)
return cmd, stdout, stderr
}
func runCommand(cmd *exec.Cmd, env map[string]string) (string, string) {
if env != nil {
var envList []string
for key, val := range env {
@@ -38,29 +75,32 @@ func runSyftCommand(t testing.TB, env map[string]string, args ...string) (*exec.
// ignore errors since this may be what the test expects
cmd.Run()
return cmd, stdout.String(), stderr.String()
return stdout.String(), stderr.String()
}
func getSyftCommand(t testing.TB, args ...string) *exec.Cmd {
return exec.Command(getSyftBinaryLocation(t), args...)
}
var binaryLocation string
func getSyftBinaryLocation(t testing.TB) string {
if os.Getenv("SYFT_BINARY_LOCATION") != "" {
// SYFT_BINARY_LOCATION is the absolute path to the snapshot binary
binaryLocation = os.Getenv("SYFT_BINARY_LOCATION")
} else {
// note: there is a subtle - vs _ difference between these versions
switch runtime.GOOS {
case "darwin":
binaryLocation = path.Join(repoRoot(t), fmt.Sprintf("snapshot/syft-macos_darwin_%s/syft", runtime.GOARCH))
case "linux":
binaryLocation = path.Join(repoRoot(t), fmt.Sprintf("snapshot/syft_linux_%s/syft", runtime.GOARCH))
default:
t.Fatalf("unsupported OS: %s", runtime.GOOS)
}
return os.Getenv("SYFT_BINARY_LOCATION")
}
return getSyftBinaryLocationByOS(t, runtime.GOOS)
}
return exec.Command(binaryLocation, args...)
func getSyftBinaryLocationByOS(t testing.TB, goOS string) string {
// note: there is a subtle - vs _ difference between these versions
switch goOS {
case "darwin":
return path.Join(repoRoot(t), fmt.Sprintf("snapshot/syft-macos_darwin_%s/syft", runtime.GOARCH))
case "linux":
return path.Join(repoRoot(t), fmt.Sprintf("snapshot/syft_linux_%s/syft", runtime.GOARCH))
default:
t.Fatalf("unsupported OS: %s", runtime.GOOS)
}
return ""
}
func repoRoot(t testing.TB) string {
+47 -5
View File
@@ -8,13 +8,12 @@ import (
"sync"
"time"
"github.com/anchore/syft/internal/ui/components"
"github.com/anchore/stereoscope/pkg/image/docker"
"github.com/dustin/go-humanize"
stereoEventParsers "github.com/anchore/stereoscope/pkg/event/parsers"
"github.com/anchore/stereoscope/pkg/image/docker"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/ui/components"
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
"github.com/dustin/go-humanize"
"github.com/gookit/color"
"github.com/wagoodman/go-partybus"
"github.com/wagoodman/go-progress"
@@ -401,6 +400,49 @@ func FileMetadataCatalogerStartedHandler(ctx context.Context, fr *frame.Frame, e
return err
}
// FileIndexingStartedHandler shows the intermittent indexing progress from a directory resolver.
// nolint:dupl
func FileIndexingStartedHandler(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error {
path, prog, err := syftEventParsers.ParseFileIndexingStarted(event)
if err != nil {
return fmt.Errorf("bad %s event: %w", event.Type, err)
}
line, err := fr.Append()
if err != nil {
return err
}
wg.Add(1)
_, spinner := startProcess()
stream := progress.Stream(ctx, prog, interval)
title := tileFormat.Sprintf("Indexing %s", path)
formatFn := func(_ progress.Progress) {
spin := color.Magenta.Sprint(spinner.Next())
if err != nil {
_, _ = io.WriteString(line, fmt.Sprintf("Error: %+v", err))
} else {
auxInfo := auxInfoFormat.Sprintf("[file: %s]", internal.TruncateMiddleEllipsis(prog.Stage(), 100))
_, _ = io.WriteString(line, fmt.Sprintf(statusTitleTemplate+"%s", spin, title, auxInfo))
}
}
go func() {
defer wg.Done()
formatFn(progress.Progress{})
for p := range stream {
formatFn(p)
}
spin := color.Green.Sprint(completedStatus)
title = tileFormat.Sprintf("Indexed %s", path)
_, _ = io.WriteString(line, fmt.Sprintf(statusTitleTemplate, spin, title))
}()
return err
}
// FileMetadataCatalogerStartedHandler shows the intermittent secrets searching progress.
// nolint:dupl
func FileDigestsCatalogerStartedHandler(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error {
+4 -1
View File
@@ -27,7 +27,7 @@ func NewHandler() *Handler {
// RespondsTo indicates if the handler is capable of handling the given event.
func (r *Handler) RespondsTo(event partybus.Event) bool {
switch event.Type {
case stereoscopeEvent.PullDockerImage, stereoscopeEvent.ReadImage, stereoscopeEvent.FetchImage, syftEvent.PackageCatalogerStarted, syftEvent.SecretsCatalogerStarted, syftEvent.FileDigestsCatalogerStarted, syftEvent.FileMetadataCatalogerStarted, syftEvent.ImportStarted:
case stereoscopeEvent.PullDockerImage, stereoscopeEvent.ReadImage, stereoscopeEvent.FetchImage, syftEvent.PackageCatalogerStarted, syftEvent.SecretsCatalogerStarted, syftEvent.FileDigestsCatalogerStarted, syftEvent.FileMetadataCatalogerStarted, syftEvent.FileIndexingStarted, syftEvent.ImportStarted:
return true
default:
return false
@@ -58,6 +58,9 @@ func (r *Handler) Handle(ctx context.Context, fr *frame.Frame, event partybus.Ev
case syftEvent.FileMetadataCatalogerStarted:
return FileMetadataCatalogerStartedHandler(ctx, fr, event, wg)
case syftEvent.FileIndexingStarted:
return FileIndexingStartedHandler(ctx, fr, event, wg)
case syftEvent.ImportStarted:
return ImportStartedHandler(ctx, fr, event, wg)
}