Compare commits

..
Author SHA1 Message Date
Alex Goodman 821210006d make updates due to linter update
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-10-05 22:51:36 -04:00
Alex Goodman 053768c6c6 update golangci + add additional lint rules
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-10-05 22:50:57 -04:00
Alex Goodman 9189ed68df Add query by MIME type to source.FileResolver (#529)
* add query by MIME type to source.FileResolver

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

* pull in stereoscope MIME type feature

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

* fix tests

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-10-05 20:50:53 -04:00
Alex Goodman ba1cd8d753 specify optional file.Digests instances to abbreviate json output (#531)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-10-05 15:35:28 -04:00
Alex Goodman dd2c178085 add --file option to readme (#533)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-10-05 15:34:32 -04:00
Alex Goodman 1b23a94015 Add option to output SBOM report to a file (#530)
* add output to file option

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

* log errors on close of the report destination

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

* remove file option from persistent args

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

* update file option comments and logging

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

* allow for multiple UI fallback options

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

* update UI select signatures + tests

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-10-05 14:47:24 -04:00
Christopher Angelo Phillips f47a6a88b1 (#495) Update documentNamespace uniqueness for spdx-json output (#528)
* add unique namespace identifier

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
2021-10-05 13:10:49 -04:00
Dan Luhring 4b7217f052 Improve documentation (#525)
* Make installation methods more obvious

Signed-off-by: Dan Luhring <dan.luhring@anchore.com>

* Create linkable section headers

Signed-off-by: Dan Luhring <dan.luhring@anchore.com>

* Add badge for joining Slack

Signed-off-by: Dan Luhring <dan.luhring@anchore.com>

* Document requirement for signed commits

Signed-off-by: Dan Luhring <dan.luhring@anchore.com>
2021-10-04 09:29:27 -04:00
Christopher Angelo Phillips 5e4b668e5a 497 stable sorted CPE array (JSON and SPDX) (#522)
* add small sorting change to our specificity

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
2021-10-01 15:31:17 -04:00
Dan Luhring 6480f06173 Update description for Slack link (#523)
Signed-off-by: Dan Luhring <dan.luhring@anchore.com>
2021-09-30 21:43:43 +00:00
Christopher Angelo Phillips 75aed5f3ec (#460) Extend license mapping for common SPDX license names (#509)
Fixes #460 
Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
2021-09-30 16:54:36 -04:00
Jamie Magee 270fbb7665 Add mariner to distro types (#520)
Signed-off-by: Jamie Magee <jamie.magee@gmail.com>
2021-09-30 13:20:15 -04:00
113 changed files with 2236 additions and 981 deletions
+1 -1
View File
@@ -3,4 +3,4 @@ contact_links:
- name: Join the Slack community 💬
# link to our community Slack registration page
url: https://anchore.com/slack
about: 'Be sure to check out the channels #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!'
+39 -5
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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
+33 -18
View File
@@ -3,8 +3,9 @@
[![Validations](https://github.com/anchore/syft/actions/workflows/validations.yaml/badge.svg)](https://github.com/anchore/syft/actions/workflows/validations.yaml)
[![Go Report Card](https://goreportcard.com/badge/github.com/anchore/syft)](https://goreportcard.com/report/github.com/anchore/syft)
[![GitHub release](https://img.shields.io/github/release/anchore/syft.svg)](https://github.com/anchore/syft/releases/latest)
[![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/anchore/syft/blob/main/LICENSE)
[![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/anchore/syft.svg)](https://github.com/anchore/syft)
[![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/anchore/syft/blob/main/LICENSE)
[![Slack Invite](https://img.shields.io/badge/Slack-Join-blue?logo=slack)](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).
@@ -28,6 +29,27 @@ We’ll have free swag, giveaways, snacks, and sips. Space will be limited, so m
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:
@@ -47,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
@@ -57,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"
@@ -66,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>
```
@@ -79,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:
@@ -116,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
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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")
}
+6 -6
View File
@@ -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
View File
@@ -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
View File
@@ -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)...,
)
}
+29
View File
@@ -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
}
}
+1
View File
@@ -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
View File
@@ -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()
+1 -1
View File
@@ -11,7 +11,7 @@ require (
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
github.com/anchore/packageurl-go v0.0.0-20210922164639-b3fa992ebd29
github.com/anchore/stereoscope v0.0.0-20210817160504-0f4abc2a5a5a
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
+5 -4
View File
@@ -117,8 +117,8 @@ github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZV
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E=
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-20210817160504-0f4abc2a5a5a h1:RQb+Gft1MKxjDfJCnHP/f1mwfy0Jz50Kp9QGgSWKQiY=
github.com/anchore/stereoscope v0.0.0-20210817160504-0f4abc2a5a5a/go.mod h1:165DfE5jApgEkHTWwu7Bijeml9fofudrgcpuWaD9+tk=
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=
@@ -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=
+4 -5
View File
@@ -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()
}
+1 -2
View File
@@ -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 {
+2 -4
View File
@@ -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 {
+1 -2
View File
@@ -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 {
+4 -7
View File
@@ -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
View File
@@ -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.
+1 -1
View File
@@ -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()
}
+1 -2
View File
@@ -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,
+8 -8
View File
@@ -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
}
+3 -3
View File
@@ -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,
}
+3 -3
View File
@@ -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{}),
},
}
+2 -1
View File
@@ -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 {
+1 -1
View File
@@ -11,7 +11,7 @@ import (
prefixed "github.com/x-cray/logrus-prefixed-formatter"
)
const defaultLogFilePermissions fs.FileMode = 0644
const defaultLogFilePermissions fs.FileMode = 0o644
// LogrusConfig contains all configurable values for the Logrus logger
type LogrusConfig struct {
+3 -3
View File
@@ -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"`
+1 -1
View File
@@ -135,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
@@ -41,14 +46,25 @@ func (pres *SPDXJsonPresenter) Present(output io.Writer) error {
// 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{
@@ -67,7 +83,7 @@ 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),
DocumentNamespace: namespace,
Packages: packages,
Files: files,
Relationships: relationships,
@@ -113,3 +129,14 @@ func newSPDXJsonElements(catalog *pkg.Catalog) ([]spdx22.Package, []spdx22.File,
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 {
@@ -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)
@@ -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,93 +0,0 @@
//go:build ignore
// +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)
}
}
+9 -1
View File
@@ -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)]
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)
}
+60
View File
@@ -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)
})
}
}
+19 -16
View File
@@ -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 -6
View File
@@ -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
+10 -4
View File
@@ -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)
}
+9 -7
View File
@@ -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
}
+7 -5
View File
@@ -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 {
+6 -9
View File
@@ -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)
}
-1
View File
@@ -6,7 +6,6 @@ import (
"strings"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/source"
)
+5
View File
@@ -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"
+3
View File
@@ -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 -9
View File
@@ -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,
+1 -1
View File
@@ -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
}
+1 -2
View File
@@ -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
+4 -7
View File
@@ -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) {
+1 -1
View File
@@ -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) {
+1 -2
View File
@@ -9,8 +9,7 @@ import (
"github.com/wagoodman/go-progress"
)
type MetadataCataloger struct {
}
type MetadataCataloger struct{}
func NewMetadataCataloger() *MetadataCataloger {
return &MetadataCataloger{}
+28 -21
View File
@@ -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: "",
},
},
}
+1 -2
View File
@@ -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 {
+4 -5
View File
@@ -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
+7 -8
View File
@@ -3,13 +3,12 @@ package pkg
import (
"sort"
"github.com/anchore/syft/syft/file"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/syft/file"
"github.com/scylladb/go-set/strset"
)
const ApkDbGlob = "**/lib/apk/db/installed"
const ApkDBGlob = "**/lib/apk/db/installed"
var _ FileOwner = (*ApkMetadata)(nil)
@@ -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)
+1 -3
View File
@@ -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
}
+1 -1
View File
@@ -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")
+2 -3
View File
@@ -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,
}
+38 -38
View File
@@ -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=",
},
@@ -29,20 +29,20 @@ var defaultCandidateAdditions = buildCandidateLookup(
{
// example image: docker.io/nuxeo:latest
pkg.JavaPkg,
candidateKey{PkgName: "elasticsearch"}, //, Vendor: "elasticsearch"},
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"},
candidateKey{PkgName: "log4j"}, // , Vendor: "apache-software-foundation"},
candidateAddition{AdditionalVendors: []string{"apache"}},
},
{
// example image: cassandra:latest
pkg.JavaPkg,
candidateKey{PkgName: "apache-cassandra"}, //, Vendor: "apache"},
candidateKey{PkgName: "apache-cassandra"}, // , Vendor: "apache"},
candidateAddition{AdditionalProducts: []string{"cassandra"}},
},
@@ -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 {
+17 -18
View File
@@ -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" {
+2 -3
View File
@@ -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:])
@@ -6,7 +6,6 @@ import (
"strings"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/pkg"
)
+7 -5
View File
@@ -6,9 +6,8 @@ import (
"path"
"strings"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/file"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/common"
)
@@ -81,7 +80,7 @@ func newJavaArchiveParser(virtualPath string, reader io.Reader, detectNested boo
// parse the loaded archive and return all packages found.
func (j *archiveParser) parse() ([]pkg.Package, error) {
var pkgs = make([]pkg.Package, 0)
pkgs := make([]pkg.Package, 0)
// find the parent package from the java manifest
parentPkg, err := j.discoverMainPackage()
@@ -190,7 +189,7 @@ func (j *archiveParser) discoverPkgsFromAllMavenFiles(parentPkg *pkg.Package) ([
// discoverPkgsFromNestedArchives finds Java archives within Java archives, returning all listed Java packages found and
// associating each discovered package to the given parent package.
func (j *archiveParser) discoverPkgsFromNestedArchives(parentPkg *pkg.Package) ([]pkg.Package, error) {
var pkgs = make([]pkg.Package, 0)
pkgs := make([]pkg.Package, 0)
// search and parse pom.properties files & fetch the contents
openers, err := file.ExtractFromZipToUniqueTempFile(j.archivePath, j.contentPath, j.fileManifest.GlobMatch(archiveFormatGlobs...)...)
@@ -326,7 +325,10 @@ func packageIdentitiesMatch(p pkg.Package, parentPkg *pkg.Package) bool {
return true
}
metadata := p.Metadata.(pkg.JavaMetadata)
metadata, ok := p.Metadata.(pkg.JavaMetadata)
if !ok {
log.Warnf("unable to get java metadata while determining package identities")
}
// the virtual path matches...
if parentPkg.Metadata.(pkg.JavaMetadata).VirtualPath == metadata.VirtualPath {
@@ -8,7 +8,6 @@ import (
"strings"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/pkg"
)
@@ -26,7 +26,7 @@ func saveArchiveToTmp(reader io.Reader) (string, string, func(), error) {
archivePath := filepath.Join(tempDir, "archive")
contentDir := filepath.Join(tempDir, "contents")
err = os.Mkdir(contentDir, 0755)
err = os.Mkdir(contentDir, 0o755)
if err != nil {
return contentDir, "", cleanupFn, fmt.Errorf("unable to create processing tempdir: %w", err)
}
@@ -7,14 +7,11 @@ import (
"io"
"regexp"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal"
"github.com/mitchellh/mapstructure"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/common"
"github.com/mitchellh/mapstructure"
)
// integrity check
@@ -168,7 +165,7 @@ func parsePackageJSON(_ string, reader io.Reader) ([]pkg.Package, error) {
for {
var p PackageJSON
if err := dec.Decode(&p); err == io.EOF {
if err := dec.Decode(&p); errors.Is(err, io.EOF) {
break
} else if err != nil {
return nil, fmt.Errorf("failed to parse package.json file: %w", err)
@@ -2,6 +2,7 @@ package javascript
import (
"encoding/json"
"errors"
"fmt"
"io"
@@ -40,7 +41,7 @@ func parsePackageLock(path string, reader io.Reader) ([]pkg.Package, error) {
for {
var lock PackageLock
if err := dec.Decode(&lock); err == io.EOF {
if err := dec.Decode(&lock); errors.Is(err, io.EOF) {
break
} else if err != nil {
return nil, fmt.Errorf("failed to parse package-lock.json file: %w", err)
+3 -3
View File
@@ -20,9 +20,9 @@ func generatePackageURL(p pkg.Package, d *distro.Distro) string {
}
}
var purlType = p.Type.PackageURLType()
var name = p.Name
var namespace = ""
purlType := p.Type.PackageURLType()
name := p.Name
namespace := ""
switch {
case purlType == "":
@@ -6,9 +6,7 @@ import (
"path/filepath"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
@@ -149,7 +147,7 @@ func (c *PackageCataloger) fetchTopLevelPackages(resolver source.FileResolver, m
// assembleEggOrWheelMetadata discovers and accumulates python package metadata from multiple file sources and returns a single metadata object as well as a list of files where the metadata was derived from.
func (c *PackageCataloger) assembleEggOrWheelMetadata(resolver source.FileResolver, metadataLocation source.Location) (*pkg.PythonPackageMetadata, []source.Location, error) {
var sources = []source.Location{metadataLocation}
sources := []source.Location{metadataLocation}
metadataContents, err := resolver.FileContentsByLocation(metadataLocation)
if err != nil {
@@ -2,6 +2,7 @@ package python
import (
"encoding/json"
"errors"
"fmt"
"io"
"strings"
@@ -43,7 +44,7 @@ func parsePipfileLock(_ string, reader io.Reader) ([]pkg.Package, error) {
for {
var lock PipfileLock
if err := dec.Decode(&lock); err == io.EOF {
if err := dec.Decode(&lock); errors.Is(err, io.EOF) {
break
} else if err != nil {
return nil, fmt.Errorf("failed to parse Pipfile.lock file: %w", err)
@@ -16,13 +16,13 @@ var _ common.ParserFn = parsePoetryLock
func parsePoetryLock(_ string, reader io.Reader) ([]pkg.Package, error) {
tree, err := toml.LoadReader(reader)
if err != nil {
return nil, fmt.Errorf("unable to load poetry.lock for parsing: %v", err)
return nil, fmt.Errorf("unable to load poetry.lock for parsing: %w", err)
}
metadata := PoetryMetadata{}
err = tree.Unmarshal(&metadata)
if err != nil {
return nil, fmt.Errorf("unable to parse poetry.lock: %v", err)
return nil, fmt.Errorf("unable to parse poetry.lock: %w", err)
}
return metadata.Pkgs(), nil
@@ -9,10 +9,8 @@ import (
"github.com/anchore/syft/internal/file"
"github.com/anchore/syft/internal/log"
"github.com/mitchellh/mapstructure"
"github.com/anchore/syft/syft/pkg"
"github.com/mitchellh/mapstructure"
)
// parseWheelOrEggMetadata takes a Python Egg or Wheel (which share the same format and values for our purposes),
@@ -2,6 +2,7 @@ package python
import (
"encoding/csv"
"errors"
"fmt"
"io"
"strings"
@@ -17,7 +18,7 @@ func parseWheelOrEggRecord(reader io.Reader) ([]pkg.PythonFileRecord, error) {
for {
recordList, err := r.Read()
if err == io.EOF {
if errors.Is(err, io.EOF) {
break
}
if err != nil {
+1 -2
View File
@@ -7,7 +7,6 @@ import (
"fmt"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
@@ -28,7 +27,7 @@ func (c *Cataloger) Name() string {
// Catalog is given an object to resolve file references and content, this function returns any discovered Packages after analyzing rpm db installation.
func (c *Cataloger) Catalog(resolver source.FileResolver) ([]pkg.Package, error) {
fileMatches, err := resolver.FilesByGlob(pkg.RpmDbGlob)
fileMatches, err := resolver.FilesByGlob(pkg.RpmDBGlob)
if err != nil {
return nil, fmt.Errorf("failed to find rpmdb's by glob: %w", err)
}
+3 -4
View File
@@ -6,11 +6,10 @@ import (
"io/ioutil"
"os"
"github.com/anchore/syft/syft/file"
rpmdb "github.com/anchore/go-rpmdb/pkg"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
@@ -90,10 +89,10 @@ func toELVersion(metadata pkg.RpmdbMetadata) string {
}
func extractRpmdbFileRecords(resolver source.FilePathResolver, entry *rpmdb.PackageInfo) []pkg.RpmdbFileRecord {
var records = make([]pkg.RpmdbFileRecord, 0)
records := make([]pkg.RpmdbFileRecord, 0)
for _, record := range entry.Files {
//only persist RPMDB file records which exist in the image/directory, otherwise ignore them
// only persist RPMDB file records which exist in the image/directory, otherwise ignore them
if resolver.HasPath(record.Path) {
records = append(records, pkg.RpmdbFileRecord{
Path: record.Path,
@@ -45,11 +45,16 @@ func (r *rpmdbTestFileResolverMock) FilesByPath(paths ...string) ([]source.Locat
func (r *rpmdbTestFileResolverMock) FilesByGlob(...string) ([]source.Location, error) {
return nil, fmt.Errorf("not implemented")
}
func (r *rpmdbTestFileResolverMock) RelativeFileByPath(source.Location, string) *source.Location {
panic(fmt.Errorf("not implemented"))
return nil
}
func (r rpmdbTestFileResolverMock) FilesByMIMEType(types ...string) ([]source.Location, error) {
panic("implement me")
}
func TestParseRpmDB(t *testing.T) {
dbLocation := source.NewLocation("test-path")
+2 -4
View File
@@ -9,11 +9,9 @@ import (
"strings"
"github.com/anchore/syft/internal"
"github.com/mitchellh/mapstructure"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/common"
"github.com/mitchellh/mapstructure"
)
// integrity check
@@ -62,7 +60,7 @@ func processList(s string) []string {
func parseGemSpecEntries(_ string, reader io.Reader) ([]pkg.Package, error) {
var pkgs []pkg.Package
var fields = make(map[string]interface{})
fields := make(map[string]interface{})
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
+2 -2
View File
@@ -16,13 +16,13 @@ var _ common.ParserFn = parseCargoLock
func parseCargoLock(_ string, reader io.Reader) ([]pkg.Package, error) {
tree, err := toml.LoadReader(reader)
if err != nil {
return nil, fmt.Errorf("unable to load Cargo.lock for parsing: %v", err)
return nil, fmt.Errorf("unable to load Cargo.lock for parsing: %w", err)
}
metadata := CargoMetadata{}
err = tree.Unmarshal(&metadata)
if err != nil {
return nil, fmt.Errorf("unable to parse Cargo.lock: %v", err)
return nil, fmt.Errorf("unable to parse Cargo.lock: %w", err)
}
return metadata.Pkgs(), nil
+2 -3
View File
@@ -3,14 +3,13 @@ package pkg
import (
"sort"
"github.com/anchore/syft/syft/file"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/file"
"github.com/scylladb/go-set/strset"
)
const DpkgDbGlob = "**/var/lib/dpkg/{status,status.d/**}"
const DpkgDBGlob = "**/var/lib/dpkg/{status,status.d/**}"
var _ FileOwner = (*DpkgMetadata)(nil)
+5 -5
View File
@@ -9,9 +9,9 @@ import (
var globsForbiddenFromBeingOwned = []string{
// any OS DBs should automatically be ignored to prevent cyclic issues (e.g. the "rpm" RPM owns the path to the
// RPM DB, so if not ignored that package would own all other packages on the system).
ApkDbGlob,
DpkgDbGlob,
RpmDbGlob,
ApkDBGlob,
DpkgDBGlob,
RpmDBGlob,
// DEB packages share common copyright info between, this does not mean that sharing these paths implies ownership.
"/usr/share/doc/**/copyright",
}
@@ -21,7 +21,7 @@ type ownershipByFilesMetadata struct {
}
func ownershipByFilesRelationships(catalog *Catalog) []Relationship {
var relationships = findOwnershipByFilesRelationships(catalog)
relationships := findOwnershipByFilesRelationships(catalog)
var edges []Relationship
for parent, children := range relationships {
@@ -43,7 +43,7 @@ func ownershipByFilesRelationships(catalog *Catalog) []Relationship {
// findOwnershipByFilesRelationships find overlaps in file ownership with a file that defines another package. Specifically, a .Location.Path of
// a package is found to be owned by another (from the owner's .Metadata.Files[]).
func findOwnershipByFilesRelationships(catalog *Catalog) map[ID]map[ID]*strset.Set {
var relationships = make(map[ID]map[ID]*strset.Set)
relationships := make(map[ID]map[ID]*strset.Set)
if catalog == nil {
return relationships
+3 -5
View File
@@ -5,15 +5,13 @@ import (
"sort"
"strconv"
"github.com/anchore/syft/syft/file"
"github.com/scylladb/go-set/strset"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/file"
"github.com/scylladb/go-set/strset"
)
const RpmDbGlob = "**/var/lib/rpm/Packages"
const RpmDBGlob = "**/var/lib/rpm/Packages"
var _ FileOwner = (*RpmdbMetadata)(nil)
+20 -2
View File
@@ -25,7 +25,7 @@ func newAllLayersResolver(img *image.Image) (*allLayersResolver, error) {
return nil, fmt.Errorf("the image does not contain any layers")
}
var layers = make([]int, 0)
layers := make([]int, 0)
for idx := range img.Layers {
layers = append(layers, idx)
}
@@ -120,7 +120,7 @@ func (r *allLayersResolver) FilesByPath(paths ...string) ([]Location, error) {
}
// FilesByGlob returns all file.References that match the given path glob pattern from any layer in the image.
// nolint:gocognit
func (r *allLayersResolver) FilesByGlob(patterns ...string) ([]Location, error) {
uniqueFileIDs := file.NewFileReferenceSet()
uniqueLocations := make([]Location, 0)
@@ -188,6 +188,24 @@ func (r *allLayersResolver) FileContentsByLocation(location Location) (io.ReadCl
return r.img.FileContentsByRef(location.ref)
}
func (r *allLayersResolver) FilesByMIMEType(types ...string) ([]Location, error) {
var locations []Location
for _, layerIdx := range r.layers {
layer := r.img.Layers[layerIdx]
refs, err := layer.FilesByMIMEType(types...)
if err != nil {
return nil, err
}
for _, ref := range refs {
locations = append(locations, NewLocationFromReference(ref))
}
}
return locations, nil
}
func (r *allLayersResolver) AllLocations() <-chan Location {
results := make(chan Location)
go func() {
+33
View File
@@ -3,6 +3,8 @@ package source
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/anchore/stereoscope/pkg/imagetest"
)
@@ -239,3 +241,34 @@ func TestAllLayersResolver_FilesByGlob(t *testing.T) {
})
}
}
func Test_imageAllLayersResolver_FilesByMIMEType(t *testing.T) {
tests := []struct {
fixtureName string
mimeType string
expectedPaths []string
}{
{
fixtureName: "image-duplicate-path",
mimeType: "text/plain",
expectedPaths: []string{"/somefile-1.txt", "/somefile-1.txt"},
},
}
for _, test := range tests {
t.Run(test.fixtureName, func(t *testing.T) {
img := imagetest.GetFixtureImage(t, "docker-archive", test.fixtureName)
resolver, err := newAllLayersResolver(img)
assert.NoError(t, err)
locations, err := resolver.FilesByMIMEType(test.mimeType)
assert.NoError(t, err)
assert.Len(t, test.expectedPaths, len(locations))
for idx, l := range locations {
assert.Equal(t, test.expectedPaths[idx], l.RealPath, "does not have path %q", l.RealPath)
}
})
}
}
+31 -26
View File
@@ -8,7 +8,6 @@ import (
"path"
"path/filepath"
"strings"
"syscall"
"github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/stereoscope/pkg/filetree"
@@ -35,10 +34,11 @@ type directoryResolver struct {
path string
cwd string
fileTree *filetree.FileTree
infos map[file.ID]os.FileInfo
metadata map[file.ID]FileMetadata
// TODO: wire up to report these paths in the json report
pathFilterFns []pathFilterFn
errPaths map[string]error
pathFilterFns []pathFilterFn
refsByMIMEType map[string][]file.Reference
errPaths map[string]error
}
func newDirectoryResolver(root string, pathFilters ...pathFilterFn) (*directoryResolver, error) {
@@ -52,12 +52,13 @@ func newDirectoryResolver(root string, pathFilters ...pathFilterFn) (*directoryR
}
resolver := directoryResolver{
path: root,
cwd: cwd,
fileTree: filetree.NewFileTree(),
infos: make(map[file.ID]os.FileInfo),
pathFilterFns: pathFilters,
errPaths: make(map[string]error),
path: root,
cwd: cwd,
fileTree: filetree.NewFileTree(),
metadata: make(map[file.ID]FileMetadata),
pathFilterFns: pathFilters,
refsByMIMEType: make(map[string][]file.Reference),
errPaths: make(map[string]error),
}
return &resolver, indexAllRoots(root, resolver.indexTree)
@@ -158,7 +159,11 @@ func (r directoryResolver) addPathToIndex(p string, info os.FileInfo) (string, e
}
}
r.infos[ref.ID()] = info
metadata := fileMetadataFromPath(p, info)
if ref != nil {
r.refsByMIMEType[metadata.MIMEType] = append(r.refsByMIMEType[metadata.MIMEType], *ref)
}
r.metadata[ref.ID()] = metadata
return newRoot, nil
}
@@ -199,7 +204,7 @@ func (r directoryResolver) String() string {
// FilesByPath returns all file.References that match the given paths from the directory.
func (r directoryResolver) FilesByPath(userPaths ...string) ([]Location, error) {
var references = make([]Location, 0)
references := make([]Location, 0)
for _, userPath := range userPaths {
userStrPath, err := r.requestPath(userPath)
@@ -207,6 +212,7 @@ func (r directoryResolver) FilesByPath(userPaths ...string) ([]Location, error)
log.Warnf("unable to get file by path=%q : %+v", userPath, err)
continue
}
// TODO: why not use stored metadata?
fileMeta, err := os.Stat(userStrPath)
if os.IsNotExist(err) {
continue
@@ -280,25 +286,24 @@ func (r *directoryResolver) AllLocations() <-chan Location {
}
func (r *directoryResolver) FileMetadataByLocation(location Location) (FileMetadata, error) {
info, exists := r.infos[location.ref.ID()]
metadata, exists := r.metadata[location.ref.ID()]
if !exists {
return FileMetadata{}, fmt.Errorf("location: %+v : %w", location, os.ErrNotExist)
}
uid := -1
gid := -1
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
uid = int(stat.Uid)
gid = int(stat.Gid)
}
return metadata, nil
}
return FileMetadata{
Mode: info.Mode(),
Type: newFileTypeFromMode(info.Mode()),
// unsupported across platforms
UserID: uid,
GroupID: gid,
}, nil
func (r *directoryResolver) FilesByMIMEType(types ...string) ([]Location, error) {
var locations []Location
for _, ty := range types {
if refs, ok := r.refsByMIMEType[ty]; ok {
for _, ref := range refs {
locations = append(locations, NewLocationFromDirectory(r.responsePath(string(ref.RealPath)), ref))
}
}
}
return locations, nil
}
func isUnixSystemRuntimePath(path string) bool {

Some files were not shown because too many files have changed in this diff Show More