mirror of
https://github.com/anchore/syft.git
synced 2026-10-25 03:22:22 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0e9b92a25 | ||
|
|
d25d3b1fb3 | ||
|
|
93b4bf9379 | ||
|
|
58f2be95fd | ||
|
|
6f038e9208 | ||
|
|
32a54b94be | ||
|
|
98d4749f86 | ||
|
|
3a5168917e | ||
|
|
58b21c03c3 | ||
|
|
2093d2a1dc | ||
|
|
dd56450f92 | ||
|
|
6f2b52971d | ||
|
|
fee74d57b8 | ||
|
|
fee35dd175 | ||
|
|
ecf4e5546a | ||
|
|
2de56c0749 | ||
|
|
fb0857ff93 | ||
|
|
c833472b6e | ||
|
|
c825ae2404 |
@@ -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)!'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||

|
||||
|
||||
**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
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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):]
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
@@ -7,5 +7,5 @@ import (
|
||||
type UI interface {
|
||||
Setup(unsubscribe func() error) error
|
||||
partybus.Handler
|
||||
Teardown() error
|
||||
Teardown(force bool) error
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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, "_", "-"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 @@
|
||||
good
|
||||
@@ -0,0 +1 @@
|
||||
bad
|
||||
@@ -0,0 +1 @@
|
||||
good
|
||||
@@ -0,0 +1 @@
|
||||
../../outside_root/link_target
|
||||
@@ -0,0 +1 @@
|
||||
bad
|
||||
@@ -0,0 +1 @@
|
||||
bad
|
||||
@@ -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, " "))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user