mirror of
https://github.com/anchore/syft.git
synced 2026-10-30 14:02:26 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e5312c72d | ||
|
|
38c4b17847 | ||
|
|
01dc78ccc3 | ||
|
|
2a7325a965 | ||
|
|
d9aa54cd00 | ||
|
|
697bad02ac | ||
|
|
a18fbacae7 | ||
|
|
7168dc7473 | ||
|
|
211b188120 |
@@ -338,7 +338,7 @@ hash_sha256_verify() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
BASENAME=${TARGET##*/}
|
BASENAME=${TARGET##*/}
|
||||||
want=$(grep "${BASENAME}\$" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1)
|
want=$(grep "${BASENAME}" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1)
|
||||||
if [ -z "$want" ]; then
|
if [ -z "$want" ]; then
|
||||||
log_err "hash_sha256_verify unable to find checksum for '${TARGET}' in '${checksums}'"
|
log_err "hash_sha256_verify unable to find checksum for '${TARGET}' in '${checksums}'"
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ bootstrap-tools: $(TEMPDIR)
|
|||||||
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/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 -sSfL https://raw.githubusercontent.com/wagoodman/go-bouncer/master/bouncer.sh | sh -s -- -b $(TEMPDIR)/ v0.2.0
|
||||||
curl -sSfL https://raw.githubusercontent.com/anchore/chronicle/main/install.sh | sh -s -- -b $(TEMPDIR)/ v0.3.0
|
curl -sSfL https://raw.githubusercontent.com/anchore/chronicle/main/install.sh | sh -s -- -b $(TEMPDIR)/ v0.3.0
|
||||||
.github/scripts/goreleaser-install.sh -b $(TEMPDIR)/ v1.2.2
|
.github/scripts/goreleaser-install.sh -b $(TEMPDIR)/ v0.177.0
|
||||||
GOBIN="$(shell realpath $(TEMPDIR))" go install github.com/neilpa/yajsv@v1.4.0
|
GOBIN="$(shell realpath $(TEMPDIR))" go install github.com/neilpa/yajsv@v1.4.0
|
||||||
|
|
||||||
.PHONY: bootstrap-go
|
.PHONY: bootstrap-go
|
||||||
|
|||||||
@@ -112,7 +112,8 @@ may attempt to expand wildcards, so put those parameters in single quotes, like:
|
|||||||
|
|
||||||
### Output formats
|
### Output formats
|
||||||
|
|
||||||
The output format for Syft is configurable as well:
|
The output format for Syft is configurable as well using the
|
||||||
|
`-o` (or `--output`) option:
|
||||||
|
|
||||||
```
|
```
|
||||||
syft packages <image> -o <format>
|
syft packages <image> -o <format>
|
||||||
@@ -127,6 +128,15 @@ Where the `formats` 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).
|
- `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).
|
- `table`: A columnar summary (default).
|
||||||
|
|
||||||
|
#### Multiple outputs
|
||||||
|
|
||||||
|
Syft can also output _multiple_ files in differing formats by appending
|
||||||
|
`=<file>` to the option, for example to output Syft JSON and SPDX JSON:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
syft packages <image> -o json=sbom.syft.json -o spdx-json=sbom.spdx.json
|
||||||
|
```
|
||||||
|
|
||||||
## Private Registry Authentication
|
## Private Registry Authentication
|
||||||
|
|
||||||
### Local Docker Credentials
|
### Local Docker Credentials
|
||||||
@@ -221,8 +231,12 @@ Configuration search paths:
|
|||||||
Configuration options (example values are the default):
|
Configuration options (example values are the default):
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# the output format of the SBOM report (options: table, text, json)
|
# the output format(s) of the SBOM report (options: table, text, json, spdx, ...)
|
||||||
# same as -o ; SYFT_OUTPUT env var
|
# same as -o, --output, and SYFT_OUTPUT env var
|
||||||
|
# to specify multiple output files in differing formats, use a list:
|
||||||
|
# output:
|
||||||
|
# - "json=<syft-json-output-file>"
|
||||||
|
# - "spdx-json=<spdx-json-output-file>"
|
||||||
output: "table"
|
output: "table"
|
||||||
|
|
||||||
# suppress all output (except for the SBOM report)
|
# suppress all output (except for the SBOM report)
|
||||||
@@ -238,12 +252,24 @@ check-for-app-update: true
|
|||||||
|
|
||||||
# a list of globs to exclude from scanning. same as --exclude ; for example:
|
# a list of globs to exclude from scanning. same as --exclude ; for example:
|
||||||
# exclude:
|
# exclude:
|
||||||
# - '/etc/**'
|
# - "/etc/**"
|
||||||
# - './out/**/*.json'
|
# - "./out/**/*.json"
|
||||||
exclude:
|
exclude:
|
||||||
|
|
||||||
# cataloging packages is exposed through the packages and power-user subcommands
|
# cataloging packages is exposed through the packages and power-user subcommands
|
||||||
package:
|
package:
|
||||||
|
|
||||||
|
# search within archives that do contain a file index to search against (zip)
|
||||||
|
# note: for now this only applies to the java package cataloger
|
||||||
|
# SYFT_PACKAGE_SEARCH_INDEXED_ARCHIVES env var
|
||||||
|
search-indexed-archives: true
|
||||||
|
|
||||||
|
# search within archives that do not contain a file index to search against (tar, tar.gz, tar.bz2, etc)
|
||||||
|
# note: enabling this may result in a performance impact since all discovered compressed tars will be decompressed
|
||||||
|
# note: for now this only applies to the java package cataloger
|
||||||
|
# SYFT_PACKAGE_SEARCH_UNINDEXED_ARCHIVES env var
|
||||||
|
search-unindexed-archives: false
|
||||||
|
|
||||||
cataloger:
|
cataloger:
|
||||||
# enable/disable cataloging of packages
|
# enable/disable cataloging of packages
|
||||||
# SYFT_PACKAGE_CATALOGER_ENABLED env var
|
# SYFT_PACKAGE_CATALOGER_ENABLED env var
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ func Test_eventLoop_gracefulExit(t *testing.T) {
|
|||||||
t.Cleanup(testBus.Close)
|
t.Cleanup(testBus.Close)
|
||||||
|
|
||||||
finalEvent := partybus.Event{
|
finalEvent := partybus.Event{
|
||||||
Type: event.PresenterReady,
|
Type: event.Exit,
|
||||||
}
|
}
|
||||||
|
|
||||||
worker := func() <-chan error {
|
worker := func() <-chan error {
|
||||||
@@ -182,7 +182,7 @@ func Test_eventLoop_unsubscribeError(t *testing.T) {
|
|||||||
t.Cleanup(testBus.Close)
|
t.Cleanup(testBus.Close)
|
||||||
|
|
||||||
finalEvent := partybus.Event{
|
finalEvent := partybus.Event{
|
||||||
Type: event.PresenterReady,
|
Type: event.Exit,
|
||||||
}
|
}
|
||||||
|
|
||||||
worker := func() <-chan error {
|
worker := func() <-chan error {
|
||||||
@@ -251,8 +251,8 @@ func Test_eventLoop_handlerError(t *testing.T) {
|
|||||||
t.Cleanup(testBus.Close)
|
t.Cleanup(testBus.Close)
|
||||||
|
|
||||||
finalEvent := partybus.Event{
|
finalEvent := partybus.Event{
|
||||||
Type: event.PresenterReady,
|
Type: event.Exit,
|
||||||
Error: fmt.Errorf("unable to create presenter"),
|
Error: fmt.Errorf("an exit error occured"),
|
||||||
}
|
}
|
||||||
|
|
||||||
worker := func() <-chan error {
|
worker := func() <-chan error {
|
||||||
@@ -376,7 +376,7 @@ func Test_eventLoop_uiTeardownError(t *testing.T) {
|
|||||||
t.Cleanup(testBus.Close)
|
t.Cleanup(testBus.Close)
|
||||||
|
|
||||||
finalEvent := partybus.Event{
|
finalEvent := partybus.Event{
|
||||||
Type: event.PresenterReady,
|
Type: event.Exit,
|
||||||
}
|
}
|
||||||
|
|
||||||
worker := func() <-chan error {
|
worker := func() <-chan error {
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal/formats"
|
||||||
|
"github.com/anchore/syft/internal/output"
|
||||||
|
"github.com/anchore/syft/syft/format"
|
||||||
|
"github.com/anchore/syft/syft/sbom"
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
)
|
||||||
|
|
||||||
|
// makeWriter creates a sbom.Writer for output or returns an error. this will either return a valid writer
|
||||||
|
// or an error but neither both and if there is no error, sbom.Writer.Close() should be called
|
||||||
|
func makeWriter(outputs []string, defaultFile string) (sbom.Writer, error) {
|
||||||
|
outputOptions, err := parseOptions(outputs, defaultFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
writer, err := output.MakeWriter(outputOptions...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return writer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseOptions utility to parse command-line option strings and retain the existing behavior of default format and file
|
||||||
|
func parseOptions(outputs []string, defaultFile string) (out []output.WriterOption, errs error) {
|
||||||
|
// always should have one option -- we generally get the default of "table", but just make sure
|
||||||
|
if len(outputs) == 0 {
|
||||||
|
outputs = append(outputs, string(format.TableOption))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range outputs {
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
|
||||||
|
// split to at most two parts for <format>=<file>
|
||||||
|
parts := strings.SplitN(name, "=", 2)
|
||||||
|
|
||||||
|
// the format option is the first part
|
||||||
|
name = parts[0]
|
||||||
|
|
||||||
|
// default to the --file or empty string if not specified
|
||||||
|
file := defaultFile
|
||||||
|
|
||||||
|
// If a file is specified as part of the output option, use that
|
||||||
|
if len(parts) > 1 {
|
||||||
|
file = parts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
option := format.ParseOption(name)
|
||||||
|
if option == format.UnknownFormatOption {
|
||||||
|
errs = multierror.Append(errs, fmt.Errorf("bad output format: '%s'", name))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
encoder := formats.ByOption(option)
|
||||||
|
if encoder == nil {
|
||||||
|
errs = multierror.Append(errs, fmt.Errorf("unknown format: %s", outputFormat))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
out = append(out, output.WriterOption{
|
||||||
|
Format: *encoder,
|
||||||
|
Path: file,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out, errs
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOutputWriterConfig(t *testing.T) {
|
||||||
|
tmp := t.TempDir() + "/"
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
outputs []string
|
||||||
|
file string
|
||||||
|
err bool
|
||||||
|
expected []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
outputs: []string{},
|
||||||
|
expected: []string{""},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
outputs: []string{"json"},
|
||||||
|
expected: []string{""},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: "test-1.json",
|
||||||
|
expected: []string{"test-1.json"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
outputs: []string{"json=test-2.json"},
|
||||||
|
expected: []string{"test-2.json"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
outputs: []string{"json=test-3-1.json", "spdx-json=test-3-2.json"},
|
||||||
|
expected: []string{"test-3-1.json", "test-3-2.json"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
outputs: []string{"text", "json=test-4.json"},
|
||||||
|
expected: []string{"", "test-4.json"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(fmt.Sprintf("%s/%s", test.outputs, test.file), func(t *testing.T) {
|
||||||
|
outputs := test.outputs
|
||||||
|
for i, val := range outputs {
|
||||||
|
outputs[i] = strings.Replace(val, "=", "="+tmp, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
file := test.file
|
||||||
|
if file != "" {
|
||||||
|
file = tmp + file
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := makeWriter(test.outputs, file)
|
||||||
|
|
||||||
|
if test.err {
|
||||||
|
assert.Error(t, err)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, expected := range test.expected {
|
||||||
|
if expected != "" {
|
||||||
|
assert.FileExists(t, tmp+expected)
|
||||||
|
} else if file != "" {
|
||||||
|
assert.FileExists(t, file)
|
||||||
|
} else {
|
||||||
|
assert.NoFileExists(t, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+23
-37
@@ -10,13 +10,13 @@ import (
|
|||||||
"github.com/anchore/syft/internal"
|
"github.com/anchore/syft/internal"
|
||||||
"github.com/anchore/syft/internal/anchore"
|
"github.com/anchore/syft/internal/anchore"
|
||||||
"github.com/anchore/syft/internal/bus"
|
"github.com/anchore/syft/internal/bus"
|
||||||
"github.com/anchore/syft/internal/formats"
|
|
||||||
"github.com/anchore/syft/internal/log"
|
"github.com/anchore/syft/internal/log"
|
||||||
"github.com/anchore/syft/internal/ui"
|
"github.com/anchore/syft/internal/ui"
|
||||||
"github.com/anchore/syft/internal/version"
|
"github.com/anchore/syft/internal/version"
|
||||||
"github.com/anchore/syft/syft/artifact"
|
"github.com/anchore/syft/syft/artifact"
|
||||||
"github.com/anchore/syft/syft/event"
|
"github.com/anchore/syft/syft/event"
|
||||||
"github.com/anchore/syft/syft/format"
|
"github.com/anchore/syft/syft/format"
|
||||||
|
"github.com/anchore/syft/syft/pkg/cataloger"
|
||||||
"github.com/anchore/syft/syft/sbom"
|
"github.com/anchore/syft/syft/sbom"
|
||||||
"github.com/anchore/syft/syft/source"
|
"github.com/anchore/syft/syft/source"
|
||||||
"github.com/pkg/profile"
|
"github.com/pkg/profile"
|
||||||
@@ -50,8 +50,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
packagesPresenterOpt format.Option
|
packagesCmd = &cobra.Command{
|
||||||
packagesCmd = &cobra.Command{
|
|
||||||
Use: "packages [SOURCE]",
|
Use: "packages [SOURCE]",
|
||||||
Short: "Generate a package SBOM",
|
Short: "Generate a package SBOM",
|
||||||
Long: "Generate a packaged-based Software Bill Of Materials (SBOM) from container images and filesystems",
|
Long: "Generate a packaged-based Software Bill Of Materials (SBOM) from container images and filesystems",
|
||||||
@@ -62,14 +61,7 @@ var (
|
|||||||
Args: validateInputArgs,
|
Args: validateInputArgs,
|
||||||
SilenceUsage: true,
|
SilenceUsage: true,
|
||||||
SilenceErrors: true,
|
SilenceErrors: true,
|
||||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
PreRunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||||
// set the presenter
|
|
||||||
presenterOption := format.ParseOption(appConfig.Output)
|
|
||||||
if presenterOption == format.UnknownFormatOption {
|
|
||||||
return fmt.Errorf("bad --output value '%s'", appConfig.Output)
|
|
||||||
}
|
|
||||||
packagesPresenterOpt = presenterOption
|
|
||||||
|
|
||||||
if appConfig.Dev.ProfileCPU && appConfig.Dev.ProfileMem {
|
if appConfig.Dev.ProfileCPU && appConfig.Dev.ProfileMem {
|
||||||
return fmt.Errorf("cannot profile CPU and memory simultaneously")
|
return fmt.Errorf("cannot profile CPU and memory simultaneously")
|
||||||
}
|
}
|
||||||
@@ -98,17 +90,17 @@ func setPackageFlags(flags *pflag.FlagSet) {
|
|||||||
// Formatting & Input options //////////////////////////////////////////////
|
// Formatting & Input options //////////////////////////////////////////////
|
||||||
|
|
||||||
flags.StringP(
|
flags.StringP(
|
||||||
"scope", "s", source.SquashedScope.String(),
|
"scope", "s", cataloger.DefaultSearchConfig().Scope.String(),
|
||||||
fmt.Sprintf("selection of layers to catalog, options=%v", source.AllScopes))
|
fmt.Sprintf("selection of layers to catalog, options=%v", source.AllScopes))
|
||||||
|
|
||||||
flags.StringP(
|
flags.StringArrayP(
|
||||||
"output", "o", string(format.TableOption),
|
"output", "o", []string{string(format.TableOption)},
|
||||||
fmt.Sprintf("report output formatter, options=%v", format.AllOptions),
|
fmt.Sprintf("report output format, options=%v", format.AllOptions),
|
||||||
)
|
)
|
||||||
|
|
||||||
flags.StringP(
|
flags.StringP(
|
||||||
"file", "", "",
|
"file", "", "",
|
||||||
"file to write the report output to (default is STDOUT)",
|
"file to write the default report output to (default is STDOUT)",
|
||||||
)
|
)
|
||||||
|
|
||||||
// Upload options //////////////////////////////////////////////////////////
|
// Upload options //////////////////////////////////////////////////////////
|
||||||
@@ -209,26 +201,26 @@ func validateInputArgs(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func packagesExec(_ *cobra.Command, args []string) error {
|
func packagesExec(_ *cobra.Command, args []string) error {
|
||||||
// could be an image or a directory, with or without a scheme
|
writer, err := makeWriter(appConfig.Output, appConfig.File)
|
||||||
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := writer.Close(); err != nil {
|
||||||
|
log.Warnf("unable to write to report destination: %w", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// could be an image or a directory, with or without a scheme
|
||||||
|
userInput := args[0]
|
||||||
|
|
||||||
return eventLoop(
|
return eventLoop(
|
||||||
packagesExecWorker(userInput),
|
packagesExecWorker(userInput, writer),
|
||||||
setupSignals(),
|
setupSignals(),
|
||||||
eventSubscription,
|
eventSubscription,
|
||||||
stereoscope.Cleanup,
|
stereoscope.Cleanup,
|
||||||
ui.Select(isVerbose(), appConfig.Quiet, reporter)...,
|
ui.Select(isVerbose(), appConfig.Quiet)...,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,7 +235,7 @@ func isVerbose() (result bool) {
|
|||||||
return appConfig.CliOptions.Verbosity > 0 || isPipedInput
|
return appConfig.CliOptions.Verbosity > 0 || isPipedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
func packagesExecWorker(userInput string) <-chan error {
|
func packagesExecWorker(userInput string, writer sbom.Writer) <-chan error {
|
||||||
errs := make(chan error)
|
errs := make(chan error)
|
||||||
go func() {
|
go func() {
|
||||||
defer close(errs)
|
defer close(errs)
|
||||||
@@ -254,12 +246,6 @@ func packagesExecWorker(userInput string) <-chan error {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
f := formats.ByOption(packagesPresenterOpt)
|
|
||||||
if f == nil {
|
|
||||||
errs <- fmt.Errorf("unknown format: %s", packagesPresenterOpt)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
src, cleanup, err := source.New(userInput, appConfig.Registry.ToOptions(), appConfig.Exclusions)
|
src, cleanup, err := source.New(userInput, appConfig.Registry.ToOptions(), appConfig.Exclusions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs <- fmt.Errorf("failed to construct source from user input %q: %w", userInput, err)
|
errs <- fmt.Errorf("failed to construct source from user input %q: %w", userInput, err)
|
||||||
@@ -295,8 +281,8 @@ func packagesExecWorker(userInput string) <-chan error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bus.Publish(partybus.Event{
|
bus.Publish(partybus.Event{
|
||||||
Type: event.PresenterReady,
|
Type: event.Exit,
|
||||||
Value: f.Presenter(s),
|
Value: func() error { return writer.Write(s) },
|
||||||
})
|
})
|
||||||
}()
|
}()
|
||||||
return errs
|
return errs
|
||||||
|
|||||||
+15
-11
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/anchore/syft/internal/bus"
|
"github.com/anchore/syft/internal/bus"
|
||||||
"github.com/anchore/syft/internal/formats/syftjson"
|
"github.com/anchore/syft/internal/formats/syftjson"
|
||||||
"github.com/anchore/syft/internal/log"
|
"github.com/anchore/syft/internal/log"
|
||||||
|
"github.com/anchore/syft/internal/output"
|
||||||
"github.com/anchore/syft/internal/ui"
|
"github.com/anchore/syft/internal/ui"
|
||||||
"github.com/anchore/syft/internal/version"
|
"github.com/anchore/syft/internal/version"
|
||||||
"github.com/anchore/syft/syft/artifact"
|
"github.com/anchore/syft/syft/artifact"
|
||||||
@@ -73,9 +74,16 @@ func powerUserExec(_ *cobra.Command, args []string) error {
|
|||||||
// could be an image or a directory, with or without a scheme
|
// could be an image or a directory, with or without a scheme
|
||||||
userInput := args[0]
|
userInput := args[0]
|
||||||
|
|
||||||
reporter, closer, err := reportWriter()
|
writer, err := output.MakeWriter(output.WriterOption{
|
||||||
|
Format: syftjson.Format(),
|
||||||
|
Path: appConfig.File,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := closer(); err != nil {
|
if err := writer.Close(); err != nil {
|
||||||
log.Warnf("unable to write to report destination: %+v", err)
|
log.Warnf("unable to write to report destination: %+v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,19 +92,15 @@ func powerUserExec(_ *cobra.Command, args []string) error {
|
|||||||
fmt.Fprintln(os.Stderr, deprecated)
|
fmt.Fprintln(os.Stderr, deprecated)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return eventLoop(
|
return eventLoop(
|
||||||
powerUserExecWorker(userInput),
|
powerUserExecWorker(userInput, writer),
|
||||||
setupSignals(),
|
setupSignals(),
|
||||||
eventSubscription,
|
eventSubscription,
|
||||||
stereoscope.Cleanup,
|
stereoscope.Cleanup,
|
||||||
ui.Select(isVerbose(), appConfig.Quiet, reporter)...,
|
ui.Select(isVerbose(), appConfig.Quiet)...,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
func powerUserExecWorker(userInput string) <-chan error {
|
func powerUserExecWorker(userInput string, writer sbom.Writer) <-chan error {
|
||||||
errs := make(chan error)
|
errs := make(chan error)
|
||||||
go func() {
|
go func() {
|
||||||
defer close(errs)
|
defer close(errs)
|
||||||
@@ -140,8 +144,8 @@ func powerUserExecWorker(userInput string) <-chan error {
|
|||||||
s.Relationships = append(s.Relationships, mergeRelationships(relationships...)...)
|
s.Relationships = append(s.Relationships, mergeRelationships(relationships...)...)
|
||||||
|
|
||||||
bus.Publish(partybus.Event{
|
bus.Publish(partybus.Event{
|
||||||
Type: event.PresenterReady,
|
Type: event.Exit,
|
||||||
Value: syftjson.Format().Presenter(s),
|
Value: func() error { return writer.Write(s) },
|
||||||
})
|
})
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
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, 0644)
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+5
-1
@@ -4,7 +4,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal"
|
||||||
"github.com/anchore/syft/internal/config"
|
"github.com/anchore/syft/internal/config"
|
||||||
|
"github.com/anchore/syft/internal/version"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
@@ -22,12 +24,14 @@ var rootCmd = &cobra.Command{
|
|||||||
PreRunE: packagesCmd.PreRunE,
|
PreRunE: packagesCmd.PreRunE,
|
||||||
RunE: packagesCmd.RunE,
|
RunE: packagesCmd.RunE,
|
||||||
ValidArgsFunction: packagesCmd.ValidArgsFunction,
|
ValidArgsFunction: packagesCmd.ValidArgsFunction,
|
||||||
|
Version: version.FromBuild().Version,
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// set universal flags
|
// set universal flags
|
||||||
rootCmd.PersistentFlags().StringVarP(&persistentOpts.ConfigPath, "config", "c", "", "application config file")
|
rootCmd.PersistentFlags().StringVarP(&persistentOpts.ConfigPath, "config", "c", "", "application config file")
|
||||||
|
// setting the version template to just print out the string since we already have a templatized version string
|
||||||
|
rootCmd.SetVersionTemplate(fmt.Sprintf("%s {{.Version}}\n", internal.ApplicationName))
|
||||||
flag := "quiet"
|
flag := "quiet"
|
||||||
rootCmd.PersistentFlags().BoolP(
|
rootCmd.PersistentFlags().BoolP(
|
||||||
flag, "q", false,
|
flag, "q", false,
|
||||||
|
|||||||
+1
-1
@@ -45,7 +45,7 @@ func generateCatalogPackagesTask() (task, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
|
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
|
||||||
packageCatalog, relationships, theDistro, err := syft.CatalogPackages(src, appConfig.Package.Cataloger.ScopeOpt)
|
packageCatalog, relationships, theDistro, err := syft.CatalogPackages(src, appConfig.Package.ToConfig())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ require (
|
|||||||
github.com/adrg/xdg v0.2.1
|
github.com/adrg/xdg v0.2.1
|
||||||
github.com/alecthomas/jsonschema v0.0.0-20210301060011-54c507b6f074
|
github.com/alecthomas/jsonschema v0.0.0-20210301060011-54c507b6f074
|
||||||
github.com/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf
|
github.com/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf
|
||||||
github.com/anchore/go-presenter v0.0.0-20211102174526-0dbf20f6c7fa
|
|
||||||
github.com/anchore/go-rpmdb v0.0.0-20210914181456-a9c52348da63
|
github.com/anchore/go-rpmdb v0.0.0-20210914181456-a9c52348da63
|
||||||
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
|
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
|
||||||
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
|
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
|
||||||
@@ -18,7 +17,7 @@ require (
|
|||||||
// go: warning: github.com/andybalholm/brotli@v1.0.1: retracted by module author: occasional panics and data corruption
|
// go: warning: github.com/andybalholm/brotli@v1.0.1: retracted by module author: occasional panics and data corruption
|
||||||
github.com/andybalholm/brotli v1.0.4 // indirect
|
github.com/andybalholm/brotli v1.0.4 // indirect
|
||||||
github.com/antihax/optional v1.0.0
|
github.com/antihax/optional v1.0.0
|
||||||
github.com/bmatcuk/doublestar/v2 v2.0.4
|
github.com/bmatcuk/doublestar/v4 v4.0.2
|
||||||
github.com/docker/docker v20.10.11+incompatible
|
github.com/docker/docker v20.10.11+incompatible
|
||||||
github.com/dustin/go-humanize v1.0.0
|
github.com/dustin/go-humanize v1.0.0
|
||||||
github.com/facebookincubator/nvdtools v0.1.4
|
github.com/facebookincubator/nvdtools v0.1.4
|
||||||
|
|||||||
@@ -101,8 +101,6 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF
|
|||||||
github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0=
|
github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0=
|
||||||
github.com/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf h1:DYssiUV1pBmKqzKsm4mqXx8artqC0Q8HgZsVI3lMsAg=
|
github.com/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf h1:DYssiUV1pBmKqzKsm4mqXx8artqC0Q8HgZsVI3lMsAg=
|
||||||
github.com/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf/go.mod h1:FaODhIA06mxO1E6R32JE0TL1JWZZkmjRIAd4ULvHUKk=
|
github.com/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf/go.mod h1:FaODhIA06mxO1E6R32JE0TL1JWZZkmjRIAd4ULvHUKk=
|
||||||
github.com/anchore/go-presenter v0.0.0-20211102174526-0dbf20f6c7fa h1:mDLUAkgXsV5Z8D0EEj8eS6FBekolV/A+Xxbs9054bPw=
|
|
||||||
github.com/anchore/go-presenter v0.0.0-20211102174526-0dbf20f6c7fa/go.mod h1:29jwxTSAS6pBcrmuwf1U3r1Tqp1o1XpuiOJ0NT9NoGg=
|
|
||||||
github.com/anchore/go-rpmdb v0.0.0-20210914181456-a9c52348da63 h1:C9W/LAydEz/qdUhx1MdjO9l8NEcFKYknkxDVyo9LAoM=
|
github.com/anchore/go-rpmdb v0.0.0-20210914181456-a9c52348da63 h1:C9W/LAydEz/qdUhx1MdjO9l8NEcFKYknkxDVyo9LAoM=
|
||||||
github.com/anchore/go-rpmdb v0.0.0-20210914181456-a9c52348da63/go.mod h1:6qH8c6U/3CBVvDDDBZnPSTbTINq3cIdADUYTaVf75EM=
|
github.com/anchore/go-rpmdb v0.0.0-20210914181456-a9c52348da63/go.mod h1:6qH8c6U/3CBVvDDDBZnPSTbTINq3cIdADUYTaVf75EM=
|
||||||
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 h1:VzprUTpc0vW0nnNKJfJieyH/TZ9UYAnTZs5/gHTdAe8=
|
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 h1:VzprUTpc0vW0nnNKJfJieyH/TZ9UYAnTZs5/gHTdAe8=
|
||||||
@@ -135,8 +133,6 @@ github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edY
|
|||||||
github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
|
github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
|
||||||
github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||||
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||||
github.com/bmatcuk/doublestar/v2 v2.0.4 h1:6I6oUiT/sU27eE2OFcWqBhL1SwjyvQuOssxT4a1yidI=
|
|
||||||
github.com/bmatcuk/doublestar/v2 v2.0.4/go.mod h1:QMmcs3H2AUQICWhfzLXz+IYln8lRQmTZRptLie8RgRw=
|
|
||||||
github.com/bmatcuk/doublestar/v4 v4.0.2 h1:X0krlUVAVmtr2cRoTqR8aDMrDqnB36ht8wpWTiQ3jsA=
|
github.com/bmatcuk/doublestar/v4 v4.0.2 h1:X0krlUVAVmtr2cRoTqR8aDMrDqnB36ht8wpWTiQ3jsA=
|
||||||
github.com/bmatcuk/doublestar/v4 v4.0.2/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
github.com/bmatcuk/doublestar/v4 v4.0.2/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
|
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ get_binaries() {
|
|||||||
darwin/arm64) BINARIES="syft" ;;
|
darwin/arm64) BINARIES="syft" ;;
|
||||||
darwin/amd64) BINARIES="syft" ;;
|
darwin/amd64) BINARIES="syft" ;;
|
||||||
linux/amd64) BINARIES="syft" ;;
|
linux/amd64) BINARIES="syft" ;;
|
||||||
|
linux/arm64) BINARIES="syft" ;;
|
||||||
windows/amd64) BINARIES="syft" ;;
|
windows/amd64) BINARIES="syft" ;;
|
||||||
*)
|
*)
|
||||||
log_crit "platform $PLATFORM is not supported. Make sure this script is up-to-date and file request at https://github.com/${PREFIX}/issues/new"
|
log_crit "platform $PLATFORM is not supported. Make sure this script is up-to-date and file request at https://github.com/${PREFIX}/issues/new"
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ type packageSBOMImportAPI interface {
|
|||||||
func packageSbomModel(s sbom.SBOM) (*external.ImagePackageManifest, error) {
|
func packageSbomModel(s sbom.SBOM) (*external.ImagePackageManifest, error) {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
|
|
||||||
err := syftjson.Format().Presenter(s).Present(&buf)
|
err := syftjson.Format().Encode(&buf, s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to serialize results: %w", err)
|
return nil, fmt.Errorf("unable to serialize results: %w", err)
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,7 @@ func packageSbomModel(s sbom.SBOM) (*external.ImagePackageManifest, error) {
|
|||||||
// the model is 1:1 the JSON output of today. As the schema changes, this will need to be converted into individual mappings.
|
// the model is 1:1 the JSON output of today. As the schema changes, this will need to be converted into individual mappings.
|
||||||
var model external.ImagePackageManifest
|
var model external.ImagePackageManifest
|
||||||
if err = json.Unmarshal(buf.Bytes(), &model); err != nil {
|
if err = json.Unmarshal(buf.Bytes(), &model); err != nil {
|
||||||
return nil, fmt.Errorf("unable to convert JSON presenter output to import model: %w", err)
|
return nil, fmt.Errorf("unable to convert JSON output to import model: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &model, nil
|
return &model, nil
|
||||||
|
|||||||
@@ -105,8 +105,7 @@ func TestPackageSbomToModel(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
pres := syftjson.Format().Presenter(s)
|
if err := syftjson.Format().Encode(&buf, s); err != nil {
|
||||||
if err := pres.Present(&buf); err != nil {
|
|
||||||
t.Fatalf("unable to get expected json: %+v", err)
|
t.Fatalf("unable to get expected json: %+v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ type parser interface {
|
|||||||
// Application is the main syft application configuration.
|
// Application is the main syft application configuration.
|
||||||
type Application struct {
|
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)
|
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
|
Output []string `yaml:"output" json:"output" mapstructure:"output"` // -o, the format to use for output
|
||||||
File string `yaml:"file" json:"file" mapstructure:"file"` // --file, the file to write report output to
|
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)
|
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
|
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
|
||||||
@@ -38,7 +38,7 @@ type Application struct {
|
|||||||
CliOptions CliOnlyOptions `yaml:"-" json:"-"` // all options only available through the CLI (not via env vars or config)
|
CliOptions CliOnlyOptions `yaml:"-" json:"-"` // all options only available through the CLI (not via env vars or config)
|
||||||
Dev development `yaml:"dev" json:"dev" mapstructure:"dev"`
|
Dev development `yaml:"dev" json:"dev" mapstructure:"dev"`
|
||||||
Log logging `yaml:"log" json:"log" mapstructure:"log"` // all logging-related options
|
Log logging `yaml:"log" json:"log" mapstructure:"log"` // all logging-related options
|
||||||
Package packages `yaml:"package" json:"package" mapstructure:"package"`
|
Package pkg `yaml:"package" json:"package" mapstructure:"package"`
|
||||||
FileMetadata FileMetadata `yaml:"file-metadata" json:"file-metadata" mapstructure:"file-metadata"`
|
FileMetadata FileMetadata `yaml:"file-metadata" json:"file-metadata" mapstructure:"file-metadata"`
|
||||||
FileClassification fileClassification `yaml:"file-classification" json:"file-classification" mapstructure:"file-classification"`
|
FileClassification fileClassification `yaml:"file-classification" json:"file-classification" mapstructure:"file-classification"`
|
||||||
FileContents fileContents `yaml:"file-contents" json:"file-contents" mapstructure:"file-contents"`
|
FileContents fileContents `yaml:"file-contents" json:"file-contents" mapstructure:"file-contents"`
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package config
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
|
||||||
"github.com/anchore/syft/syft/source"
|
"github.com/anchore/syft/syft/source"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -12,6 +14,10 @@ type catalogerOptions struct {
|
|||||||
ScopeOpt source.Scope `yaml:"-" json:"-"`
|
ScopeOpt source.Scope `yaml:"-" json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cfg catalogerOptions) loadDefaultValues(v *viper.Viper) {
|
||||||
|
v.SetDefault("package.cataloger.enabled", true)
|
||||||
|
}
|
||||||
|
|
||||||
func (cfg *catalogerOptions) parseConfigValues() error {
|
func (cfg *catalogerOptions) parseConfigValues() error {
|
||||||
scopeOption := source.ParseScope(cfg.Scope)
|
scopeOption := source.ParseScope(cfg.Scope)
|
||||||
if scopeOption == source.UnknownScope {
|
if scopeOption == source.UnknownScope {
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import "github.com/spf13/viper"
|
|
||||||
|
|
||||||
type packages struct {
|
|
||||||
Cataloger catalogerOptions `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg packages) loadDefaultValues(v *viper.Viper) {
|
|
||||||
v.SetDefault("package.cataloger.enabled", true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *packages) parseConfigValues() error {
|
|
||||||
return cfg.Cataloger.parseConfigValues()
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/anchore/syft/syft/pkg/cataloger"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
type pkg struct {
|
||||||
|
Cataloger catalogerOptions `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"`
|
||||||
|
SearchUnindexedArchives bool `yaml:"search-unindexed-archives" json:"search-unindexed-archives" mapstructure:"search-unindexed-archives"`
|
||||||
|
SearchIndexedArchives bool `yaml:"search-indexed-archives" json:"search-indexed-archives" mapstructure:"search-indexed-archives"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg pkg) loadDefaultValues(v *viper.Viper) {
|
||||||
|
cfg.Cataloger.loadDefaultValues(v)
|
||||||
|
c := cataloger.DefaultSearchConfig()
|
||||||
|
v.SetDefault("package.search-unindexed-archives", c.IncludeUnindexedArchives)
|
||||||
|
v.SetDefault("package.search-indexed-archives", c.IncludeIndexedArchives)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *pkg) parseConfigValues() error {
|
||||||
|
return cfg.Cataloger.parseConfigValues()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg pkg) ToConfig() cataloger.Config {
|
||||||
|
return cataloger.Config{
|
||||||
|
Search: cataloger.SearchConfig{
|
||||||
|
IncludeIndexedArchives: cfg.SearchIndexedArchives,
|
||||||
|
IncludeUnindexedArchives: cfg.SearchUnindexedArchives,
|
||||||
|
Scope: cfg.Cataloger.ScopeOpt,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,9 +2,10 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/anchore/stereoscope/pkg/image"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/anchore/stereoscope/pkg/image"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const (
|
|||||||
// ApplicationName is the non-capitalized name of the application (do not change this)
|
// ApplicationName is the non-capitalized name of the application (do not change this)
|
||||||
ApplicationName = "syft"
|
ApplicationName = "syft"
|
||||||
|
|
||||||
// JSONSchemaVersion is the current schema version output by the JSON presenter
|
// JSONSchemaVersion is the current schema version output by the JSON encoder
|
||||||
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
|
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
|
||||||
JSONSchemaVersion = "2.0.2"
|
JSONSchemaVersion = "2.0.2"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package file
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
const perFileReadLimit = 2 * GB
|
||||||
|
|
||||||
|
// safeCopy limits the copy from the reader. This is useful when extracting files from archives to
|
||||||
|
// protect against decompression bomb attacks.
|
||||||
|
func safeCopy(writer io.Writer, reader io.Reader) error {
|
||||||
|
numBytes, err := io.Copy(writer, io.LimitReader(reader, perFileReadLimit))
|
||||||
|
if numBytes >= perFileReadLimit || errors.Is(err, io.EOF) {
|
||||||
|
return fmt.Errorf("zip read limit hit (potential decompression bomb attack)")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package file
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/bmatcuk/doublestar/v4"
|
||||||
|
"github.com/mholt/archiver/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExtractGlobsFromTarToUniqueTempFile extracts paths matching the given globs within the given archive to a temporary directory, returning file openers for each file extracted.
|
||||||
|
func ExtractGlobsFromTarToUniqueTempFile(archivePath, dir string, globs ...string) (map[string]Opener, error) {
|
||||||
|
results := make(map[string]Opener)
|
||||||
|
|
||||||
|
// don't allow for full traversal, only select traversal from given paths
|
||||||
|
if len(globs) == 0 {
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
visitor := func(file archiver.File) error {
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// ignore directories
|
||||||
|
if file.FileInfo.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore any filename that doesn't match the given globs...
|
||||||
|
if !matchesAnyGlob(file.Name(), globs...) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// we have a file we want to extract....
|
||||||
|
tempfilePrefix := filepath.Base(filepath.Clean(file.Name())) + "-"
|
||||||
|
tempFile, err := ioutil.TempFile(dir, tempfilePrefix)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to create temp file: %w", err)
|
||||||
|
}
|
||||||
|
// we shouldn't try and keep the tempfile open as the returned result may have several files, which takes up
|
||||||
|
// resources (leading to "too many open files"). Instead we'll return a file opener to the caller which
|
||||||
|
// provides a ReadCloser. It is up to the caller to handle closing the file explicitly.
|
||||||
|
defer tempFile.Close()
|
||||||
|
|
||||||
|
if err := safeCopy(tempFile, file.ReadCloser); err != nil {
|
||||||
|
return fmt.Errorf("unable to copy source=%q for tar=%q: %w", file.Name(), archivePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
results[file.Name()] = Opener{path: tempFile.Name()}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, archiver.Walk(archivePath, visitor)
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchesAnyGlob(name string, globs ...string) bool {
|
||||||
|
for _, glob := range globs {
|
||||||
|
if matches, err := doublestar.PathMatch(glob, name); err == nil && matches {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
//go:build !windows
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
package file
|
package file
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ package file
|
|||||||
import (
|
import (
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -22,8 +20,6 @@ const (
|
|||||||
GB
|
GB
|
||||||
)
|
)
|
||||||
|
|
||||||
const perFileReadLimit = 2 * GB
|
|
||||||
|
|
||||||
type errZipSlipDetected struct {
|
type errZipSlipDetected struct {
|
||||||
Prefix string
|
Prefix string
|
||||||
JoinArgs []string
|
JoinArgs []string
|
||||||
@@ -110,21 +106,10 @@ func ExtractFromZipToUniqueTempFile(archivePath, dir string, paths ...string) (m
|
|||||||
return fmt.Errorf("unable to extract directories, only files: %s", file.Name)
|
return fmt.Errorf("unable to extract directories, only files: %s", file.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// limit the zip reader on each file read to prevent decompression bomb attacks
|
if err := safeCopy(tempFile, zippedFile); err != nil {
|
||||||
numBytes, err := io.Copy(tempFile, io.LimitReader(zippedFile, perFileReadLimit))
|
|
||||||
if numBytes >= perFileReadLimit || errors.Is(err, io.EOF) {
|
|
||||||
return fmt.Errorf("zip read limit hit (potential decompression bomb attack)")
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to copy source=%q for zip=%q: %w", file.Name, archivePath, err)
|
return fmt.Errorf("unable to copy source=%q for zip=%q: %w", file.Name, archivePath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// the file pointer is at the end due to the copy operation, reset back to the beginning
|
|
||||||
_, err = tempFile.Seek(0, io.SeekStart)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to reset file pointer (%s): %w", tempFile.Name(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
results[file.Name] = Opener{path: tempFile.Name()}
|
results[file.Name] = Opener{path: tempFile.Name()}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -153,13 +138,7 @@ func ContentsFromZip(archivePath string, paths ...string) (map[string]string, er
|
|||||||
}
|
}
|
||||||
|
|
||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
|
if err := safeCopy(&buffer, zippedFile); err != nil {
|
||||||
// limit the zip reader on each file read to prevent decompression bomb attacks
|
|
||||||
numBytes, err := io.Copy(&buffer, io.LimitReader(zippedFile, perFileReadLimit))
|
|
||||||
if numBytes >= perFileReadLimit || errors.Is(err, io.EOF) {
|
|
||||||
return fmt.Errorf("zip read limit hit (potential decompression bomb attack)")
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to copy source=%q for zip=%q: %w", file.Name, archivePath, err)
|
return fmt.Errorf("unable to copy source=%q for zip=%q: %w", file.Name, archivePath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,12 +207,7 @@ func extractSingleFile(file *zip.File, expandedFilePath, archivePath string) err
|
|||||||
return fmt.Errorf("unable to create dest file=%q from zip=%q: %w", expandedFilePath, archivePath, err)
|
return fmt.Errorf("unable to create dest file=%q from zip=%q: %w", expandedFilePath, archivePath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// limit the zip reader on each file read to prevent decompression bomb attacks
|
if err := safeCopy(outputFile, zippedFile); err != nil {
|
||||||
numBytes, err := io.Copy(outputFile, io.LimitReader(zippedFile, perFileReadLimit))
|
|
||||||
if numBytes >= perFileReadLimit || errors.Is(err, io.EOF) {
|
|
||||||
return fmt.Errorf("zip read limit hit (potential decompression bomb attack)")
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to copy source=%q to dest=%q for zip=%q: %w", file.Name, outputFile.Name(), archivePath, err)
|
return fmt.Errorf("unable to copy source=%q to dest=%q for zip=%q: %w", file.Name, outputFile.Name(), archivePath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
//go:build !windows
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
package file
|
package file
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
//go:build !windows
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
package file
|
package file
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ func ExternalRefs(p pkg.Package) (externalRefs []model.ExternalRef) {
|
|||||||
for _, c := range p.CPEs {
|
for _, c := range p.CPEs {
|
||||||
externalRefs = append(externalRefs, model.ExternalRef{
|
externalRefs = append(externalRefs, model.ExternalRef{
|
||||||
ReferenceCategory: model.SecurityReferenceCategory,
|
ReferenceCategory: model.SecurityReferenceCategory,
|
||||||
ReferenceLocator: c.BindToFmtString(),
|
ReferenceLocator: pkg.CPEString(c),
|
||||||
ReferenceType: model.Cpe23ExternalRefType,
|
ReferenceType: model.Cpe23ExternalRefType,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ func Test_ExternalRefs(t *testing.T) {
|
|||||||
expected: []model.ExternalRef{
|
expected: []model.ExternalRef{
|
||||||
{
|
{
|
||||||
ReferenceCategory: model.SecurityReferenceCategory,
|
ReferenceCategory: model.SecurityReferenceCategory,
|
||||||
ReferenceLocator: testCPE.BindToFmtString(),
|
ReferenceLocator: pkg.CPEString(testCPE),
|
||||||
ReferenceType: model.Cpe23ExternalRefType,
|
ReferenceType: model.Cpe23ExternalRefType,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,14 +2,15 @@ package testutils
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/anchore/go-presenter"
|
|
||||||
"github.com/anchore/go-testutils"
|
"github.com/anchore/go-testutils"
|
||||||
"github.com/anchore/stereoscope/pkg/filetree"
|
"github.com/anchore/stereoscope/pkg/filetree"
|
||||||
"github.com/anchore/stereoscope/pkg/image"
|
"github.com/anchore/stereoscope/pkg/image"
|
||||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||||
"github.com/anchore/syft/syft/distro"
|
"github.com/anchore/syft/syft/distro"
|
||||||
|
"github.com/anchore/syft/syft/format"
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
"github.com/anchore/syft/syft/sbom"
|
"github.com/anchore/syft/syft/sbom"
|
||||||
"github.com/anchore/syft/syft/source"
|
"github.com/anchore/syft/syft/source"
|
||||||
@@ -31,7 +32,7 @@ func FromSnapshot() ImageOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func AssertPresenterAgainstGoldenImageSnapshot(t *testing.T, pres presenter.Presenter, testImage string, updateSnapshot bool, redactors ...redactor) {
|
func AssertEncoderAgainstGoldenImageSnapshot(t *testing.T, format format.Format, sbom sbom.SBOM, testImage string, updateSnapshot bool, redactors ...redactor) {
|
||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
|
|
||||||
// grab the latest image contents and persist
|
// grab the latest image contents and persist
|
||||||
@@ -39,11 +40,11 @@ func AssertPresenterAgainstGoldenImageSnapshot(t *testing.T, pres presenter.Pres
|
|||||||
imagetest.UpdateGoldenFixtureImage(t, testImage)
|
imagetest.UpdateGoldenFixtureImage(t, testImage)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := pres.Present(&buffer)
|
err := format.Encode(&buffer, sbom)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
actual := buffer.Bytes()
|
actual := buffer.Bytes()
|
||||||
|
|
||||||
// replace the expected snapshot contents with the current presenter contents
|
// replace the expected snapshot contents with the current encoder contents
|
||||||
if updateSnapshot {
|
if updateSnapshot {
|
||||||
testutils.UpdateGoldenFileContents(t, actual)
|
testutils.UpdateGoldenFileContents(t, actual)
|
||||||
}
|
}
|
||||||
@@ -51,6 +52,7 @@ func AssertPresenterAgainstGoldenImageSnapshot(t *testing.T, pres presenter.Pres
|
|||||||
var expected = testutils.GetGoldenFileContents(t)
|
var expected = testutils.GetGoldenFileContents(t)
|
||||||
|
|
||||||
// remove dynamic values, which should be tested independently
|
// remove dynamic values, which should be tested independently
|
||||||
|
redactors = append(redactors, carriageRedactor)
|
||||||
for _, r := range redactors {
|
for _, r := range redactors {
|
||||||
actual = r(actual)
|
actual = r(actual)
|
||||||
expected = r(expected)
|
expected = r(expected)
|
||||||
@@ -64,14 +66,14 @@ func AssertPresenterAgainstGoldenImageSnapshot(t *testing.T, pres presenter.Pres
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func AssertPresenterAgainstGoldenSnapshot(t *testing.T, pres presenter.Presenter, updateSnapshot bool, redactors ...redactor) {
|
func AssertEncoderAgainstGoldenSnapshot(t *testing.T, format format.Format, sbom sbom.SBOM, updateSnapshot bool, redactors ...redactor) {
|
||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
|
|
||||||
err := pres.Present(&buffer)
|
err := format.Encode(&buffer, sbom)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
actual := buffer.Bytes()
|
actual := buffer.Bytes()
|
||||||
|
|
||||||
// replace the expected snapshot contents with the current presenter contents
|
// replace the expected snapshot contents with the current encoder contents
|
||||||
if updateSnapshot {
|
if updateSnapshot {
|
||||||
testutils.UpdateGoldenFileContents(t, actual)
|
testutils.UpdateGoldenFileContents(t, actual)
|
||||||
}
|
}
|
||||||
@@ -79,6 +81,7 @@ func AssertPresenterAgainstGoldenSnapshot(t *testing.T, pres presenter.Presenter
|
|||||||
var expected = testutils.GetGoldenFileContents(t)
|
var expected = testutils.GetGoldenFileContents(t)
|
||||||
|
|
||||||
// remove dynamic values, which should be tested independently
|
// remove dynamic values, which should be tested independently
|
||||||
|
redactors = append(redactors, carriageRedactor)
|
||||||
for _, r := range redactors {
|
for _, r := range redactors {
|
||||||
actual = r(actual)
|
actual = r(actual)
|
||||||
expected = r(expected)
|
expected = r(expected)
|
||||||
@@ -138,6 +141,11 @@ func ImageInput(t testing.TB, testImage string, options ...ImageOption) sbom.SBO
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func carriageRedactor(s []byte) []byte {
|
||||||
|
msg := strings.ReplaceAll(string(s), "\r\n", "\n")
|
||||||
|
return []byte(msg)
|
||||||
|
}
|
||||||
|
|
||||||
func populateImageCatalog(catalog *pkg.Catalog, img *image.Image) {
|
func populateImageCatalog(catalog *pkg.Catalog, img *image.Image) {
|
||||||
_, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks)
|
_, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks)
|
||||||
_, ref2, _ := img.SquashedTree().File("/somefile-2.txt", filetree.FollowBasenameLinks)
|
_, ref2, _ := img.SquashedTree().File("/somefile-2.txt", filetree.FollowBasenameLinks)
|
||||||
|
|||||||
@@ -8,20 +8,22 @@ import (
|
|||||||
"github.com/anchore/syft/internal/formats/common/testutils"
|
"github.com/anchore/syft/internal/formats/common/testutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
var updateCycloneDx = flag.Bool("update-cyclonedx", false, "update the *.golden files for cyclone-dx presenters")
|
var updateCycloneDx = flag.Bool("update-cyclonedx", false, "update the *.golden files for cyclone-dx encoders")
|
||||||
|
|
||||||
func TestCycloneDxDirectoryPresenter(t *testing.T) {
|
func TestCycloneDxDirectoryEncoder(t *testing.T) {
|
||||||
testutils.AssertPresenterAgainstGoldenSnapshot(t,
|
testutils.AssertEncoderAgainstGoldenSnapshot(t,
|
||||||
Format().Presenter(testutils.DirectoryInput(t)),
|
Format(),
|
||||||
|
testutils.DirectoryInput(t),
|
||||||
*updateCycloneDx,
|
*updateCycloneDx,
|
||||||
cycloneDxRedactor,
|
cycloneDxRedactor,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCycloneDxImagePresenter(t *testing.T) {
|
func TestCycloneDxImageEncoder(t *testing.T) {
|
||||||
testImage := "image-simple"
|
testImage := "image-simple"
|
||||||
testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
|
testutils.AssertEncoderAgainstGoldenImageSnapshot(t,
|
||||||
Format().Presenter(testutils.ImageInput(t, testImage)),
|
Format(),
|
||||||
|
testutils.ImageInput(t, testImage),
|
||||||
testImage,
|
testImage,
|
||||||
*updateCycloneDx,
|
*updateCycloneDx,
|
||||||
cycloneDxRedactor,
|
cycloneDxRedactor,
|
||||||
|
|||||||
@@ -8,20 +8,22 @@ import (
|
|||||||
"github.com/anchore/syft/internal/formats/common/testutils"
|
"github.com/anchore/syft/internal/formats/common/testutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
var updateCycloneDx = flag.Bool("update-cyclonedx", false, "update the *.golden files for cyclone-dx presenters")
|
var updateCycloneDx = flag.Bool("update-cyclonedx", false, "update the *.golden files for cyclone-dx encoders")
|
||||||
|
|
||||||
func TestCycloneDxDirectoryPresenter(t *testing.T) {
|
func TestCycloneDxDirectoryEncoder(t *testing.T) {
|
||||||
testutils.AssertPresenterAgainstGoldenSnapshot(t,
|
testutils.AssertEncoderAgainstGoldenSnapshot(t,
|
||||||
Format().Presenter(testutils.DirectoryInput(t)),
|
Format(),
|
||||||
|
testutils.DirectoryInput(t),
|
||||||
*updateCycloneDx,
|
*updateCycloneDx,
|
||||||
cycloneDxRedactor,
|
cycloneDxRedactor,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCycloneDxImagePresenter(t *testing.T) {
|
func TestCycloneDxImageEncoder(t *testing.T) {
|
||||||
testImage := "image-simple"
|
testImage := "image-simple"
|
||||||
testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
|
testutils.AssertEncoderAgainstGoldenImageSnapshot(t,
|
||||||
Format().Presenter(testutils.ImageInput(t, testImage)),
|
Format(),
|
||||||
|
testutils.ImageInput(t, testImage),
|
||||||
testImage,
|
testImage,
|
||||||
*updateCycloneDx,
|
*updateCycloneDx,
|
||||||
cycloneDxRedactor,
|
cycloneDxRedactor,
|
||||||
|
|||||||
@@ -8,20 +8,22 @@ import (
|
|||||||
"github.com/anchore/syft/internal/formats/common/testutils"
|
"github.com/anchore/syft/internal/formats/common/testutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
var updateSpdxJson = flag.Bool("update-spdx-json", false, "update the *.golden files for spdx-json presenters")
|
var updateSpdxJson = flag.Bool("update-spdx-json", false, "update the *.golden files for spdx-json encoders")
|
||||||
|
|
||||||
func TestSPDXJSONDirectoryPresenter(t *testing.T) {
|
func TestSPDXJSONDirectoryEncoder(t *testing.T) {
|
||||||
testutils.AssertPresenterAgainstGoldenSnapshot(t,
|
testutils.AssertEncoderAgainstGoldenSnapshot(t,
|
||||||
Format().Presenter(testutils.DirectoryInput(t)),
|
Format(),
|
||||||
|
testutils.DirectoryInput(t),
|
||||||
*updateSpdxJson,
|
*updateSpdxJson,
|
||||||
spdxJsonRedactor,
|
spdxJsonRedactor,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSPDXJSONImagePresenter(t *testing.T) {
|
func TestSPDXJSONImageEncoder(t *testing.T) {
|
||||||
testImage := "image-simple"
|
testImage := "image-simple"
|
||||||
testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
|
testutils.AssertEncoderAgainstGoldenImageSnapshot(t,
|
||||||
Format().Presenter(testutils.ImageInput(t, testImage, testutils.FromSnapshot())),
|
Format(),
|
||||||
|
testutils.ImageInput(t, testImage, testutils.FromSnapshot()),
|
||||||
testImage,
|
testImage,
|
||||||
*updateSpdxJson,
|
*updateSpdxJson,
|
||||||
spdxJsonRedactor,
|
spdxJsonRedactor,
|
||||||
|
|||||||
@@ -8,21 +8,23 @@ import (
|
|||||||
"github.com/anchore/syft/internal/formats/common/testutils"
|
"github.com/anchore/syft/internal/formats/common/testutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
var updateSpdxTagValue = flag.Bool("update-spdx-tv", false, "update the *.golden files for spdx-tv presenters")
|
var updateSpdxTagValue = flag.Bool("update-spdx-tv", false, "update the *.golden files for spdx-tv encoders")
|
||||||
|
|
||||||
func TestSPDXTagValueDirectoryPresenter(t *testing.T) {
|
func TestSPDXTagValueDirectoryEncoder(t *testing.T) {
|
||||||
|
|
||||||
testutils.AssertPresenterAgainstGoldenSnapshot(t,
|
testutils.AssertEncoderAgainstGoldenSnapshot(t,
|
||||||
Format().Presenter(testutils.DirectoryInput(t)),
|
Format(),
|
||||||
|
testutils.DirectoryInput(t),
|
||||||
*updateSpdxTagValue,
|
*updateSpdxTagValue,
|
||||||
spdxTagValueRedactor,
|
spdxTagValueRedactor,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSPDXTagValueImagePresenter(t *testing.T) {
|
func TestSPDXTagValueImageEncoder(t *testing.T) {
|
||||||
testImage := "image-simple"
|
testImage := "image-simple"
|
||||||
testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
|
testutils.AssertEncoderAgainstGoldenImageSnapshot(t,
|
||||||
Format().Presenter(testutils.ImageInput(t, testImage, testutils.FromSnapshot())),
|
Format(),
|
||||||
|
testutils.ImageInput(t, testImage, testutils.FromSnapshot()),
|
||||||
testImage,
|
testImage,
|
||||||
*updateSpdxTagValue,
|
*updateSpdxTagValue,
|
||||||
spdxTagValueRedactor,
|
spdxTagValueRedactor,
|
||||||
|
|||||||
@@ -16,19 +16,21 @@ import (
|
|||||||
"github.com/anchore/syft/internal/formats/common/testutils"
|
"github.com/anchore/syft/internal/formats/common/testutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
var updateJson = flag.Bool("update-json", false, "update the *.golden files for json presenters")
|
var updateJson = flag.Bool("update-json", false, "update the *.golden files for json encoders")
|
||||||
|
|
||||||
func TestDirectoryPresenter(t *testing.T) {
|
func TestDirectoryEncoder(t *testing.T) {
|
||||||
testutils.AssertPresenterAgainstGoldenSnapshot(t,
|
testutils.AssertEncoderAgainstGoldenSnapshot(t,
|
||||||
Format().Presenter(testutils.DirectoryInput(t)),
|
Format(),
|
||||||
|
testutils.DirectoryInput(t),
|
||||||
*updateJson,
|
*updateJson,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestImagePresenter(t *testing.T) {
|
func TestImageEncoder(t *testing.T) {
|
||||||
testImage := "image-simple"
|
testImage := "image-simple"
|
||||||
testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
|
testutils.AssertEncoderAgainstGoldenImageSnapshot(t,
|
||||||
Format().Presenter(testutils.ImageInput(t, testImage, testutils.FromSnapshot())),
|
Format(),
|
||||||
|
testutils.ImageInput(t, testImage, testutils.FromSnapshot()),
|
||||||
testImage,
|
testImage,
|
||||||
*updateJson,
|
*updateJson,
|
||||||
)
|
)
|
||||||
@@ -192,8 +194,9 @@ func TestEncodeFullJSONDocument(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
testutils.AssertPresenterAgainstGoldenSnapshot(t,
|
testutils.AssertEncoderAgainstGoldenSnapshot(t,
|
||||||
Format().Presenter(s),
|
Format(),
|
||||||
|
s,
|
||||||
*updateJson,
|
*updateJson,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ func toPackageModels(catalog *pkg.Catalog) []model.Package {
|
|||||||
func toPackageModel(p pkg.Package) model.Package {
|
func toPackageModel(p pkg.Package) model.Package {
|
||||||
var cpes = make([]string, len(p.CPEs))
|
var cpes = make([]string, len(p.CPEs))
|
||||||
for i, c := range p.CPEs {
|
for i, c := range p.CPEs {
|
||||||
cpes[i] = c.BindToFmtString()
|
cpes[i] = pkg.CPEString(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
var licenses = make([]string, 0)
|
var licenses = make([]string, 0)
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ import (
|
|||||||
|
|
||||||
var updateTableGoldenFiles = flag.Bool("update-table", false, "update the *.golden files for table format")
|
var updateTableGoldenFiles = flag.Bool("update-table", false, "update the *.golden files for table format")
|
||||||
|
|
||||||
func TestTablePresenter(t *testing.T) {
|
func TestTableEncoder(t *testing.T) {
|
||||||
testutils.AssertPresenterAgainstGoldenSnapshot(t,
|
testutils.AssertEncoderAgainstGoldenSnapshot(t,
|
||||||
Format().Presenter(testutils.DirectoryInput(t)),
|
Format(),
|
||||||
|
testutils.DirectoryInput(t),
|
||||||
*updateTableGoldenFiles,
|
*updateTableGoldenFiles,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,20 +7,22 @@ import (
|
|||||||
"github.com/anchore/syft/internal/formats/common/testutils"
|
"github.com/anchore/syft/internal/formats/common/testutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
var updateTextPresenterGoldenFiles = flag.Bool("update-text", false, "update the *.golden files for text presenters")
|
var updateTextEncoderGoldenFiles = flag.Bool("update-text", false, "update the *.golden files for text encoder")
|
||||||
|
|
||||||
func TestTextDirectoryPresenter(t *testing.T) {
|
func TestTextDirectoryEncoder(t *testing.T) {
|
||||||
testutils.AssertPresenterAgainstGoldenSnapshot(t,
|
testutils.AssertEncoderAgainstGoldenSnapshot(t,
|
||||||
Format().Presenter(testutils.DirectoryInput(t)),
|
Format(),
|
||||||
*updateTextPresenterGoldenFiles,
|
testutils.DirectoryInput(t),
|
||||||
|
*updateTextEncoderGoldenFiles,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTextImagePresenter(t *testing.T) {
|
func TestTextImageEncoder(t *testing.T) {
|
||||||
testImage := "image-simple"
|
testImage := "image-simple"
|
||||||
testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
|
testutils.AssertEncoderAgainstGoldenImageSnapshot(t,
|
||||||
Format().Presenter(testutils.ImageInput(t, testImage, testutils.FromSnapshot())),
|
Format(),
|
||||||
|
testutils.ImageInput(t, testImage, testutils.FromSnapshot()),
|
||||||
testImage,
|
testImage,
|
||||||
*updateTextPresenterGoldenFiles,
|
*updateTextEncoderGoldenFiles,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package output
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/format"
|
||||||
|
"github.com/anchore/syft/syft/sbom"
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
)
|
||||||
|
|
||||||
|
// streamWriter implements sbom.Writer for a given format and io.Writer, also providing a close function for cleanup
|
||||||
|
type streamWriter struct {
|
||||||
|
format format.Format
|
||||||
|
out io.Writer
|
||||||
|
close func() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the provided SBOM to the data stream
|
||||||
|
func (w *streamWriter) Write(s sbom.SBOM) error {
|
||||||
|
return w.format.Encode(w.out, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close any resources, such as open files
|
||||||
|
func (w *streamWriter) Close() error {
|
||||||
|
if w.close != nil {
|
||||||
|
return w.close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// multiWriter holds a list of child sbom.Writers to apply all Write and Close operations to
|
||||||
|
type multiWriter struct {
|
||||||
|
writers []sbom.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write writes the SBOM to all writers
|
||||||
|
func (m *multiWriter) Write(s sbom.SBOM) (errs error) {
|
||||||
|
for _, w := range m.writers {
|
||||||
|
err := w.Write(s)
|
||||||
|
if err != nil {
|
||||||
|
errs = multierror.Append(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes all writers
|
||||||
|
func (m *multiWriter) Close() (errs error) {
|
||||||
|
for _, w := range m.writers {
|
||||||
|
err := w.Close()
|
||||||
|
if err != nil {
|
||||||
|
errs = multierror.Append(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriterOption Format and path strings used to create sbom.Writer
|
||||||
|
type WriterOption struct {
|
||||||
|
Format format.Format
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeWriter create all report writers from input options; if a file is not specified, os.Stdout is used
|
||||||
|
func MakeWriter(options ...WriterOption) (_ sbom.Writer, errs error) {
|
||||||
|
if len(options) == 0 {
|
||||||
|
return nil, fmt.Errorf("no output options provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
out := &multiWriter{}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if errs != nil {
|
||||||
|
// close any previously opened files; we can't really recover from any errors
|
||||||
|
_ = out.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for _, option := range options {
|
||||||
|
switch len(option.Path) {
|
||||||
|
case 0:
|
||||||
|
out.writers = append(out.writers, &streamWriter{
|
||||||
|
format: option.Format,
|
||||||
|
out: os.Stdout,
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
// create any missing subdirectories
|
||||||
|
dir := path.Dir(option.Path)
|
||||||
|
if dir != "" {
|
||||||
|
s, err := os.Stat(dir)
|
||||||
|
if err != nil {
|
||||||
|
err = os.MkdirAll(dir, 0755) // maybe should be os.ModePerm ?
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else if !s.IsDir() {
|
||||||
|
return nil, fmt.Errorf("output path does not contain a valid directory: %s", option.Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fileOut, err := os.OpenFile(option.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to create report file: %w", err)
|
||||||
|
}
|
||||||
|
out.writers = append(out.writers, &streamWriter{
|
||||||
|
format: option.Format,
|
||||||
|
out: fileOut,
|
||||||
|
close: fileOut.Close,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
package output
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal/formats/spdx22json"
|
||||||
|
"github.com/anchore/syft/internal/formats/syftjson"
|
||||||
|
"github.com/anchore/syft/internal/formats/table"
|
||||||
|
"github.com/anchore/syft/internal/formats/text"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
type writerConfig struct {
|
||||||
|
format string
|
||||||
|
file string
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOutputWriter(t *testing.T) {
|
||||||
|
tmp := t.TempDir()
|
||||||
|
|
||||||
|
testName := func(options []WriterOption, err bool) string {
|
||||||
|
var out []string
|
||||||
|
for _, opt := range options {
|
||||||
|
out = append(out, string(opt.Format.Option)+"="+opt.Path)
|
||||||
|
}
|
||||||
|
errs := ""
|
||||||
|
if err {
|
||||||
|
errs = "(err)"
|
||||||
|
}
|
||||||
|
return strings.Join(out, ", ") + errs
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
outputs []WriterOption
|
||||||
|
err bool
|
||||||
|
expected []writerConfig
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
outputs: []WriterOption{},
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
outputs: []WriterOption{
|
||||||
|
{
|
||||||
|
Format: table.Format(),
|
||||||
|
Path: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: []writerConfig{
|
||||||
|
{
|
||||||
|
format: "table",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
outputs: []WriterOption{
|
||||||
|
{
|
||||||
|
Format: syftjson.Format(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: []writerConfig{
|
||||||
|
{
|
||||||
|
format: "json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
outputs: []WriterOption{
|
||||||
|
{
|
||||||
|
Format: syftjson.Format(),
|
||||||
|
Path: "test-2.json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: []writerConfig{
|
||||||
|
{
|
||||||
|
format: "json",
|
||||||
|
file: "test-2.json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
outputs: []WriterOption{
|
||||||
|
{
|
||||||
|
Format: syftjson.Format(),
|
||||||
|
Path: "test-3/1.json",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Format: spdx22json.Format(),
|
||||||
|
Path: "test-3/2.json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: []writerConfig{
|
||||||
|
{
|
||||||
|
format: "json",
|
||||||
|
file: "test-3/1.json",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: "spdx-json",
|
||||||
|
file: "test-3/2.json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
outputs: []WriterOption{
|
||||||
|
{
|
||||||
|
Format: text.Format(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Format: spdx22json.Format(),
|
||||||
|
Path: "test-4.json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: []writerConfig{
|
||||||
|
{
|
||||||
|
format: "text",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: "spdx-json",
|
||||||
|
file: "test-4.json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(testName(test.outputs, test.err), func(t *testing.T) {
|
||||||
|
outputs := test.outputs
|
||||||
|
for i := range outputs {
|
||||||
|
if outputs[i].Path != "" {
|
||||||
|
outputs[i].Path = tmp + outputs[i].Path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writer, err := MakeWriter(outputs...)
|
||||||
|
|
||||||
|
if test.err {
|
||||||
|
assert.Error(t, err)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mw := writer.(*multiWriter)
|
||||||
|
|
||||||
|
assert.Len(t, mw.writers, len(test.expected))
|
||||||
|
|
||||||
|
for i, e := range test.expected {
|
||||||
|
w := mw.writers[i].(*streamWriter)
|
||||||
|
|
||||||
|
assert.Equal(t, string(w.format.Option), e.format)
|
||||||
|
|
||||||
|
if e.file != "" {
|
||||||
|
assert.FileExists(t, tmp+e.file)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.file != "" {
|
||||||
|
assert.NotNil(t, w.out)
|
||||||
|
assert.NotNil(t, w.close)
|
||||||
|
} else {
|
||||||
|
assert.NotNil(t, w.out)
|
||||||
|
assert.Nil(t, w.close)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,23 +2,22 @@ package ui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
|
|
||||||
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
|
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
|
||||||
"github.com/wagoodman/go-partybus"
|
"github.com/wagoodman/go-partybus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// handleCatalogerPresenterReady is a UI function for processing the CatalogerFinished bus event, displaying the catalog
|
// handleExit is a UI function for processing the Exit bus event,
|
||||||
// via the given presenter to stdout.
|
// and calling the given function to output the contents.
|
||||||
func handleCatalogerPresenterReady(event partybus.Event, reportOutput io.Writer) error {
|
func handleExit(event partybus.Event) error {
|
||||||
// show the report to stdout
|
// show the report to stdout
|
||||||
pres, err := syftEventParsers.ParsePresenterReady(event)
|
fn, err := syftEventParsers.ParseExit(event)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("bad CatalogerFinished event: %w", err)
|
return fmt.Errorf("bad CatalogerFinished event: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := pres.Present(reportOutput); err != nil {
|
if err := fn(); err != nil {
|
||||||
return fmt.Errorf("unable to show package catalog report: %w", err)
|
return fmt.Errorf("unable to show package catalog report: %v", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,22 +36,20 @@ import (
|
|||||||
// or in the shared ui package as a function on the main handler object. All handler functions should be completed
|
// 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)
|
// processing an event before the ETUI exits (coordinated with a sync.WaitGroup)
|
||||||
type ephemeralTerminalUI struct {
|
type ephemeralTerminalUI struct {
|
||||||
unsubscribe func() error
|
unsubscribe func() error
|
||||||
handler *ui.Handler
|
handler *ui.Handler
|
||||||
waitGroup *sync.WaitGroup
|
waitGroup *sync.WaitGroup
|
||||||
frame *frame.Frame
|
frame *frame.Frame
|
||||||
logBuffer *bytes.Buffer
|
logBuffer *bytes.Buffer
|
||||||
uiOutput *os.File
|
uiOutput *os.File
|
||||||
reportOutput io.Writer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewEphemeralTerminalUI writes all events to a TUI and writes the final report to the given writer.
|
// NewEphemeralTerminalUI writes all events to a TUI and writes the final report to the given writer.
|
||||||
func NewEphemeralTerminalUI(reportWriter io.Writer) UI {
|
func NewEphemeralTerminalUI() UI {
|
||||||
return &ephemeralTerminalUI{
|
return &ephemeralTerminalUI{
|
||||||
handler: ui.NewHandler(),
|
handler: ui.NewHandler(),
|
||||||
waitGroup: &sync.WaitGroup{},
|
waitGroup: &sync.WaitGroup{},
|
||||||
uiOutput: os.Stderr,
|
uiOutput: os.Stderr,
|
||||||
reportOutput: reportWriter,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,12 +80,12 @@ func (h *ephemeralTerminalUI) Handle(event partybus.Event) error {
|
|||||||
log.Errorf("unable to show %s event: %+v", event.Type, err)
|
log.Errorf("unable to show %s event: %+v", event.Type, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
case event.Type == syftEvent.PresenterReady:
|
case event.Type == syftEvent.Exit:
|
||||||
// we need to close the screen now since signaling the the presenter is ready means that we
|
// we need to close the screen now since signaling the sbom is ready means that we
|
||||||
// are about to write bytes to stdout, so we should reset the terminal state first
|
// are about to write bytes to stdout, so we should reset the terminal state first
|
||||||
h.closeScreen(false)
|
h.closeScreen(false)
|
||||||
|
|
||||||
if err := handleCatalogerPresenterReady(event, h.reportOutput); err != nil {
|
if err := handleExit(event); err != nil {
|
||||||
log.Errorf("unable to show %s event: %+v", event.Type, err)
|
log.Errorf("unable to show %s event: %+v", event.Type, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,18 @@
|
|||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/anchore/syft/internal/log"
|
"github.com/anchore/syft/internal/log"
|
||||||
syftEvent "github.com/anchore/syft/syft/event"
|
syftEvent "github.com/anchore/syft/syft/event"
|
||||||
"github.com/wagoodman/go-partybus"
|
"github.com/wagoodman/go-partybus"
|
||||||
)
|
)
|
||||||
|
|
||||||
type loggerUI struct {
|
type loggerUI struct {
|
||||||
unsubscribe func() error
|
unsubscribe func() error
|
||||||
reportOutput io.Writer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLoggerUI writes all events to the common application logger and writes the final report to the given writer.
|
// NewLoggerUI writes all events to the common application logger and writes the final report to the given writer.
|
||||||
func NewLoggerUI(reportWriter io.Writer) UI {
|
func NewLoggerUI() UI {
|
||||||
return &loggerUI{
|
return &loggerUI{}
|
||||||
reportOutput: reportWriter,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *loggerUI) Setup(unsubscribe func() error) error {
|
func (l *loggerUI) Setup(unsubscribe func() error) error {
|
||||||
@@ -27,11 +22,11 @@ func (l *loggerUI) Setup(unsubscribe func() error) error {
|
|||||||
|
|
||||||
func (l loggerUI) Handle(event partybus.Event) error {
|
func (l loggerUI) Handle(event partybus.Event) error {
|
||||||
// ignore all events except for the final event
|
// ignore all events except for the final event
|
||||||
if event.Type != syftEvent.PresenterReady {
|
if event.Type != syftEvent.Exit {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := handleCatalogerPresenterReady(event, l.reportOutput); err != nil {
|
if err := handleExit(event); err != nil {
|
||||||
log.Warnf("unable to show catalog image finished event: %+v", err)
|
log.Warnf("unable to show catalog image finished event: %+v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
@@ -16,16 +15,16 @@ import (
|
|||||||
// is intended to be used and the UIs that follow are meant to be attempted only in a fallback posture when there
|
// 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
|
// are environmental problems (e.g. cannot write to the terminal). A writer is provided to capture the output of
|
||||||
// the final SBOM report.
|
// the final SBOM report.
|
||||||
func Select(verbose, quiet bool, reportWriter io.Writer) (uis []UI) {
|
func Select(verbose, quiet bool) (uis []UI) {
|
||||||
isStdoutATty := term.IsTerminal(int(os.Stdout.Fd()))
|
isStdoutATty := term.IsTerminal(int(os.Stdout.Fd()))
|
||||||
isStderrATty := term.IsTerminal(int(os.Stderr.Fd()))
|
isStderrATty := term.IsTerminal(int(os.Stderr.Fd()))
|
||||||
notATerminal := !isStderrATty && !isStdoutATty
|
notATerminal := !isStderrATty && !isStdoutATty
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case runtime.GOOS == "windows" || verbose || quiet || notATerminal || !isStderrATty:
|
case runtime.GOOS == "windows" || verbose || quiet || notATerminal || !isStderrATty:
|
||||||
uis = append(uis, NewLoggerUI(reportWriter))
|
uis = append(uis, NewLoggerUI())
|
||||||
default:
|
default:
|
||||||
uis = append(uis, NewEphemeralTerminalUI(reportWriter), NewLoggerUI(reportWriter))
|
uis = append(uis, NewEphemeralTerminalUI())
|
||||||
}
|
}
|
||||||
|
|
||||||
return uis
|
return uis
|
||||||
|
|||||||
@@ -3,15 +3,11 @@
|
|||||||
|
|
||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Select is responsible for determining the specific UI function given select user option, the current platform
|
// 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). The first UI in the returned slice of UIs
|
// 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
|
// 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
|
// are environmental problems (e.g. cannot write to the terminal). A writer is provided to capture the output of
|
||||||
// the final SBOM report.
|
// the final SBOM report.
|
||||||
func Select(verbose, quiet bool, reportWriter io.Writer) (uis []UI) {
|
func Select(verbose, quiet bool) (uis []UI) {
|
||||||
return append(uis, NewLoggerUI(reportWriter))
|
return append(uis, NewLoggerUI())
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -26,8 +26,8 @@ const (
|
|||||||
// FileIndexingStarted is a partybus event that occurs when the directory resolver begins indexing a filesystem
|
// FileIndexingStarted is a partybus event that occurs when the directory resolver begins indexing a filesystem
|
||||||
FileIndexingStarted partybus.EventType = "syft-file-indexing-started-event"
|
FileIndexingStarted partybus.EventType = "syft-file-indexing-started-event"
|
||||||
|
|
||||||
// PresenterReady is a partybus event that occurs when an analysis result is ready for final presentation
|
// Exit is a partybus event that occurs when an analysis result is ready for final presentation
|
||||||
PresenterReady partybus.EventType = "syft-presenter-ready-event"
|
Exit partybus.EventType = "syft-exit-event"
|
||||||
|
|
||||||
// ImportStarted is a partybus event that occurs when an SBOM upload process has begun
|
// ImportStarted is a partybus event that occurs when an SBOM upload process has begun
|
||||||
ImportStarted partybus.EventType = "syft-import-started-event"
|
ImportStarted partybus.EventType = "syft-import-started-event"
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ package parsers
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/anchore/go-presenter"
|
|
||||||
"github.com/anchore/syft/syft/event"
|
"github.com/anchore/syft/syft/event"
|
||||||
"github.com/anchore/syft/syft/file"
|
"github.com/anchore/syft/syft/file"
|
||||||
"github.com/anchore/syft/syft/pkg/cataloger"
|
"github.com/anchore/syft/syft/pkg/cataloger"
|
||||||
@@ -109,17 +108,17 @@ func ParseFileIndexingStarted(e partybus.Event) (string, progress.StagedProgress
|
|||||||
return path, prog, nil
|
return path, prog, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParsePresenterReady(e partybus.Event) (presenter.Presenter, error) {
|
func ParseExit(e partybus.Event) (func() error, error) {
|
||||||
if err := checkEventType(e.Type, event.PresenterReady); err != nil {
|
if err := checkEventType(e.Type, event.Exit); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
pres, ok := e.Value.(presenter.Presenter)
|
fn, ok := e.Value.(func() error)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, newPayloadErr(e.Type, "Value", e.Value)
|
return nil, newPayloadErr(e.Type, "Value", e.Value)
|
||||||
}
|
}
|
||||||
|
|
||||||
return pres, nil
|
return fn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseAppUpdateAvailable(e partybus.Event) (string, error) {
|
func ParseAppUpdateAvailable(e partybus.Event) (string, error) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
"github.com/bmatcuk/doublestar/v2"
|
"github.com/bmatcuk/doublestar/v4"
|
||||||
"github.com/hashicorp/go-multierror"
|
"github.com/hashicorp/go-multierror"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -50,10 +50,3 @@ func (f Format) Validate(reader io.Reader) error {
|
|||||||
|
|
||||||
return f.validator(reader)
|
return f.validator(reader)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f Format) Presenter(s sbom.SBOM) *Presenter {
|
|
||||||
if f.encoder == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return NewPresenter(f.encoder, s)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
package format
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/anchore/syft/syft/sbom"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Presenter struct {
|
|
||||||
sbom sbom.SBOM
|
|
||||||
encoder Encoder
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewPresenter(encoder Encoder, s sbom.SBOM) *Presenter {
|
|
||||||
return &Presenter{
|
|
||||||
sbom: s,
|
|
||||||
encoder: encoder,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pres *Presenter) Present(output io.Writer) error {
|
|
||||||
return pres.encoder(output, pres.sbom)
|
|
||||||
}
|
|
||||||
+7
-7
@@ -1,11 +1,11 @@
|
|||||||
/*
|
/*
|
||||||
A "one-stop-shop" for helper utilities for all major functionality provided by child packages of the syft library.
|
Package syft is a "one-stop-shop" for helper utilities for all major functionality provided by child packages of the syft library.
|
||||||
|
|
||||||
Here is what the main execution path for syft does:
|
Here is what the main execution path for syft does:
|
||||||
|
|
||||||
1. Parse a user image string to get a stereoscope image.Source object
|
1. Parse a user image string to get a stereoscope image.Source object
|
||||||
2. Invoke all catalogers to catalog the image, adding discovered packages to a single catalog object
|
2. Invoke all catalogers to catalog the image, adding discovered packages to a single catalog object
|
||||||
3. Invoke a single presenter to show the contents of the catalog
|
3. Invoke one or more encoders to output contents of the catalog
|
||||||
|
|
||||||
A Source object encapsulates the image object to be cataloged and the user options (catalog all layers vs. squashed layer),
|
A Source object encapsulates the image object to be cataloged and the user options (catalog all layers vs. squashed layer),
|
||||||
providing a way to inspect paths and file content within the image. The Source object, not the image object, is used
|
providing a way to inspect paths and file content within the image. The Source object, not the image object, is used
|
||||||
@@ -34,8 +34,8 @@ import (
|
|||||||
// CatalogPackages takes an inventory of packages from the given image from a particular perspective
|
// CatalogPackages takes an inventory of packages from the given image from a particular perspective
|
||||||
// (e.g. squashed source, all-layers source). Returns the discovered set of packages, the identified Linux
|
// (e.g. squashed source, all-layers source). Returns the discovered set of packages, the identified Linux
|
||||||
// distribution, and the source object used to wrap the data source.
|
// distribution, and the source object used to wrap the data source.
|
||||||
func CatalogPackages(src *source.Source, scope source.Scope) (*pkg.Catalog, []artifact.Relationship, *distro.Distro, error) {
|
func CatalogPackages(src *source.Source, cfg cataloger.Config) (*pkg.Catalog, []artifact.Relationship, *distro.Distro, error) {
|
||||||
resolver, err := src.FileResolver(scope)
|
resolver, err := src.FileResolver(cfg.Search.Scope)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, fmt.Errorf("unable to determine resolver while cataloging packages: %w", err)
|
return nil, nil, nil, fmt.Errorf("unable to determine resolver while cataloging packages: %w", err)
|
||||||
}
|
}
|
||||||
@@ -53,13 +53,13 @@ func CatalogPackages(src *source.Source, scope source.Scope) (*pkg.Catalog, []ar
|
|||||||
switch src.Metadata.Scheme {
|
switch src.Metadata.Scheme {
|
||||||
case source.ImageScheme:
|
case source.ImageScheme:
|
||||||
log.Info("cataloging image")
|
log.Info("cataloging image")
|
||||||
catalogers = cataloger.ImageCatalogers()
|
catalogers = cataloger.ImageCatalogers(cfg)
|
||||||
case source.FileScheme:
|
case source.FileScheme:
|
||||||
log.Info("cataloging file")
|
log.Info("cataloging file")
|
||||||
catalogers = cataloger.AllCatalogers()
|
catalogers = cataloger.AllCatalogers(cfg)
|
||||||
case source.DirectoryScheme:
|
case source.DirectoryScheme:
|
||||||
log.Info("cataloging directory")
|
log.Info("cataloging directory")
|
||||||
catalogers = cataloger.DirectoryCatalogers()
|
catalogers = cataloger.DirectoryCatalogers(cfg)
|
||||||
default:
|
default:
|
||||||
return nil, nil, nil, fmt.Errorf("unable to determine cataloger set from scheme=%+v", src.Metadata.Scheme)
|
return nil, nil, nil, fmt.Errorf("unable to determine cataloger set from scheme=%+v", src.Metadata.Scheme)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,9 +105,7 @@ func TestApkMetadata_FileOwner(t *testing.T) {
|
|||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(strings.Join(test.expected, ","), func(t *testing.T) {
|
t.Run(strings.Join(test.expected, ","), func(t *testing.T) {
|
||||||
var i interface{}
|
actual := test.metadata.OwnedFiles()
|
||||||
i = test.metadata
|
|
||||||
actual := i.(FileOwner).OwnedFiles()
|
|
||||||
for _, d := range deep.Equal(test.expected, actual) {
|
for _, d := range deep.Equal(test.expected, actual) {
|
||||||
t.Errorf("diff: %+v", d)
|
t.Errorf("diff: %+v", d)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-4
@@ -4,12 +4,10 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal"
|
||||||
|
"github.com/anchore/syft/internal/log"
|
||||||
"github.com/anchore/syft/syft/artifact"
|
"github.com/anchore/syft/syft/artifact"
|
||||||
"github.com/jinzhu/copier"
|
"github.com/jinzhu/copier"
|
||||||
|
|
||||||
"github.com/anchore/syft/internal"
|
|
||||||
|
|
||||||
"github.com/anchore/syft/internal/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Catalog represents a collection of Packages.
|
// Catalog represents a collection of Packages.
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ type Cataloger interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ImageCatalogers returns a slice of locally implemented catalogers that are fit for detecting installations of packages.
|
// ImageCatalogers returns a slice of locally implemented catalogers that are fit for detecting installations of packages.
|
||||||
func ImageCatalogers() []Cataloger {
|
func ImageCatalogers(cfg Config) []Cataloger {
|
||||||
return []Cataloger{
|
return []Cataloger{
|
||||||
ruby.NewGemSpecCataloger(),
|
ruby.NewGemSpecCataloger(),
|
||||||
python.NewPythonPackageCataloger(),
|
python.NewPythonPackageCataloger(),
|
||||||
@@ -40,14 +40,14 @@ func ImageCatalogers() []Cataloger {
|
|||||||
javascript.NewJavascriptPackageCataloger(),
|
javascript.NewJavascriptPackageCataloger(),
|
||||||
deb.NewDpkgdbCataloger(),
|
deb.NewDpkgdbCataloger(),
|
||||||
rpmdb.NewRpmdbCataloger(),
|
rpmdb.NewRpmdbCataloger(),
|
||||||
java.NewJavaCataloger(),
|
java.NewJavaCataloger(cfg.Java()),
|
||||||
apkdb.NewApkdbCataloger(),
|
apkdb.NewApkdbCataloger(),
|
||||||
golang.NewGoModuleBinaryCataloger(),
|
golang.NewGoModuleBinaryCataloger(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DirectoryCatalogers returns a slice of locally implemented catalogers that are fit for detecting packages from index files (and select installations)
|
// DirectoryCatalogers returns a slice of locally implemented catalogers that are fit for detecting packages from index files (and select installations)
|
||||||
func DirectoryCatalogers() []Cataloger {
|
func DirectoryCatalogers(cfg Config) []Cataloger {
|
||||||
return []Cataloger{
|
return []Cataloger{
|
||||||
ruby.NewGemFileLockCataloger(),
|
ruby.NewGemFileLockCataloger(),
|
||||||
python.NewPythonIndexCataloger(),
|
python.NewPythonIndexCataloger(),
|
||||||
@@ -56,7 +56,7 @@ func DirectoryCatalogers() []Cataloger {
|
|||||||
javascript.NewJavascriptLockCataloger(),
|
javascript.NewJavascriptLockCataloger(),
|
||||||
deb.NewDpkgdbCataloger(),
|
deb.NewDpkgdbCataloger(),
|
||||||
rpmdb.NewRpmdbCataloger(),
|
rpmdb.NewRpmdbCataloger(),
|
||||||
java.NewJavaCataloger(),
|
java.NewJavaCataloger(cfg.Java()),
|
||||||
apkdb.NewApkdbCataloger(),
|
apkdb.NewApkdbCataloger(),
|
||||||
golang.NewGoModuleBinaryCataloger(),
|
golang.NewGoModuleBinaryCataloger(),
|
||||||
golang.NewGoModFileCataloger(),
|
golang.NewGoModFileCataloger(),
|
||||||
@@ -65,7 +65,7 @@ func DirectoryCatalogers() []Cataloger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AllCatalogers returns all implemented catalogers
|
// AllCatalogers returns all implemented catalogers
|
||||||
func AllCatalogers() []Cataloger {
|
func AllCatalogers(cfg Config) []Cataloger {
|
||||||
return []Cataloger{
|
return []Cataloger{
|
||||||
ruby.NewGemFileLockCataloger(),
|
ruby.NewGemFileLockCataloger(),
|
||||||
ruby.NewGemSpecCataloger(),
|
ruby.NewGemSpecCataloger(),
|
||||||
@@ -75,7 +75,7 @@ func AllCatalogers() []Cataloger {
|
|||||||
javascript.NewJavascriptPackageCataloger(),
|
javascript.NewJavascriptPackageCataloger(),
|
||||||
deb.NewDpkgdbCataloger(),
|
deb.NewDpkgdbCataloger(),
|
||||||
rpmdb.NewRpmdbCataloger(),
|
rpmdb.NewRpmdbCataloger(),
|
||||||
java.NewJavaCataloger(),
|
java.NewJavaCataloger(cfg.Java()),
|
||||||
apkdb.NewApkdbCataloger(),
|
apkdb.NewApkdbCataloger(),
|
||||||
golang.NewGoModuleBinaryCataloger(),
|
golang.NewGoModuleBinaryCataloger(),
|
||||||
golang.NewGoModFileCataloger(),
|
golang.NewGoModFileCataloger(),
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ cpeLoop:
|
|||||||
}
|
}
|
||||||
|
|
||||||
func disallowNonParseableCPEs(cpe pkg.CPE, _ pkg.Package) bool {
|
func disallowNonParseableCPEs(cpe pkg.CPE, _ pkg.Package) bool {
|
||||||
v := cpe.BindToFmtString()
|
v := pkg.CPEString(cpe)
|
||||||
_, err := pkg.NewCPE(v)
|
_, err := pkg.NewCPE(v)
|
||||||
|
|
||||||
cannotParse := err != nil
|
cannotParse := err != nil
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ func newCPE(product, vendor, version, targetSW string) wfn.Attributes {
|
|||||||
cpe.Vendor = vendor
|
cpe.Vendor = vendor
|
||||||
cpe.Version = version
|
cpe.Version = version
|
||||||
cpe.TargetSW = targetSW
|
cpe.TargetSW = targetSW
|
||||||
|
|
||||||
return cpe
|
return cpe
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,7 +28,6 @@ func newCPE(product, vendor, version, targetSW string) wfn.Attributes {
|
|||||||
func Generate(p pkg.Package) []pkg.CPE {
|
func Generate(p pkg.Package) []pkg.CPE {
|
||||||
vendors := candidateVendors(p)
|
vendors := candidateVendors(p)
|
||||||
products := candidateProducts(p)
|
products := candidateProducts(p)
|
||||||
|
|
||||||
if len(products) == 0 {
|
if len(products) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -44,7 +42,6 @@ func Generate(p pkg.Package) []pkg.CPE {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
keys.Add(key)
|
keys.Add(key)
|
||||||
|
|
||||||
// add a new entry...
|
// add a new entry...
|
||||||
cpes = append(cpes, newCPE(product, vendor, p.Version, wfn.Any))
|
cpes = append(cpes, newCPE(product, vendor, p.Version, wfn.Any))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -327,6 +327,38 @@ func TestGeneratePackageCPEs(t *testing.T) {
|
|||||||
"cpe:2.3:a:some_vendor:name:3.2:*:*:*:*:*:*:*",
|
"cpe:2.3:a:some_vendor:name:3.2:*:*:*:*:*:*:*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "rpm with epoch",
|
||||||
|
p: pkg.Package{
|
||||||
|
Name: "name",
|
||||||
|
Version: "1:3.2",
|
||||||
|
FoundBy: "some-analyzer",
|
||||||
|
Type: pkg.RpmPkg,
|
||||||
|
MetadataType: pkg.RpmdbMetadataType,
|
||||||
|
Metadata: pkg.RpmdbMetadata{
|
||||||
|
Vendor: "some-vendor",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: []string{
|
||||||
|
"cpe:2.3:a:name:name:1\\:3.2:*:*:*:*:*:*:*",
|
||||||
|
"cpe:2.3:a:some-vendor:name:1\\:3.2:*:*:*:*:*:*:*",
|
||||||
|
"cpe:2.3:a:some_vendor:name:1\\:3.2:*:*:*:*:*:*:*",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "deb with epoch",
|
||||||
|
p: pkg.Package{
|
||||||
|
Name: "name",
|
||||||
|
Version: "1:3.2",
|
||||||
|
FoundBy: "some-analyzer",
|
||||||
|
Type: pkg.DebPkg,
|
||||||
|
MetadataType: pkg.DpkgMetadataType,
|
||||||
|
Metadata: pkg.DpkgMetadata{},
|
||||||
|
},
|
||||||
|
expected: []string{
|
||||||
|
"cpe:2.3:a:name:name:1\\:3.2:*:*:*:*:*:*:*",
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "cloudbees jenkins package identified via groupId",
|
name: "cloudbees jenkins package identified via groupId",
|
||||||
p: pkg.Package{
|
p: pkg.Package{
|
||||||
@@ -522,7 +554,7 @@ func TestGeneratePackageCPEs(t *testing.T) {
|
|||||||
expectedCpeSet := set.NewStringSet(test.expected...)
|
expectedCpeSet := set.NewStringSet(test.expected...)
|
||||||
actualCpeSet := set.NewStringSet()
|
actualCpeSet := set.NewStringSet()
|
||||||
for _, a := range actual {
|
for _, a := range actual {
|
||||||
actualCpeSet.Add(a.BindToFmtString())
|
actualCpeSet.Add(pkg.CPEString(a))
|
||||||
}
|
}
|
||||||
|
|
||||||
extra := strset.Difference(actualCpeSet, expectedCpeSet).List()
|
extra := strset.Difference(actualCpeSet, expectedCpeSet).List()
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ func (c BySpecificity) Less(i, j int) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// if score and length are equal then text sort
|
// if score and length are equal then text sort
|
||||||
|
// note that we are not using CPEString from the syft pkg
|
||||||
|
// as we are not encoding/decoding this CPE string so we don't
|
||||||
|
// need the proper quoted version of the CPE.
|
||||||
return c[i].BindToFmtString() < c[j].BindToFmtString()
|
return c[i].BindToFmtString() < c[j].BindToFmtString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package cataloger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/anchore/syft/syft/pkg/cataloger/java"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Search SearchConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultConfig() Config {
|
||||||
|
return Config{
|
||||||
|
Search: DefaultSearchConfig(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Config) Java() java.Config {
|
||||||
|
return java.Config{
|
||||||
|
SearchUnindexedArchives: c.Search.IncludeUnindexedArchives,
|
||||||
|
SearchIndexedArchives: c.Search.IncludeIndexedArchives,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -113,7 +113,7 @@ func (a archiveFilename) extension() string {
|
|||||||
|
|
||||||
func (a archiveFilename) pkgType() pkg.Type {
|
func (a archiveFilename) pkgType() pkg.Type {
|
||||||
switch strings.ToLower(a.extension()) {
|
switch strings.ToLower(a.extension()) {
|
||||||
case "jar", "war", "ear":
|
case "jar", "war", "ear", "lpkg", "par":
|
||||||
return pkg.JavaPkg
|
return pkg.JavaPkg
|
||||||
case "jpi", "hpi":
|
case "jpi", "hpi":
|
||||||
return pkg.JenkinsPluginPkg
|
return pkg.JenkinsPluginPkg
|
||||||
|
|||||||
@@ -36,6 +36,13 @@ func TestExtractInfoFromJavaArchiveFilename(t *testing.T) {
|
|||||||
name: "pkg-extra-field-maven",
|
name: "pkg-extra-field-maven",
|
||||||
ty: pkg.JavaPkg,
|
ty: pkg.JavaPkg,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
filename: "liferay-package.lpkg",
|
||||||
|
version: "",
|
||||||
|
extension: "lpkg",
|
||||||
|
name: "liferay-package",
|
||||||
|
ty: pkg.JavaPkg,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
filename: "pkg-extra-field-maven-4.3.2-rc1.ear",
|
filename: "pkg-extra-field-maven-4.3.2-rc1.ear",
|
||||||
version: "4.3.2-rc1",
|
version: "4.3.2-rc1",
|
||||||
@@ -43,6 +50,13 @@ func TestExtractInfoFromJavaArchiveFilename(t *testing.T) {
|
|||||||
name: "pkg-extra-field-maven",
|
name: "pkg-extra-field-maven",
|
||||||
ty: pkg.JavaPkg,
|
ty: pkg.JavaPkg,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
filename: "pkg-extra-field-maven-4.3.2-rc1.par",
|
||||||
|
version: "4.3.2-rc1",
|
||||||
|
extension: "par",
|
||||||
|
name: "pkg-extra-field-maven",
|
||||||
|
ty: pkg.JavaPkg,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
filename: "/some/path/pkg-extra-field-maven-4.3.2-rc1.jpi",
|
filename: "/some/path/pkg-extra-field-maven-4.3.2-rc1.jpi",
|
||||||
version: "4.3.2-rc1",
|
version: "4.3.2-rc1",
|
||||||
|
|||||||
@@ -6,9 +6,8 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/anchore/syft/internal/log"
|
|
||||||
|
|
||||||
"github.com/anchore/syft/internal/file"
|
"github.com/anchore/syft/internal/file"
|
||||||
|
"github.com/anchore/syft/internal/log"
|
||||||
"github.com/anchore/syft/syft/artifact"
|
"github.com/anchore/syft/syft/artifact"
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
"github.com/anchore/syft/syft/pkg/cataloger/common"
|
"github.com/anchore/syft/syft/pkg/cataloger/common"
|
||||||
@@ -21,8 +20,17 @@ var archiveFormatGlobs = []string{
|
|||||||
"**/*.jar",
|
"**/*.jar",
|
||||||
"**/*.war",
|
"**/*.war",
|
||||||
"**/*.ear",
|
"**/*.ear",
|
||||||
|
"**/*.par",
|
||||||
"**/*.jpi",
|
"**/*.jpi",
|
||||||
"**/*.hpi",
|
"**/*.hpi",
|
||||||
|
"**/*.lpkg", // Zip-compressed package used to deploy applications
|
||||||
|
// (aka plugins) to Liferay Portal server. Those files contains .JAR(s) and a .PROPERTIES file, the latter
|
||||||
|
// has information about the application and installation requirements.
|
||||||
|
// NOTE(jonasagx): If you would like to test it with lpkg file,
|
||||||
|
// use: https://web.liferay.com/marketplace/-/mp/download/25019275/7403
|
||||||
|
// LifeRay makes it pretty cumbersome to make a such plugins; their docs are
|
||||||
|
// out of date, and they charge for their IDE. If you find an example
|
||||||
|
// project that we can build in CI feel free to include it
|
||||||
}
|
}
|
||||||
|
|
||||||
type archiveParser struct {
|
type archiveParser struct {
|
||||||
@@ -56,7 +64,11 @@ func uniquePkgKey(p *pkg.Package) string {
|
|||||||
// newJavaArchiveParser returns a new java archive parser object for the given archive. Can be configured to discover
|
// newJavaArchiveParser returns a new java archive parser object for the given archive. Can be configured to discover
|
||||||
// and parse nested archives or ignore them.
|
// and parse nested archives or ignore them.
|
||||||
func newJavaArchiveParser(virtualPath string, reader io.Reader, detectNested bool) (*archiveParser, func(), error) {
|
func newJavaArchiveParser(virtualPath string, reader io.Reader, detectNested bool) (*archiveParser, func(), error) {
|
||||||
contentPath, archivePath, cleanupFn, err := saveArchiveToTmp(reader)
|
// fetch the last element of the virtual path
|
||||||
|
virtualElements := strings.Split(virtualPath, ":")
|
||||||
|
currentFilepath := virtualElements[len(virtualElements)-1]
|
||||||
|
|
||||||
|
contentPath, archivePath, cleanupFn, err := saveArchiveToTmp(currentFilepath, reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, cleanupFn, fmt.Errorf("unable to process java archive: %w", err)
|
return nil, cleanupFn, fmt.Errorf("unable to process java archive: %w", err)
|
||||||
}
|
}
|
||||||
@@ -66,10 +78,6 @@ func newJavaArchiveParser(virtualPath string, reader io.Reader, detectNested boo
|
|||||||
return nil, cleanupFn, fmt.Errorf("unable to read files from java archive: %w", err)
|
return nil, cleanupFn, fmt.Errorf("unable to read files from java archive: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch the last element of the virtual path
|
|
||||||
virtualElements := strings.Split(virtualPath, ":")
|
|
||||||
currentFilepath := virtualElements[len(virtualElements)-1]
|
|
||||||
|
|
||||||
return &archiveParser{
|
return &archiveParser{
|
||||||
fileManifest: fileManifest,
|
fileManifest: fileManifest,
|
||||||
virtualPath: virtualPath,
|
virtualPath: virtualPath,
|
||||||
@@ -190,34 +198,33 @@ func (j *archiveParser) discoverPkgsFromAllMavenFiles(parentPkg *pkg.Package) ([
|
|||||||
return pkgs, nil
|
return pkgs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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, []artifact.Relationship, error) {
|
func (j *archiveParser) discoverPkgsFromNestedArchives(parentPkg *pkg.Package) ([]*pkg.Package, []artifact.Relationship, error) {
|
||||||
var pkgs []*pkg.Package
|
// we know that all java archives are zip formatted files, so we can use the shared zip helper
|
||||||
var relationships []artifact.Relationship
|
return discoverPkgsFromZip(j.virtualPath, j.archivePath, j.contentPath, j.fileManifest, parentPkg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// discoverPkgsFromZip finds Java archives within Java archives, returning all listed Java packages found and
|
||||||
|
// associating each discovered package to the given parent package.
|
||||||
|
func discoverPkgsFromZip(virtualPath, archivePath, contentPath string, fileManifest file.ZipFileManifest, parentPkg *pkg.Package) ([]*pkg.Package, []artifact.Relationship, error) {
|
||||||
// search and parse pom.properties files & fetch the contents
|
// search and parse pom.properties files & fetch the contents
|
||||||
openers, err := file.ExtractFromZipToUniqueTempFile(j.archivePath, j.contentPath, j.fileManifest.GlobMatch(archiveFormatGlobs...)...)
|
openers, err := file.ExtractFromZipToUniqueTempFile(archivePath, contentPath, fileManifest.GlobMatch(archiveFormatGlobs...)...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("unable to extract files from zip: %w", err)
|
return nil, nil, fmt.Errorf("unable to extract files from zip: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// discover nested artifacts
|
return discoverPkgsFromOpeners(virtualPath, openers, parentPkg)
|
||||||
for archivePath, archiveOpener := range openers {
|
}
|
||||||
archiveReadCloser, err := archiveOpener.Open()
|
|
||||||
|
// discoverPkgsFromOpeners finds Java archives within the given files and associates them with the given parent package.
|
||||||
|
func discoverPkgsFromOpeners(virtualPath string, openers map[string]file.Opener, parentPkg *pkg.Package) ([]*pkg.Package, []artifact.Relationship, error) {
|
||||||
|
var pkgs []*pkg.Package
|
||||||
|
var relationships []artifact.Relationship
|
||||||
|
|
||||||
|
for pathWithinArchive, archiveOpener := range openers {
|
||||||
|
nestedPkgs, nestedRelationships, err := discoverPkgsFromOpener(virtualPath, pathWithinArchive, archiveOpener)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("unable to open archived file from tempdir: %w", err)
|
log.Warnf("unable to discover java packages from opener (%s): %+v", virtualPath, err)
|
||||||
}
|
continue
|
||||||
nestedPath := fmt.Sprintf("%s:%s", j.virtualPath, archivePath)
|
|
||||||
nestedPkgs, nestedRelationships, err := parseJavaArchive(nestedPath, archiveReadCloser)
|
|
||||||
if err != nil {
|
|
||||||
if closeErr := archiveReadCloser.Close(); closeErr != nil {
|
|
||||||
log.Warnf("unable to close archived file from tempdir: %+v", closeErr)
|
|
||||||
}
|
|
||||||
return nil, nil, fmt.Errorf("unable to process nested java archive (%s): %w", archivePath, err)
|
|
||||||
}
|
|
||||||
if err = archiveReadCloser.Close(); err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("unable to close archived file from tempdir: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// attach the parent package to all discovered packages that are not already associated with a java archive
|
// attach the parent package to all discovered packages that are not already associated with a java archive
|
||||||
@@ -237,6 +244,27 @@ func (j *archiveParser) discoverPkgsFromNestedArchives(parentPkg *pkg.Package) (
|
|||||||
return pkgs, relationships, nil
|
return pkgs, relationships, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// discoverPkgsFromOpener finds Java archives within the given file.
|
||||||
|
func discoverPkgsFromOpener(virtualPath, pathWithinArchive string, archiveOpener file.Opener) ([]*pkg.Package, []artifact.Relationship, error) {
|
||||||
|
archiveReadCloser, err := archiveOpener.Open()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("unable to open archived file from tempdir: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if closeErr := archiveReadCloser.Close(); closeErr != nil {
|
||||||
|
log.Warnf("unable to close archived file from tempdir: %+v", closeErr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
nestedPath := fmt.Sprintf("%s:%s", virtualPath, pathWithinArchive)
|
||||||
|
nestedPkgs, nestedRelationships, err := parseJavaArchive(nestedPath, archiveReadCloser)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("unable to process nested java archive (%s): %w", pathWithinArchive, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nestedPkgs, nestedRelationships, nil
|
||||||
|
}
|
||||||
|
|
||||||
func pomPropertiesByParentPath(archivePath string, extractPaths []string, virtualPath string) (map[string]pkg.PomProperties, error) {
|
func pomPropertiesByParentPath(archivePath string, extractPaths []string, virtualPath string) (map[string]pkg.PomProperties, error) {
|
||||||
contentsOfMavenPropertiesFiles, err := file.ContentsFromZip(archivePath, extractPaths...)
|
contentsOfMavenPropertiesFiles, err := file.ContentsFromZip(archivePath, extractPaths...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -6,18 +6,17 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
|
|
||||||
"github.com/anchore/syft/internal"
|
"github.com/anchore/syft/internal"
|
||||||
|
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
"github.com/go-test/deep"
|
"github.com/go-test/deep"
|
||||||
"github.com/gookit/color"
|
"github.com/gookit/color"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func generateJavaBuildFixture(t *testing.T, fixturePath string) {
|
func generateJavaBuildFixture(t *testing.T, fixturePath string) {
|
||||||
@@ -227,7 +226,7 @@ func TestParseJar(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.fixture, func(t *testing.T) {
|
t.Run(path.Base(test.fixture), func(t *testing.T) {
|
||||||
|
|
||||||
generateJavaBuildFixture(t, test.fixture)
|
generateJavaBuildFixture(t, test.fixture)
|
||||||
|
|
||||||
@@ -283,9 +282,7 @@ func TestParseJar(t *testing.T) {
|
|||||||
// ignore select fields (only works for the main section)
|
// ignore select fields (only works for the main section)
|
||||||
for _, field := range test.ignoreExtras {
|
for _, field := range test.ignoreExtras {
|
||||||
if metadata.Manifest != nil && metadata.Manifest.Main != nil {
|
if metadata.Manifest != nil && metadata.Manifest.Main != nil {
|
||||||
if _, ok := metadata.Manifest.Main[field]; ok {
|
delete(metadata.Manifest.Main, field)
|
||||||
delete(metadata.Manifest.Main, field)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Package java provides a concrete Cataloger implementation for Java archives (jar, war, ear, jpi, hpi formats).
|
Package java provides a concrete Cataloger implementation for Java archives (jar, war, ear, par, jpi, hpi formats).
|
||||||
*/
|
*/
|
||||||
package java
|
package java
|
||||||
|
|
||||||
@@ -8,11 +8,27 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// NewJavaCataloger returns a new Java archive cataloger object.
|
// NewJavaCataloger returns a new Java archive cataloger object.
|
||||||
func NewJavaCataloger() *common.GenericCataloger {
|
func NewJavaCataloger(cfg Config) *common.GenericCataloger {
|
||||||
globParsers := make(map[string]common.ParserFn)
|
globParsers := make(map[string]common.ParserFn)
|
||||||
|
|
||||||
|
// java archive formats
|
||||||
for _, pattern := range archiveFormatGlobs {
|
for _, pattern := range archiveFormatGlobs {
|
||||||
globParsers[pattern] = parseJavaArchive
|
globParsers[pattern] = parseJavaArchive
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cfg.SearchIndexedArchives {
|
||||||
|
// java archives wrapped within zip files
|
||||||
|
for _, pattern := range genericZipGlobs {
|
||||||
|
globParsers[pattern] = parseZipWrappedJavaArchive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.SearchUnindexedArchives {
|
||||||
|
// java archives wrapped within tar files
|
||||||
|
for _, pattern := range genericTarGlobs {
|
||||||
|
globParsers[pattern] = parseTarWrappedJavaArchive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return common.NewGenericCataloger(nil, globParsers, "java-cataloger")
|
return common.NewGenericCataloger(nil, globParsers, "java-cataloger")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package java
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
SearchUnindexedArchives bool
|
||||||
|
SearchIndexedArchives bool
|
||||||
|
}
|
||||||
@@ -5,25 +5,27 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/anchore/syft/internal/log"
|
"github.com/anchore/syft/internal/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func saveArchiveToTmp(reader io.Reader) (string, string, func(), error) {
|
func saveArchiveToTmp(archiveVirtualPath string, reader io.Reader) (string, string, func(), error) {
|
||||||
tempDir, err := ioutil.TempDir("", "syft-jar-contents-")
|
name := path.Base(archiveVirtualPath)
|
||||||
|
tempDir, err := ioutil.TempDir("", "syft-archive-contents-")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", func() {}, fmt.Errorf("unable to create tempdir for jar processing: %w", err)
|
return "", "", func() {}, fmt.Errorf("unable to create tempdir for archive processing: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanupFn := func() {
|
cleanupFn := func() {
|
||||||
err = os.RemoveAll(tempDir)
|
err = os.RemoveAll(tempDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("unable to cleanup jar tempdir: %+v", err)
|
log.Errorf("unable to cleanup archive tempdir: %+v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
archivePath := filepath.Join(tempDir, "archive")
|
archivePath := filepath.Join(tempDir, "archive-"+name)
|
||||||
contentDir := filepath.Join(tempDir, "contents")
|
contentDir := filepath.Join(tempDir, "contents")
|
||||||
|
|
||||||
err = os.Mkdir(contentDir, 0755)
|
err = os.Mkdir(contentDir, 0755)
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package java
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal/file"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/artifact"
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/anchore/syft/syft/pkg/cataloger/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
// integrity check
|
||||||
|
var _ common.ParserFn = parseTarWrappedJavaArchive
|
||||||
|
|
||||||
|
var genericTarGlobs = []string{
|
||||||
|
"**/*.tar",
|
||||||
|
// gzipped tar
|
||||||
|
"**/*.tar.gz",
|
||||||
|
"**/*.tgz",
|
||||||
|
// bzip2
|
||||||
|
"**/*.tar.bz",
|
||||||
|
"**/*.tar.bz2",
|
||||||
|
"**/*.tbz",
|
||||||
|
"**/*.tbz2",
|
||||||
|
// brotli
|
||||||
|
"**/*.tar.br",
|
||||||
|
"**/*.tbr",
|
||||||
|
// lz4
|
||||||
|
"**/*.tar.lz4",
|
||||||
|
"**/*.tlz4",
|
||||||
|
// sz
|
||||||
|
"**/*.tar.sz",
|
||||||
|
"**/*.tsz",
|
||||||
|
// xz
|
||||||
|
"**/*.tar.xz",
|
||||||
|
"**/*.txz",
|
||||||
|
// zst
|
||||||
|
"**/*.tar.zst",
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: when the generic archive cataloger is implemented, this should be removed (https://github.com/anchore/syft/issues/246)
|
||||||
|
|
||||||
|
// parseTarWrappedJavaArchive is a parser function for java archive contents contained within arbitrary tar files.
|
||||||
|
// note: for compressed tars this is an extremely expensive operation and can lead to performance degradation. This is
|
||||||
|
// due to the fact that there is no central directory header (say as in zip), which means that in order to get
|
||||||
|
// a file listing within the archive you must decompress the entire archive and seek through all of the entries.
|
||||||
|
func parseTarWrappedJavaArchive(virtualPath string, reader io.Reader) ([]*pkg.Package, []artifact.Relationship, error) {
|
||||||
|
contentPath, archivePath, cleanupFn, err := saveArchiveToTmp(virtualPath, reader)
|
||||||
|
// note: even on error, we should always run cleanup functions
|
||||||
|
defer cleanupFn()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// look for java archives within the tar archive
|
||||||
|
return discoverPkgsFromTar(virtualPath, archivePath, contentPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func discoverPkgsFromTar(virtualPath, archivePath, contentPath string) ([]*pkg.Package, []artifact.Relationship, error) {
|
||||||
|
openers, err := file.ExtractGlobsFromTarToUniqueTempFile(archivePath, contentPath, archiveFormatGlobs...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("unable to extract files from tar: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return discoverPkgsFromOpeners(virtualPath, openers, nil)
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package java
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_parseTarWrappedJavaArchive(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
fixture string
|
||||||
|
expected []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
fixture: "test-fixtures/java-builds/packages/example-java-app-maven-0.1.0.tar",
|
||||||
|
expected: []string{
|
||||||
|
"example-java-app-maven",
|
||||||
|
"joda-time",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fixture: "test-fixtures/java-builds/packages/example-java-app-maven-0.1.0.tar.gz",
|
||||||
|
expected: []string{
|
||||||
|
"example-java-app-maven",
|
||||||
|
"joda-time",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(path.Base(test.fixture), func(t *testing.T) {
|
||||||
|
generateJavaBuildFixture(t, test.fixture)
|
||||||
|
|
||||||
|
fixture, err := os.Open(test.fixture)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to open fixture: %+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
actualPkgs, _, err := parseTarWrappedJavaArchive(test.fixture, fixture)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var actualNames []string
|
||||||
|
for _, p := range actualPkgs {
|
||||||
|
actualNames = append(actualNames, p.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.ElementsMatch(t, test.expected, actualNames)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
/packages/sb
|
/packages/*
|
||||||
*.fingerprint
|
*.fingerprint
|
||||||
# maven when running in a volume may spit out directories like this
|
# maven when running in a volume may spit out directories like this
|
||||||
**/\?/
|
**/\?/
|
||||||
|
|||||||
@@ -4,14 +4,29 @@ ifndef PKGSDIR
|
|||||||
$(error PKGSDIR is not set)
|
$(error PKGSDIR is not set)
|
||||||
endif
|
endif
|
||||||
|
|
||||||
all: $(PKGSDIR)/example-java-app-maven-0.1.0.jar $(PKGSDIR)/example-java-app-gradle-0.1.0.jar $(PKGSDIR)/example-jenkins-plugin.hpi $(PKGSDIR)/spring-boot-0.0.1-SNAPSHOT.jar
|
all: jars archives
|
||||||
|
|
||||||
clean: clean-examples
|
clean: clean-examples
|
||||||
rm -f $(PKGSDIR)/*
|
rm -f $(PKGSDIR)/*
|
||||||
|
|
||||||
clean-examples: clean-gradle clean-maven clean-jenkins clean-nestedjar
|
clean-examples: clean-gradle clean-maven clean-jenkins clean-nestedjar
|
||||||
|
|
||||||
.PHONY: maven gradle clean clean-gradle clean-maven clean-jenkins clean-examples clean-nestedjar
|
.PHONY: maven gradle clean clean-gradle clean-maven clean-jenkins clean-examples clean-nestedjar jars archives
|
||||||
|
|
||||||
|
jars: $(PKGSDIR)/example-java-app-maven-0.1.0.jar $(PKGSDIR)/example-java-app-gradle-0.1.0.jar $(PKGSDIR)/example-jenkins-plugin.hpi $(PKGSDIR)/spring-boot-0.0.1-SNAPSHOT.jar
|
||||||
|
|
||||||
|
archives: $(PKGSDIR)/example-java-app-maven-0.1.0.zip $(PKGSDIR)/example-java-app-maven-0.1.0.tar $(PKGSDIR)/example-java-app-maven-0.1.0.tar.gz
|
||||||
|
|
||||||
|
# jars within archives...
|
||||||
|
|
||||||
|
$(PKGSDIR)/example-java-app-maven-0.1.0.zip: $(PKGSDIR)/example-java-app-maven-0.1.0.jar
|
||||||
|
zip $(PKGSDIR)/example-java-app-maven-0.1.0.zip $(PKGSDIR)/example-java-app-maven-0.1.0.jar
|
||||||
|
|
||||||
|
$(PKGSDIR)/example-java-app-maven-0.1.0.tar: $(PKGSDIR)/example-java-app-maven-0.1.0.jar
|
||||||
|
tar -cvf $(PKGSDIR)/example-java-app-maven-0.1.0.tar $(PKGSDIR)/example-java-app-maven-0.1.0.jar
|
||||||
|
|
||||||
|
$(PKGSDIR)/example-java-app-maven-0.1.0.tar.gz: $(PKGSDIR)/example-java-app-maven-0.1.0.jar
|
||||||
|
tar -czvf $(PKGSDIR)/example-java-app-maven-0.1.0.tar.gz $(PKGSDIR)/example-java-app-maven-0.1.0.jar
|
||||||
|
|
||||||
# Nested jar...
|
# Nested jar...
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package java
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal/file"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/artifact"
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/anchore/syft/syft/pkg/cataloger/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
// integrity check
|
||||||
|
var _ common.ParserFn = parseZipWrappedJavaArchive
|
||||||
|
|
||||||
|
var genericZipGlobs = []string{
|
||||||
|
"**/*.zip",
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: when the generic archive cataloger is implemented, this should be removed (https://github.com/anchore/syft/issues/246)
|
||||||
|
|
||||||
|
// parseZipWrappedJavaArchive is a parser function for java archive contents contained within arbitrary zip files.
|
||||||
|
func parseZipWrappedJavaArchive(virtualPath string, reader io.Reader) ([]*pkg.Package, []artifact.Relationship, error) {
|
||||||
|
contentPath, archivePath, cleanupFn, err := saveArchiveToTmp(virtualPath, reader)
|
||||||
|
// note: even on error, we should always run cleanup functions
|
||||||
|
defer cleanupFn()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// we use our zip helper functions instead of that from the archiver package or the standard lib. Why? These helper
|
||||||
|
// functions support zips with shell scripts prepended to the file. Specifically, the helpers use the central
|
||||||
|
// header at the end of the file to determine where the beginning of the zip payload is (unlike the standard lib
|
||||||
|
// or archiver).
|
||||||
|
fileManifest, err := file.NewZipFileManifest(archivePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("unable to read files from java archive: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// look for java archives within the zip archive
|
||||||
|
return discoverPkgsFromZip(virtualPath, archivePath, contentPath, fileManifest, nil)
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package java
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_parseZipWrappedJavaArchive(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
fixture string
|
||||||
|
expected []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
fixture: "test-fixtures/java-builds/packages/example-java-app-maven-0.1.0.zip",
|
||||||
|
expected: []string{
|
||||||
|
"example-java-app-maven",
|
||||||
|
"joda-time",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(path.Base(test.fixture), func(t *testing.T) {
|
||||||
|
generateJavaBuildFixture(t, test.fixture)
|
||||||
|
|
||||||
|
fixture, err := os.Open(test.fixture)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to open fixture: %+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
actualPkgs, _, err := parseZipWrappedJavaArchive(test.fixture, fixture)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var actualNames []string
|
||||||
|
for _, p := range actualPkgs {
|
||||||
|
actualNames = append(actualNames, p.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.ElementsMatch(t, test.expected, actualNames)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -162,7 +162,7 @@ func (p PackageJSON) licensesFromJSON() ([]string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// parsePackageJSON parses a package.json and returns the discovered JavaScript packages.
|
// parsePackageJSON parses a package.json and returns the discovered JavaScript packages.
|
||||||
func parsePackageJSON(_ string, reader io.Reader) ([]*pkg.Package, []artifact.Relationship, error) {
|
func parsePackageJSON(path string, reader io.Reader) ([]*pkg.Package, []artifact.Relationship, error) {
|
||||||
var packages []*pkg.Package
|
var packages []*pkg.Package
|
||||||
dec := json.NewDecoder(reader)
|
dec := json.NewDecoder(reader)
|
||||||
|
|
||||||
@@ -175,7 +175,7 @@ func parsePackageJSON(_ string, reader io.Reader) ([]*pkg.Package, []artifact.Re
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !p.hasNameAndVersionValues() {
|
if !p.hasNameAndVersionValues() {
|
||||||
log.Debug("encountered package.json file without a name and/or version field, ignoring this file")
|
log.Debugf("encountered package.json file without a name and/or version field, ignoring (path=%q)", path)
|
||||||
return nil, nil, nil
|
return nil, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package cataloger
|
||||||
|
|
||||||
|
import "github.com/anchore/syft/syft/source"
|
||||||
|
|
||||||
|
type SearchConfig struct {
|
||||||
|
IncludeIndexedArchives bool
|
||||||
|
IncludeUnindexedArchives bool
|
||||||
|
Scope source.Scope
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultSearchConfig() SearchConfig {
|
||||||
|
return SearchConfig{
|
||||||
|
IncludeIndexedArchives: true,
|
||||||
|
IncludeUnindexedArchives: false,
|
||||||
|
Scope: source.SquashedScope,
|
||||||
|
}
|
||||||
|
}
|
||||||
+73
-1
@@ -2,6 +2,7 @@ package pkg
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/facebookincubator/nvdtools/wfn"
|
"github.com/facebookincubator/nvdtools/wfn"
|
||||||
@@ -9,7 +10,22 @@ import (
|
|||||||
|
|
||||||
type CPE = wfn.Attributes
|
type CPE = wfn.Attributes
|
||||||
|
|
||||||
|
// This regex string is taken from
|
||||||
|
// https://csrc.nist.gov/schema/cpe/2.3/cpe-naming_2.3.xsd which has the official cpe spec
|
||||||
|
// This first part matches CPE urls and the second part matches binding strings
|
||||||
|
const cpeRegexString = ((`^([c][pP][eE]:/[AHOaho]?(:[A-Za-z0-9\._\-~%]*){0,6})`) +
|
||||||
|
// Or match the CPE binding string
|
||||||
|
// Note that we had to replace '`' with '\x60' to escape the backticks
|
||||||
|
`|(cpe:2\.3:[aho\*\-](:(((\?*|\*?)([a-zA-Z0-9\-\._]|(\\[\\\*\?!"#$$%&'\(\)\+,/:;<=>@\[\]\^\x60\{\|}~]))+(\?*|\*?))|[\*\-])){5}(:(([a-zA-Z]{2,3}(-([a-zA-Z]{2}|[0-9]{3}))?)|[\*\-]))(:(((\?*|\*?)([a-zA-Z0-9\-\._]|(\\[\\\*\?!"#$$%&'\(\)\+,/:;<=>@\[\]\^\x60\{\|}~]))+(\?*|\*?))|[\*\-])){4})$`)
|
||||||
|
|
||||||
|
var cpeRegex = regexp.MustCompile(cpeRegexString)
|
||||||
|
|
||||||
func NewCPE(cpeStr string) (CPE, error) {
|
func NewCPE(cpeStr string) (CPE, error) {
|
||||||
|
// We should filter out all CPEs that do not match the official CPE regex
|
||||||
|
// The facebook nvdtools parser can sometimes incorrectly parse invalid CPE strings
|
||||||
|
if !cpeRegex.MatchString(cpeStr) {
|
||||||
|
return CPE{}, fmt.Errorf("failed to parse CPE=%q as it doesn't match the regex=%s", cpeStr, cpeRegexString)
|
||||||
|
}
|
||||||
value, err := wfn.Parse(cpeStr)
|
value, err := wfn.Parse(cpeStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return CPE{}, fmt.Errorf("failed to parse CPE=%q: %w", cpeStr, err)
|
return CPE{}, fmt.Errorf("failed to parse CPE=%q: %w", cpeStr, err)
|
||||||
@@ -48,5 +64,61 @@ func normalizeCpeField(field string) string {
|
|||||||
if field == "*" {
|
if field == "*" {
|
||||||
return wfn.Any
|
return wfn.Any
|
||||||
}
|
}
|
||||||
return strings.ReplaceAll(wfn.StripSlashes(field), `\/`, "/")
|
return stripSlashes(field)
|
||||||
|
}
|
||||||
|
|
||||||
|
// stripSlashes is a reverse of the sanitize function below.
|
||||||
|
// It correctly removes slashes that are followed by allowed puncts.
|
||||||
|
// This is to allow for a correct round trip parsing of cpes with quoted characters.
|
||||||
|
func stripSlashes(s string) string {
|
||||||
|
const allowedPunct = "-!\"#$%&'()+,./:;<=>@[]^`{|}!~"
|
||||||
|
sb := strings.Builder{}
|
||||||
|
for i, c := range s {
|
||||||
|
if c == '\\' && i+1 < len(s) && strings.ContainsRune(allowedPunct, rune(s[i+1])) {
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
sb.WriteRune(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func CPEString(c CPE) string {
|
||||||
|
output := CPE{}
|
||||||
|
output.Vendor = sanitize(c.Vendor)
|
||||||
|
output.Product = sanitize(c.Product)
|
||||||
|
output.Language = sanitize(c.Language)
|
||||||
|
output.Version = sanitize(c.Version)
|
||||||
|
output.TargetSW = sanitize(c.TargetSW)
|
||||||
|
output.Part = sanitize(c.Part)
|
||||||
|
output.Edition = sanitize(c.Edition)
|
||||||
|
output.Other = sanitize(c.Other)
|
||||||
|
output.SWEdition = sanitize(c.SWEdition)
|
||||||
|
output.TargetHW = sanitize(c.TargetHW)
|
||||||
|
output.Update = sanitize(c.Update)
|
||||||
|
return output.BindToFmtString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// sanitize is a modified version of WFNize function from nvdtools
|
||||||
|
// that quotes all the allowed punctation chars with a slash and replaces
|
||||||
|
// spaces with underscores. It differs from the upstream implmentation as
|
||||||
|
// it does not use the buggy nvdtools implementation, specifically the "addSlashesAt" part of the
|
||||||
|
// function which stops the loop as soon as it encounters ":" a valid
|
||||||
|
// character for a WFN attribute after quoting, but the way nvdtools
|
||||||
|
// handles it causes it to truncate strings that container ":". As a result
|
||||||
|
// strings like "prefix:1.2" which would have been quoted as "prefix\:1.2"
|
||||||
|
// end up becoming "prefix" instead causing loss of information and
|
||||||
|
// incorrect CPEs being generated.
|
||||||
|
func sanitize(s string) string {
|
||||||
|
const allowedPunct = "-!\"#$%&'()+,./:;<=>@[]^`{|}!~"
|
||||||
|
// replace spaces with underscores
|
||||||
|
in := strings.ReplaceAll(s, " ", "_")
|
||||||
|
sb := strings.Builder{}
|
||||||
|
for _, c := range in {
|
||||||
|
if strings.ContainsRune(allowedPunct, c) {
|
||||||
|
sb.WriteRune('\\')
|
||||||
|
}
|
||||||
|
sb.WriteRune(c)
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|||||||
+101
-3
@@ -1,6 +1,9 @@
|
|||||||
package pkg
|
package pkg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -32,7 +35,7 @@ func TestNewCPE(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "URL escape characters",
|
name: "URL escape characters",
|
||||||
input: `cpe:/a:%240.99_kindle_books_project:%240.99_kindle_books:6::~~~android~~`,
|
input: `cpe:/a:%240.99_kindle_books_project:%240.99_kindle_books:6::~~~android~~`,
|
||||||
expected: must(NewCPE(`cpe:2.3:a:$0.99_kindle_books_project:$0.99_kindle_books:6:*:*:*:*:android:*:*`)),
|
expected: must(NewCPE(`cpe:2.3:a:\$0.99_kindle_books_project:\$0.99_kindle_books:6:*:*:*:*:android:*:*`)),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,8 +46,8 @@ func TestNewCPE(t *testing.T) {
|
|||||||
t.Fatalf("got an error while creating CPE: %+v", err)
|
t.Fatalf("got an error while creating CPE: %+v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if actual.BindToFmtString() != test.expected.BindToFmtString() {
|
if CPEString(actual) != CPEString(test.expected) {
|
||||||
t.Errorf("mismatched entries:\n\texpected:%+v\n\t actual:%+v\n", test.expected.BindToFmtString(), actual.BindToFmtString())
|
t.Errorf("mismatched entries:\n\texpected:%+v\n\t actual:%+v\n", CPEString(test.expected), CPEString(actual))
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
@@ -80,3 +83,98 @@ func Test_normalizeCpeField(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_CPEParser(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
CPEString string `json:"cpe-string"`
|
||||||
|
CPEUrl string `json:"cpe-url"`
|
||||||
|
WFN CPE `json:"wfn"`
|
||||||
|
}{}
|
||||||
|
out, err := ioutil.ReadFile("test-fixtures/cpe-data.json")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Unable to read test-fixtures/cpe-data.json: ", err)
|
||||||
|
}
|
||||||
|
json.Unmarshal(out, &testCases)
|
||||||
|
for _, test := range testCases {
|
||||||
|
t.Run(test.CPEString, func(t *testing.T) {
|
||||||
|
c1, err := NewCPE(test.CPEString)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
c2, err := NewCPE(test.CPEUrl)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, c1, c2)
|
||||||
|
assert.Equal(t, c1, test.WFN)
|
||||||
|
assert.Equal(t, c2, test.WFN)
|
||||||
|
assert.Equal(t, CPEString(test.WFN), test.CPEString)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_InvalidCPE(t *testing.T) {
|
||||||
|
|
||||||
|
testCases := []string{
|
||||||
|
"cpe:2.3:a:some-vendor:name:1:3.2:*:*:*:*:*:*:*",
|
||||||
|
"cpe:2.3:a:some-vendor:name:1^:*:*:*:*:*:*:*",
|
||||||
|
"cpe:2.3:a:some-vendor:name:**:*:*:*:*:*:*:*",
|
||||||
|
"cpe:2.3:a:some-vendor:name:*\\:*:*:*:*:*:*:*",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
t.Run(test, func(t *testing.T) {
|
||||||
|
_, err := NewCPE(test)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, fmt.Sprint(err), "regex")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_RoundTrip(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
cpe string
|
||||||
|
parsedCPE CPE
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "normal",
|
||||||
|
cpe: "cpe:2.3:a:some-vendor:name:3.2:*:*:*:*:*:*:*",
|
||||||
|
parsedCPE: CPE{
|
||||||
|
Part: "a",
|
||||||
|
Vendor: "some-vendor",
|
||||||
|
Product: "name",
|
||||||
|
Version: "3.2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "escaped colon",
|
||||||
|
cpe: "cpe:2.3:a:some-vendor:name:1\\:3.2:*:*:*:*:*:*:*",
|
||||||
|
parsedCPE: CPE{
|
||||||
|
Part: "a",
|
||||||
|
Vendor: "some-vendor",
|
||||||
|
Product: "name",
|
||||||
|
Version: "1:3.2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "escaped forward slash",
|
||||||
|
cpe: "cpe:2.3:a:test\\/some-vendor:name:3.2:*:*:*:*:*:*:*",
|
||||||
|
parsedCPE: CPE{
|
||||||
|
Part: "a",
|
||||||
|
Vendor: "test/some-vendor",
|
||||||
|
Product: "name",
|
||||||
|
Version: "3.2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
// CPE string must be preserved through a round trip
|
||||||
|
assert.Equal(t, test.cpe, CPEString(MustCPE(test.cpe)))
|
||||||
|
// The parsed CPE must be the same after a round trip
|
||||||
|
assert.Equal(t, MustCPE(test.cpe), MustCPE(CPEString(MustCPE(test.cpe))))
|
||||||
|
// The test case parsed CPE must be the same after parsing the input string
|
||||||
|
assert.Equal(t, test.parsedCPE, MustCPE(test.cpe))
|
||||||
|
// The test case parsed CPE must produce the same string as the input cpe
|
||||||
|
assert.Equal(t, CPEString(test.parsedCPE), test.cpe)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -86,9 +86,7 @@ func TestDpkgMetadata_FileOwner(t *testing.T) {
|
|||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(strings.Join(test.expected, ","), func(t *testing.T) {
|
t.Run(strings.Join(test.expected, ","), func(t *testing.T) {
|
||||||
var i interface{}
|
actual := test.metadata.OwnedFiles()
|
||||||
i = test.metadata
|
|
||||||
actual := i.(FileOwner).OwnedFiles()
|
|
||||||
for _, d := range deep.Equal(test.expected, actual) {
|
for _, d := range deep.Equal(test.expected, actual) {
|
||||||
t.Errorf("diff: %+v", d)
|
t.Errorf("diff: %+v", d)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,9 +39,7 @@ func TestPythonMetadata_FileOwner(t *testing.T) {
|
|||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(strings.Join(test.expected, ","), func(t *testing.T) {
|
t.Run(strings.Join(test.expected, ","), func(t *testing.T) {
|
||||||
var i interface{}
|
actual := test.metadata.OwnedFiles()
|
||||||
i = test.metadata
|
|
||||||
actual := i.(FileOwner).OwnedFiles()
|
|
||||||
for _, d := range deep.Equal(test.expected, actual) {
|
for _, d := range deep.Equal(test.expected, actual) {
|
||||||
t.Errorf("diff: %+v", d)
|
t.Errorf("diff: %+v", d)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package pkg
|
|||||||
import (
|
import (
|
||||||
"github.com/anchore/syft/internal/log"
|
"github.com/anchore/syft/internal/log"
|
||||||
"github.com/anchore/syft/syft/artifact"
|
"github.com/anchore/syft/syft/artifact"
|
||||||
"github.com/bmatcuk/doublestar/v2"
|
"github.com/bmatcuk/doublestar/v4"
|
||||||
"github.com/scylladb/go-set/strset"
|
"github.com/scylladb/go-set/strset"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -88,9 +88,7 @@ func TestRpmMetadata_FileOwner(t *testing.T) {
|
|||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(strings.Join(test.expected, ","), func(t *testing.T) {
|
t.Run(strings.Join(test.expected, ","), func(t *testing.T) {
|
||||||
var i interface{}
|
actual := test.metadata.OwnedFiles()
|
||||||
i = test.metadata
|
|
||||||
actual := i.(FileOwner).OwnedFiles()
|
|
||||||
for _, d := range deep.Equal(test.expected, actual) {
|
for _, d := range deep.Equal(test.expected, actual) {
|
||||||
t.Errorf("diff: %+v", d)
|
t.Errorf("diff: %+v", d)
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,13 @@
|
|||||||
|
package sbom
|
||||||
|
|
||||||
|
import "io"
|
||||||
|
|
||||||
|
// Writer an interface to write SBOMs
|
||||||
|
type Writer interface {
|
||||||
|
// Write writes the provided SBOM
|
||||||
|
Write(SBOM) error
|
||||||
|
|
||||||
|
// Closer a resource cleanup hook which will be called after SBOM
|
||||||
|
// is written or if an error occurs before Write is called
|
||||||
|
io.Closer
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/anchore/syft/internal"
|
"github.com/anchore/syft/internal"
|
||||||
@@ -21,6 +22,8 @@ import (
|
|||||||
"github.com/wagoodman/go-progress"
|
"github.com/wagoodman/go-progress"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const WindowsOS = "windows"
|
||||||
|
|
||||||
var unixSystemRuntimePrefixes = []string{
|
var unixSystemRuntimePrefixes = []string{
|
||||||
"/proc",
|
"/proc",
|
||||||
"/dev",
|
"/dev",
|
||||||
@@ -143,6 +146,11 @@ func (r *directoryResolver) indexPath(path string, info os.FileInfo, err error)
|
|||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// here we check to see if we need to normalize paths to posix on the way in coming from windows
|
||||||
|
if runtime.GOOS == WindowsOS {
|
||||||
|
path = windowsToPosix(path)
|
||||||
|
}
|
||||||
|
|
||||||
newRoot, err := r.addPathToIndex(path, info)
|
newRoot, err := r.addPathToIndex(path, info)
|
||||||
if r.isFileAccessErr(path, err) {
|
if r.isFileAccessErr(path, err) {
|
||||||
return "", nil
|
return "", nil
|
||||||
@@ -258,6 +266,11 @@ func (r directoryResolver) requestPath(userPath string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r directoryResolver) responsePath(path string) string {
|
func (r directoryResolver) responsePath(path string) string {
|
||||||
|
// check to see if we need to encode back to Windows from posix
|
||||||
|
if runtime.GOOS == WindowsOS {
|
||||||
|
path = posixToWindows(path)
|
||||||
|
}
|
||||||
|
|
||||||
// always return references relative to the request path (not absolute path)
|
// always return references relative to the request path (not absolute path)
|
||||||
if filepath.IsAbs(path) {
|
if filepath.IsAbs(path) {
|
||||||
// we need to account for the cwd relative to the running process and the given root for the directory resolver
|
// we need to account for the cwd relative to the running process and the given root for the directory resolver
|
||||||
@@ -314,6 +327,10 @@ func (r directoryResolver) FilesByPath(userPaths ...string) ([]Location, error)
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if runtime.GOOS == WindowsOS {
|
||||||
|
userStrPath = windowsToPosix(userStrPath)
|
||||||
|
}
|
||||||
|
|
||||||
exists, ref, err := r.fileTree.File(file.Path(userStrPath))
|
exists, ref, err := r.fileTree.File(file.Path(userStrPath))
|
||||||
if err == nil && exists {
|
if err == nil && exists {
|
||||||
references = append(references, NewLocationFromDirectory(r.responsePath(userStrPath), *ref))
|
references = append(references, NewLocationFromDirectory(r.responsePath(userStrPath), *ref))
|
||||||
@@ -367,7 +384,13 @@ func (r directoryResolver) FileContentsByLocation(location Location) (io.ReadClo
|
|||||||
// by preference or these files are not readable by the current user).
|
// by preference or these files are not readable by the current user).
|
||||||
return nil, fmt.Errorf("file content is inaccessible path=%q", location.ref.RealPath)
|
return nil, fmt.Errorf("file content is inaccessible path=%q", location.ref.RealPath)
|
||||||
}
|
}
|
||||||
return file.NewLazyReadCloser(string(location.ref.RealPath)), nil
|
// RealPath is posix so for windows directory resolver we need to translate
|
||||||
|
// to its true on disk path.
|
||||||
|
filePath := string(location.ref.RealPath)
|
||||||
|
if runtime.GOOS == WindowsOS {
|
||||||
|
filePath = posixToWindows(filePath)
|
||||||
|
}
|
||||||
|
return file.NewLazyReadCloser(filePath), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r directoryResolver) isInIndex(location Location) bool {
|
func (r directoryResolver) isInIndex(location Location) bool {
|
||||||
@@ -409,6 +432,33 @@ func (r *directoryResolver) FilesByMIMEType(types ...string) ([]Location, error)
|
|||||||
return locations, nil
|
return locations, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func windowsToPosix(windowsPath string) (posixPath string) {
|
||||||
|
// volume should be encoded at the start (e.g /c/<path>) where c is the volume
|
||||||
|
volumeName := filepath.VolumeName(windowsPath)
|
||||||
|
pathWithoutVolume := strings.TrimPrefix(windowsPath, volumeName)
|
||||||
|
volumeLetter := strings.ToLower(strings.TrimSuffix(volumeName, ":"))
|
||||||
|
|
||||||
|
// translate non-escaped backslash to forwardslash
|
||||||
|
translatedPath := strings.ReplaceAll(pathWithoutVolume, "\\", "/")
|
||||||
|
|
||||||
|
// always have `/` as the root... join all components, e.g.:
|
||||||
|
// convert: C:\\some\windows\Place
|
||||||
|
// into: /c/some/windows/Place
|
||||||
|
return path.Clean("/" + strings.Join([]string{volumeLetter, translatedPath}, "/"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func posixToWindows(posixPath string) (windowsPath string) {
|
||||||
|
// decode the volume (e.g. /c/<path> --> C:\\) - There should always be a volume name.
|
||||||
|
pathFields := strings.Split(posixPath, "/")
|
||||||
|
volumeName := strings.ToUpper(pathFields[1]) + `:\\`
|
||||||
|
|
||||||
|
// translate non-escaped forward slashes into backslashes
|
||||||
|
remainingTranslatedPath := strings.Join(pathFields[2:], "\\")
|
||||||
|
|
||||||
|
// combine volume name and backslash components
|
||||||
|
return filepath.Clean(volumeName + remainingTranslatedPath)
|
||||||
|
}
|
||||||
|
|
||||||
func isUnixSystemRuntimePath(path string, _ os.FileInfo) bool {
|
func isUnixSystemRuntimePath(path string, _ os.FileInfo) bool {
|
||||||
return internal.HasAnyOfPrefixes(path, unixSystemRuntimePrefixes...)
|
return internal.HasAnyOfPrefixes(path, unixSystemRuntimePrefixes...)
|
||||||
}
|
}
|
||||||
@@ -421,8 +471,8 @@ func isUnallowableFileType(_ string, info os.FileInfo) bool {
|
|||||||
switch newFileTypeFromMode(info.Mode()) {
|
switch newFileTypeFromMode(info.Mode()) {
|
||||||
case CharacterDevice, Socket, BlockDevice, FIFONode, IrregularFile:
|
case CharacterDevice, Socket, BlockDevice, FIFONode, IrregularFile:
|
||||||
return true
|
return true
|
||||||
// note: symlinks that point to these files may still get by. We handle this later in processing to help prevent
|
// note: symlinks that point to these files may still get by.
|
||||||
// against infinite links traversal.
|
// We handle this later in processing to help prevent against infinite links traversal.
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
//go:build !windows
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
package source
|
package source
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -175,12 +178,12 @@ func TestDirectoryResolver_FilesByPath(t *testing.T) {
|
|||||||
hasPath := resolver.HasPath(c.input)
|
hasPath := resolver.HasPath(c.input)
|
||||||
if !c.forcePositiveHasPath {
|
if !c.forcePositiveHasPath {
|
||||||
if c.refCount != 0 && !hasPath {
|
if c.refCount != 0 && !hasPath {
|
||||||
t.Errorf("expected HasPath() to indicate existance, but did not")
|
t.Errorf("expected HasPath() to indicate existence, but did not")
|
||||||
} else if c.refCount == 0 && hasPath {
|
} else if c.refCount == 0 && hasPath {
|
||||||
t.Errorf("expeced HasPath() to NOT indicate existance, but does")
|
t.Errorf("expected HasPath() to NOT indicate existence, but does")
|
||||||
}
|
}
|
||||||
} else if !hasPath {
|
} else if !hasPath {
|
||||||
t.Errorf("expected HasPath() to indicate existance, but did not (force path)")
|
t.Errorf("expected HasPath() to indicate existence, but did not (force path)")
|
||||||
}
|
}
|
||||||
|
|
||||||
refs, err := resolver.FilesByPath(c.input)
|
refs, err := resolver.FilesByPath(c.input)
|
||||||
@@ -553,7 +556,6 @@ func Test_indexAllRoots(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Test_directoryResolver_FilesByMIMEType(t *testing.T) {
|
func Test_directoryResolver_FilesByMIMEType(t *testing.T) {
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
fixturePath string
|
fixturePath string
|
||||||
mimeType string
|
mimeType string
|
||||||
@@ -567,7 +569,6 @@ func Test_directoryResolver_FilesByMIMEType(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.fixturePath, func(t *testing.T) {
|
t.Run(test.fixturePath, func(t *testing.T) {
|
||||||
|
|
||||||
resolver, err := newDirectoryResolver(test.fixturePath)
|
resolver, err := newDirectoryResolver(test.fixturePath)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
locations, err := resolver.FilesByMIMEType(test.mimeType)
|
locations, err := resolver.FilesByMIMEType(test.mimeType)
|
||||||
@@ -648,7 +649,6 @@ func Test_IndexingNestedSymLinksOutsideOfRoot(t *testing.T) {
|
|||||||
locations, err = resolver.FilesByPath("./link_to_link_to_readme")
|
locations, err = resolver.FilesByPath("./link_to_link_to_readme")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, locations, 1)
|
assert.Len(t, locations, 1)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_directoryResolver_FileContentsByLocation(t *testing.T) {
|
func Test_directoryResolver_FileContentsByLocation(t *testing.T) {
|
||||||
@@ -683,10 +683,9 @@ func Test_directoryResolver_FileContentsByLocation(t *testing.T) {
|
|||||||
if test.err {
|
if test.err {
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
return
|
return
|
||||||
} else {
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
if test.expects != "" {
|
if test.expects != "" {
|
||||||
b, err := ioutil.ReadAll(actual)
|
b, err := ioutil.ReadAll(actual)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
package source
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func Test_windowsToPosix(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
windowsPath string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
wantPosixPath string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "basic case",
|
||||||
|
args: args{
|
||||||
|
windowsPath: `C:\some\windows\place`,
|
||||||
|
},
|
||||||
|
wantPosixPath: "/c/some/windows/place",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "escaped case",
|
||||||
|
args: args{
|
||||||
|
windowsPath: `C:\\some\\windows\\place`,
|
||||||
|
},
|
||||||
|
wantPosixPath: "/c/some/windows/place",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "forward slash",
|
||||||
|
args: args{
|
||||||
|
windowsPath: `C:/foo/bar`,
|
||||||
|
},
|
||||||
|
wantPosixPath: "/c/foo/bar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mix slash",
|
||||||
|
args: args{
|
||||||
|
windowsPath: `C:\foo/bar\`,
|
||||||
|
},
|
||||||
|
wantPosixPath: "/c/foo/bar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "case sensitive case",
|
||||||
|
args: args{
|
||||||
|
windowsPath: `C:\Foo/bAr\`,
|
||||||
|
},
|
||||||
|
wantPosixPath: "/c/Foo/bAr",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "special char case",
|
||||||
|
args: args{
|
||||||
|
windowsPath: `C:\ふー\バー`,
|
||||||
|
},
|
||||||
|
wantPosixPath: "/c/ふー/バー",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if gotPosixPath := windowsToPosix(tt.args.windowsPath); gotPosixPath != tt.wantPosixPath {
|
||||||
|
t.Errorf("windowsToPosix() = %v, want %v", gotPosixPath, tt.wantPosixPath)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_posixToWindows(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
posixPath string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
wantWindowsPath string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "basic case",
|
||||||
|
args: args{
|
||||||
|
posixPath: "/c/some/windows/place",
|
||||||
|
},
|
||||||
|
wantWindowsPath: `C:\some\windows\place`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "escaped case",
|
||||||
|
args: args{
|
||||||
|
posixPath: "/c/some/windows/place",
|
||||||
|
},
|
||||||
|
wantWindowsPath: `C:\\some\\windows\\place`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "forward slash",
|
||||||
|
args: args{
|
||||||
|
posixPath: "/c/foo/bar",
|
||||||
|
},
|
||||||
|
wantWindowsPath: `C:/foo/bar`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mix slash",
|
||||||
|
args: args{
|
||||||
|
posixPath: "/c/foo/bar",
|
||||||
|
},
|
||||||
|
wantWindowsPath: `C:\foo/bar\`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "case sensitive case",
|
||||||
|
args: args{
|
||||||
|
posixPath: "/c/Foo/bAr",
|
||||||
|
},
|
||||||
|
wantWindowsPath: `C:\Foo/bAr\`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "special char case",
|
||||||
|
args: args{
|
||||||
|
posixPath: "/c/ふー/バー",
|
||||||
|
},
|
||||||
|
wantWindowsPath: `C:\ふー\バー`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if gotWindowsPath := posixToWindows(tt.args.posixPath); gotWindowsPath != tt.wantWindowsPath {
|
||||||
|
t.Errorf("posixToWindows() = %v, want %v", gotWindowsPath, tt.wantWindowsPath)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
//go:build !windows
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
package source
|
package source
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user