mirror of
https://github.com/anchore/syft.git
synced 2026-10-13 22:57:19 +02:00
Compare commits
22
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
821210006d | ||
|
|
053768c6c6 | ||
|
|
9189ed68df | ||
|
|
ba1cd8d753 | ||
|
|
dd2c178085 | ||
|
|
1b23a94015 | ||
|
|
f47a6a88b1 | ||
|
|
4b7217f052 | ||
|
|
5e4b668e5a | ||
|
|
6480f06173 | ||
|
|
75aed5f3ec | ||
|
|
270fbb7665 | ||
|
|
0395c47445 | ||
|
|
e5caba043d | ||
|
|
d1ac476986 | ||
|
|
dd23d49986 | ||
|
|
6d4d083acc | ||
|
|
316d4341c8 | ||
|
|
3e8afc5274 | ||
|
|
93d00dc340 | ||
|
|
9fe1da8ee6 | ||
|
|
95df7dd035 |
+2
-2
@@ -6,7 +6,7 @@ permit:
|
||||
- ISC
|
||||
ignore-packages:
|
||||
# packageurl-go is released under the MIT license located in the root of the repo at /mit.LICENSE
|
||||
- github.com/package-url/packageurl-go
|
||||
- github.com/anchore/packageurl-go
|
||||
|
||||
# from: https://github.com/spdx/tools-golang/blob/main/LICENSE.code
|
||||
# The tools-golang source code is provided and may be used, at your option,
|
||||
@@ -14,4 +14,4 @@ ignore-packages:
|
||||
# * Apache License, version 2.0 (Apache-2.0), OR
|
||||
# * GNU General Public License, version 2.0 or later (GPL-2.0-or-later).
|
||||
# (we choose Apache-2.0)
|
||||
- github.com/spdx/tools-golang
|
||||
- github.com/spdx/tools-golang
|
||||
|
||||
@@ -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 #syft-help (ask us questions) and #syft-dev (collaborate with us on design and development)!'
|
||||
about: 'Come chat with us! Ask for help, join our software development efforts, or just give us feedback!'
|
||||
|
||||
@@ -158,6 +158,10 @@ jobs:
|
||||
AC_USERNAME: ${{ secrets.ENG_CI_APPLE_ID }} # Used during macOS notarization.
|
||||
AC_PASSWORD: ${{ secrets.ENG_CI_APPLE_ID_PASS }} # Used during macOS notarization.
|
||||
|
||||
- uses: anchore/sbom-action@v0
|
||||
with:
|
||||
artifact-name: sbom.spdx.json
|
||||
|
||||
- uses: 8398a7/action-slack@v3
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
|
||||
+39
-5
@@ -4,35 +4,59 @@
|
||||
# include:
|
||||
# - EXC0002 # disable excluding of issues about comments from golint
|
||||
|
||||
linters-settings:
|
||||
|
||||
cyclop:
|
||||
# the maximal code complexity to report
|
||||
max-complexity: 15
|
||||
|
||||
linters:
|
||||
# inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint
|
||||
disable-all: true
|
||||
enable:
|
||||
- asciicheck
|
||||
- bodyclose
|
||||
- cyclop
|
||||
- deadcode
|
||||
- depguard
|
||||
- dogsled
|
||||
- dupl
|
||||
- durationcheck
|
||||
- errcheck
|
||||
- errname
|
||||
- errorlint
|
||||
- exportloopref
|
||||
- forbidigo
|
||||
- forcetypeassert
|
||||
- funlen
|
||||
- gci
|
||||
- gocognit
|
||||
- goconst
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- gofmt
|
||||
- gofumpt
|
||||
- goimports
|
||||
- golint
|
||||
- gomoddirectives
|
||||
- gomodguard
|
||||
- goprintffuncname
|
||||
- gosec
|
||||
- gosimple
|
||||
- govet
|
||||
- ifshort
|
||||
- importas
|
||||
- ineffassign
|
||||
# - ireturn # will be added in 1.43.0 (accept interfaces, return structs)
|
||||
- makezero
|
||||
- misspell
|
||||
- nakedret
|
||||
- nilerr
|
||||
# - nilnil # will be added in 1.43.0
|
||||
- nolintlint
|
||||
- paralleltest
|
||||
- predeclared
|
||||
- revive
|
||||
- rowserrcheck
|
||||
- scopelint
|
||||
- staticcheck
|
||||
- structcheck
|
||||
- stylecheck
|
||||
@@ -41,19 +65,29 @@ linters:
|
||||
- unparam
|
||||
- unused
|
||||
- varcheck
|
||||
- wastedassign
|
||||
- whitespace
|
||||
# - wrapcheck
|
||||
|
||||
|
||||
# do not enable...
|
||||
# - exhaustive # makes to many assumptions about enums and if a switch case should contain all enum instances
|
||||
# - exhaustivestruct # we like to depend on zero values
|
||||
# - gochecknoglobals
|
||||
# - gochecknoinits # this is too aggressive
|
||||
# - godot
|
||||
# - godox
|
||||
# - goerr113
|
||||
# - goerr113 # does not allow use of dynamic errors
|
||||
# - golint # replaced by revive
|
||||
# - gomnd # this is too aggressive
|
||||
# - interfacer # this is a good idea, but is no longer supported and is prone to false positives
|
||||
# - lll # without a way to specify per-line exception cases, this is not usable
|
||||
# - maligned # this is an excellent linter, but tricky to optimize and we are not sensitive to memory layout optimizations
|
||||
# - nestif
|
||||
# - nestif # other complexity linters already cover this well
|
||||
# - nlreturn # no robust auto-fixers supported
|
||||
# - noctx # we don't use context a lot
|
||||
# - prealloc # following this rule isn't consistently a good idea, as it sometimes forces unnecessary allocations that result in less idiomatic code
|
||||
# - scopelint # replaced by exportloopref
|
||||
# - tagliatelle # too presumptive at which struct tag conventions should be used
|
||||
# - testpackage
|
||||
# - wsl
|
||||
# - wsl # very verbose and there is no auto-fixer for this project yet https://github.com/bombsimon/wsl/issues/90
|
||||
|
||||
+11
-6
@@ -1,12 +1,12 @@
|
||||
# Contributing to `syft`
|
||||
# Contributing to Syft
|
||||
|
||||
If you are looking to contribute to this project and want to open a Github pull request ("PR"), there are a few guidelines of what we are looking for in patches. Make sure you go through this document and ensure that your code proposal is aligned.
|
||||
If you are looking to contribute to this project and want to open a GitHub pull request ("PR"), there are a few guidelines of what we are looking for in patches. Make sure you go through this document and ensure that your code proposal is aligned.
|
||||
|
||||
## Sign your work
|
||||
## Sign off your work
|
||||
|
||||
The `sign-off` is an added line at the end of the explanation for the commit, certifying that you wrote it or otherwise have the right to submit it as an open-source patch. By submitting a contribution, you agree to be bound by the terms of the DCO Version 1.1 and Apache License Version 2.0.
|
||||
|
||||
Signing a commit certifies the below Developer's Certificate of Origin (DCO):
|
||||
Signing off a commit certifies the below Developer's Certificate of Origin (DCO):
|
||||
|
||||
```text
|
||||
Developer's Certificate of Origin 1.1
|
||||
@@ -51,13 +51,13 @@ $ git config --global user.name "John Doe"
|
||||
$ git config --global user.email "john.doe@example.com"
|
||||
```
|
||||
|
||||
Creating a signed commit is then possible with `-s`:
|
||||
Creating a signed-off commit is then possible with `-s` or `--signoff`:
|
||||
|
||||
```text
|
||||
$ git commit -s -m "this is a commit message"
|
||||
```
|
||||
|
||||
To double check if the commit was signed, look at the log output:
|
||||
To double-check that the commit was signed-off, look at the log output:
|
||||
|
||||
```text
|
||||
$ git log -1
|
||||
@@ -82,6 +82,11 @@ Date: Mon Aug 1 11:27:13 2020 -0400
|
||||
|
||||
[//]: # (TODO: PR title and description)
|
||||
|
||||
## Sign your commits
|
||||
|
||||
To ensure the authenticity and integrity of code contributions, **we require that all commits are signed**. Signing commits proves that your commits were truly created by you, as the holder of a private key.
|
||||
|
||||
Configuring git to sign your commits is a straightforward process. To get set up, see [GitHub's documentation on signing commits](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits).
|
||||
|
||||
## Test your changes
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ $(TEMPDIR):
|
||||
.PHONY: bootstrap-tools
|
||||
bootstrap-tools: $(TEMPDIR)
|
||||
GO111MODULE=off GOBIN=$(shell realpath $(TEMPDIR)) go get -u golang.org/x/perf/cmd/benchstat
|
||||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(TEMPDIR)/ v1.26.0
|
||||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(TEMPDIR)/ v1.42.1
|
||||
curl -sSfL https://raw.githubusercontent.com/wagoodman/go-bouncer/master/bouncer.sh | sh -s -- -b $(TEMPDIR)/ v0.2.0
|
||||
curl -sfL https://install.goreleaser.com/github.com/goreleaser/goreleaser.sh | sh -s -- -b $(TEMPDIR)/ v0.177.0
|
||||
|
||||
|
||||
@@ -3,11 +3,20 @@
|
||||
[](https://github.com/anchore/syft/actions/workflows/validations.yaml)
|
||||
[](https://goreportcard.com/report/github.com/anchore/syft)
|
||||
[](https://github.com/anchore/syft/releases/latest)
|
||||
[](https://github.com/anchore/syft/blob/main/LICENSE)
|
||||
[](https://github.com/anchore/syft)
|
||||
[](https://github.com/anchore/syft/blob/main/LICENSE)
|
||||
[](https://anchore.com/slack)
|
||||
|
||||
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).
|
||||
|
||||
## We'll be at KubeCon 2021!
|
||||
|
||||
Attending KubeCon 2021 in person? Join us for a meetup on **Tuesday, October 12th**.
|
||||
|
||||
We’ll have free swag, giveaways, snacks, and sips. Space will be limited, so make sure to [save your seat](https://get.anchore.com/2021-kubecon-na-opensource-happy-hour/)!
|
||||
|
||||
---
|
||||
|
||||

|
||||
|
||||
**Features**
|
||||
@@ -20,6 +29,27 @@ A CLI tool and go library for generating a Software Bill of Materials (SBOM) fro
|
||||
|
||||
If you encounter an issue, please [let us know using the issue tracker](https://github.com/anchore/syft/issues).
|
||||
|
||||
## Installation
|
||||
|
||||
### Recommended
|
||||
```bash
|
||||
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
|
||||
```
|
||||
|
||||
...or, you can specify a release version and destination directory for the installation:
|
||||
|
||||
```
|
||||
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b <DESTINATION_DIR> <RELEASE_VERSION>
|
||||
```
|
||||
|
||||
### Homebrew
|
||||
```bash
|
||||
brew tap anchore/syft
|
||||
brew install syft
|
||||
```
|
||||
|
||||
**Note**: Currently, Syft is built only for macOS and Linux.
|
||||
|
||||
## Getting started
|
||||
|
||||
To generate an SBOM for a Docker or OCI image:
|
||||
@@ -39,7 +69,10 @@ To include software from all image layers in the SBOM, regardless of its presenc
|
||||
syft packages <image> --scope all-layers
|
||||
```
|
||||
|
||||
### Supported sources
|
||||
|
||||
Syft can generate a SBOM from a variety of sources:
|
||||
|
||||
```
|
||||
# catalog a container image archive (from the result of `docker image save ...`, `podman save ...`, or `skopeo copy` commands)
|
||||
syft packages path/to/image.tar
|
||||
@@ -49,6 +82,7 @@ syft packages path/to/dir
|
||||
```
|
||||
|
||||
Sources can be explicitly provided with a scheme:
|
||||
|
||||
```
|
||||
docker:yourrepo/yourimage:tag use images from the Docker daemon
|
||||
docker-archive:path/to/yourimage.tar use a tarball from disk for archives created from "docker save"
|
||||
@@ -58,7 +92,10 @@ dir:path/to/yourproject read directly from a path on disk (any di
|
||||
registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required)
|
||||
```
|
||||
|
||||
### Output formats
|
||||
|
||||
The output format for Syft is configurable as well:
|
||||
|
||||
```
|
||||
syft packages <image> -o <format>
|
||||
```
|
||||
@@ -71,23 +108,6 @@ Where the `format`s available are:
|
||||
- `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
|
||||
|
||||
**Recommended (macOS and Linux)**
|
||||
```bash
|
||||
# install the latest version to /usr/local/bin
|
||||
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
|
||||
|
||||
# install a specific version into a specific dir
|
||||
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b <SOME_BIN_PATH> <RELEASE_VERSION>
|
||||
```
|
||||
|
||||
**Homebrew (macOS)**
|
||||
```bash
|
||||
brew tap anchore/syft
|
||||
brew install syft
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration search paths:
|
||||
@@ -108,6 +128,9 @@ output: "table"
|
||||
# same as -q ; SYFT_QUIET env var
|
||||
quiet: false
|
||||
|
||||
# same as --file; write output report to a file (default is to write to stdout)
|
||||
file: ""
|
||||
|
||||
# enable/disable checking for application updates on startup
|
||||
# same as SYFT_CHECK_FOR_APP_UPDATE env var
|
||||
check-for-app-update: true
|
||||
|
||||
+5
-2
@@ -87,9 +87,12 @@ This release process itself should be as automated as possible, and have only a
|
||||
|
||||
1. Tag the main branch with a full semantic-version, prefixed with a `v`. If there is a
|
||||
milestone with a partial version, the full version should be used for the git tag (e.g.
|
||||
with a Milestone of `v0.1` the tag should be `v0.1.0`).
|
||||
with a Milestone of `v0.1` the tag should be `v0.1.0`). You can determine the changes going
|
||||
into a release by running `make changelog-unreleased`. Use this change list to determine the
|
||||
release increment. After determining the release increment (major, minor, patch), create the tag.
|
||||
Given the above example the command to create the tag would be `git tag v0.1.0`.
|
||||
|
||||
1. Push the tag.
|
||||
1. Push the tag. Given the above example the command to push the tag would be `git push origin v0.1.0`.
|
||||
|
||||
1. A release admin must approve the release on the GitHub Actions release pipeline run page.
|
||||
Once approved, the release pipeline will generate all assets and draft a GitHub Release.
|
||||
|
||||
+2
-2
@@ -4,14 +4,13 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/anchore/stereoscope"
|
||||
"github.com/anchore/syft/internal/config"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/logger"
|
||||
"github.com/anchore/syft/syft"
|
||||
"github.com/gookit/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
)
|
||||
@@ -67,6 +66,7 @@ func initCmdAliasBindings() {
|
||||
}
|
||||
}
|
||||
|
||||
// nolint:forbidigo
|
||||
func initAppConfig() {
|
||||
cfg, err := config.LoadApplicationConfig(viper.GetViper(), persistentOpts)
|
||||
if err != nil {
|
||||
|
||||
+1
-2
@@ -8,7 +8,6 @@ import (
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/client"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -87,7 +86,7 @@ func dockerImageValidArgsFunction(cmd *cobra.Command, args []string, toComplete
|
||||
}
|
||||
|
||||
func listLocalDockerImages(prefix string) ([]string, error) {
|
||||
var repoTags = make([]string, 0)
|
||||
repoTags := make([]string, 0)
|
||||
ctx := context.Background()
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
|
||||
+16
-10
@@ -2,6 +2,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
@@ -14,11 +15,13 @@ import (
|
||||
// signal interrupts. Is responsible for handling each event relative to a given UI an to coordinate eventing until
|
||||
// an eventual graceful exit.
|
||||
// nolint:gocognit,funlen
|
||||
func eventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription *partybus.Subscription, ux ui.UI, cleanupFn func()) error {
|
||||
func eventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription *partybus.Subscription, cleanupFn func(), uxs ...ui.UI) error {
|
||||
defer cleanupFn()
|
||||
events := subscription.Events()
|
||||
var err error
|
||||
if ux, err = setupUI(subscription.Unsubscribe, ux); err != nil {
|
||||
var ux ui.UI
|
||||
|
||||
if ux, err = setupUI(subscription.Unsubscribe, uxs...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -78,15 +81,18 @@ func eventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription *
|
||||
return retErr
|
||||
}
|
||||
|
||||
func setupUI(unsubscribe func() error, ux ui.UI) (ui.UI, error) {
|
||||
if err := ux.Setup(unsubscribe); err != nil {
|
||||
// replace the existing UI with a (simpler) logger UI
|
||||
ux = ui.NewLoggerUI()
|
||||
// setupUI takes one or more UIs that responds to events and takes a event bus unsubscribe function for use
|
||||
// during teardown. With the given UIs, the first UI which the ui.Setup() function does not return an error
|
||||
// will be utilized in execution. Providing a set of UIs allows for the caller to provide graceful fallbacks
|
||||
// when there are environmental problem (e.g. unable to setup a TUI with the current TTY).
|
||||
func setupUI(unsubscribe func() error, uis ...ui.UI) (ui.UI, error) {
|
||||
for _, ux := range uis {
|
||||
if err := ux.Setup(unsubscribe); err != nil {
|
||||
// something is very wrong, bail.
|
||||
return ux, err
|
||||
log.Warnf("unable to setup given UI, falling back to alternative UI: %+v", err)
|
||||
continue
|
||||
}
|
||||
log.Errorf("unable to setup given UI, falling back to logger: %+v", err)
|
||||
|
||||
return ux, nil
|
||||
}
|
||||
return ux, nil
|
||||
return nil, fmt.Errorf("unable to setup any UI")
|
||||
}
|
||||
|
||||
@@ -96,8 +96,8 @@ func Test_eventLoop_gracefulExit(t *testing.T) {
|
||||
worker(),
|
||||
signaler(),
|
||||
subscription,
|
||||
ux,
|
||||
cleanupFn,
|
||||
ux,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -159,8 +159,8 @@ func Test_eventLoop_workerError(t *testing.T) {
|
||||
worker(),
|
||||
signaler(),
|
||||
subscription,
|
||||
ux,
|
||||
cleanupFn,
|
||||
ux,
|
||||
),
|
||||
workerErr,
|
||||
"should have seen a worker error, but did not",
|
||||
@@ -230,8 +230,8 @@ func Test_eventLoop_unsubscribeError(t *testing.T) {
|
||||
worker(),
|
||||
signaler(),
|
||||
subscription,
|
||||
ux,
|
||||
cleanupFn,
|
||||
ux,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -300,8 +300,8 @@ func Test_eventLoop_handlerError(t *testing.T) {
|
||||
worker(),
|
||||
signaler(),
|
||||
subscription,
|
||||
ux,
|
||||
cleanupFn,
|
||||
ux,
|
||||
),
|
||||
finalEvent.Error,
|
||||
"should have seen a event error, but did not",
|
||||
@@ -355,8 +355,8 @@ func Test_eventLoop_signalsStopExecution(t *testing.T) {
|
||||
worker(),
|
||||
signaler(),
|
||||
subscription,
|
||||
ux,
|
||||
cleanupFn,
|
||||
ux,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -425,8 +425,8 @@ func Test_eventLoop_uiTeardownError(t *testing.T) {
|
||||
worker(),
|
||||
signaler(),
|
||||
subscription,
|
||||
ux,
|
||||
cleanupFn,
|
||||
ux,
|
||||
),
|
||||
teardownError,
|
||||
"should have seen a UI teardown error, but did not",
|
||||
|
||||
+28
-7
@@ -35,7 +35,7 @@ const (
|
||||
|
||||
Supports the following image sources:
|
||||
{{.appName}} {{.command}} yourrepo/yourimage:tag defaults to using images from a Docker daemon. If Docker is not present, the image is pulled directly from the registry.
|
||||
{{.appName}} {{.command}} path/to/a/file/or/dir a Docker tar, OCI tar, OCI directory, or generic filesystem directory
|
||||
{{.appName}} {{.command}} path/to/a/file/or/dir a Docker tar, OCI tar, OCI directory, or generic filesystem directory
|
||||
|
||||
You can also explicitly specify the scheme to use:
|
||||
{{.appName}} {{.command}} docker:yourrepo/yourimage:tag explicitly use the Docker daemon
|
||||
@@ -102,7 +102,7 @@ func init() {
|
||||
}
|
||||
|
||||
func setPackageFlags(flags *pflag.FlagSet) {
|
||||
///////// Formatting & Input options //////////////////////////////////////////////
|
||||
// Formatting & Input options //////////////////////////////////////////////
|
||||
|
||||
flags.StringP(
|
||||
"scope", "s", source.SquashedScope.String(),
|
||||
@@ -113,7 +113,12 @@ func setPackageFlags(flags *pflag.FlagSet) {
|
||||
fmt.Sprintf("report output formatter, options=%v", packages.AllPresenters),
|
||||
)
|
||||
|
||||
///////// Upload options //////////////////////////////////////////////////////////
|
||||
flags.StringP(
|
||||
"file", "", "",
|
||||
"file to write the report output to (default is STDOUT)",
|
||||
)
|
||||
|
||||
// Upload options //////////////////////////////////////////////////////////
|
||||
flags.StringP(
|
||||
"host", "H", "",
|
||||
"the hostname or URL of the Anchore Enterprise instance to upload to",
|
||||
@@ -146,7 +151,7 @@ func setPackageFlags(flags *pflag.FlagSet) {
|
||||
}
|
||||
|
||||
func bindPackagesConfigOptions(flags *pflag.FlagSet) error {
|
||||
///////// Formatting & Input options //////////////////////////////////////////////
|
||||
// Formatting & Input options //////////////////////////////////////////////
|
||||
|
||||
if err := viper.BindPFlag("package.cataloger.scope", flags.Lookup("scope")); err != nil {
|
||||
return err
|
||||
@@ -156,7 +161,11 @@ func bindPackagesConfigOptions(flags *pflag.FlagSet) error {
|
||||
return err
|
||||
}
|
||||
|
||||
///////// Upload options //////////////////////////////////////////////////////////
|
||||
if err := viper.BindPFlag("file", flags.Lookup("file")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Upload options //////////////////////////////////////////////////////////
|
||||
|
||||
if err := viper.BindPFlag("anchore.host", flags.Lookup("host")); err != nil {
|
||||
return err
|
||||
@@ -188,12 +197,24 @@ func bindPackagesConfigOptions(flags *pflag.FlagSet) error {
|
||||
func packagesExec(_ *cobra.Command, args []string) error {
|
||||
// could be an image or a directory, with or without a scheme
|
||||
userInput := args[0]
|
||||
|
||||
reporter, closer, err := reportWriter()
|
||||
defer func() {
|
||||
if err := closer(); err != nil {
|
||||
log.Warnf("unable to write to report destination: %+v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return eventLoop(
|
||||
packagesExecWorker(userInput),
|
||||
setupSignals(),
|
||||
eventSubscription,
|
||||
ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet),
|
||||
stereoscope.Cleanup,
|
||||
ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet, reporter)...,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -282,7 +303,7 @@ func runPackageSbomUpload(src *source.Source, s source.Metadata, catalog *pkg.Ca
|
||||
}
|
||||
|
||||
if err := c.Import(context.Background(), importCfg); err != nil {
|
||||
return fmt.Errorf("failed to upload results to host=%s: %+v", appConfig.Anchore.Host, err)
|
||||
return fmt.Errorf("failed to upload results to host=%s: %w", appConfig.Anchore.Host, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
+14
-1
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/anchore/stereoscope"
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/presenter/poweruser"
|
||||
"github.com/anchore/syft/internal/ui"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
@@ -73,12 +74,24 @@ func init() {
|
||||
func powerUserExec(_ *cobra.Command, args []string) error {
|
||||
// could be an image or a directory, with or without a scheme
|
||||
userInput := args[0]
|
||||
|
||||
reporter, closer, err := reportWriter()
|
||||
defer func() {
|
||||
if err := closer(); err != nil {
|
||||
log.Warnf("unable to write to report destination: %+v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return eventLoop(
|
||||
powerUserExecWorker(userInput),
|
||||
setupSignals(),
|
||||
eventSubscription,
|
||||
ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet),
|
||||
stereoscope.Cleanup,
|
||||
ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet, reporter)...,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
)
|
||||
|
||||
func reportWriter() (io.Writer, func() error, error) {
|
||||
nop := func() error { return nil }
|
||||
|
||||
path := strings.TrimSpace(appConfig.File)
|
||||
switch len(path) {
|
||||
case 0:
|
||||
return os.Stdout, nop, nil
|
||||
default:
|
||||
reportFile, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o644)
|
||||
if err != nil {
|
||||
return nil, nop, fmt.Errorf("unable to create report file: %w", err)
|
||||
}
|
||||
return reportFile, func() error {
|
||||
log.Infof("report written to file=%q", path)
|
||||
return reportFile.Close()
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ var rootCmd = &cobra.Command{
|
||||
ValidArgsFunction: packagesCmd.ValidArgsFunction,
|
||||
}
|
||||
|
||||
// nolint:forbidigo
|
||||
func init() {
|
||||
// set universal flags
|
||||
rootCmd.PersistentFlags().StringVarP(&persistentOpts.ConfigPath, "config", "c", "", "application config file")
|
||||
|
||||
+1
-1
@@ -6,7 +6,6 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
|
||||
"github.com/anchore/syft/internal/version"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -24,6 +23,7 @@ func init() {
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
}
|
||||
|
||||
// nolint:forbidigo
|
||||
func printVersion(_ *cobra.Command, _ []string) {
|
||||
versionInfo := version.FromBuild()
|
||||
|
||||
|
||||
@@ -7,10 +7,11 @@ require (
|
||||
github.com/adrg/xdg v0.2.1
|
||||
github.com/alecthomas/jsonschema v0.0.0-20210301060011-54c507b6f074
|
||||
github.com/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf
|
||||
github.com/anchore/go-rpmdb v0.0.0-20210602151223-1f0f707a2894
|
||||
github.com/anchore/go-rpmdb v0.0.0-20210914181456-a9c52348da63
|
||||
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-20210817160504-0f4abc2a5a5a
|
||||
github.com/anchore/packageurl-go v0.0.0-20210922164639-b3fa992ebd29
|
||||
github.com/anchore/stereoscope v0.0.0-20211005213828-538011008578
|
||||
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
|
||||
@@ -24,7 +25,6 @@ require (
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/mitchellh/mapstructure v1.3.1
|
||||
github.com/olekukonko/tablewriter v0.0.4
|
||||
github.com/package-url/packageurl-go v0.1.0
|
||||
github.com/pelletier/go-toml v1.8.0
|
||||
github.com/pkg/profile v1.5.0
|
||||
github.com/scylladb/go-set v1.0.2
|
||||
@@ -47,5 +47,3 @@ require (
|
||||
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d
|
||||
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
|
||||
|
||||
@@ -109,16 +109,16 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf h1:DYssiUV1pBmKqzKsm4mqXx8artqC0Q8HgZsVI3lMsAg=
|
||||
github.com/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf/go.mod h1:FaODhIA06mxO1E6R32JE0TL1JWZZkmjRIAd4ULvHUKk=
|
||||
github.com/anchore/go-rpmdb v0.0.0-20210602151223-1f0f707a2894 h1:VvCq7fFNU8dwWkCTGUykm4p64nVaDCYkKrj87x350Sk=
|
||||
github.com/anchore/go-rpmdb v0.0.0-20210602151223-1f0f707a2894/go.mod h1:8jNYOxCJC5kyD/Ct4MbzsDN2hOhRoCAzQcb/7KdYYGw=
|
||||
github.com/anchore/go-rpmdb v0.0.0-20210914181456-a9c52348da63 h1:C9W/LAydEz/qdUhx1MdjO9l8NEcFKYknkxDVyo9LAoM=
|
||||
github.com/anchore/go-rpmdb v0.0.0-20210914181456-a9c52348da63/go.mod h1:6qH8c6U/3CBVvDDDBZnPSTbTINq3cIdADUYTaVf75EM=
|
||||
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 h1:VzprUTpc0vW0nnNKJfJieyH/TZ9UYAnTZs5/gHTdAe8=
|
||||
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/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/anchore/packageurl-go v0.0.0-20210922164639-b3fa992ebd29 h1:K9LfnxwhqvihqU0+MF325FNy7fsKV9EGaUxdfR4gnWk=
|
||||
github.com/anchore/packageurl-go v0.0.0-20210922164639-b3fa992ebd29/go.mod h1:Oc1UkGaJwY6ND6vtAqPSlYrptKRJngHwkwB6W7l1uP0=
|
||||
github.com/anchore/stereoscope v0.0.0-20211005213828-538011008578 h1:gSpftl0RWfdTwlPmsOLgEawHIw16xwwG1mFD/wrVDRE=
|
||||
github.com/anchore/stereoscope v0.0.0-20211005213828-538011008578/go.mod h1:kL7jfbXblrDcBhu5ja/s+VTYL3Mzof4eQNMJiSqcwXQ=
|
||||
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=
|
||||
@@ -242,6 +242,8 @@ github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHqu
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/gabriel-vasile/mimetype v1.3.0 h1:4YOHITFLyYwF+iqG0ybSLGArRItynpfwdlWRmJnd75E=
|
||||
github.com/gabriel-vasile/mimetype v1.3.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8=
|
||||
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-critic/go-critic v0.4.1/go.mod h1:7/14rZGnZbY6E38VEGk2kVhoq6itzc1E68facVDK23g=
|
||||
@@ -267,8 +269,8 @@ github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8
|
||||
github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
|
||||
github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-restruct/restruct v0.0.0-20191227155143-5734170a48a1 h1:LoN2wx/aN8JPGebG+2DaUyk4M+xRcqJXfuIbs8AWHdE=
|
||||
github.com/go-restruct/restruct v0.0.0-20191227155143-5734170a48a1/go.mod h1:KqrpKpn4M8OLznErihXTGLlsXFGeLxHUrLRRI/1YjGk=
|
||||
github.com/go-restruct/restruct v1.2.0-alpha h1:2Lp474S/9660+SJjpVxoKuWX09JsXHSrdV7Nv3/gkvc=
|
||||
github.com/go-restruct/restruct v1.2.0-alpha/go.mod h1:KqrpKpn4M8OLznErihXTGLlsXFGeLxHUrLRRI/1YjGk=
|
||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
@@ -872,7 +874,7 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d h1:LO7XpTYMwTqxjLcGWPijK3vRXg1aWdlNOVOHRq45d7c=
|
||||
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
@@ -939,7 +941,6 @@ golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
||||
@@ -6,8 +6,6 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/antihax/optional"
|
||||
|
||||
"github.com/anchore/client-go/pkg/external"
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
@@ -15,6 +13,7 @@ import (
|
||||
"github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/antihax/optional"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
)
|
||||
@@ -64,7 +63,7 @@ func (c *Client) Import(ctx context.Context, cfg ImportConfig) error {
|
||||
stage.Current = "starting session"
|
||||
startOperation, _, err := c.client.ImportsApi.CreateOperation(authedCtx)
|
||||
if err != nil {
|
||||
var detail = "no details given"
|
||||
detail := "no details given"
|
||||
var openAPIErr external.GenericOpenAPIError
|
||||
if errors.As(err, &openAPIErr) {
|
||||
detail = string(openAPIErr.Body())
|
||||
@@ -106,7 +105,7 @@ func (c *Client) Import(ctx context.Context, cfg ImportConfig) error {
|
||||
|
||||
_, _, err = c.client.ImagesApi.AddImage(authedCtx, imageModel, &opts)
|
||||
if err != nil {
|
||||
var detail = "no details given"
|
||||
detail := "no details given"
|
||||
var openAPIErr external.GenericOpenAPIError
|
||||
if errors.As(err, &openAPIErr) {
|
||||
detail = string(openAPIErr.Body())
|
||||
@@ -122,7 +121,7 @@ func (c *Client) Import(ctx context.Context, cfg ImportConfig) error {
|
||||
}
|
||||
|
||||
func addImageModel(imageMetadata image.Metadata, packageDigest, manifestDigest, dockerfileDigest, configDigest, sessionID string) external.ImageAnalysisRequest {
|
||||
var tags = make([]string, len(imageMetadata.Tags))
|
||||
tags := make([]string, len(imageMetadata.Tags))
|
||||
for i, t := range imageMetadata.Tags {
|
||||
tags[i] = t.String()
|
||||
}
|
||||
|
||||
@@ -8,10 +8,9 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/client-go/pkg/external"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/wagoodman/go-progress"
|
||||
)
|
||||
|
||||
type configImportAPI interface {
|
||||
|
||||
@@ -6,11 +6,9 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
|
||||
"github.com/anchore/client-go/pkg/external"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/wagoodman/go-progress"
|
||||
)
|
||||
|
||||
type dockerfileImportAPI interface {
|
||||
|
||||
@@ -8,10 +8,9 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/client-go/pkg/external"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/wagoodman/go-progress"
|
||||
)
|
||||
|
||||
type manifestImportAPI interface {
|
||||
|
||||
@@ -8,16 +8,13 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/anchore/syft/internal/presenter/packages"
|
||||
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/syft/syft/distro"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
|
||||
"github.com/anchore/client-go/pkg/external"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/presenter/packages"
|
||||
"github.com/anchore/syft/syft/distro"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/wagoodman/go-progress"
|
||||
)
|
||||
|
||||
type packageSBOMImportAPI interface {
|
||||
|
||||
+4
-2
@@ -15,8 +15,10 @@ package bus
|
||||
|
||||
import "github.com/wagoodman/go-partybus"
|
||||
|
||||
var publisher partybus.Publisher
|
||||
var active bool
|
||||
var (
|
||||
publisher partybus.Publisher
|
||||
active bool
|
||||
)
|
||||
|
||||
// SetPublisher sets the singleton event bus publisher. This is optional; if no bus is provided, the library will
|
||||
// behave no differently than if a bus had been provided.
|
||||
|
||||
@@ -29,6 +29,7 @@ type parser interface {
|
||||
type Application struct {
|
||||
ConfigPath string `yaml:",omitempty" json:"configPath"` // the location where the application config was read from (either from -c or discovered while loading)
|
||||
Output string `yaml:"output" json:"output" mapstructure:"output"` // -o, the Presenter hint string to use for report formatting
|
||||
File string `yaml:"file" json:"file" mapstructure:"file"` // --file, the file to write report output to
|
||||
Quiet bool `yaml:"quiet" json:"quiet" mapstructure:"quiet"` // -q, indicates to not show any status output to stderr (ETUI or logging UI)
|
||||
CheckForAppUpdate bool `yaml:"check-for-app-update" json:"check-for-app-update" mapstructure:"check-for-app-update"` // whether to check for an application update on start up or not
|
||||
Anchore anchore `yaml:"anchore" json:"anchore" mapstructure:"anchore"` // options for interacting with Anchore Engine/Enterprise
|
||||
@@ -143,7 +144,6 @@ func (cfg *Application) parseConfigValues() error {
|
||||
func (cfg Application) String() string {
|
||||
// yaml is pretty human friendly (at least when compared to json)
|
||||
appCfgStr, err := yaml.Marshal(&cfg)
|
||||
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
@@ -58,7 +57,7 @@ func hasNonEmptyCredentials(username, password, token string) bool {
|
||||
}
|
||||
|
||||
func (cfg *registry) ToOptions() *image.RegistryOptions {
|
||||
var auth = make([]image.RegistryCredentials, len(cfg.Auth))
|
||||
auth := make([]image.RegistryCredentials, len(cfg.Auth))
|
||||
for i, a := range cfg.Auth {
|
||||
auth[i] = image.RegistryCredentials{
|
||||
Authority: a.Authority,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -15,24 +16,23 @@ func CloseAndLogError(closer io.Closer, location string) {
|
||||
}
|
||||
}
|
||||
|
||||
type ErrPath struct {
|
||||
type PathError struct {
|
||||
Path string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e ErrPath) Error() string {
|
||||
func (e PathError) Error() string {
|
||||
return fmt.Sprintf("unable to observe contents of %+v: %v", e.Path, e.Err)
|
||||
}
|
||||
|
||||
func IsErrPath(err error) bool {
|
||||
_, ok := err.(ErrPath)
|
||||
return ok
|
||||
func IsPathError(err error) bool {
|
||||
return errors.As(err, &PathError{})
|
||||
}
|
||||
|
||||
func IsErrPathPermission(err error) bool {
|
||||
pathErr, ok := err.(ErrPath)
|
||||
if ok {
|
||||
var pathErr *PathError
|
||||
if errors.As(err, pathErr) {
|
||||
return os.IsPermission(pathErr.Err)
|
||||
}
|
||||
return ok
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -24,12 +24,12 @@ const (
|
||||
|
||||
const perFileReadLimit = 2 * GB
|
||||
|
||||
type errZipSlipDetected struct {
|
||||
type zipSlipDetectedError struct {
|
||||
Prefix string
|
||||
JoinArgs []string
|
||||
}
|
||||
|
||||
func (e *errZipSlipDetected) Error() string {
|
||||
func (e *zipSlipDetectedError) Error() string {
|
||||
return fmt.Sprintf("paths are not allowed to resolve outside of the root prefix (%q). Destination: %q", e.Prefix, e.JoinArgs)
|
||||
}
|
||||
|
||||
@@ -197,7 +197,7 @@ func safeJoin(prefix string, dest ...string) (string, error) {
|
||||
joinResult := filepath.Join(append([]string{prefix}, dest...)...)
|
||||
cleanJoinResult := filepath.Clean(joinResult)
|
||||
if !strings.HasPrefix(cleanJoinResult, filepath.Clean(prefix)) {
|
||||
return "", &errZipSlipDetected{
|
||||
return "", &zipSlipDetectedError{
|
||||
Prefix: prefix,
|
||||
JoinArgs: dest,
|
||||
}
|
||||
|
||||
@@ -299,7 +299,7 @@ func TestSafeJoin(t *testing.T) {
|
||||
"../../../etc/passwd",
|
||||
},
|
||||
expected: "",
|
||||
errAssertion: assertErrorAs(&errZipSlipDetected{}),
|
||||
errAssertion: assertErrorAs(&zipSlipDetectedError{}),
|
||||
},
|
||||
{
|
||||
prefix: "/a/place",
|
||||
@@ -308,7 +308,7 @@ func TestSafeJoin(t *testing.T) {
|
||||
"../",
|
||||
},
|
||||
expected: "",
|
||||
errAssertion: assertErrorAs(&errZipSlipDetected{}),
|
||||
errAssertion: assertErrorAs(&zipSlipDetectedError{}),
|
||||
},
|
||||
{
|
||||
prefix: "/a/place",
|
||||
@@ -316,7 +316,7 @@ func TestSafeJoin(t *testing.T) {
|
||||
"../",
|
||||
},
|
||||
expected: "",
|
||||
errAssertion: assertErrorAs(&errZipSlipDetected{}),
|
||||
errAssertion: assertErrorAs(&zipSlipDetectedError{}),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package file
|
||||
import (
|
||||
"archive/zip"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -91,7 +92,7 @@ func findArchiveStartOffset(r io.ReaderAt, size int64) (startOfArchive uint64, e
|
||||
bLen = size
|
||||
}
|
||||
buf = make([]byte, int(bLen))
|
||||
if _, err := r.ReadAt(buf, size-bLen); err != nil && err != io.EOF {
|
||||
if _, err := r.ReadAt(buf, size-bLen); err != nil && !errors.Is(err, io.EOF) {
|
||||
return 0, err
|
||||
}
|
||||
if p := findSignatureInBlock(buf); p >= 0 {
|
||||
|
||||
@@ -3,6 +3,7 @@ package logger
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
@@ -10,6 +11,8 @@ import (
|
||||
prefixed "github.com/x-cray/logrus-prefixed-formatter"
|
||||
)
|
||||
|
||||
const defaultLogFilePermissions fs.FileMode = 0o644
|
||||
|
||||
// LogrusConfig contains all configurable values for the Logrus logger
|
||||
type LogrusConfig struct {
|
||||
EnableConsole bool
|
||||
@@ -38,7 +41,7 @@ func NewLogrusLogger(cfg LogrusConfig) *LogrusLogger {
|
||||
var output io.Writer
|
||||
switch {
|
||||
case cfg.EnableConsole && cfg.EnableFile:
|
||||
logFile, err := os.OpenFile(cfg.FileLocation, os.O_WRONLY|os.O_CREATE, 0755)
|
||||
logFile, err := os.OpenFile(cfg.FileLocation, os.O_WRONLY|os.O_CREATE, defaultLogFilePermissions)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("unable to setup log file: %w", err))
|
||||
}
|
||||
@@ -46,7 +49,7 @@ func NewLogrusLogger(cfg LogrusConfig) *LogrusLogger {
|
||||
case cfg.EnableConsole:
|
||||
output = os.Stderr
|
||||
case cfg.EnableFile:
|
||||
logFile, err := os.OpenFile(cfg.FileLocation, os.O_WRONLY|os.O_CREATE, 0755)
|
||||
logFile, err := os.OpenFile(cfg.FileLocation, os.O_WRONLY|os.O_CREATE, defaultLogFilePermissions)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("unable to setup log file: %w", err))
|
||||
}
|
||||
|
||||
@@ -38,18 +38,18 @@ func NewJSONPackages(catalog *pkg.Catalog) ([]JSONPackage, error) {
|
||||
|
||||
// NewJSONPackage crates a new JSONPackage from the given pkg.Package.
|
||||
func NewJSONPackage(p *pkg.Package) (JSONPackage, error) {
|
||||
var cpes = make([]string, len(p.CPEs))
|
||||
cpes := make([]string, len(p.CPEs))
|
||||
for i, c := range p.CPEs {
|
||||
cpes[i] = c.BindToFmtString()
|
||||
}
|
||||
|
||||
// ensure collections are never nil for presentation reasons
|
||||
var locations = make([]source.Location, 0)
|
||||
locations := make([]source.Location, 0)
|
||||
if p.Locations != nil {
|
||||
locations = p.Locations
|
||||
}
|
||||
|
||||
var licenses = make([]string, 0)
|
||||
licenses := make([]string, 0)
|
||||
if p.Licenses != nil {
|
||||
licenses = p.Licenses
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ type CreationInfo struct {
|
||||
Created time.Time `json:"created"`
|
||||
// Identify who (or what, in the case of a tool) created the SPDX file. If the SPDX file was created by an
|
||||
// individual, indicate the person's name. If the SPDX file was created on behalf of a company or organization,
|
||||
//indicate the entity name. If the SPDX file was created using a software tool, indicate the name and version
|
||||
// indicate the entity name. If the SPDX file was created using a software tool, indicate the name and version
|
||||
// for that tool. If multiple participants or tools were involved, use multiple instances of this field. Person
|
||||
// name or organization name may be designated as “anonymous” if appropriate.
|
||||
Creators []string `json:"creators"`
|
||||
|
||||
@@ -40,4 +40,6 @@ type Document struct {
|
||||
Files []File `json:"files,omitempty"`
|
||||
// Snippets referenced in the SPDX document
|
||||
Snippets []Snippet `json:"snippets,omitempty"`
|
||||
// Relationships referenced in the SPDX document
|
||||
Relationships []Relationship `json:"relationships,omitempty"`
|
||||
}
|
||||
|
||||
@@ -20,22 +20,22 @@ type File struct {
|
||||
Item
|
||||
// (At least one is required.) The checksum property provides a mechanism that can be used to verify that the
|
||||
// contents of a File or Package have not changed.
|
||||
Checksums []Checksum `json:"checksums"`
|
||||
Checksums []Checksum `json:"checksums,omitempty"`
|
||||
// This field provides a place for the SPDX file creator to record file contributors. Contributors could include
|
||||
// names of copyright holders and/or authors who may not be copyright holders yet contributed to the file content.
|
||||
FileContributors []string `json:"fileContributors"`
|
||||
FileContributors []string `json:"fileContributors,omitempty"`
|
||||
// Each element is a SPDX ID for a File.
|
||||
FileDependencies []string `json:"fileDependencies"`
|
||||
FileDependencies []string `json:"fileDependencies,omitempty"`
|
||||
// The name of the file relative to the root of the package.
|
||||
FileName string `json:"fileName"`
|
||||
// The type of the file
|
||||
FileTypes []string `json:"fileTypes"`
|
||||
FileTypes []string `json:"fileTypes,omitempty"`
|
||||
// This field provides a place for the SPDX file creator to record potential legal notices found in the file.
|
||||
// This may or may not include copyright statements.
|
||||
NoticeText string `json:"noticeText,omitempty"`
|
||||
// Indicates the project in which the SpdxElement originated. Tools must preserve doap:homepage and doap:name
|
||||
// properties and the URI (if one is known) of doap:Project resources that are values of this property. All other
|
||||
// properties of doap:Projects are not directly supported by SPDX and may be dropped when translating to or
|
||||
// from some SPDX formats.
|
||||
ArtifactOf []string `json:"artifactOf"`
|
||||
// from some SPDX formats(deprecated).
|
||||
ArtifactOf []string `json:"artifactOf,omitempty"`
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package spdx22
|
||||
|
||||
type Relationship struct {
|
||||
// SPDX ID for SpdxElement. A related SpdxElement.
|
||||
RelatedSpdxElement string `json:"relatedSpdxElement"`
|
||||
// Id to which the SPDX element is related
|
||||
SpdxElementID string `json:"spdxElementId"`
|
||||
// Describes the type of relationship between two SPDX elements.
|
||||
RelationshipType RelationshipType `json:"relationshipType"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
// SPDX ID for SpdxElement. A related SpdxElement.
|
||||
RelatedSpdxElement string `json:"relatedSpdxElement"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// source: https://spdx.github.io/spdx-spec/7-relationships-between-SPDX-elements/
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package packages
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/internal/presenter/packages/model/spdx22"
|
||||
@@ -29,6 +31,43 @@ func getSPDXExternalRefs(p *pkg.Package) (externalRefs []spdx22.ExternalRef) {
|
||||
return externalRefs
|
||||
}
|
||||
|
||||
func getSPDXFiles(packageSpdxID string, p *pkg.Package) (files []spdx22.File, fileIDs []string, relationships []spdx22.Relationship) {
|
||||
files = make([]spdx22.File, 0)
|
||||
fileIDs = make([]string, 0)
|
||||
relationships = make([]spdx22.Relationship, 0)
|
||||
|
||||
pkgFileOwner, ok := p.Metadata.(pkg.FileOwner)
|
||||
if !ok {
|
||||
return files, fileIDs, relationships
|
||||
}
|
||||
|
||||
for _, ownedFilePath := range pkgFileOwner.OwnedFiles() {
|
||||
baseFileName := filepath.Base(ownedFilePath)
|
||||
pathHash := sha256.Sum256([]byte(ownedFilePath))
|
||||
fileSpdxID := spdx22.ElementID(fmt.Sprintf("File-%s-%x", p.Name, pathHash)).String()
|
||||
|
||||
fileIDs = append(fileIDs, fileSpdxID)
|
||||
|
||||
files = append(files, spdx22.File{
|
||||
FileName: ownedFilePath,
|
||||
Item: spdx22.Item{
|
||||
Element: spdx22.Element{
|
||||
SPDXID: fileSpdxID,
|
||||
Name: baseFileName,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
relationships = append(relationships, spdx22.Relationship{
|
||||
SpdxElementID: packageSpdxID,
|
||||
RelationshipType: spdx22.ContainsRelationship,
|
||||
RelatedSpdxElement: fileSpdxID,
|
||||
})
|
||||
}
|
||||
|
||||
return files, fileIDs, relationships
|
||||
}
|
||||
|
||||
func getSPDXLicense(p *pkg.Package) string {
|
||||
// source: https://spdx.github.io/spdx-spec/3-package-information/#313-concluded-license
|
||||
// The options to populate this field are limited to:
|
||||
@@ -96,7 +135,7 @@ func getSPDXHomepage(p *pkg.Package) string {
|
||||
}
|
||||
|
||||
func getSPDXSourceInfo(p *pkg.Package) string {
|
||||
answer := ""
|
||||
var answer string
|
||||
switch p.Type {
|
||||
case pkg.RpmPkg:
|
||||
answer = "acquired package info from RPM DB"
|
||||
|
||||
@@ -95,6 +95,15 @@ func Test_getSPDXLicense(t *testing.T) {
|
||||
},
|
||||
expected: "GPL-3.0",
|
||||
},
|
||||
{
|
||||
name: "debian to spdx conversion",
|
||||
input: pkg.Package{
|
||||
Licenses: []string{
|
||||
"GPL-2",
|
||||
},
|
||||
},
|
||||
expected: "GPL-2.0",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
@@ -12,8 +14,11 @@ import (
|
||||
"github.com/anchore/syft/internal/version"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const anchoreNamespace = "https://anchore.com/syft"
|
||||
|
||||
// SPDXJsonPresenter is a SPDX presentation object for the syft results (see https://github.com/spdx/spdx-spec)
|
||||
type SPDXJsonPresenter struct {
|
||||
catalog *pkg.Catalog
|
||||
@@ -39,15 +44,29 @@ func (pres *SPDXJsonPresenter) Present(output io.Writer) error {
|
||||
return enc.Encode(&doc)
|
||||
}
|
||||
|
||||
// newSPDXJsonDocument creates and populates a new JSON document struct that follows the SPDX 2.2 spec from the given cataloging results.
|
||||
func newSPDXJsonDocument(catalog *pkg.Catalog, srcMetadata source.Metadata) spdx22.Document {
|
||||
var name string
|
||||
uniqueID := uuid.Must(uuid.NewRandom())
|
||||
|
||||
var name, input, identifier string
|
||||
switch srcMetadata.Scheme {
|
||||
case source.ImageScheme:
|
||||
name = srcMetadata.ImageMetadata.UserInput
|
||||
name = cleanSPDXName(srcMetadata.ImageMetadata.UserInput)
|
||||
input = "image"
|
||||
case source.DirectoryScheme:
|
||||
name = srcMetadata.Path
|
||||
name = cleanSPDXName(srcMetadata.Path)
|
||||
input = "dir"
|
||||
}
|
||||
|
||||
if name != "." {
|
||||
identifier = path.Join(input, fmt.Sprintf("%s-%s", name, uniqueID.String()))
|
||||
} else {
|
||||
identifier = path.Join(input, uniqueID.String())
|
||||
}
|
||||
|
||||
namespace := path.Join(anchoreNamespace, identifier)
|
||||
packages, files, relationships := newSPDXJsonElements(catalog)
|
||||
|
||||
return spdx22.Document{
|
||||
Element: spdx22.Element{
|
||||
SPDXID: spdx22.ElementID("DOCUMENT").String(),
|
||||
@@ -64,23 +83,35 @@ func newSPDXJsonDocument(catalog *pkg.Catalog, srcMetadata source.Metadata) spdx
|
||||
LicenseListVersion: spdxlicense.Version,
|
||||
},
|
||||
DataLicense: "CC0-1.0",
|
||||
DocumentNamespace: fmt.Sprintf("https://anchore.com/syft/image/%s", srcMetadata.ImageMetadata.UserInput),
|
||||
Packages: newSPDXJsonPackages(catalog),
|
||||
DocumentNamespace: namespace,
|
||||
Packages: packages,
|
||||
Files: files,
|
||||
Relationships: relationships,
|
||||
}
|
||||
}
|
||||
|
||||
func newSPDXJsonPackages(catalog *pkg.Catalog) []spdx22.Package {
|
||||
results := make([]spdx22.Package, 0)
|
||||
func newSPDXJsonElements(catalog *pkg.Catalog) ([]spdx22.Package, []spdx22.File, []spdx22.Relationship) {
|
||||
packages := make([]spdx22.Package, 0)
|
||||
relationships := make([]spdx22.Relationship, 0)
|
||||
files := make([]spdx22.File, 0)
|
||||
|
||||
for _, p := range catalog.Sorted() {
|
||||
license := getSPDXLicense(p)
|
||||
packageSpdxID := spdx22.ElementID(fmt.Sprintf("Package-%+v-%s-%s", p.Type, p.Name, p.Version)).String()
|
||||
|
||||
packageFiles, fileIDs, packageFileRelationships := getSPDXFiles(packageSpdxID, p)
|
||||
files = append(files, packageFiles...)
|
||||
|
||||
relationships = append(relationships, packageFileRelationships...)
|
||||
|
||||
// note: the license concluded and declared should be the same since we are collecting license information
|
||||
// from the project data itself (the installed package files).
|
||||
results = append(results, spdx22.Package{
|
||||
packages = append(packages, spdx22.Package{
|
||||
Description: getSPDXDescription(p),
|
||||
DownloadLocation: getSPDXDownloadLocation(p),
|
||||
ExternalRefs: getSPDXExternalRefs(p),
|
||||
FilesAnalyzed: false,
|
||||
HasFiles: fileIDs,
|
||||
Homepage: getSPDXHomepage(p),
|
||||
LicenseDeclared: license, // The Declared License is what the authors of a project believe govern the package
|
||||
Originator: getSPDXOriginator(p),
|
||||
@@ -89,11 +120,23 @@ func newSPDXJsonPackages(catalog *pkg.Catalog) []spdx22.Package {
|
||||
Item: spdx22.Item{
|
||||
LicenseConcluded: license, // The Concluded License field is the license the SPDX file creator believes governs the package
|
||||
Element: spdx22.Element{
|
||||
SPDXID: spdx22.ElementID(fmt.Sprintf("Package-%+v-%s-%s", p.Type, p.Name, p.Version)).String(),
|
||||
SPDXID: packageSpdxID,
|
||||
Name: p.Name,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
return results
|
||||
|
||||
return packages, files, relationships
|
||||
}
|
||||
|
||||
func cleanSPDXName(name string) string {
|
||||
// remove # according to specification
|
||||
name = strings.ReplaceAll(name, "#", "-")
|
||||
|
||||
// remove : for url construction
|
||||
name = strings.ReplaceAll(name, ":", "-")
|
||||
|
||||
// clean relative pathing
|
||||
return path.Clean(name)
|
||||
}
|
||||
|
||||
@@ -31,6 +31,10 @@ func TestSPDXJSONImagePresenter(t *testing.T) {
|
||||
func spdxJsonRedactor(s []byte) []byte {
|
||||
// each SBOM reports the time it was generated, which is not useful during snapshot testing
|
||||
s = regexp.MustCompile(`"created": .*`).ReplaceAll(s, []byte("redacted"))
|
||||
|
||||
// each SBOM reports a unique documentNamespace when generated, this is not useful for snapshot testing
|
||||
s = regexp.MustCompile(`"documentNamespace": .*`).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"))
|
||||
}
|
||||
|
||||
@@ -5,9 +5,8 @@ import (
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/anchore/syft/internal/spdxlicense"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/spdxlicense"
|
||||
"github.com/anchore/syft/internal/version"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
|
||||
@@ -6,9 +6,8 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
)
|
||||
|
||||
type TablePresenter struct {
|
||||
|
||||
@@ -27,6 +27,11 @@
|
||||
"author": "",
|
||||
"authorEmail": "",
|
||||
"platform": "",
|
||||
"files": [
|
||||
{
|
||||
"path": "/some/path/pkg1/depedencies/foo"
|
||||
}
|
||||
],
|
||||
"sitePackagesRootPath": ""
|
||||
}
|
||||
},
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"locations": [
|
||||
{
|
||||
"path": "/somefile-1.txt",
|
||||
"layerID": "sha256:fb6beecb75b39f4bb813dbf177e501edd5ddb3e69bb45cedeb78c676ee1b7a59"
|
||||
"layerID": "sha256:ffb5e9eaa453a002110719d12c294960117ca2903953d1faa40f01dc3f77045c"
|
||||
}
|
||||
],
|
||||
"licenses": [
|
||||
@@ -40,7 +40,7 @@
|
||||
"locations": [
|
||||
{
|
||||
"path": "/somefile-2.txt",
|
||||
"layerID": "sha256:319b588ce64253a87b533c8ed01cf0025e0eac98e7b516e12532957e1244fdec"
|
||||
"layerID": "sha256:8463854829fc53d47b9dcdf7ee79fe7eb4ca7933c910f67f8521412f7a2f5c21"
|
||||
}
|
||||
],
|
||||
"licenses": [],
|
||||
@@ -67,7 +67,7 @@
|
||||
"type": "image",
|
||||
"target": {
|
||||
"userInput": "user-image-input",
|
||||
"imageID": "sha256:5900c94a5bc1e083aa24ad1a223bf6eb9910dc8a6b01cb979ec306cb91709ea1",
|
||||
"imageID": "sha256:112851310e48e604f7379e2a3acddab50e91ce926edacb598a532e60ff6b776a",
|
||||
"manifestDigest": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368",
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"tags": [
|
||||
@@ -77,17 +77,17 @@
|
||||
"layers": [
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"digest": "sha256:fb6beecb75b39f4bb813dbf177e501edd5ddb3e69bb45cedeb78c676ee1b7a59",
|
||||
"digest": "sha256:ffb5e9eaa453a002110719d12c294960117ca2903953d1faa40f01dc3f77045c",
|
||||
"size": 22
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"digest": "sha256:319b588ce64253a87b533c8ed01cf0025e0eac98e7b516e12532957e1244fdec",
|
||||
"digest": "sha256:8463854829fc53d47b9dcdf7ee79fe7eb4ca7933c910f67f8521412f7a2f5c21",
|
||||
"size": 16
|
||||
}
|
||||
],
|
||||
"manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjo2NjUsImRpZ2VzdCI6InNoYTI1Njo1OTAwYzk0YTViYzFlMDgzYWEyNGFkMWEyMjNiZjZlYjk5MTBkYzhhNmIwMWNiOTc5ZWMzMDZjYjkxNzA5ZWExIn0sImxheWVycyI6W3sibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjIwNDgsImRpZ2VzdCI6InNoYTI1NjpmYjZiZWVjYjc1YjM5ZjRiYjgxM2RiZjE3N2U1MDFlZGQ1ZGRiM2U2OWJiNDVjZWRlYjc4YzY3NmVlMWI3YTU5In0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6MjA0OCwiZGlnZXN0Ijoic2hhMjU2OjMxOWI1ODhjZTY0MjUzYTg3YjUzM2M4ZWQwMWNmMDAyNWUwZWFjOThlN2I1MTZlMTI1MzI5NTdlMTI0NGZkZWMifV19",
|
||||
"config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiV29ya2luZ0RpciI6Ii8iLCJPbkJ1aWxkIjpudWxsfSwiY3JlYXRlZCI6IjIwMjEtMDQtMDZUMTk6MTM6NTIuNTI0Mzc4WiIsImhpc3RvcnkiOlt7ImNyZWF0ZWQiOiIyMDIxLTA0LTA2VDE5OjEzOjUyLjQ1ODIwNjFaIiwiY3JlYXRlZF9ieSI6IkFERCBmaWxlLTEudHh0IC9zb21lZmlsZS0xLnR4dCAjIGJ1aWxka2l0IiwiY29tbWVudCI6ImJ1aWxka2l0LmRvY2tlcmZpbGUudjAifSx7ImNyZWF0ZWQiOiIyMDIxLTA0LTA2VDE5OjEzOjUyLjUyNDM3OFoiLCJjcmVhdGVkX2J5IjoiQUREIGZpbGUtMi50eHQgL3NvbWVmaWxlLTIudHh0ICMgYnVpbGRraXQiLCJjb21tZW50IjoiYnVpbGRraXQuZG9ja2VyZmlsZS52MCJ9XSwib3MiOiJsaW51eCIsInJvb3RmcyI6eyJ0eXBlIjoibGF5ZXJzIiwiZGlmZl9pZHMiOlsic2hhMjU2OmZiNmJlZWNiNzViMzlmNGJiODEzZGJmMTc3ZTUwMWVkZDVkZGIzZTY5YmI0NWNlZGViNzhjNjc2ZWUxYjdhNTkiLCJzaGEyNTY6MzE5YjU4OGNlNjQyNTNhODdiNTMzYzhlZDAxY2YwMDI1ZTBlYWM5OGU3YjUxNmUxMjUzMjk1N2UxMjQ0ZmRlYyJdfX0=",
|
||||
"manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjo2NzMsImRpZ2VzdCI6InNoYTI1NjoxMTI4NTEzMTBlNDhlNjA0ZjczNzllMmEzYWNkZGFiNTBlOTFjZTkyNmVkYWNiNTk4YTUzMmU2MGZmNmI3NzZhIn0sImxheWVycyI6W3sibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjIwNDgsImRpZ2VzdCI6InNoYTI1NjpmZmI1ZTllYWE0NTNhMDAyMTEwNzE5ZDEyYzI5NDk2MDExN2NhMjkwMzk1M2QxZmFhNDBmMDFkYzNmNzcwNDVjIn0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6MjA0OCwiZGlnZXN0Ijoic2hhMjU2Ojg0NjM4NTQ4MjlmYzUzZDQ3YjlkY2RmN2VlNzlmZTdlYjRjYTc5MzNjOTEwZjY3Zjg1MjE0MTJmN2EyZjVjMjEifV19",
|
||||
"config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiV29ya2luZ0RpciI6Ii8iLCJPbkJ1aWxkIjpudWxsfSwiY3JlYXRlZCI6IjIwMjEtMDktMDhUMTc6MjE6NTguODk2NTI5MTkyWiIsImhpc3RvcnkiOlt7ImNyZWF0ZWQiOiIyMDIxLTA5LTA4VDE3OjIxOjU4Ljg3OTY5MDgyNFoiLCJjcmVhdGVkX2J5IjoiQUREIGZpbGUtMS50eHQgL3NvbWVmaWxlLTEudHh0ICMgYnVpbGRraXQiLCJjb21tZW50IjoiYnVpbGRraXQuZG9ja2VyZmlsZS52MCJ9LHsiY3JlYXRlZCI6IjIwMjEtMDktMDhUMTc6MjE6NTguODk2NTI5MTkyWiIsImNyZWF0ZWRfYnkiOiJBREQgZmlsZS0yLnR4dCAvc29tZWZpbGUtMi50eHQgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn1dLCJvcyI6ImxpbnV4Iiwicm9vdGZzIjp7InR5cGUiOiJsYXllcnMiLCJkaWZmX2lkcyI6WyJzaGEyNTY6ZmZiNWU5ZWFhNDUzYTAwMjExMDcxOWQxMmMyOTQ5NjAxMTdjYTI5MDM5NTNkMWZhYTQwZjAxZGMzZjc3MDQ1YyIsInNoYTI1Njo4NDYzODU0ODI5ZmM1M2Q0N2I5ZGNkZjdlZTc5ZmU3ZWI0Y2E3OTMzYzkxMGY2N2Y4NTIxNDEyZjdhMmY1YzIxIl19fQ==",
|
||||
"repoDigests": [],
|
||||
"scope": "Squashed"
|
||||
}
|
||||
|
||||
+20
-2
@@ -3,12 +3,12 @@
|
||||
"name": "/some/path",
|
||||
"spdxVersion": "SPDX-2.2",
|
||||
"creationInfo": {
|
||||
"created": "2021-06-23T17:48:32.734847Z",
|
||||
"created": "2021-09-16T20:44:35.198887Z",
|
||||
"creators": [
|
||||
"Organization: Anchore, Inc",
|
||||
"Tool: syft-[not provided]"
|
||||
],
|
||||
"licenseListVersion": "3.13"
|
||||
"licenseListVersion": "3.14"
|
||||
},
|
||||
"dataLicense": "CC0-1.0",
|
||||
"documentNamespace": "https://anchore.com/syft/image/",
|
||||
@@ -31,6 +31,9 @@
|
||||
}
|
||||
],
|
||||
"filesAnalyzed": false,
|
||||
"hasFiles": [
|
||||
"SPDXRef-File-package-1-04cd22424378dcd6c77fce08beb52493b5494a60ea5e1f9bdf9b16dc0cacffe9"
|
||||
],
|
||||
"licenseDeclared": "MIT",
|
||||
"sourceInfo": "acquired package info from installed python package manifest file: /some/path/pkg1",
|
||||
"versionInfo": "1.0.1"
|
||||
@@ -57,5 +60,20 @@
|
||||
"sourceInfo": "acquired package info from DPKG DB: /some/path/pkg1",
|
||||
"versionInfo": "2.0.1"
|
||||
}
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"SPDXID": "SPDXRef-File-package-1-04cd22424378dcd6c77fce08beb52493b5494a60ea5e1f9bdf9b16dc0cacffe9",
|
||||
"name": "foo",
|
||||
"licenseConcluded": "",
|
||||
"fileName": "/some/path/pkg1/depedencies/foo"
|
||||
}
|
||||
],
|
||||
"relationships": [
|
||||
{
|
||||
"spdxElementId": "SPDXRef-Package-python-package-1-1.0.1",
|
||||
"relationshipType": "CONTAINS",
|
||||
"relatedSpdxElement": "SPDXRef-File-package-1-04cd22424378dcd6c77fce08beb52493b5494a60ea5e1f9bdf9b16dc0cacffe9"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+2
-2
@@ -3,12 +3,12 @@
|
||||
"name": "user-image-input",
|
||||
"spdxVersion": "SPDX-2.2",
|
||||
"creationInfo": {
|
||||
"created": "2021-06-23T17:48:32.7379Z",
|
||||
"created": "2021-09-16T20:44:35.203911Z",
|
||||
"creators": [
|
||||
"Organization: Anchore, Inc",
|
||||
"Tool: syft-[not provided]"
|
||||
],
|
||||
"licenseListVersion": "3.13"
|
||||
"licenseListVersion": "3.14"
|
||||
},
|
||||
"dataLicense": "CC0-1.0",
|
||||
"documentNamespace": "https://anchore.com/syft/image/user-image-input",
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
[Image]
|
||||
Layer: 0
|
||||
Digest: sha256:fb6beecb75b39f4bb813dbf177e501edd5ddb3e69bb45cedeb78c676ee1b7a59
|
||||
Digest: sha256:ffb5e9eaa453a002110719d12c294960117ca2903953d1faa40f01dc3f77045c
|
||||
Size: 22
|
||||
MediaType: application/vnd.docker.image.rootfs.diff.tar.gzip
|
||||
|
||||
Layer: 1
|
||||
Digest: sha256:319b588ce64253a87b533c8ed01cf0025e0eac98e7b516e12532957e1244fdec
|
||||
Digest: sha256:8463854829fc53d47b9dcdf7ee79fe7eb4ca7933c910f67f8521412f7a2f5c21
|
||||
Size: 16
|
||||
MediaType: application/vnd.docker.image.rootfs.diff.tar.gzip
|
||||
|
||||
|
||||
BIN
Binary file not shown.
@@ -2,7 +2,6 @@ package packages
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"io"
|
||||
"text/tabwriter"
|
||||
|
||||
@@ -32,7 +31,7 @@ func (pres *TextPresenter) Present(output io.Writer) error {
|
||||
|
||||
switch pres.srcMetadata.Scheme {
|
||||
case source.DirectoryScheme:
|
||||
fmt.Fprintln(w, fmt.Sprintf("[Path: %s]", pres.srcMetadata.Path))
|
||||
fmt.Fprintf(w, "[Path: %s]\n", pres.srcMetadata.Path)
|
||||
case source.ImageScheme:
|
||||
fmt.Fprintln(w, "[Image]")
|
||||
|
||||
@@ -51,7 +50,7 @@ func (pres *TextPresenter) Present(output io.Writer) error {
|
||||
// populate artifacts...
|
||||
rows := 0
|
||||
for _, p := range pres.catalog.Sorted() {
|
||||
fmt.Fprintln(w, fmt.Sprintf("[%s]", p.Name))
|
||||
fmt.Fprintf(w, "[%s]\n", p.Name)
|
||||
fmt.Fprintln(w, " Version:\t", p.Version)
|
||||
fmt.Fprintln(w, " Type:\t", string(p.Type))
|
||||
fmt.Fprintln(w, " Found by:\t", p.FoundBy)
|
||||
|
||||
@@ -158,6 +158,11 @@ func presenterDirectoryInput(t testing.TB) (*pkg.Catalog, source.Metadata, *dist
|
||||
Metadata: pkg.PythonPackageMetadata{
|
||||
Name: "package-1",
|
||||
Version: "1.0.1",
|
||||
Files: []pkg.PythonFileRecord{
|
||||
{
|
||||
Path: "/some/path/pkg1/depedencies/foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
PURL: "a-purl-2",
|
||||
CPEs: []pkg.CPE{
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/anchore/syft/syft/file"
|
||||
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
@@ -22,6 +21,7 @@ type JSONFileMetadataEntry struct {
|
||||
UserID int `json:"userID"`
|
||||
GroupID int `json:"groupID"`
|
||||
Digests []file.Digest `json:"digests,omitempty"`
|
||||
MIMEType string `json:"mimeType"`
|
||||
}
|
||||
|
||||
func NewJSONFileMetadata(data map[source.Location]source.FileMetadata, digests map[source.Location][]file.Digest) ([]JSONFileMetadata, error) {
|
||||
@@ -46,6 +46,7 @@ func NewJSONFileMetadata(data map[source.Location]source.FileMetadata, digests m
|
||||
UserID: metadata.UserID,
|
||||
GroupID: metadata.GroupID,
|
||||
Digests: digestResults,
|
||||
MIMEType: metadata.MIMEType,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
"mode": 775,
|
||||
"type": "directory",
|
||||
"userID": 0,
|
||||
"groupID": 0
|
||||
"groupID": 0,
|
||||
"mimeType": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -33,7 +34,8 @@
|
||||
"algorithm": "sha256",
|
||||
"value": "366a3f5653e34673b875891b021647440d0127c2ef041e3b1a22da2a7d4f3703"
|
||||
}
|
||||
]
|
||||
],
|
||||
"mimeType": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -45,7 +47,8 @@
|
||||
"type": "symbolicLink",
|
||||
"linkDestination": "/c",
|
||||
"userID": 0,
|
||||
"groupID": 0
|
||||
"groupID": 0,
|
||||
"mimeType": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -62,7 +65,8 @@
|
||||
"algorithm": "sha256",
|
||||
"value": "1b3722da2a7d90d033b87581a2a3f12021647445653e34666ef041e3b4f3707c"
|
||||
}
|
||||
]
|
||||
],
|
||||
"mimeType": ""
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -168,6 +172,7 @@
|
||||
"configuration": {
|
||||
"configPath": "",
|
||||
"output": "",
|
||||
"file": "",
|
||||
"quiet": false,
|
||||
"check-for-app-update": false,
|
||||
"anchore": {
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/scylladb/go-set/strset"
|
||||
)
|
||||
|
||||
// This program generates license_list.go.
|
||||
const (
|
||||
source = "license_list.go"
|
||||
url = "https://spdx.org/licenses/licenses.json"
|
||||
)
|
||||
|
||||
var tmp = template.Must(template.New("").Parse(`// Code generated by go generate; DO NOT EDIT.
|
||||
// This file was generated by robots at {{ .Timestamp }}
|
||||
// using data from {{ .URL }}
|
||||
package spdxlicense
|
||||
|
||||
const Version = {{ printf "%q" .Version }}
|
||||
|
||||
var licenseIDs = map[string]string{
|
||||
{{- range $k, $v := .LicenseIDs }}
|
||||
{{ printf "%q" $k }}: {{ printf "%q" $v }},
|
||||
{{- end }}
|
||||
}
|
||||
`))
|
||||
|
||||
var versionMatch = regexp.MustCompile(`-([0-9]+)\.?([0-9]+)?\.?([0-9]+)?\.?`)
|
||||
|
||||
type LicenseList struct {
|
||||
Version string `json:"licenseListVersion"`
|
||||
Licenses []struct {
|
||||
ID string `json:"licenseId"`
|
||||
Name string `json:"name"`
|
||||
Text string `json:"licenseText"`
|
||||
Deprecated bool `json:"isDeprecatedLicenseId"`
|
||||
OSIApproved bool `json:"isOsiApproved"`
|
||||
SeeAlso []string `json:"seeAlso"`
|
||||
} `json:"licenses"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := run(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run() error {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get licenses list: %w", err)
|
||||
}
|
||||
|
||||
var result LicenseList
|
||||
if err = json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return fmt.Errorf("unable to decode license list: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
log.Fatalf("unable to close body: %+v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
f, err := os.Create(source)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create %q: %w", source, err)
|
||||
}
|
||||
defer func() {
|
||||
if err := f.Close(); err != nil {
|
||||
log.Fatalf("unable to close %q: %+v", source, err)
|
||||
}
|
||||
}()
|
||||
|
||||
licenseIDs := processSPDXLicense(result)
|
||||
|
||||
err = tmp.Execute(f, struct {
|
||||
Timestamp time.Time
|
||||
URL string
|
||||
Version string
|
||||
LicenseIDs map[string]string
|
||||
}{
|
||||
Timestamp: time.Now(),
|
||||
URL: url,
|
||||
Version: result.Version,
|
||||
LicenseIDs: licenseIDs,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to generate template: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parsing the provided SPDX license list necessitates a two pass approach.
|
||||
// The first pass is only related to what SPDX considers the truth. These K:V pairs will never be overwritten.
|
||||
// The second pass attempts to generate known short/long version listings for each key.
|
||||
// For info on some short name conventions see this document:
|
||||
// https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/#license-short-name.
|
||||
// The short long listing generation attempts to build all license permutations for a given key.
|
||||
// The new keys are then also associated with their relative SPDX value. If a key has already been entered
|
||||
// we know to ignore it since it came from the first pass which is considered the SPDX source of truth.
|
||||
// We also sort the licenses for the second pass so that cases like `GPL-1` associate to `GPL-1.0` and not `GPL-1.1`.
|
||||
func processSPDXLicense(result LicenseList) map[string]string {
|
||||
// first pass build map
|
||||
licenseIDs := make(map[string]string)
|
||||
for _, l := range result.Licenses {
|
||||
cleanID := strings.ToLower(l.ID)
|
||||
if _, exists := licenseIDs[cleanID]; exists {
|
||||
log.Fatalf("duplicate license ID found: %q", cleanID)
|
||||
}
|
||||
licenseIDs[cleanID] = l.ID
|
||||
}
|
||||
|
||||
sort.Slice(result.Licenses, func(i, j int) bool {
|
||||
return result.Licenses[i].ID < result.Licenses[j].ID
|
||||
})
|
||||
|
||||
// second pass build exceptions
|
||||
// do not overwrite if already exists
|
||||
for _, l := range result.Licenses {
|
||||
var multipleID []string
|
||||
cleanID := strings.ToLower(l.ID)
|
||||
multipleID = append(multipleID, buildLicensePermutations(cleanID)...)
|
||||
for _, id := range multipleID {
|
||||
if _, exists := licenseIDs[id]; !exists {
|
||||
licenseIDs[id] = l.ID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return licenseIDs
|
||||
}
|
||||
|
||||
func buildLicensePermutations(license string) (perms []string) {
|
||||
lv := findLicenseVersion(license)
|
||||
vp := versionPermutations(lv)
|
||||
|
||||
version := strings.Join(lv, ".")
|
||||
for _, p := range vp {
|
||||
perms = append(perms, strings.Replace(license, version, p, 1))
|
||||
}
|
||||
|
||||
return perms
|
||||
}
|
||||
|
||||
func findLicenseVersion(license string) (version []string) {
|
||||
versionList := versionMatch.FindAllStringSubmatch(license, -1)
|
||||
|
||||
if len(versionList) == 0 {
|
||||
return version
|
||||
}
|
||||
|
||||
for i, v := range versionList[0] {
|
||||
if v != "" && i != 0 {
|
||||
version = append(version, v)
|
||||
}
|
||||
}
|
||||
|
||||
return version
|
||||
}
|
||||
|
||||
func versionPermutations(version []string) []string {
|
||||
ver := append([]string(nil), version...)
|
||||
perms := strset.New()
|
||||
for i := 1; i <= 3; i++ {
|
||||
if len(ver) < i+1 {
|
||||
ver = append(ver, "0")
|
||||
}
|
||||
|
||||
perm := strings.Join(ver[:i], ".")
|
||||
badCount := strings.Count(perm, "0") + strings.Count(perm, ".")
|
||||
|
||||
if badCount != len(perm) {
|
||||
perms.Add(perm)
|
||||
}
|
||||
}
|
||||
|
||||
return perms.List()
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLicensePermutations(t *testing.T) {
|
||||
var tests = []struct {
|
||||
shortName string
|
||||
permutations []string
|
||||
}{
|
||||
{
|
||||
"GPL-1-only",
|
||||
[]string{
|
||||
"GPL-1-only",
|
||||
"GPL-1.0-only",
|
||||
"GPL-1.0.0-only",
|
||||
},
|
||||
},
|
||||
{
|
||||
"GPL-2",
|
||||
[]string{
|
||||
"GPL-2",
|
||||
"GPL-2.0",
|
||||
"GPL-2.0.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
"GPL-2.0+",
|
||||
[]string{
|
||||
"GPL-2+",
|
||||
"GPL-2.0+",
|
||||
"GPL-2.0.0+",
|
||||
},
|
||||
},
|
||||
{
|
||||
"GPL-3.0.0-or-later",
|
||||
[]string{
|
||||
"GPL-3-or-later",
|
||||
"GPL-3.0-or-later",
|
||||
"GPL-3.0.0-or-later",
|
||||
},
|
||||
},
|
||||
{
|
||||
"oldap-2.0",
|
||||
[]string{
|
||||
"oldap-2",
|
||||
"oldap-2.0",
|
||||
"oldap-2.0.0",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.shortName, func(t *testing.T) {
|
||||
perms := buildLicensePermutations(test.shortName)
|
||||
assert.ElementsMatch(t, test.permutations, perms)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionPermutations(t *testing.T) {
|
||||
var tests = []struct {
|
||||
version []string
|
||||
permutations []string
|
||||
}{
|
||||
{
|
||||
[]string{"1", "0"},
|
||||
[]string{"1", "1.0", "1.0.0"},
|
||||
},
|
||||
{
|
||||
[]string{"2"},
|
||||
[]string{"2", "2.0", "2.0.0"},
|
||||
},
|
||||
{
|
||||
[]string{"2", "0"},
|
||||
[]string{"2", "2.0", "2.0.0"},
|
||||
},
|
||||
|
||||
{
|
||||
[]string{"3", "0", "0"},
|
||||
[]string{"3", "3.0", "3.0.0"},
|
||||
},
|
||||
{
|
||||
[]string{"0", "3"},
|
||||
[]string{"0.3", "0.3.0"},
|
||||
},
|
||||
{
|
||||
[]string{"0", "0", "3"},
|
||||
[]string{"0.0.3"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(strings.Join(test.version, "."), func(t *testing.T) {
|
||||
got := versionPermutations(test.version)
|
||||
assert.ElementsMatch(t, test.permutations, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindLicenseVersion(t *testing.T) {
|
||||
var tests = []struct {
|
||||
license string
|
||||
version []string
|
||||
}{
|
||||
{
|
||||
"GPL-1.0-only",
|
||||
[]string{"1", "0"},
|
||||
},
|
||||
{
|
||||
"GPL-2.0",
|
||||
[]string{"2", "0"},
|
||||
},
|
||||
{
|
||||
"GPL-2.0.0",
|
||||
[]string{"2", "0", "0"},
|
||||
},
|
||||
{
|
||||
"GPL-2",
|
||||
[]string{"2"},
|
||||
},
|
||||
{
|
||||
"bzip2-1",
|
||||
[]string{"1"},
|
||||
},
|
||||
{
|
||||
"php-3.01",
|
||||
[]string{"3", "01"},
|
||||
},
|
||||
{
|
||||
"oldap-2.0",
|
||||
[]string{"2", "0"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.license, func(t *testing.T) {
|
||||
got := findLicenseVersion(test.license)
|
||||
assert.Equal(t, test.version, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
// +build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
// This program generates license_list.go.
|
||||
const (
|
||||
source = "license_list.go"
|
||||
url = "https://spdx.org/licenses/licenses.json"
|
||||
)
|
||||
|
||||
var tmp = template.Must(template.New("").Parse(`// Code generated by go generate; DO NOT EDIT.
|
||||
// This file was generated by robots at {{ .Timestamp }}
|
||||
// using data from {{ .URL }}
|
||||
package spdxlicense
|
||||
|
||||
const Version = {{ printf "%q" .Version }}
|
||||
|
||||
var licenseIDs = map[string]string{
|
||||
{{- range $k, $v := .LicenseIDs }}
|
||||
{{ printf "%q" $k }}: {{ printf "%q" $v }},
|
||||
{{- end }}
|
||||
}
|
||||
`))
|
||||
|
||||
type LicenseList struct {
|
||||
Version string `json:"licenseListVersion"`
|
||||
Licenses []struct {
|
||||
ID string `json:"licenseId"`
|
||||
Name string `json:"name"`
|
||||
Text string `json:"licenseText"`
|
||||
Deprecated bool `json:"isDeprecatedLicenseId"`
|
||||
OSIApproved bool `json:"isOsiApproved"`
|
||||
SeeAlso []string `json:"seeAlso"`
|
||||
} `json:"licenses"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
log.Fatalf("unable to get licenses list: %+v", err)
|
||||
}
|
||||
|
||||
var result LicenseList
|
||||
if err = json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
log.Fatalf("unable to decode license list: %+v", err)
|
||||
}
|
||||
|
||||
f, err := os.Create(source)
|
||||
if err != nil {
|
||||
log.Fatalf("unable to create %q: %+v", source, err)
|
||||
}
|
||||
defer func() {
|
||||
if err := f.Close(); err != nil {
|
||||
log.Fatalf("unable to close %q: %+v", source, err)
|
||||
}
|
||||
}()
|
||||
|
||||
var licenseIDs = make(map[string]string)
|
||||
for _, l := range result.Licenses {
|
||||
cleanID := strings.ToLower(l.ID)
|
||||
if _, exists := licenseIDs[cleanID]; exists {
|
||||
log.Fatalf("duplicate license ID found: %q", cleanID)
|
||||
}
|
||||
licenseIDs[cleanID] = l.ID
|
||||
}
|
||||
|
||||
err = tmp.Execute(f, struct {
|
||||
Timestamp time.Time
|
||||
URL string
|
||||
Version string
|
||||
LicenseIDs map[string]string
|
||||
}{
|
||||
Timestamp: time.Now(),
|
||||
URL: url,
|
||||
Version: result.Version,
|
||||
LicenseIDs: licenseIDs,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("unable to generate template: %+v", err)
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,15 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
//go:generate go run generate_license_list.go
|
||||
// https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/#license-short-name
|
||||
// License generated in license_list.go uses a regular expression to help resolve cases where
|
||||
// x.0.0 and x are supplied as version numbers. For SPDX compatibility, versions with trailing
|
||||
// dot-zeroes are considered to be equivalent to versions without (e.g., “2.0.0” is considered equal to “2.0” and “2”).
|
||||
// EX: gpl-2+ ---> GPL-2.0+
|
||||
// EX: gpl-2.0.0-only ---> GPL-2.0-only
|
||||
// See the debian link for more details on the spdx license differences
|
||||
|
||||
//go:generate go run generate/generate_license_list.go
|
||||
|
||||
func ID(id string) (string, bool) {
|
||||
value, exists := licenseIDs[strings.ToLower(id)]
|
||||
|
||||
+1026
-475
File diff suppressed because it is too large
Load Diff
@@ -10,5 +10,6 @@ func TestLicenceListIDs(t *testing.T) {
|
||||
// do a sanity check on the generated data
|
||||
assert.Equal(t, "0BSD", licenseIDs["0bsd"])
|
||||
assert.Equal(t, "ZPL-2.1", licenseIDs["zpl-2.1"])
|
||||
assert.Equal(t, "GPL-2.0", licenseIDs["gpl-2"])
|
||||
assert.NotEmpty(t, Version)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package spdxlicense
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIDParse(t *testing.T) {
|
||||
var tests = []struct {
|
||||
shortName string
|
||||
spdx string
|
||||
}{
|
||||
{
|
||||
"GPL-1-only",
|
||||
"GPL-1.0-only",
|
||||
},
|
||||
{
|
||||
"GPL-2",
|
||||
"GPL-2.0",
|
||||
},
|
||||
{
|
||||
"GPL-2+",
|
||||
"GPL-2.0+",
|
||||
},
|
||||
{
|
||||
"GPL-3.0.0-or-later",
|
||||
"GPL-3.0-or-later",
|
||||
},
|
||||
{
|
||||
"GPL-3-with-autoconf-exception",
|
||||
"GPL-3.0-with-autoconf-exception",
|
||||
},
|
||||
{
|
||||
"CC-by-nc-3-de",
|
||||
"CC-BY-NC-3.0-DE",
|
||||
},
|
||||
// the below few cases are NOT expected, however, seem unavoidable given the current approach
|
||||
{
|
||||
"w3c-20150513.0.0",
|
||||
"W3C-20150513",
|
||||
},
|
||||
{
|
||||
"spencer-86.0.0",
|
||||
"Spencer-86",
|
||||
},
|
||||
{
|
||||
"unicode-dfs-2015.0.0",
|
||||
"Unicode-DFS-2015",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.shortName, func(t *testing.T) {
|
||||
got, exists := ID(test.shortName)
|
||||
assert.True(t, exists)
|
||||
assert.Equal(t, test.spdx, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -33,25 +33,28 @@ import (
|
||||
// or in the shared ui package as a function on the main handler object. All handler functions should be completed
|
||||
// processing an event before the ETUI exits (coordinated with a sync.WaitGroup)
|
||||
type ephemeralTerminalUI struct {
|
||||
unsubscribe func() error
|
||||
handler *ui.Handler
|
||||
waitGroup *sync.WaitGroup
|
||||
frame *frame.Frame
|
||||
logBuffer *bytes.Buffer
|
||||
output *os.File
|
||||
unsubscribe func() error
|
||||
handler *ui.Handler
|
||||
waitGroup *sync.WaitGroup
|
||||
frame *frame.Frame
|
||||
logBuffer *bytes.Buffer
|
||||
uiOutput *os.File
|
||||
reportOutput io.Writer
|
||||
}
|
||||
|
||||
func NewEphemeralTerminalUI() UI {
|
||||
// NewEphemeralTerminalUI writes all events to a TUI and writes the final report to the given writer.
|
||||
func NewEphemeralTerminalUI(reportWriter io.Writer) UI {
|
||||
return &ephemeralTerminalUI{
|
||||
handler: ui.NewHandler(),
|
||||
waitGroup: &sync.WaitGroup{},
|
||||
output: os.Stderr,
|
||||
handler: ui.NewHandler(),
|
||||
waitGroup: &sync.WaitGroup{},
|
||||
uiOutput: os.Stderr,
|
||||
reportOutput: reportWriter,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ephemeralTerminalUI) Setup(unsubscribe func() error) error {
|
||||
h.unsubscribe = unsubscribe
|
||||
hideCursor(h.output)
|
||||
hideCursor(h.uiOutput)
|
||||
|
||||
// prep the logger to not clobber the screen from now on (logrus only)
|
||||
h.logBuffer = bytes.NewBufferString("")
|
||||
@@ -81,7 +84,7 @@ func (h *ephemeralTerminalUI) Handle(event partybus.Event) error {
|
||||
// are about to write bytes to stdout, so we should reset the terminal state first
|
||||
h.closeScreen(false)
|
||||
|
||||
if err := handleCatalogerPresenterReady(event); err != nil {
|
||||
if err := handleCatalogerPresenterReady(event, h.reportOutput); err != nil {
|
||||
log.Errorf("unable to show %s event: %+v", event.Type, err)
|
||||
}
|
||||
|
||||
@@ -95,7 +98,7 @@ func (h *ephemeralTerminalUI) openScreen() error {
|
||||
config := frame.Config{
|
||||
PositionPolicy: frame.PolicyFloatForward,
|
||||
// only report output to stderr, reserve report output for stdout
|
||||
Output: h.output,
|
||||
Output: h.uiOutput,
|
||||
}
|
||||
|
||||
fr, err := frame.New(config)
|
||||
@@ -128,15 +131,15 @@ func (h *ephemeralTerminalUI) flushLog() {
|
||||
logWrapper, ok := log.Log.(*logger.LogrusLogger)
|
||||
if ok {
|
||||
fmt.Fprint(logWrapper.Output, h.logBuffer.String())
|
||||
logWrapper.Logger.SetOutput(h.output)
|
||||
logWrapper.Logger.SetOutput(h.uiOutput)
|
||||
} else {
|
||||
fmt.Fprint(h.output, h.logBuffer.String())
|
||||
fmt.Fprint(h.uiOutput, h.logBuffer.String())
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ephemeralTerminalUI) Teardown(force bool) error {
|
||||
h.closeScreen(force)
|
||||
showCursor(h.output)
|
||||
showCursor(h.uiOutput)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -4,27 +4,25 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/gookit/color"
|
||||
"github.com/wagoodman/jotframe/pkg/frame"
|
||||
|
||||
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
|
||||
"github.com/gookit/color"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/jotframe/pkg/frame"
|
||||
)
|
||||
|
||||
// handleCatalogerPresenterReady is a UI function for processing the CatalogerFinished bus event, displaying the catalog
|
||||
// via the given presenter to stdout.
|
||||
func handleCatalogerPresenterReady(event partybus.Event) error {
|
||||
func handleCatalogerPresenterReady(event partybus.Event, reportOutput io.Writer) error {
|
||||
// show the report to stdout
|
||||
pres, err := syftEventParsers.ParsePresenterReady(event)
|
||||
if err != nil {
|
||||
return fmt.Errorf("bad CatalogerFinished event: %w", err)
|
||||
}
|
||||
|
||||
if err := pres.Present(os.Stdout); err != nil {
|
||||
if err := pres.Present(reportOutput); err != nil {
|
||||
return fmt.Errorf("unable to show package catalog report: %w", err)
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
syftEvent "github.com/anchore/syft/syft/event"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
)
|
||||
|
||||
type loggerUI struct {
|
||||
unsubscribe func() error
|
||||
unsubscribe func() error
|
||||
reportOutput io.Writer
|
||||
}
|
||||
|
||||
func NewLoggerUI() UI {
|
||||
return &loggerUI{}
|
||||
// NewLoggerUI writes all events to the common application logger and writes the final report to the given writer.
|
||||
func NewLoggerUI(reportWriter io.Writer) UI {
|
||||
return &loggerUI{
|
||||
reportOutput: reportWriter,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *loggerUI) Setup(unsubscribe func() error) error {
|
||||
@@ -25,7 +31,7 @@ func (l loggerUI) Handle(event partybus.Event) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := handleCatalogerPresenterReady(event); err != nil {
|
||||
if err := handleCatalogerPresenterReady(event, l.reportOutput); err != nil {
|
||||
log.Warnf("unable to show catalog image finished event: %+v", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
@@ -10,20 +11,21 @@ import (
|
||||
// TODO: build tags to exclude options from windows
|
||||
|
||||
// Select is responsible for determining the specific UI function given select user option, the current platform
|
||||
// config values, and environment status (such as a TTY being present).
|
||||
func Select(verbose, quiet bool) UI {
|
||||
var ui UI
|
||||
|
||||
// config values, and environment status (such as a TTY being present). The first UI in the returned slice of UIs
|
||||
// is intended to be used and the UIs that follow are meant to be attempted only in a fallback posture when there
|
||||
// are environmental problems (e.g. cannot write to the terminal). A writer is provided to capture the output of
|
||||
// the final SBOM report.
|
||||
func Select(verbose, quiet bool, reportWriter io.Writer) (uis []UI) {
|
||||
isStdoutATty := terminal.IsTerminal(int(os.Stdout.Fd()))
|
||||
isStderrATty := terminal.IsTerminal(int(os.Stderr.Fd()))
|
||||
notATerminal := !isStderrATty && !isStdoutATty
|
||||
|
||||
switch {
|
||||
case runtime.GOOS == "windows" || verbose || quiet || notATerminal || !isStderrATty:
|
||||
ui = NewLoggerUI()
|
||||
uis = append(uis, NewLoggerUI(reportWriter))
|
||||
default:
|
||||
ui = NewEphemeralTerminalUI()
|
||||
uis = append(uis, NewEphemeralTerminalUI(reportWriter), NewLoggerUI(reportWriter))
|
||||
}
|
||||
|
||||
return ui
|
||||
return uis
|
||||
}
|
||||
|
||||
@@ -12,11 +12,13 @@ import (
|
||||
const valueNotProvided = "[not provided]"
|
||||
|
||||
// all variables here are provided as build-time arguments, with clear default values
|
||||
var version = valueNotProvided
|
||||
var gitCommit = valueNotProvided
|
||||
var gitTreeState = valueNotProvided
|
||||
var buildDate = valueNotProvided
|
||||
var platform = fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)
|
||||
var (
|
||||
version = valueNotProvided
|
||||
gitCommit = valueNotProvided
|
||||
gitTreeState = valueNotProvided
|
||||
buildDate = valueNotProvided
|
||||
platform = fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)
|
||||
)
|
||||
|
||||
// Version defines the application version details (generally from build information)
|
||||
type Version struct {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// nolint:forbidigo
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -45,11 +46,7 @@ func build() *jsonschema.Schema {
|
||||
reflector := &jsonschema.Reflector{
|
||||
AllowAdditionalProperties: true,
|
||||
TypeNamer: func(r reflect.Type) string {
|
||||
name := r.Name()
|
||||
if strings.HasPrefix(name, "JSON") {
|
||||
name = strings.TrimPrefix(name, "JSON")
|
||||
}
|
||||
return name
|
||||
return strings.TrimPrefix(r.Name(), "JSON")
|
||||
},
|
||||
}
|
||||
documentSchema := reflector.ReflectFromType(reflect.TypeOf(&poweruser.JSONDocument{}))
|
||||
@@ -74,7 +71,7 @@ func build() *jsonschema.Schema {
|
||||
// ensure the generated list of names is stable between runs
|
||||
sort.Strings(metadataNames)
|
||||
|
||||
var metadataTypes = []map[string]string{
|
||||
metadataTypes := []map[string]string{
|
||||
// allow for no metadata to be provided
|
||||
{"type": "null"},
|
||||
}
|
||||
@@ -93,13 +90,13 @@ func build() *jsonschema.Schema {
|
||||
}
|
||||
|
||||
func encode(schema *jsonschema.Schema) []byte {
|
||||
var newSchemaBuffer = new(bytes.Buffer)
|
||||
newSchemaBuffer := new(bytes.Buffer)
|
||||
enc := json.NewEncoder(newSchemaBuffer)
|
||||
// prevent > and < from being escaped in the payload
|
||||
enc.SetEscapeHTML(false)
|
||||
enc.SetIndent("", " ")
|
||||
err := enc.Encode(&schema)
|
||||
if err != nil {
|
||||
|
||||
if err := enc.Encode(&schema); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
@@ -118,6 +118,11 @@ func TestIdentifyDistro(t *testing.T) {
|
||||
Type: CentOS,
|
||||
Version: "5.7.0",
|
||||
},
|
||||
{
|
||||
fixture: "test-fixtures/os/mariner",
|
||||
Type: Mariner,
|
||||
Version: "1.0.0",
|
||||
},
|
||||
}
|
||||
|
||||
observedDistros := internal.NewStringSet()
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
NAME="Common Base Linux Mariner"
|
||||
VERSION="1.0.20210901"
|
||||
ID=mariner
|
||||
VERSION_ID=1.0
|
||||
PRETTY_NAME="CBL-Mariner/Linux"
|
||||
ANSI_COLOR="1;34"
|
||||
HOME_URL="https://aka.ms/cbl-mariner"
|
||||
BUG_REPORT_URL="https://aka.ms/cbl-mariner"
|
||||
SUPPORT_URL="https://aka.ms/cbl-mariner"
|
||||
@@ -20,6 +20,7 @@ const (
|
||||
SLES Type = "sles"
|
||||
Photon Type = "photon"
|
||||
Windows Type = "windows"
|
||||
Mariner Type = "mariner"
|
||||
)
|
||||
|
||||
// All contains all Linux distribution options
|
||||
@@ -38,6 +39,7 @@ var All = []Type{
|
||||
SLES,
|
||||
Photon,
|
||||
Windows,
|
||||
Mariner,
|
||||
}
|
||||
|
||||
// IDMapping connects a distro ID like "ubuntu" to a Distro type
|
||||
@@ -56,6 +58,7 @@ var IDMapping = map[string]Type{
|
||||
"sles": SLES,
|
||||
"photon": Photon,
|
||||
"windows": Windows,
|
||||
"mariner": Mariner,
|
||||
}
|
||||
|
||||
// String returns the string representation of the given Linux distribution.
|
||||
|
||||
@@ -6,29 +6,26 @@ package parsers
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/anchore/syft/syft/presenter"
|
||||
|
||||
"github.com/anchore/syft/syft/file"
|
||||
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger"
|
||||
"github.com/anchore/syft/syft/presenter"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
)
|
||||
|
||||
type ErrBadPayload struct {
|
||||
type badPayloadError struct {
|
||||
Type partybus.EventType
|
||||
Field string
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
func (e *ErrBadPayload) Error() string {
|
||||
func (e *badPayloadError) Error() string {
|
||||
return fmt.Sprintf("event='%s' has bad event payload field='%v': '%+v'", string(e.Type), e.Field, e.Value)
|
||||
}
|
||||
|
||||
func newPayloadErr(t partybus.EventType, field string, value interface{}) error {
|
||||
return &ErrBadPayload{
|
||||
return &badPayloadError{
|
||||
Type: t,
|
||||
Field: field,
|
||||
Value: value,
|
||||
|
||||
@@ -31,7 +31,7 @@ func (i *ClassificationCataloger) Catalog(resolver source.FileResolver) (map[sou
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Debugf("classification cataloger discovered %d results", numResults)
|
||||
log.Debugf("file classifier discovered %d results", numResults)
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"io"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
@@ -65,7 +64,7 @@ func (i *ContentsCataloger) catalogLocation(resolver source.FileResolver, locati
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
if _, err = io.Copy(base64.NewEncoder(base64.StdEncoding, buf), contentReader); err != nil {
|
||||
return "", internal.ErrPath{Path: location.RealPath, Err: err}
|
||||
return "", internal.PathError{Path: location.RealPath, Err: err}
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
|
||||
@@ -8,15 +8,12 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
type DigestsCataloger struct {
|
||||
@@ -72,7 +69,7 @@ func (i *DigestsCataloger) catalogLocation(resolver source.FileResolver, locatio
|
||||
|
||||
size, err := io.Copy(io.MultiWriter(writers...), contentReader)
|
||||
if err != nil {
|
||||
return nil, internal.ErrPath{Path: location.RealPath, Err: err}
|
||||
return nil, internal.PathError{Path: location.RealPath, Err: err}
|
||||
}
|
||||
|
||||
if size == 0 {
|
||||
@@ -99,7 +96,7 @@ func DigestAlgorithmName(hash crypto.Hash) string {
|
||||
|
||||
func CleanDigestAlgorithmName(name string) string {
|
||||
lower := strings.ToLower(name)
|
||||
return strings.Replace(lower, "-", "", -1)
|
||||
return strings.ReplaceAll(lower, "-", "")
|
||||
}
|
||||
|
||||
func digestsCatalogingProgress(locations int64) (*progress.Stage, *progress.Manual) {
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
// GenerateSearchPatterns takes a set of named base patterns, a set of additional named patterns and an name exclusion list and generates a final
|
||||
// set of regular expressions (indexed by name). The sets are aggregated roughly as such: (base - excluded) + additional.
|
||||
func GenerateSearchPatterns(basePatterns map[string]string, additionalPatterns map[string]string, excludePatternNames []string) (map[string]*regexp.Regexp, error) {
|
||||
var regexObjs = make(map[string]*regexp.Regexp)
|
||||
regexObjs := make(map[string]*regexp.Regexp)
|
||||
var errs error
|
||||
|
||||
addFn := func(name, pattern string) {
|
||||
|
||||
@@ -9,8 +9,7 @@ import (
|
||||
"github.com/wagoodman/go-progress"
|
||||
)
|
||||
|
||||
type MetadataCataloger struct {
|
||||
}
|
||||
type MetadataCataloger struct{}
|
||||
|
||||
func NewMetadataCataloger() *MetadataCataloger {
|
||||
return &MetadataCataloger{}
|
||||
|
||||
@@ -49,11 +49,12 @@ func TestFileMetadataCataloger(t *testing.T) {
|
||||
path: "/file-1.txt",
|
||||
exists: true,
|
||||
expected: source.FileMetadata{
|
||||
Mode: 0644,
|
||||
Type: "RegularFile",
|
||||
UserID: 1,
|
||||
GroupID: 2,
|
||||
Size: 7,
|
||||
Mode: 0644,
|
||||
Type: "RegularFile",
|
||||
UserID: 1,
|
||||
GroupID: 2,
|
||||
Size: 7,
|
||||
MIMEType: "text/plain",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -65,6 +66,7 @@ func TestFileMetadataCataloger(t *testing.T) {
|
||||
LinkDestination: "file-1.txt",
|
||||
UserID: 1,
|
||||
GroupID: 2,
|
||||
MIMEType: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -76,46 +78,51 @@ func TestFileMetadataCataloger(t *testing.T) {
|
||||
LinkDestination: "file-1.txt",
|
||||
UserID: 0,
|
||||
GroupID: 0,
|
||||
MIMEType: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/char-device-1",
|
||||
exists: true,
|
||||
expected: source.FileMetadata{
|
||||
Mode: 0644 | os.ModeDevice | os.ModeCharDevice,
|
||||
Type: "CharacterDevice",
|
||||
UserID: 0,
|
||||
GroupID: 0,
|
||||
Mode: 0644 | os.ModeDevice | os.ModeCharDevice,
|
||||
Type: "CharacterDevice",
|
||||
UserID: 0,
|
||||
GroupID: 0,
|
||||
MIMEType: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/block-device-1",
|
||||
exists: true,
|
||||
expected: source.FileMetadata{
|
||||
Mode: 0644 | os.ModeDevice,
|
||||
Type: "BlockDevice",
|
||||
UserID: 0,
|
||||
GroupID: 0,
|
||||
Mode: 0644 | os.ModeDevice,
|
||||
Type: "BlockDevice",
|
||||
UserID: 0,
|
||||
GroupID: 0,
|
||||
MIMEType: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/fifo-1",
|
||||
exists: true,
|
||||
expected: source.FileMetadata{
|
||||
Mode: 0644 | os.ModeNamedPipe,
|
||||
Type: "FIFONode",
|
||||
UserID: 0,
|
||||
GroupID: 0,
|
||||
Mode: 0644 | os.ModeNamedPipe,
|
||||
Type: "FIFONode",
|
||||
UserID: 0,
|
||||
GroupID: 0,
|
||||
MIMEType: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/bin",
|
||||
exists: true,
|
||||
expected: source.FileMetadata{
|
||||
Mode: 0755 | os.ModeDir,
|
||||
Type: "Directory",
|
||||
UserID: 0,
|
||||
GroupID: 0,
|
||||
Mode: 0755 | os.ModeDir,
|
||||
Type: "Directory",
|
||||
UserID: 0,
|
||||
GroupID: 0,
|
||||
MIMEType: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"sort"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
@@ -82,7 +81,7 @@ func (i *SecretsCataloger) catalogLocation(resolver source.FileResolver, locatio
|
||||
// TODO: in the future we can swap out search strategies here
|
||||
secrets, err := catalogLocationByLine(resolver, location, i.patterns)
|
||||
if err != nil {
|
||||
return nil, internal.ErrPath{Path: location.RealPath, Err: err}
|
||||
return nil, internal.PathError{Path: location.RealPath, Err: err}
|
||||
}
|
||||
|
||||
if i.revealValues {
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"regexp"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
@@ -20,7 +19,7 @@ func catalogLocationByLine(resolver source.FileResolver, location source.Locatio
|
||||
}
|
||||
defer internal.CloseAndLogError(readCloser, location.VirtualPath)
|
||||
|
||||
var scanner = bufio.NewReader(readCloser)
|
||||
scanner := bufio.NewReader(readCloser)
|
||||
var position int64
|
||||
var allSecrets []SearchResult
|
||||
var lineNo int64
|
||||
@@ -30,7 +29,7 @@ func catalogLocationByLine(resolver source.FileResolver, location source.Locatio
|
||||
var line []byte
|
||||
// TODO: we're at risk of large memory usage for very long lines
|
||||
line, readErr = scanner.ReadBytes('\n')
|
||||
if readErr != nil && readErr != io.EOF {
|
||||
if readErr != nil && !errors.Is(readErr, io.EOF) {
|
||||
return nil, readErr
|
||||
}
|
||||
|
||||
@@ -119,9 +118,9 @@ func extractSecretFromPosition(readCloser io.ReadCloser, name string, pattern *r
|
||||
}
|
||||
|
||||
// lineNoOfSecret are the number of lines which occur before the start of the secret value
|
||||
var lineNoOfSecret = lineNo + int64(reader.newlinesBefore(start))
|
||||
lineNoOfSecret := lineNo + int64(reader.newlinesBefore(start))
|
||||
// lineOffsetOfSecret are the number of bytes that occur after the last newline but before the secret value.
|
||||
var lineOffsetOfSecret = start - reader.newlinePositionBefore(start)
|
||||
lineOffsetOfSecret := start - reader.newlinePositionBefore(start)
|
||||
if lineNoOfSecret == lineNo {
|
||||
// the secret value starts in the same line as the overall match, so we must consider that line offset
|
||||
lineOffsetOfSecret += lineOffset
|
||||
|
||||
@@ -3,15 +3,14 @@ package pkg
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/anchore/packageurl-go"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
|
||||
"github.com/package-url/packageurl-go"
|
||||
"github.com/scylladb/go-set/strset"
|
||||
)
|
||||
|
||||
const ApkDbGlob = "**/lib/apk/db/installed"
|
||||
const ApkDBGlob = "**/lib/apk/db/installed"
|
||||
|
||||
var _ fileOwner = (*ApkMetadata)(nil)
|
||||
var _ FileOwner = (*ApkMetadata)(nil)
|
||||
|
||||
// ApkMetadata represents all captured data for a Alpine DB package entry.
|
||||
// See the following sources for more information:
|
||||
@@ -37,11 +36,11 @@ type ApkMetadata struct {
|
||||
|
||||
// ApkFileRecord represents a single file listing and metadata from a APK DB entry (which may have many of these file records).
|
||||
type ApkFileRecord struct {
|
||||
Path string `json:"path"`
|
||||
OwnerUID string `json:"ownerUid,omitempty"`
|
||||
OwnerGID string `json:"ownerGid,omitempty"`
|
||||
Permissions string `json:"permissions,omitempty"`
|
||||
Digest file.Digest `json:"digest,omitempty"`
|
||||
Path string `json:"path"`
|
||||
OwnerUID string `json:"ownerUid,omitempty"`
|
||||
OwnerGID string `json:"ownerGid,omitempty"`
|
||||
Permissions string `json:"permissions,omitempty"`
|
||||
Digest *file.Digest `json:"digest,omitempty"`
|
||||
}
|
||||
|
||||
// PackageURL returns the PURL for the specific Alpine package (see https://github.com/package-url/purl-spec)
|
||||
@@ -63,7 +62,7 @@ func (m ApkMetadata) PackageURL() string {
|
||||
return pURL.ToString()
|
||||
}
|
||||
|
||||
func (m ApkMetadata) ownedFiles() (result []string) {
|
||||
func (m ApkMetadata) OwnedFiles() (result []string) {
|
||||
s := strset.New()
|
||||
for _, f := range m.Files {
|
||||
if f.Path != "" {
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/packageurl-go"
|
||||
"github.com/go-test/deep"
|
||||
"github.com/package-url/packageurl-go"
|
||||
"github.com/sergi/go-diff/diffmatchpatch"
|
||||
)
|
||||
|
||||
@@ -73,7 +73,7 @@ func TestApkMetadata_pURL(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestApkMetadata_fileOwner(t *testing.T) {
|
||||
func TestApkMetadata_FileOwner(t *testing.T) {
|
||||
tests := []struct {
|
||||
metadata ApkMetadata
|
||||
expected []string
|
||||
@@ -107,7 +107,7 @@ func TestApkMetadata_fileOwner(t *testing.T) {
|
||||
t.Run(strings.Join(test.expected, ","), func(t *testing.T) {
|
||||
var i interface{}
|
||||
i = test.metadata
|
||||
actual := i.(fileOwner).ownedFiles()
|
||||
actual := i.(FileOwner).OwnedFiles()
|
||||
for _, d := range deep.Equal(test.expected, actual) {
|
||||
t.Errorf("diff: %+v", d)
|
||||
}
|
||||
|
||||
+1
-3
@@ -5,7 +5,6 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
)
|
||||
|
||||
@@ -67,8 +66,7 @@ func (c *Catalog) Add(p Package) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
_, exists := c.byID[p.ID]
|
||||
if exists {
|
||||
if _, exists := c.byID[p.ID]; exists {
|
||||
log.Errorf("package ID already exists in the catalog : id=%+v %+v", p.ID, p)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
// NewApkdbCataloger returns a new Alpine DB cataloger object.
|
||||
func NewApkdbCataloger() *common.GenericCataloger {
|
||||
globParsers := map[string]common.ParserFn{
|
||||
pkg.ApkDbGlob: parseApkDB,
|
||||
pkg.ApkDBGlob: parseApkDB,
|
||||
}
|
||||
|
||||
return common.NewGenericCataloger(nil, globParsers, "apkdb-cataloger")
|
||||
|
||||
@@ -8,9 +8,8 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/syft/file"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/common"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
@@ -128,7 +127,7 @@ func parseApkDBEntry(reader io.Reader) (*pkg.ApkMetadata, error) {
|
||||
log.Warnf("checksum field with no parent record: %q", value)
|
||||
continue
|
||||
}
|
||||
fileRecord.Digest = file.Digest{
|
||||
fileRecord.Digest = &file.Digest{
|
||||
Algorithm: "sha1",
|
||||
Value: value,
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ func TestExtraFileAttributes(t *testing.T) {
|
||||
OwnerUID: "0",
|
||||
OwnerGID: "0",
|
||||
Permissions: "755",
|
||||
Digest: file.Digest{
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "sha1",
|
||||
Value: "Q1M0C9qfC/+kdRiOodeihG2GMRtkE=",
|
||||
},
|
||||
@@ -110,7 +110,7 @@ func TestSinglePackageDetails(t *testing.T) {
|
||||
OwnerUID: "0",
|
||||
OwnerGID: "0",
|
||||
Permissions: "755",
|
||||
Digest: file.Digest{
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "sha1",
|
||||
Value: "Q1Kja2+POZKxEkUOZqwSjC6kmaED4=",
|
||||
},
|
||||
@@ -126,7 +126,7 @@ func TestSinglePackageDetails(t *testing.T) {
|
||||
OwnerUID: "0",
|
||||
OwnerGID: "0",
|
||||
Permissions: "755",
|
||||
Digest: file.Digest{
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "sha1",
|
||||
Value: "Q1CVmFbdY+Hv6/jAHl1gec2Kbx1EY=",
|
||||
},
|
||||
@@ -136,7 +136,7 @@ func TestSinglePackageDetails(t *testing.T) {
|
||||
OwnerUID: "0",
|
||||
OwnerGID: "0",
|
||||
Permissions: "755",
|
||||
Digest: file.Digest{
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "sha1",
|
||||
Value: "Q1yFAhGggmL7ERgbIA7KQxyTzf3ks=",
|
||||
},
|
||||
@@ -146,7 +146,7 @@ func TestSinglePackageDetails(t *testing.T) {
|
||||
OwnerUID: "0",
|
||||
OwnerGID: "0",
|
||||
Permissions: "755",
|
||||
Digest: file.Digest{
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "sha1",
|
||||
Value: "Q1dAdYK8M/INibRQF5B3Rw7cmNDDA=",
|
||||
},
|
||||
@@ -156,7 +156,7 @@ func TestSinglePackageDetails(t *testing.T) {
|
||||
OwnerUID: "0",
|
||||
OwnerGID: "0",
|
||||
Permissions: "755",
|
||||
Digest: file.Digest{
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "sha1",
|
||||
Value: "Q1eR2Dz/WylabgbWMTkd2+hGmEya4=",
|
||||
},
|
||||
@@ -195,49 +195,49 @@ func TestSinglePackageDetails(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Path: "/etc/fstab",
|
||||
Digest: file.Digest{
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "sha1",
|
||||
Value: "Q11Q7hNe8QpDS531guqCdrXBzoA/o=",
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "/etc/group",
|
||||
Digest: file.Digest{
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "sha1",
|
||||
Value: "Q1oJ16xWudgKOrXIEquEDzlF2Lsm4=",
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "/etc/hostname",
|
||||
Digest: file.Digest{
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "sha1",
|
||||
Value: "Q16nVwYVXP/tChvUPdukVD2ifXOmc=",
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "/etc/hosts",
|
||||
Digest: file.Digest{
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "sha1",
|
||||
Value: "Q1BD6zJKZTRWyqGnPi4tSfd3krsMU=",
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "/etc/inittab",
|
||||
Digest: file.Digest{
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "sha1",
|
||||
Value: "Q1TsthbhW7QzWRe1E/NKwTOuD4pHc=",
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "/etc/modules",
|
||||
Digest: file.Digest{
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "sha1",
|
||||
Value: "Q1toogjUipHGcMgECgPJX64SwUT1M=",
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "/etc/motd",
|
||||
Digest: file.Digest{
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "sha1",
|
||||
Value: "Q1XmduVVNURHQ27TvYp1Lr5TMtFcA=",
|
||||
},
|
||||
@@ -247,35 +247,35 @@ func TestSinglePackageDetails(t *testing.T) {
|
||||
OwnerUID: "0",
|
||||
OwnerGID: "0",
|
||||
Permissions: "777",
|
||||
Digest: file.Digest{
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "sha1",
|
||||
Value: "Q1kiljhXXH1LlQroHsEJIkPZg2eiw=",
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "/etc/passwd",
|
||||
Digest: file.Digest{
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "sha1",
|
||||
Value: "Q1TchuuLUfur0izvfZQZxgN/LJhB8=",
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "/etc/profile",
|
||||
Digest: file.Digest{
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "sha1",
|
||||
Value: "Q1KpFb8kl5LvwXWlY3e58FNsjrI34=",
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "/etc/protocols",
|
||||
Digest: file.Digest{
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "sha1",
|
||||
Value: "Q13FqXUnvuOpMDrH/6rehxuYAEE34=",
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "/etc/services",
|
||||
Digest: file.Digest{
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "sha1",
|
||||
Value: "Q1C6HJNgQvLWqt5VY+n7MZJ1rsDuY=",
|
||||
},
|
||||
@@ -285,21 +285,21 @@ func TestSinglePackageDetails(t *testing.T) {
|
||||
OwnerUID: "0",
|
||||
OwnerGID: "42",
|
||||
Permissions: "640",
|
||||
Digest: file.Digest{
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "sha1",
|
||||
Value: "Q1ltrPIAW2zHeDiajsex2Bdmq3uqA=",
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "/etc/shells",
|
||||
Digest: file.Digest{
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "sha1",
|
||||
Value: "Q1ojm2YdpCJ6B/apGDaZ/Sdb2xJkA=",
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "/etc/sysctl.conf",
|
||||
Digest: file.Digest{
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "sha1",
|
||||
Value: "Q14upz3tfnNxZkIEsUhWn7Xoiw96g=",
|
||||
},
|
||||
@@ -318,7 +318,7 @@ func TestSinglePackageDetails(t *testing.T) {
|
||||
OwnerUID: "0",
|
||||
OwnerGID: "0",
|
||||
Permissions: "600",
|
||||
Digest: file.Digest{
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "sha1",
|
||||
Value: "Q1vfk1apUWI4yLJGhhNRd0kJixfvY=",
|
||||
},
|
||||
@@ -331,28 +331,28 @@ func TestSinglePackageDetails(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Path: "/etc/modprobe.d/aliases.conf",
|
||||
Digest: file.Digest{
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "sha1",
|
||||
Value: "Q1WUbh6TBYNVK7e4Y+uUvLs/7viqk=",
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "/etc/modprobe.d/blacklist.conf",
|
||||
Digest: file.Digest{
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "sha1",
|
||||
Value: "Q1xxYGU6S6TLQvb7ervPrWWwAWqMg=",
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "/etc/modprobe.d/i386.conf",
|
||||
Digest: file.Digest{
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "sha1",
|
||||
Value: "Q1pnay/njn6ol9cCssL7KiZZ8etlc=",
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "/etc/modprobe.d/kms.conf",
|
||||
Digest: file.Digest{
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "sha1",
|
||||
Value: "Q1ynbLn3GYDpvajba/ldp1niayeog=",
|
||||
},
|
||||
@@ -401,14 +401,14 @@ func TestSinglePackageDetails(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Path: "/etc/profile.d/color_prompt",
|
||||
Digest: file.Digest{
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "sha1",
|
||||
Value: "Q10wL23GuSCVfumMRgakabUI6EsSk=",
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "/etc/profile.d/locale",
|
||||
Digest: file.Digest{
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "sha1",
|
||||
Value: "Q1R4bIEpnKxxOSrlnZy9AoawqZ5DU=",
|
||||
},
|
||||
@@ -436,7 +436,7 @@ func TestSinglePackageDetails(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Path: "/lib/sysctl.d/00-alpine.conf",
|
||||
Digest: file.Digest{
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "sha1",
|
||||
Value: "Q1HpElzW1xEgmKfERtTy7oommnq6c=",
|
||||
},
|
||||
@@ -479,7 +479,7 @@ func TestSinglePackageDetails(t *testing.T) {
|
||||
OwnerUID: "0",
|
||||
OwnerGID: "0",
|
||||
Permissions: "755",
|
||||
Digest: file.Digest{
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "sha1",
|
||||
Value: "Q1YeuSmC7iDbEWrusPzA/zUQF6YSg=",
|
||||
},
|
||||
@@ -537,7 +537,7 @@ func TestSinglePackageDetails(t *testing.T) {
|
||||
OwnerUID: "0",
|
||||
OwnerGID: "0",
|
||||
Permissions: "777",
|
||||
Digest: file.Digest{
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "sha1",
|
||||
Value: "Q11/SNZz/8cK2dSKK+cJpVrZIuF4Q=",
|
||||
},
|
||||
@@ -586,7 +586,7 @@ func TestSinglePackageDetails(t *testing.T) {
|
||||
OwnerUID: "0",
|
||||
OwnerGID: "0",
|
||||
Permissions: "777",
|
||||
Digest: file.Digest{
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "sha1",
|
||||
Value: "Q1dzbdazYZA2nTzSIG3YyNw7d4Juc=",
|
||||
},
|
||||
@@ -599,7 +599,7 @@ func TestSinglePackageDetails(t *testing.T) {
|
||||
OwnerUID: "0",
|
||||
OwnerGID: "0",
|
||||
Permissions: "777",
|
||||
Digest: file.Digest{
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "sha1",
|
||||
Value: "Q1OFZt+ZMp7j0Gny0rqSKuWJyqYmA=",
|
||||
},
|
||||
@@ -704,7 +704,7 @@ func TestMultiplePackages(t *testing.T) {
|
||||
OwnerUID: "0",
|
||||
OwnerGID: "0",
|
||||
Permissions: "755",
|
||||
Digest: file.Digest{
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "sha1",
|
||||
Value: "Q1Kja2+POZKxEkUOZqwSjC6kmaED4=",
|
||||
},
|
||||
@@ -720,7 +720,7 @@ func TestMultiplePackages(t *testing.T) {
|
||||
OwnerUID: "0",
|
||||
OwnerGID: "0",
|
||||
Permissions: "755",
|
||||
Digest: file.Digest{
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "sha1",
|
||||
Value: "Q1CVmFbdY+Hv6/jAHl1gec2Kbx1EY=",
|
||||
},
|
||||
@@ -730,7 +730,7 @@ func TestMultiplePackages(t *testing.T) {
|
||||
OwnerUID: "0",
|
||||
OwnerGID: "0",
|
||||
Permissions: "755",
|
||||
Digest: file.Digest{
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "sha1",
|
||||
Value: "Q1yFAhGggmL7ERgbIA7KQxyTzf3ks=",
|
||||
},
|
||||
@@ -740,7 +740,7 @@ func TestMultiplePackages(t *testing.T) {
|
||||
OwnerUID: "0",
|
||||
OwnerGID: "0",
|
||||
Permissions: "755",
|
||||
Digest: file.Digest{
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "sha1",
|
||||
Value: "Q1dAdYK8M/INibRQF5B3Rw7cmNDDA=",
|
||||
},
|
||||
@@ -750,7 +750,7 @@ func TestMultiplePackages(t *testing.T) {
|
||||
OwnerUID: "0",
|
||||
OwnerGID: "0",
|
||||
Permissions: "755",
|
||||
Digest: file.Digest{
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "sha1",
|
||||
Value: "Q1eR2Dz/WylabgbWMTkd2+hGmEya4=",
|
||||
},
|
||||
|
||||
@@ -1,18 +1,188 @@
|
||||
package cpe
|
||||
|
||||
import "github.com/anchore/syft/syft/pkg"
|
||||
import (
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
// this is a static mapping of known package names (keys) to official cpe names for each package
|
||||
type candidatesByPackageType map[pkg.Type]map[string][]string
|
||||
// candidateComposite is a convenience when creating the defaultCandidateAdditions set
|
||||
type candidateComposite struct {
|
||||
pkg.Type
|
||||
candidateKey
|
||||
candidateAddition
|
||||
}
|
||||
|
||||
func (s candidatesByPackageType) getCandidates(t pkg.Type, key string) []string {
|
||||
if _, ok := s[t]; !ok {
|
||||
return nil
|
||||
// defaultCandidateAdditions is all of the known cases for product and vendor field values that should be used when
|
||||
// select package information is discovered
|
||||
var defaultCandidateAdditions = buildCandidateLookup(
|
||||
[]candidateComposite{
|
||||
// Java packages
|
||||
{
|
||||
pkg.JavaPkg,
|
||||
candidateKey{PkgName: "springframework"},
|
||||
candidateAddition{AdditionalProducts: []string{"spring_framework", "springsource_spring_framework"}},
|
||||
},
|
||||
{
|
||||
pkg.JavaPkg,
|
||||
candidateKey{PkgName: "spring-core"},
|
||||
candidateAddition{AdditionalProducts: []string{"spring_framework", "springsource_spring_framework"}},
|
||||
},
|
||||
{
|
||||
// example image: docker.io/nuxeo:latest
|
||||
pkg.JavaPkg,
|
||||
candidateKey{PkgName: "elasticsearch"}, // , Vendor: "elasticsearch"},
|
||||
candidateAddition{AdditionalVendors: []string{"elastic"}},
|
||||
},
|
||||
{
|
||||
// example image: docker.io/kaazing-gateway:latest
|
||||
pkg.JavaPkg,
|
||||
candidateKey{PkgName: "log4j"}, // , Vendor: "apache-software-foundation"},
|
||||
candidateAddition{AdditionalVendors: []string{"apache"}},
|
||||
},
|
||||
|
||||
{
|
||||
// example image: cassandra:latest
|
||||
pkg.JavaPkg,
|
||||
candidateKey{PkgName: "apache-cassandra"}, // , Vendor: "apache"},
|
||||
candidateAddition{AdditionalProducts: []string{"cassandra"}},
|
||||
},
|
||||
|
||||
// NPM packages
|
||||
{
|
||||
pkg.NpmPkg,
|
||||
candidateKey{PkgName: "hapi"},
|
||||
candidateAddition{AdditionalProducts: []string{"hapi_server_framework"}},
|
||||
},
|
||||
{
|
||||
pkg.NpmPkg,
|
||||
candidateKey{PkgName: "handlebars.js"},
|
||||
candidateAddition{AdditionalProducts: []string{"handlebars"}},
|
||||
},
|
||||
{
|
||||
pkg.NpmPkg,
|
||||
candidateKey{PkgName: "is-my-json-valid"},
|
||||
candidateAddition{AdditionalProducts: []string{"is_my_json_valid"}},
|
||||
},
|
||||
{
|
||||
pkg.NpmPkg,
|
||||
candidateKey{PkgName: "mustache"},
|
||||
candidateAddition{AdditionalProducts: []string{"mustache.js"}},
|
||||
},
|
||||
|
||||
// Gem packages
|
||||
{
|
||||
pkg.GemPkg,
|
||||
candidateKey{PkgName: "Arabic-Prawn"},
|
||||
candidateAddition{AdditionalProducts: []string{"arabic_prawn"}},
|
||||
},
|
||||
{
|
||||
pkg.GemPkg,
|
||||
candidateKey{PkgName: "bio-basespace-sdk"},
|
||||
candidateAddition{AdditionalProducts: []string{"basespace_ruby_sdk"}},
|
||||
},
|
||||
{
|
||||
pkg.GemPkg,
|
||||
candidateKey{PkgName: "cremefraiche"},
|
||||
candidateAddition{AdditionalProducts: []string{"creme_fraiche"}},
|
||||
},
|
||||
{
|
||||
pkg.GemPkg,
|
||||
candidateKey{PkgName: "html-sanitizer"},
|
||||
candidateAddition{AdditionalProducts: []string{"html_sanitizer"}},
|
||||
},
|
||||
{
|
||||
pkg.GemPkg,
|
||||
candidateKey{PkgName: "sentry-raven"},
|
||||
candidateAddition{AdditionalProducts: []string{"raven-ruby"}},
|
||||
},
|
||||
{
|
||||
pkg.GemPkg,
|
||||
candidateKey{PkgName: "RedCloth"},
|
||||
candidateAddition{AdditionalProducts: []string{"redcloth_library"}},
|
||||
},
|
||||
{
|
||||
pkg.GemPkg,
|
||||
candidateKey{PkgName: "VladTheEnterprising"},
|
||||
candidateAddition{AdditionalProducts: []string{"vladtheenterprising"}},
|
||||
},
|
||||
{
|
||||
pkg.GemPkg,
|
||||
candidateKey{PkgName: "yajl-ruby"},
|
||||
candidateAddition{AdditionalProducts: []string{"yajl-ruby_gem"}},
|
||||
},
|
||||
// Python packages
|
||||
{
|
||||
pkg.PythonPkg,
|
||||
candidateKey{PkgName: "python-rrdtool"},
|
||||
candidateAddition{AdditionalProducts: []string{"rrdtool"}},
|
||||
},
|
||||
})
|
||||
|
||||
// buildCandidateLookup is a convenience function for creating the defaultCandidateAdditions set
|
||||
func buildCandidateLookup(cc []candidateComposite) (ca map[pkg.Type]map[candidateKey]candidateAddition) {
|
||||
ca = make(map[pkg.Type]map[candidateKey]candidateAddition)
|
||||
for _, c := range cc {
|
||||
if _, ok := ca[c.Type]; !ok {
|
||||
ca[c.Type] = make(map[candidateKey]candidateAddition)
|
||||
}
|
||||
ca[c.Type][c.candidateKey] = c.candidateAddition
|
||||
}
|
||||
value, ok := s[t][key]
|
||||
|
||||
return ca
|
||||
}
|
||||
|
||||
// candidateKey represents the set of inputs that should be matched on in order to signal more candidate additions to be used.
|
||||
type candidateKey struct {
|
||||
Vendor string
|
||||
PkgName string
|
||||
}
|
||||
|
||||
// candidateAddition are the specific additions that should be considered during CPE generation (given a specific candidateKey)
|
||||
type candidateAddition struct {
|
||||
AdditionalProducts []string
|
||||
AdditionalVendors []string
|
||||
}
|
||||
|
||||
// findAdditionalVendors searches all possible vendor additions that could be added during the CPE generation process (given package info + a vendor candidate)
|
||||
func findAdditionalVendors(allAdditions map[pkg.Type]map[candidateKey]candidateAddition, ty pkg.Type, pkgName, vendor string) (vendors []string) {
|
||||
additions, ok := allAdditions[ty]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return value
|
||||
if addition, ok := additions[candidateKey{
|
||||
Vendor: vendor,
|
||||
PkgName: pkgName,
|
||||
}]; ok {
|
||||
vendors = append(vendors, addition.AdditionalVendors...)
|
||||
}
|
||||
|
||||
if addition, ok := additions[candidateKey{
|
||||
PkgName: pkgName,
|
||||
}]; ok {
|
||||
vendors = append(vendors, addition.AdditionalVendors...)
|
||||
}
|
||||
|
||||
if addition, ok := additions[candidateKey{
|
||||
Vendor: vendor,
|
||||
}]; ok {
|
||||
vendors = append(vendors, addition.AdditionalVendors...)
|
||||
}
|
||||
|
||||
return vendors
|
||||
}
|
||||
|
||||
// findAdditionalProducts searches all possible product additions that could be added during the CPE generation process (given package info)
|
||||
func findAdditionalProducts(allAdditions map[pkg.Type]map[candidateKey]candidateAddition, ty pkg.Type, pkgName string) (products []string) {
|
||||
additions, ok := allAdditions[ty]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if addition, ok := additions[candidateKey{
|
||||
PkgName: pkgName,
|
||||
}]; ok {
|
||||
products = append(products, addition.AdditionalProducts...)
|
||||
}
|
||||
|
||||
return products
|
||||
}
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
package cpe
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_additionalProducts(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
allAdditions map[pkg.Type]map[candidateKey]candidateAddition
|
||||
ty pkg.Type
|
||||
pkgName string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "product name addition",
|
||||
allAdditions: map[pkg.Type]map[candidateKey]candidateAddition{
|
||||
pkg.JavaPkg: {
|
||||
candidateKey{
|
||||
PkgName: "spring-core",
|
||||
}: {
|
||||
AdditionalProducts: []string{"spring_framework", "springsource_spring_framework"},
|
||||
},
|
||||
},
|
||||
},
|
||||
ty: pkg.JavaPkg,
|
||||
pkgName: "spring-core",
|
||||
expected: []string{"spring_framework", "springsource_spring_framework"},
|
||||
},
|
||||
{
|
||||
name: "no addition found",
|
||||
allAdditions: map[pkg.Type]map[candidateKey]candidateAddition{
|
||||
pkg.JavaPkg: {
|
||||
candidateKey{
|
||||
PkgName: "spring-core",
|
||||
}: {
|
||||
AdditionalProducts: []string{"spring_framework", "springsource_spring_framework"},
|
||||
},
|
||||
},
|
||||
},
|
||||
ty: pkg.JavaPkg,
|
||||
pkgName: "nothing",
|
||||
expected: nil,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, findAdditionalProducts(test.allAdditions, test.ty, test.pkgName))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_additionalVendors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
allAdditions map[pkg.Type]map[candidateKey]candidateAddition
|
||||
ty pkg.Type
|
||||
pkgName string
|
||||
vendor string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "vendor addition by input vendor",
|
||||
allAdditions: map[pkg.Type]map[candidateKey]candidateAddition{
|
||||
pkg.JavaPkg: {
|
||||
candidateKey{
|
||||
Vendor: "my-vendor",
|
||||
}: {
|
||||
AdditionalVendors: []string{"awesome-vendor-addition"},
|
||||
},
|
||||
// note: the below keys should not be matched
|
||||
candidateKey{
|
||||
PkgName: "my-package-name",
|
||||
Vendor: "my-vendor",
|
||||
}: {
|
||||
AdditionalVendors: []string{"bad-addition"},
|
||||
},
|
||||
candidateKey{
|
||||
PkgName: "my-package-name",
|
||||
}: {
|
||||
AdditionalVendors: []string{"bad-addition"},
|
||||
},
|
||||
},
|
||||
},
|
||||
ty: pkg.JavaPkg,
|
||||
pkgName: "NOT-MY-PACKAGE",
|
||||
vendor: "my-vendor",
|
||||
expected: []string{"awesome-vendor-addition"},
|
||||
},
|
||||
{
|
||||
name: "vendor addition by input package name",
|
||||
allAdditions: map[pkg.Type]map[candidateKey]candidateAddition{
|
||||
pkg.JavaPkg: {
|
||||
candidateKey{
|
||||
PkgName: "my-package-name",
|
||||
}: {
|
||||
AdditionalVendors: []string{"awesome-vendor-addition"},
|
||||
},
|
||||
// note: the below keys should not be matched
|
||||
candidateKey{
|
||||
PkgName: "my-package-name",
|
||||
Vendor: "my-vendor",
|
||||
}: {
|
||||
AdditionalVendors: []string{"bad-addition"},
|
||||
},
|
||||
candidateKey{
|
||||
Vendor: "my-vendor",
|
||||
}: {
|
||||
AdditionalVendors: []string{"bad-addition"},
|
||||
},
|
||||
},
|
||||
},
|
||||
ty: pkg.JavaPkg,
|
||||
pkgName: "my-package-name",
|
||||
vendor: "NOT-MY-VENDOR",
|
||||
expected: []string{"awesome-vendor-addition"},
|
||||
},
|
||||
{
|
||||
name: "vendor addition by input package name + vendor",
|
||||
allAdditions: map[pkg.Type]map[candidateKey]candidateAddition{
|
||||
pkg.JavaPkg: {
|
||||
candidateKey{
|
||||
PkgName: "my-package-name",
|
||||
Vendor: "my-vendor",
|
||||
}: {
|
||||
AdditionalVendors: []string{"awesome-vendor-addition"},
|
||||
},
|
||||
// note: the below keys should not be matched
|
||||
candidateKey{
|
||||
PkgName: "my-package-name",
|
||||
}: {
|
||||
AdditionalVendors: []string{"one-good-addition"},
|
||||
},
|
||||
candidateKey{
|
||||
Vendor: "my-vendor",
|
||||
}: {
|
||||
AdditionalVendors: []string{"another-good-addition"},
|
||||
},
|
||||
},
|
||||
},
|
||||
ty: pkg.JavaPkg,
|
||||
pkgName: "my-package-name",
|
||||
vendor: "my-vendor",
|
||||
expected: []string{"awesome-vendor-addition", "one-good-addition", "another-good-addition"},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, findAdditionalVendors(test.allAdditions, test.ty, test.pkgName, test.vendor))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package cpe
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/scylladb/go-set/strset"
|
||||
)
|
||||
|
||||
@@ -16,7 +18,7 @@ type fieldCandidate struct {
|
||||
|
||||
type fieldCandidateSet map[fieldCandidate]struct{}
|
||||
|
||||
func newFieldCandidateFromSets(sets ...fieldCandidateSet) fieldCandidateSet {
|
||||
func newFieldCandidateSetFromSets(sets ...fieldCandidateSet) fieldCandidateSet {
|
||||
s := newFieldCandidateSet()
|
||||
for _, set := range sets {
|
||||
s.add(set.list()...)
|
||||
@@ -34,7 +36,7 @@ func (s fieldCandidateSet) addValue(values ...string) {
|
||||
for _, value := range values {
|
||||
// default candidate as an allow-all
|
||||
candidate := fieldCandidate{
|
||||
value: value,
|
||||
value: cleanCandidateField(value),
|
||||
}
|
||||
s[candidate] = struct{}{}
|
||||
}
|
||||
@@ -42,10 +44,26 @@ func (s fieldCandidateSet) addValue(values ...string) {
|
||||
|
||||
func (s fieldCandidateSet) add(candidates ...fieldCandidate) {
|
||||
for _, candidate := range candidates {
|
||||
candidate.value = cleanCandidateField(candidate.value)
|
||||
s[candidate] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
func (s fieldCandidateSet) removeByValue(values ...string) {
|
||||
for _, value := range values {
|
||||
s.removeWhere(valueEquals(value))
|
||||
}
|
||||
}
|
||||
|
||||
// removeWhere removes all entries from the fieldCandidateSet for which the condition function returns true.
|
||||
func (s fieldCandidateSet) removeWhere(condition fieldCandidateCondition) {
|
||||
for candidate := range s {
|
||||
if condition(candidate) {
|
||||
delete(s, candidate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s fieldCandidateSet) clear() {
|
||||
for k := range s {
|
||||
delete(s, k)
|
||||
@@ -58,26 +76,37 @@ func (s fieldCandidateSet) union(others ...fieldCandidateSet) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s fieldCandidateSet) list(filters ...filterFieldCandidateFn) (results []fieldCandidate) {
|
||||
candidateLoop:
|
||||
func (s fieldCandidateSet) list() (results []fieldCandidate) {
|
||||
for c := range s {
|
||||
for _, fn := range filters {
|
||||
if fn(c) {
|
||||
continue candidateLoop
|
||||
}
|
||||
}
|
||||
results = append(results, c)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func (s fieldCandidateSet) values(filters ...filterFieldCandidateFn) (results []string) {
|
||||
for _, c := range s.list(filters...) {
|
||||
func (s fieldCandidateSet) values() (results []string) {
|
||||
for _, c := range s.list() {
|
||||
results = append(results, c.value)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func (s fieldCandidateSet) uniqueValues(filters ...filterFieldCandidateFn) []string {
|
||||
return strset.New(s.values(filters...)...).List()
|
||||
func (s fieldCandidateSet) uniqueValues() []string {
|
||||
return strset.New(s.values()...).List()
|
||||
}
|
||||
|
||||
func (s fieldCandidateSet) copy() fieldCandidateSet {
|
||||
newSet := newFieldCandidateSet()
|
||||
newSet.add(s.list()...)
|
||||
|
||||
return newSet
|
||||
}
|
||||
|
||||
func cleanCandidateField(field string) string {
|
||||
cleanedValue, err := strconv.Unquote(field)
|
||||
if err != nil {
|
||||
return field
|
||||
}
|
||||
return cleanedValue
|
||||
}
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
package cpe
|
||||
|
||||
// filterFieldCandidateFn instances should return true if the given fieldCandidate should be removed from a collection
|
||||
type filterFieldCandidateFn func(fieldCandidate) bool
|
||||
// A fieldCandidateCondition returns true if the condition is true for a given fieldCandidate.
|
||||
type fieldCandidateCondition func(fieldCandidate) bool
|
||||
|
||||
func filterOutBySubselection(c fieldCandidate) bool {
|
||||
func subSelectionsDisallowed(c fieldCandidate) bool {
|
||||
return c.disallowSubSelections
|
||||
}
|
||||
|
||||
func filterOutByDelimiterVariations(c fieldCandidate) bool {
|
||||
func delimiterVariationsDisallowed(c fieldCandidate) bool {
|
||||
return c.disallowDelimiterVariations
|
||||
}
|
||||
|
||||
func valueEquals(v string) fieldCandidateCondition {
|
||||
return func(candidate fieldCandidate) bool {
|
||||
return candidate.value == v
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package cpe
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -8,10 +9,10 @@ import (
|
||||
|
||||
func Test_cpeCandidateValues_filter(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []fieldCandidate
|
||||
filters []filterFieldCandidateFn
|
||||
expect []string
|
||||
name string
|
||||
input []fieldCandidate
|
||||
exclusionConditions []fieldCandidateCondition
|
||||
expect []string
|
||||
}{
|
||||
{
|
||||
name: "gocase",
|
||||
@@ -60,8 +61,8 @@ func Test_cpeCandidateValues_filter(t *testing.T) {
|
||||
disallowDelimiterVariations: true,
|
||||
},
|
||||
},
|
||||
filters: []filterFieldCandidateFn{
|
||||
filterOutBySubselection,
|
||||
exclusionConditions: []fieldCandidateCondition{
|
||||
subSelectionsDisallowed,
|
||||
},
|
||||
expect: []string{
|
||||
"allow anything",
|
||||
@@ -88,8 +89,8 @@ func Test_cpeCandidateValues_filter(t *testing.T) {
|
||||
disallowDelimiterVariations: true,
|
||||
},
|
||||
},
|
||||
filters: []filterFieldCandidateFn{
|
||||
filterOutByDelimiterVariations,
|
||||
exclusionConditions: []fieldCandidateCondition{
|
||||
delimiterVariationsDisallowed,
|
||||
},
|
||||
expect: []string{
|
||||
"allow anything",
|
||||
@@ -97,7 +98,7 @@ func Test_cpeCandidateValues_filter(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "all filters",
|
||||
name: "all exclusionConditions",
|
||||
input: []fieldCandidate{
|
||||
{
|
||||
value: "allow anything",
|
||||
@@ -116,9 +117,9 @@ func Test_cpeCandidateValues_filter(t *testing.T) {
|
||||
disallowDelimiterVariations: true,
|
||||
},
|
||||
},
|
||||
filters: []filterFieldCandidateFn{
|
||||
filterOutByDelimiterVariations,
|
||||
filterOutBySubselection,
|
||||
exclusionConditions: []fieldCandidateCondition{
|
||||
delimiterVariationsDisallowed,
|
||||
subSelectionsDisallowed,
|
||||
},
|
||||
expect: []string{
|
||||
"allow anything",
|
||||
@@ -130,11 +131,32 @@ func Test_cpeCandidateValues_filter(t *testing.T) {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
set := newFieldCandidateSet()
|
||||
set.add(test.input...)
|
||||
assert.ElementsMatch(t, test.expect, set.values(test.filters...))
|
||||
|
||||
for _, condition := range test.exclusionConditions {
|
||||
set.removeWhere(condition)
|
||||
}
|
||||
|
||||
assert.ElementsMatch(t, test.expect, set.values())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_cpeFieldCandidateSet_addValue(t *testing.T) {
|
||||
s := newFieldCandidateSet()
|
||||
// we should clean all values (unquote strings)
|
||||
s.addValue(`"string!"`)
|
||||
assert.ElementsMatch(t, []string{"string!"}, s.values())
|
||||
}
|
||||
|
||||
func Test_cpeFieldCandidateSet_add(t *testing.T) {
|
||||
s := newFieldCandidateSet()
|
||||
// we should clean all values (unquote strings)
|
||||
s.add(fieldCandidate{
|
||||
value: `"string!"`,
|
||||
})
|
||||
assert.ElementsMatch(t, []string{"string!"}, s.values())
|
||||
}
|
||||
|
||||
func Test_cpeFieldCandidateSet_clear(t *testing.T) {
|
||||
s := newFieldCandidateSet("1", "2")
|
||||
assert.NotEmpty(t, s.values())
|
||||
@@ -261,3 +283,68 @@ func Test_cpeFieldCandidateSet_uniqueValues(t *testing.T) {
|
||||
assert.ElementsMatch(t, []string{"1", "2", "3"}, set.uniqueValues())
|
||||
|
||||
}
|
||||
|
||||
func Test_cpeFieldCandidateSet_removeByValue(t *testing.T) {
|
||||
s := newFieldCandidateSet()
|
||||
|
||||
// should be removed
|
||||
s.add(fieldCandidate{
|
||||
value: "1",
|
||||
disallowSubSelections: true,
|
||||
disallowDelimiterVariations: true,
|
||||
})
|
||||
s.add(fieldCandidate{
|
||||
value: "1",
|
||||
disallowSubSelections: true,
|
||||
})
|
||||
s.add(fieldCandidate{
|
||||
value: "1",
|
||||
disallowDelimiterVariations: true,
|
||||
})
|
||||
s.add(fieldCandidate{
|
||||
value: "1",
|
||||
})
|
||||
|
||||
// should not be removed
|
||||
s.add(fieldCandidate{
|
||||
value: "2",
|
||||
})
|
||||
|
||||
assert.Len(t, s.values(), 5)
|
||||
|
||||
s.removeByValue("1")
|
||||
|
||||
assert.Len(t, s.values(), 1)
|
||||
}
|
||||
|
||||
func Test_cpeFieldCandidateSet_removeByCondition(t *testing.T) {
|
||||
s := newFieldCandidateSet()
|
||||
|
||||
// should be removed
|
||||
s.add(fieldCandidate{
|
||||
value: "1",
|
||||
disallowSubSelections: true,
|
||||
})
|
||||
s.add(fieldCandidate{
|
||||
value: "hello-world",
|
||||
})
|
||||
|
||||
// should not be removed
|
||||
s.add(fieldCandidate{
|
||||
value: "2",
|
||||
})
|
||||
|
||||
assert.Len(t, s.values(), 3)
|
||||
|
||||
s.removeWhere(func(candidate fieldCandidate) bool {
|
||||
return candidate.disallowSubSelections == true
|
||||
})
|
||||
|
||||
assert.Len(t, s.values(), 2)
|
||||
|
||||
s.removeWhere(func(candidate fieldCandidate) bool {
|
||||
return strings.Contains(candidate.value, "-")
|
||||
})
|
||||
|
||||
assert.Len(t, s.values(), 1)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ var cpeFilters = []filterFn{
|
||||
disallowJiraClientServerMismatch,
|
||||
disallowJenkinsServerCPEForPluginPackage,
|
||||
disallowJenkinsCPEsNotAssociatedWithJenkins,
|
||||
disallowNonParseableCPEs,
|
||||
}
|
||||
|
||||
func filter(cpes []pkg.CPE, p pkg.Package, filters ...filterFn) (result []pkg.CPE) {
|
||||
@@ -32,6 +33,15 @@ cpeLoop:
|
||||
return result
|
||||
}
|
||||
|
||||
func disallowNonParseableCPEs(cpe pkg.CPE, _ pkg.Package) bool {
|
||||
v := cpe.BindToFmtString()
|
||||
_, err := pkg.NewCPE(v)
|
||||
|
||||
cannotParse := err != nil
|
||||
|
||||
return cannotParse
|
||||
}
|
||||
|
||||
// jenkins plugins should not match against jenkins
|
||||
func disallowJenkinsServerCPEForPluginPackage(cpe pkg.CPE, p pkg.Package) bool {
|
||||
if p.Type == pkg.JenkinsPluginPkg && cpe.Product == jenkinsName {
|
||||
|
||||
@@ -12,32 +12,6 @@ import (
|
||||
"github.com/facebookincubator/nvdtools/wfn"
|
||||
)
|
||||
|
||||
var productCandidatesByPkgType = candidatesByPackageType{
|
||||
pkg.JavaPkg: {
|
||||
"springframework": []string{"spring_framework", "springsource_spring_framework"},
|
||||
"spring-core": []string{"spring_framework", "springsource_spring_framework"},
|
||||
},
|
||||
pkg.NpmPkg: {
|
||||
"hapi": []string{"hapi_server_framework"},
|
||||
"handlebars.js": []string{"handlebars"},
|
||||
"is-my-json-valid": []string{"is_my_json_valid"},
|
||||
"mustache": []string{"mustache.js"},
|
||||
},
|
||||
pkg.GemPkg: {
|
||||
"Arabic-Prawn": []string{"arabic_prawn"},
|
||||
"bio-basespace-sdk": []string{"basespace_ruby_sdk"},
|
||||
"cremefraiche": []string{"creme_fraiche"},
|
||||
"html-sanitizer": []string{"html_sanitizer"},
|
||||
"sentry-raven": []string{"raven-ruby"},
|
||||
"RedCloth": []string{"redcloth_library"},
|
||||
"VladTheEnterprising": []string{"vladtheenterprising"},
|
||||
"yajl-ruby": []string{"yajl-ruby_gem"},
|
||||
},
|
||||
pkg.PythonPkg: {
|
||||
"python-rrdtool": []string{"rrdtool"},
|
||||
},
|
||||
}
|
||||
|
||||
func newCPE(product, vendor, version, targetSW string) wfn.Attributes {
|
||||
cpe := *(wfn.NewAttributesWithAny())
|
||||
cpe.Part = "a"
|
||||
@@ -129,6 +103,11 @@ func candidateVendors(p pkg.Package) []string {
|
||||
// generate sub-selections of each candidate based on separators (e.g. jenkins-ci -> [jenkins, jenkins-ci])
|
||||
addAllSubSelections(vendors)
|
||||
|
||||
// add more candidates based on the package info for each vendor candidate
|
||||
for _, vendor := range vendors.uniqueValues() {
|
||||
vendors.addValue(findAdditionalVendors(defaultCandidateAdditions, p.Type, p.Name, vendor)...)
|
||||
}
|
||||
|
||||
return vendors.uniqueValues()
|
||||
}
|
||||
|
||||
@@ -151,17 +130,25 @@ func candidateProducts(p pkg.Package) []string {
|
||||
products.addValue(prod)
|
||||
}
|
||||
}
|
||||
// it is never OK to have candidates with these values ["" and "*"] (since CPEs will match any other value)
|
||||
products.removeByValue("")
|
||||
products.removeByValue("*")
|
||||
|
||||
// try swapping hyphens for underscores, vice versa, and removing separators altogether
|
||||
addDelimiterVariations(products)
|
||||
|
||||
// prepend any known product names for the given package type and name (note: this is not a replacement)
|
||||
return append(productCandidatesByPkgType.getCandidates(p.Type, p.Name), products.uniqueValues()...)
|
||||
// add known candidate additions
|
||||
products.addValue(findAdditionalProducts(defaultCandidateAdditions, p.Type, p.Name)...)
|
||||
|
||||
return products.uniqueValues()
|
||||
}
|
||||
|
||||
func addAllSubSelections(set fieldCandidateSet) {
|
||||
for _, candidate := range set.values(filterOutBySubselection) {
|
||||
set.addValue(generateSubSelections(candidate)...)
|
||||
func addAllSubSelections(fields fieldCandidateSet) {
|
||||
candidatesForVariations := fields.copy()
|
||||
candidatesForVariations.removeWhere(subSelectionsDisallowed)
|
||||
|
||||
for _, candidate := range candidatesForVariations.values() {
|
||||
fields.addValue(generateSubSelections(candidate)...)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,7 +210,10 @@ func scanByHyphenOrUnderscore(data []byte, atEOF bool) (advance int, token []byt
|
||||
}
|
||||
|
||||
func addDelimiterVariations(fields fieldCandidateSet) {
|
||||
for _, candidate := range fields.list(filterOutByDelimiterVariations) {
|
||||
candidatesForVariations := fields.copy()
|
||||
candidatesForVariations.removeWhere(delimiterVariationsDisallowed)
|
||||
|
||||
for _, candidate := range candidatesForVariations.list() {
|
||||
field := candidate.value
|
||||
hasHyphen := strings.Contains(field, "-")
|
||||
hasUnderscore := strings.Contains(field, "_")
|
||||
|
||||
@@ -179,6 +179,43 @@ func TestGeneratePackageCPEs(t *testing.T) {
|
||||
"cpe:2.3:a:sonatype:nexus:3.2:*:*:*:*:*:*:*",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "java with URL in metadata", // regression: https://github.com/anchore/grype/issues/417
|
||||
p: pkg.Package{
|
||||
Name: "wstx-asl",
|
||||
Version: "3.2.7",
|
||||
Type: pkg.JavaPkg,
|
||||
MetadataType: pkg.JavaMetadataType,
|
||||
Metadata: pkg.JavaMetadata{
|
||||
Manifest: &pkg.JavaManifest{
|
||||
Main: map[string]string{
|
||||
"Ant-Version": "Apache Ant 1.6.5",
|
||||
"Built-By": "tatu",
|
||||
"Created-By": "1.4.2_03-b02 (Sun Microsystems Inc.)",
|
||||
"Implementation-Title": "WoodSToX XML-processor",
|
||||
"Implementation-Vendor": "woodstox.codehaus.org",
|
||||
"Implementation-Version": "3.2.7",
|
||||
"Manifest-Version": "1.0",
|
||||
"Specification-Title": "StAX 1.0 API",
|
||||
"Specification-Vendor": "http://jcp.org/en/jsr/detail?id=173",
|
||||
"Specification-Version": "1.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []string{
|
||||
"cpe:2.3:a:woodstox_codehaus_org:wstx-asl:3.2.7:*:*:*:*:*:*:*",
|
||||
"cpe:2.3:a:woodstox_codehaus_org:wstx_asl:3.2.7:*:*:*:*:*:*:*",
|
||||
"cpe:2.3:a:woodstox-codehaus-org:wstx_asl:3.2.7:*:*:*:*:*:*:*",
|
||||
"cpe:2.3:a:woodstox-codehaus-org:wstx-asl:3.2.7:*:*:*:*:*:*:*",
|
||||
"cpe:2.3:a:wstx_asl:wstx-asl:3.2.7:*:*:*:*:*:*:*",
|
||||
"cpe:2.3:a:wstx-asl:wstx-asl:3.2.7:*:*:*:*:*:*:*",
|
||||
"cpe:2.3:a:wstx-asl:wstx_asl:3.2.7:*:*:*:*:*:*:*",
|
||||
"cpe:2.3:a:wstx_asl:wstx_asl:3.2.7:*:*:*:*:*:*:*",
|
||||
"cpe:2.3:a:wstx:wstx_asl:3.2.7:*:*:*:*:*:*:*",
|
||||
"cpe:2.3:a:wstx:wstx-asl:3.2.7:*:*:*:*:*:*:*",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "jenkins package identified via pkg type",
|
||||
p: pkg.Package{
|
||||
@@ -488,7 +525,7 @@ func TestGeneratePackageCPEs(t *testing.T) {
|
||||
actualCpeSet.Add(a.BindToFmtString())
|
||||
}
|
||||
|
||||
extra := strset.Difference(expectedCpeSet, actualCpeSet).List()
|
||||
extra := strset.Difference(actualCpeSet, expectedCpeSet).List()
|
||||
sort.Strings(extra)
|
||||
if len(extra) > 0 {
|
||||
t.Errorf("found extra CPEs:")
|
||||
@@ -497,7 +534,7 @@ func TestGeneratePackageCPEs(t *testing.T) {
|
||||
fmt.Printf(" %q,\n", d)
|
||||
}
|
||||
|
||||
missing := strset.Difference(actualCpeSet, expectedCpeSet).List()
|
||||
missing := strset.Difference(expectedCpeSet, actualCpeSet).List()
|
||||
sort.Strings(missing)
|
||||
if len(missing) > 0 {
|
||||
t.Errorf("missing CPEs:")
|
||||
@@ -511,10 +548,20 @@ func TestGeneratePackageCPEs(t *testing.T) {
|
||||
|
||||
func TestCandidateProducts(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
p pkg.Package
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "apache-cassandra",
|
||||
p: pkg.Package{
|
||||
Name: "apache-cassandra",
|
||||
Type: pkg.JavaPkg,
|
||||
},
|
||||
expected: []string{"cassandra" /* <-- known good names | default guess --> */, "apache-cassandra", "apache_cassandra"},
|
||||
},
|
||||
{
|
||||
name: "springframework",
|
||||
p: pkg.Package{
|
||||
Name: "springframework",
|
||||
Type: pkg.JavaPkg,
|
||||
@@ -522,6 +569,7 @@ func TestCandidateProducts(t *testing.T) {
|
||||
expected: []string{"spring_framework", "springsource_spring_framework" /* <-- known good names | default guess --> */, "springframework"},
|
||||
},
|
||||
{
|
||||
name: "java",
|
||||
p: pkg.Package{
|
||||
Name: "some-java-package-with-group-id",
|
||||
Type: pkg.JavaPkg,
|
||||
@@ -535,6 +583,21 @@ func TestCandidateProducts(t *testing.T) {
|
||||
expected: []string{"itunes", "some-java-package-with-group-id", "some_java_package_with_group_id"},
|
||||
},
|
||||
{
|
||||
name: "java-with-asterisk",
|
||||
p: pkg.Package{
|
||||
Name: "some-java-package-with-group-id",
|
||||
Type: pkg.JavaPkg,
|
||||
Language: pkg.Java,
|
||||
Metadata: pkg.JavaMetadata{
|
||||
PomProperties: &pkg.PomProperties{
|
||||
GroupID: "com.apple.itunes.*",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []string{"itunes", "some-java-package-with-group-id", "some_java_package_with_group_id"},
|
||||
},
|
||||
{
|
||||
name: "jenkins-plugin",
|
||||
p: pkg.Package{
|
||||
Name: "some-jenkins-plugin",
|
||||
Type: pkg.JenkinsPluginPkg,
|
||||
@@ -548,6 +611,7 @@ func TestCandidateProducts(t *testing.T) {
|
||||
expected: []string{"some-jenkins-plugin", "some_jenkins_plugin", "jenkins"},
|
||||
},
|
||||
{
|
||||
name: "javascript",
|
||||
p: pkg.Package{
|
||||
Name: "handlebars.js",
|
||||
Type: pkg.NpmPkg,
|
||||
@@ -555,6 +619,7 @@ func TestCandidateProducts(t *testing.T) {
|
||||
expected: []string{"handlebars" /* <-- known good names | default guess --> */, "handlebars.js"},
|
||||
},
|
||||
{
|
||||
name: "gem",
|
||||
p: pkg.Package{
|
||||
Name: "RedCloth",
|
||||
Type: pkg.GemPkg,
|
||||
@@ -562,6 +627,7 @@ func TestCandidateProducts(t *testing.T) {
|
||||
expected: []string{"redcloth_library" /* <-- known good names | default guess --> */, "RedCloth"},
|
||||
},
|
||||
{
|
||||
name: "python",
|
||||
p: pkg.Package{
|
||||
Name: "python-rrdtool",
|
||||
Type: pkg.PythonPkg,
|
||||
@@ -577,6 +643,37 @@ func TestCandidateProducts(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCandidateVendor(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
p pkg.Package
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "elasticsearch",
|
||||
p: pkg.Package{
|
||||
Name: "elasticsearch",
|
||||
Type: pkg.JavaPkg,
|
||||
},
|
||||
expected: []string{"elastic" /* <-- known good names | default guess --> */, "elasticsearch"},
|
||||
},
|
||||
{
|
||||
name: "log4j",
|
||||
p: pkg.Package{
|
||||
Name: "log4j",
|
||||
Type: pkg.JavaPkg,
|
||||
},
|
||||
expected: []string{"apache" /* <-- known good names | default guess --> */, "log4j"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprintf("%+v %+v", test.p, test.expected), func(t *testing.T) {
|
||||
assert.ElementsMatch(t, test.expected, candidateVendors(test.p))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_generateSubSelections(t *testing.T) {
|
||||
tests := []struct {
|
||||
field string
|
||||
|
||||
@@ -46,7 +46,7 @@ func candidateProductsForJava(p pkg.Package) []string {
|
||||
func candidateVendorsForJava(p pkg.Package) fieldCandidateSet {
|
||||
gidVendors := vendorsFromGroupIDs(groupIDsFromJavaPackage(p))
|
||||
nameVendors := vendorsFromJavaManifestNames(p)
|
||||
return newFieldCandidateFromSets(gidVendors, nameVendors)
|
||||
return newFieldCandidateSetFromSets(gidVendors, nameVendors)
|
||||
}
|
||||
|
||||
func vendorsFromJavaManifestNames(p pkg.Package) fieldCandidateSet {
|
||||
@@ -192,13 +192,13 @@ func groupIDsFromPomProperties(properties *pkg.PomProperties) (groupIDs []string
|
||||
}
|
||||
|
||||
if startsWithTopLevelDomain(properties.GroupID) {
|
||||
groupIDs = append(groupIDs, strings.TrimSpace(properties.GroupID))
|
||||
groupIDs = append(groupIDs, cleanGroupID(properties.GroupID))
|
||||
}
|
||||
|
||||
// sometimes the publisher puts the group ID in the artifact ID field unintentionally
|
||||
if startsWithTopLevelDomain(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))
|
||||
groupIDs = append(groupIDs, cleanGroupID(properties.ArtifactID))
|
||||
}
|
||||
|
||||
return groupIDs
|
||||
@@ -224,13 +224,13 @@ func groupIDsFromPomProject(project *pkg.PomProject) (groupIDs []string) {
|
||||
|
||||
func addGroupIDsFromGroupIDsAndArtifactID(groupID, artifactID string) (groupIDs []string) {
|
||||
if startsWithTopLevelDomain(groupID) {
|
||||
groupIDs = append(groupIDs, strings.TrimSpace(groupID))
|
||||
groupIDs = append(groupIDs, cleanGroupID(groupID))
|
||||
}
|
||||
|
||||
// sometimes the publisher puts the group ID in the artifact ID field unintentionally
|
||||
if startsWithTopLevelDomain(artifactID) && len(strings.Split(artifactID, ".")) > 1 {
|
||||
// there is a strong indication that the artifact ID is really a group ID
|
||||
groupIDs = append(groupIDs, strings.TrimSpace(artifactID))
|
||||
groupIDs = append(groupIDs, cleanGroupID(artifactID))
|
||||
}
|
||||
return groupIDs
|
||||
}
|
||||
@@ -263,13 +263,13 @@ func getManifestFieldGroupIDs(manifest *pkg.JavaManifest, fields []string) (grou
|
||||
for _, name := range fields {
|
||||
if value, exists := manifest.Main[name]; exists {
|
||||
if startsWithTopLevelDomain(value) {
|
||||
groupIDs = append(groupIDs, value)
|
||||
groupIDs = append(groupIDs, cleanGroupID(value))
|
||||
}
|
||||
}
|
||||
for _, section := range manifest.NamedSections {
|
||||
if value, exists := section[name]; exists {
|
||||
if startsWithTopLevelDomain(value) {
|
||||
groupIDs = append(groupIDs, value)
|
||||
groupIDs = append(groupIDs, cleanGroupID(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -278,6 +278,17 @@ func getManifestFieldGroupIDs(manifest *pkg.JavaManifest, fields []string) (grou
|
||||
return groupIDs
|
||||
}
|
||||
|
||||
func cleanGroupID(groupID string) string {
|
||||
return strings.TrimSpace(removeOSCIDirectives(groupID))
|
||||
}
|
||||
|
||||
func removeOSCIDirectives(groupID string) string {
|
||||
// for example:
|
||||
// org.bar;uses:=“org.foo” -> org.bar
|
||||
// more about OSGI directives see https://spring.io/blog/2008/10/20/understanding-the-osgi-uses-directive/
|
||||
return strings.Split(groupID, ";")[0]
|
||||
}
|
||||
|
||||
func startsWithTopLevelDomain(value string) bool {
|
||||
return internal.HasAnyOfPrefixes(value, domains...)
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ func Test_groupIDsFromJavaPackage(t *testing.T) {
|
||||
pkg: pkg.Package{
|
||||
Metadata: pkg.JavaMetadata{
|
||||
PomProperties: &pkg.PomProperties{
|
||||
GroupID: "io.jenkins-ci.plugin.thing",
|
||||
GroupID: "io.jenkins-ci.plugin.thing;version='[2,3)'",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -164,7 +164,7 @@ func Test_groupIDsFromJavaPackage(t *testing.T) {
|
||||
pkg: pkg.Package{
|
||||
Metadata: pkg.JavaMetadata{
|
||||
PomProperties: &pkg.PomProperties{
|
||||
ArtifactID: "io.jenkins-ci.plugin.thing",
|
||||
ArtifactID: "io.jenkins-ci.plugin.thing; version='[2,3)' ; org.something.else",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -18,10 +18,18 @@ func (c BySpecificity) Less(i, j int) bool {
|
||||
iScore := weightedCountForSpecifiedFields(c[i])
|
||||
jScore := weightedCountForSpecifiedFields(c[j])
|
||||
|
||||
if iScore == jScore {
|
||||
// check weighted sort first
|
||||
if iScore != jScore {
|
||||
return iScore > jScore
|
||||
}
|
||||
|
||||
// sort longer fields to top
|
||||
if countFieldLength(c[i]) != countFieldLength(c[j]) {
|
||||
return countFieldLength(c[i]) > countFieldLength(c[j])
|
||||
}
|
||||
return iScore > jScore
|
||||
|
||||
// if score and length are equal then text sort
|
||||
return c[i].BindToFmtString() < c[j].BindToFmtString()
|
||||
}
|
||||
|
||||
func countFieldLength(cpe wfn.Attributes) int {
|
||||
|
||||
@@ -80,6 +80,25 @@ func TestCPESpecificity(t *testing.T) {
|
||||
mustCPE("cpe:2.3:a:1:*:333:*:*:*:*:*:*:*"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sort by mix of field length, specificity, dash",
|
||||
input: []pkg.CPE{
|
||||
mustCPE("cpe:2.3:a:alpine:alpine_keys:2.3-r1:*:*:*:*:*:*:*"),
|
||||
mustCPE("cpe:2.3:a:alpine_keys:alpine_keys:2.3-r1:*:*:*:*:*:*:*"),
|
||||
mustCPE("cpe:2.3:a:alpine-keys:alpine_keys:2.3-r1:*:*:*:*:*:*:*"),
|
||||
mustCPE("cpe:2.3:a:alpine:alpine-keys:2.3-r1:*:*:*:*:*:*:*"),
|
||||
mustCPE("cpe:2.3:a:alpine-keys:alpine-keys:2.3-r1:*:*:*:*:*:*:*"),
|
||||
mustCPE("cpe:2.3:a:alpine_keys:alpine-keys:2.3-r1:*:*:*:*:*:*:*"),
|
||||
},
|
||||
expected: []pkg.CPE{
|
||||
mustCPE("cpe:2.3:a:alpine-keys:alpine-keys:2.3-r1:*:*:*:*:*:*:*"),
|
||||
mustCPE("cpe:2.3:a:alpine-keys:alpine_keys:2.3-r1:*:*:*:*:*:*:*"),
|
||||
mustCPE("cpe:2.3:a:alpine_keys:alpine-keys:2.3-r1:*:*:*:*:*:*:*"),
|
||||
mustCPE("cpe:2.3:a:alpine_keys:alpine_keys:2.3-r1:*:*:*:*:*:*:*"),
|
||||
mustCPE("cpe:2.3:a:alpine:alpine-keys:2.3-r1:*:*:*:*:*:*:*"),
|
||||
mustCPE("cpe:2.3:a:alpine:alpine_keys:2.3-r1:*:*:*:*:*:*:*"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
||||
@@ -66,7 +66,7 @@ func (c *GenericCataloger) Catalog(resolver source.FileResolver) ([]pkg.Package,
|
||||
|
||||
// SelectFiles takes a set of file trees and resolves and file references of interest for future cataloging
|
||||
func (c *GenericCataloger) selectFiles(resolver source.FilePathResolver) map[source.Location]ParserFn {
|
||||
var parserByLocation = make(map[source.Location]ParserFn)
|
||||
parserByLocation := make(map[source.Location]ParserFn)
|
||||
|
||||
// select by exact path
|
||||
for path, parser := range c.pathParsers {
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"sort"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
@@ -36,9 +35,9 @@ func (c *Cataloger) Name() string {
|
||||
}
|
||||
|
||||
// Catalog is given an object to resolve file references and content, this function returns any discovered Packages after analyzing dpkg support files.
|
||||
// nolint:funlen
|
||||
|
||||
func (c *Cataloger) Catalog(resolver source.FileResolver) ([]pkg.Package, error) {
|
||||
dbFileMatches, err := resolver.FilesByGlob(pkg.DpkgDbGlob)
|
||||
dbFileMatches, err := resolver.FilesByGlob(pkg.DpkgDBGlob)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to find dpkg status files's by glob: %w", err)
|
||||
}
|
||||
@@ -80,20 +79,21 @@ func addLicenses(resolver source.FileResolver, dbLocation source.Location, p *pk
|
||||
// get license information from the copyright file
|
||||
copyrightReader, copyrightLocation := fetchCopyrightContents(resolver, dbLocation, p)
|
||||
|
||||
if copyrightReader != nil {
|
||||
if copyrightReader != nil && copyrightLocation != nil {
|
||||
defer internal.CloseAndLogError(copyrightReader, copyrightLocation.VirtualPath)
|
||||
// attach the licenses
|
||||
p.Licenses = parseLicensesFromCopyright(copyrightReader)
|
||||
|
||||
// keep a record of the file where this was discovered
|
||||
if copyrightLocation != nil {
|
||||
p.Locations = append(p.Locations, *copyrightLocation)
|
||||
}
|
||||
p.Locations = append(p.Locations, *copyrightLocation)
|
||||
}
|
||||
}
|
||||
|
||||
func mergeFileListing(resolver source.FileResolver, dbLocation source.Location, p *pkg.Package) {
|
||||
metadata := p.Metadata.(pkg.DpkgMetadata)
|
||||
metadata, ok := p.Metadata.(pkg.DpkgMetadata)
|
||||
if !ok {
|
||||
log.Warnf("unable to get DPKG metadata while merging file info")
|
||||
}
|
||||
|
||||
// get file listing (package files + additional config files)
|
||||
files, infoLocations := getAdditionalFileListing(resolver, dbLocation, p)
|
||||
@@ -122,33 +122,29 @@ loopNewFiles:
|
||||
|
||||
func getAdditionalFileListing(resolver source.FileResolver, dbLocation source.Location, p *pkg.Package) ([]pkg.DpkgFileRecord, []source.Location) {
|
||||
// ensure the default value for a collection is never nil since this may be shown as JSON
|
||||
var files = make([]pkg.DpkgFileRecord, 0)
|
||||
files := make([]pkg.DpkgFileRecord, 0)
|
||||
var locations []source.Location
|
||||
|
||||
md5Reader, md5Location := fetchMd5Contents(resolver, dbLocation, p)
|
||||
|
||||
if md5Reader != nil {
|
||||
if md5Reader != nil && md5Location != nil {
|
||||
defer internal.CloseAndLogError(md5Reader, md5Location.VirtualPath)
|
||||
// attach the file list
|
||||
files = append(files, parseDpkgMD5Info(md5Reader)...)
|
||||
|
||||
// keep a record of the file where this was discovered
|
||||
if md5Location != nil {
|
||||
locations = append(locations, *md5Location)
|
||||
}
|
||||
locations = append(locations, *md5Location)
|
||||
}
|
||||
|
||||
conffilesReader, conffilesLocation := fetchConffileContents(resolver, dbLocation, p)
|
||||
|
||||
if conffilesReader != nil {
|
||||
if conffilesReader != nil && conffilesLocation != nil {
|
||||
defer internal.CloseAndLogError(conffilesReader, conffilesLocation.VirtualPath)
|
||||
// attach the file list
|
||||
files = append(files, parseDpkgConffileInfo(md5Reader)...)
|
||||
|
||||
// keep a record of the file where this was discovered
|
||||
if conffilesLocation != nil {
|
||||
locations = append(locations, *conffilesLocation)
|
||||
}
|
||||
locations = append(locations, *conffilesLocation)
|
||||
}
|
||||
|
||||
return files, locations
|
||||
@@ -228,7 +224,10 @@ func fetchCopyrightContents(resolver source.FileResolver, dbLocation source.Loca
|
||||
}
|
||||
|
||||
func md5Key(p *pkg.Package) string {
|
||||
metadata := p.Metadata.(pkg.DpkgMetadata)
|
||||
metadata, ok := p.Metadata.(pkg.DpkgMetadata)
|
||||
if !ok {
|
||||
log.Warnf("unable to get DPKG metadata while fetching md5 key")
|
||||
}
|
||||
|
||||
contentKey := p.Name
|
||||
if metadata.Architecture != "" && metadata.Architecture != "all" {
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
@@ -23,7 +22,7 @@ var (
|
||||
// parseDpkgStatus is a parser function for Debian DB status contents, returning all Debian packages listed.
|
||||
func parseDpkgStatus(reader io.Reader) ([]pkg.Package, error) {
|
||||
buffedReader := bufio.NewReader(reader)
|
||||
var packages = make([]pkg.Package, 0)
|
||||
packages := make([]pkg.Package, 0)
|
||||
|
||||
continueProcessing := true
|
||||
for continueProcessing {
|
||||
@@ -152,7 +151,7 @@ func extractSourceVersion(source string) (string, string) {
|
||||
// handleNewKeyValue parse a new key-value pair from the given unprocessed line
|
||||
func handleNewKeyValue(line string) (string, interface{}, error) {
|
||||
if i := strings.Index(line, ":"); i > 0 {
|
||||
var key = strings.TrimSpace(line[0:i])
|
||||
key := strings.TrimSpace(line[0:i])
|
||||
// mapstruct cant handle "-"
|
||||
key = strings.ReplaceAll(key, "-", "")
|
||||
val := strings.TrimSpace(line[i+1:])
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user