mirror of
https://github.com/anchore/syft.git
synced 2026-10-15 15:47:23 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b77ddfc29c | ||
|
|
5e5312c72d | ||
|
|
38c4b17847 | ||
|
|
01dc78ccc3 | ||
|
|
2a7325a965 | ||
|
|
d9aa54cd00 | ||
|
|
697bad02ac | ||
|
|
a18fbacae7 | ||
|
|
7168dc7473 | ||
|
|
211b188120 | ||
|
|
7f8cb0bd80 | ||
|
|
8535ee5b2e | ||
|
|
a689fb1213 | ||
|
|
4903b4d73f | ||
|
|
7de5e1288f | ||
|
|
474ed682bc | ||
|
|
b7979dbc7d | ||
|
|
cc20a8f341 | ||
|
|
006ba9b557 | ||
|
|
a27907659d |
@@ -93,9 +93,27 @@ file:path/to/yourproject/file read directly from a path on disk (any si
|
||||
registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required)
|
||||
```
|
||||
|
||||
### Excluding file paths
|
||||
|
||||
Syft can exclude files and paths from being scanned within a source by using glob expressions
|
||||
with one or more `--exclude` parameters:
|
||||
```
|
||||
syft <source> --exclude './out/**/*.json' --exclude /etc
|
||||
```
|
||||
**Note:** in the case of _image scanning_, since the entire filesystem is scanned it is
|
||||
possible to use absolute paths like `/etc` or `/usr/**/*.txt` whereas _directory scans_
|
||||
exclude files _relative to the specified directory_. For example: scanning `/usr/foo` with
|
||||
`--exclude ./package.json` would exclude `/usr/foo/package.json` and `--exclude '**/package.json'`
|
||||
would exclude all `package.json` files under `/usr/foo`. For _directory scans_,
|
||||
it is required to begin path expressions with `./`, `*/`, or `**/`, all of which
|
||||
will be resolved _relative to the specified scan directory_. Keep in mind, your shell
|
||||
may attempt to expand wildcards, so put those parameters in single quotes, like:
|
||||
`'**/*.json'`.
|
||||
|
||||
### 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>
|
||||
@@ -110,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).
|
||||
- `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
|
||||
|
||||
### Local Docker Credentials
|
||||
@@ -204,8 +231,12 @@ Configuration search paths:
|
||||
Configuration options (example values are the default):
|
||||
|
||||
```yaml
|
||||
# the output format of the SBOM report (options: table, text, json)
|
||||
# same as -o ; SYFT_OUTPUT env var
|
||||
# the output format(s) of the SBOM report (options: table, text, json, spdx, ...)
|
||||
# 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"
|
||||
|
||||
# suppress all output (except for the SBOM report)
|
||||
@@ -219,8 +250,26 @@ file: ""
|
||||
# same as SYFT_CHECK_FOR_APP_UPDATE env var
|
||||
check-for-app-update: true
|
||||
|
||||
# a list of globs to exclude from scanning. same as --exclude ; for example:
|
||||
# exclude:
|
||||
# - "/etc/**"
|
||||
# - "./out/**/*.json"
|
||||
exclude:
|
||||
|
||||
# cataloging packages is exposed through the packages and power-user subcommands
|
||||
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:
|
||||
# enable/disable cataloging of packages
|
||||
# SYFT_PACKAGE_CATALOGER_ENABLED env var
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
func checkForApplicationUpdate() {
|
||||
if appConfig.CheckForAppUpdate {
|
||||
log.Debugf("checking if new vesion of %s is available", internal.ApplicationName)
|
||||
isAvailable, newVersion, err := version.IsUpdateAvailable()
|
||||
if err != nil {
|
||||
// this should never stop the application
|
||||
|
||||
+36
-2
@@ -1,17 +1,19 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"sort"
|
||||
|
||||
"github.com/anchore/stereoscope"
|
||||
"github.com/anchore/syft/internal/config"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/logger"
|
||||
"github.com/anchore/syft/internal/version"
|
||||
"github.com/anchore/syft/syft"
|
||||
"github.com/gookit/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
)
|
||||
@@ -28,6 +30,8 @@ func init() {
|
||||
initAppConfig,
|
||||
initLogging,
|
||||
logAppConfig,
|
||||
checkForApplicationUpdate,
|
||||
logAppVersion,
|
||||
initEventBus,
|
||||
)
|
||||
}
|
||||
@@ -110,3 +114,33 @@ func initEventBus() {
|
||||
stereoscope.SetBus(eventBus)
|
||||
syft.SetBus(eventBus)
|
||||
}
|
||||
|
||||
func logAppVersion() {
|
||||
versionInfo := version.FromBuild()
|
||||
log.Infof("syft version: %s", versionInfo.Version)
|
||||
|
||||
var fields map[string]interface{}
|
||||
bytes, err := json.Marshal(versionInfo)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(bytes, &fields)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(fields))
|
||||
for k := range fields {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for idx, field := range keys {
|
||||
value := fields[field]
|
||||
branch := "├──"
|
||||
if idx == len(fields)-1 {
|
||||
branch = "└──"
|
||||
}
|
||||
log.Debugf(" %s %s: %s", branch, field, value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ func Test_eventLoop_gracefulExit(t *testing.T) {
|
||||
t.Cleanup(testBus.Close)
|
||||
|
||||
finalEvent := partybus.Event{
|
||||
Type: event.PresenterReady,
|
||||
Type: event.Exit,
|
||||
}
|
||||
|
||||
worker := func() <-chan error {
|
||||
@@ -182,7 +182,7 @@ func Test_eventLoop_unsubscribeError(t *testing.T) {
|
||||
t.Cleanup(testBus.Close)
|
||||
|
||||
finalEvent := partybus.Event{
|
||||
Type: event.PresenterReady,
|
||||
Type: event.Exit,
|
||||
}
|
||||
|
||||
worker := func() <-chan error {
|
||||
@@ -251,8 +251,8 @@ func Test_eventLoop_handlerError(t *testing.T) {
|
||||
t.Cleanup(testBus.Close)
|
||||
|
||||
finalEvent := partybus.Event{
|
||||
Type: event.PresenterReady,
|
||||
Error: fmt.Errorf("unable to create presenter"),
|
||||
Type: event.Exit,
|
||||
Error: fmt.Errorf("an exit error occured"),
|
||||
}
|
||||
|
||||
worker := func() <-chan error {
|
||||
@@ -376,7 +376,7 @@ func Test_eventLoop_uiTeardownError(t *testing.T) {
|
||||
t.Cleanup(testBus.Close)
|
||||
|
||||
finalEvent := partybus.Event{
|
||||
Type: event.PresenterReady,
|
||||
Type: event.Exit,
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+34
-41
@@ -10,13 +10,13 @@ import (
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/anchore"
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/formats"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/ui"
|
||||
"github.com/anchore/syft/internal/version"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/syft/format"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/pkg/profile"
|
||||
@@ -50,8 +50,7 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
packagesPresenterOpt format.Option
|
||||
packagesCmd = &cobra.Command{
|
||||
packagesCmd = &cobra.Command{
|
||||
Use: "packages [SOURCE]",
|
||||
Short: "Generate a package SBOM",
|
||||
Long: "Generate a packaged-based Software Bill Of Materials (SBOM) from container images and filesystems",
|
||||
@@ -62,14 +61,7 @@ var (
|
||||
Args: validateInputArgs,
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
// set the presenter
|
||||
presenterOption := format.ParseOption(appConfig.Output)
|
||||
if presenterOption == format.UnknownFormatOption {
|
||||
return fmt.Errorf("bad --output value '%s'", appConfig.Output)
|
||||
}
|
||||
packagesPresenterOpt = presenterOption
|
||||
|
||||
PreRunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
if appConfig.Dev.ProfileCPU && appConfig.Dev.ProfileMem {
|
||||
return fmt.Errorf("cannot profile CPU and memory simultaneously")
|
||||
}
|
||||
@@ -98,17 +90,17 @@ func setPackageFlags(flags *pflag.FlagSet) {
|
||||
// Formatting & Input options //////////////////////////////////////////////
|
||||
|
||||
flags.StringP(
|
||||
"scope", "s", source.SquashedScope.String(),
|
||||
"scope", "s", cataloger.DefaultSearchConfig().Scope.String(),
|
||||
fmt.Sprintf("selection of layers to catalog, options=%v", source.AllScopes))
|
||||
|
||||
flags.StringP(
|
||||
"output", "o", string(format.TableOption),
|
||||
fmt.Sprintf("report output formatter, options=%v", format.AllOptions),
|
||||
flags.StringArrayP(
|
||||
"output", "o", []string{string(format.TableOption)},
|
||||
fmt.Sprintf("report output format, options=%v", format.AllOptions),
|
||||
)
|
||||
|
||||
flags.StringP(
|
||||
"file", "", "",
|
||||
"file to write the report output to (default is STDOUT)",
|
||||
"file to write the default report output to (default is STDOUT)",
|
||||
)
|
||||
|
||||
// Upload options //////////////////////////////////////////////////////////
|
||||
@@ -132,6 +124,11 @@ func setPackageFlags(flags *pflag.FlagSet) {
|
||||
"include dockerfile for upload to Anchore Enterprise",
|
||||
)
|
||||
|
||||
flags.StringArrayP(
|
||||
"exclude", "", nil,
|
||||
"exclude paths from being scanned using a glob expression",
|
||||
)
|
||||
|
||||
flags.Bool(
|
||||
"overwrite-existing-image", false,
|
||||
"overwrite an existing image during the upload to Anchore Enterprise",
|
||||
@@ -158,6 +155,10 @@ func bindPackagesConfigOptions(flags *pflag.FlagSet) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := viper.BindPFlag("exclude", flags.Lookup("exclude")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Upload options //////////////////////////////////////////////////////////
|
||||
|
||||
if err := viper.BindPFlag("anchore.host", flags.Lookup("host")); err != nil {
|
||||
@@ -200,26 +201,26 @@ func validateInputArgs(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
func packagesExec(_ *cobra.Command, args []string) error {
|
||||
// could be an image or a directory, with or without a scheme
|
||||
userInput := args[0]
|
||||
|
||||
reporter, closer, err := reportWriter()
|
||||
defer func() {
|
||||
if err := closer(); err != nil {
|
||||
log.Warnf("unable to write to report destination: %+v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
writer, err := makeWriter(appConfig.Output, appConfig.File)
|
||||
if err != nil {
|
||||
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(
|
||||
packagesExecWorker(userInput),
|
||||
packagesExecWorker(userInput, writer),
|
||||
setupSignals(),
|
||||
eventSubscription,
|
||||
stereoscope.Cleanup,
|
||||
ui.Select(isVerbose(), appConfig.Quiet, reporter)...,
|
||||
ui.Select(isVerbose(), appConfig.Quiet)...,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -234,7 +235,7 @@ func isVerbose() (result bool) {
|
||||
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)
|
||||
go func() {
|
||||
defer close(errs)
|
||||
@@ -245,17 +246,9 @@ func packagesExecWorker(userInput string) <-chan error {
|
||||
return
|
||||
}
|
||||
|
||||
f := formats.ByOption(packagesPresenterOpt)
|
||||
if f == nil {
|
||||
errs <- fmt.Errorf("unknown format: %s", packagesPresenterOpt)
|
||||
return
|
||||
}
|
||||
|
||||
checkForApplicationUpdate()
|
||||
|
||||
src, cleanup, err := source.New(userInput, appConfig.Registry.ToOptions())
|
||||
src, cleanup, err := source.New(userInput, appConfig.Registry.ToOptions(), appConfig.Exclusions)
|
||||
if err != nil {
|
||||
errs <- fmt.Errorf("failed to determine image source: %w", err)
|
||||
errs <- fmt.Errorf("failed to construct source from user input %q: %w", userInput, err)
|
||||
return
|
||||
}
|
||||
if cleanup != nil {
|
||||
@@ -288,8 +281,8 @@ func packagesExecWorker(userInput string) <-chan error {
|
||||
}
|
||||
|
||||
bus.Publish(partybus.Event{
|
||||
Type: event.PresenterReady,
|
||||
Value: f.Presenter(s),
|
||||
Type: event.Exit,
|
||||
Value: func() error { return writer.Write(s) },
|
||||
})
|
||||
}()
|
||||
return errs
|
||||
|
||||
+16
-14
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/formats/syftjson"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/output"
|
||||
"github.com/anchore/syft/internal/ui"
|
||||
"github.com/anchore/syft/internal/version"
|
||||
"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
|
||||
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() {
|
||||
if err := closer(); err != nil {
|
||||
if err := writer.Close(); err != nil {
|
||||
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)
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return eventLoop(
|
||||
powerUserExecWorker(userInput),
|
||||
powerUserExecWorker(userInput, writer),
|
||||
setupSignals(),
|
||||
eventSubscription,
|
||||
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)
|
||||
go func() {
|
||||
defer close(errs)
|
||||
@@ -111,9 +115,7 @@ func powerUserExecWorker(userInput string) <-chan error {
|
||||
return
|
||||
}
|
||||
|
||||
checkForApplicationUpdate()
|
||||
|
||||
src, cleanup, err := source.New(userInput, appConfig.Registry.ToOptions())
|
||||
src, cleanup, err := source.New(userInput, appConfig.Registry.ToOptions(), appConfig.Exclusions)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return
|
||||
@@ -142,8 +144,8 @@ func powerUserExecWorker(userInput string) <-chan error {
|
||||
s.Relationships = append(s.Relationships, mergeRelationships(relationships...)...)
|
||||
|
||||
bus.Publish(partybus.Event{
|
||||
Type: event.PresenterReady,
|
||||
Value: syftjson.Format().Presenter(s),
|
||||
Type: event.Exit,
|
||||
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"
|
||||
"os"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/config"
|
||||
"github.com/anchore/syft/internal/version"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
@@ -22,12 +24,14 @@ var rootCmd = &cobra.Command{
|
||||
PreRunE: packagesCmd.PreRunE,
|
||||
RunE: packagesCmd.RunE,
|
||||
ValidArgsFunction: packagesCmd.ValidArgsFunction,
|
||||
Version: version.FromBuild().Version,
|
||||
}
|
||||
|
||||
func init() {
|
||||
// set universal flags
|
||||
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"
|
||||
rootCmd.PersistentFlags().BoolP(
|
||||
flag, "q", false,
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ func generateCatalogPackagesTask() (task, 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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -8,17 +8,16 @@ require (
|
||||
github.com/adrg/xdg v0.2.1
|
||||
github.com/alecthomas/jsonschema v0.0.0-20210301060011-54c507b6f074
|
||||
github.com/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf
|
||||
github.com/anchore/go-presenter v0.0.0-20211102174526-0dbf20f6c7fa
|
||||
github.com/anchore/go-rpmdb v0.0.0-20210914181456-a9c52348da63
|
||||
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
|
||||
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
|
||||
github.com/anchore/packageurl-go v0.0.0-20210922164639-b3fa992ebd29
|
||||
github.com/anchore/stereoscope v0.0.0-20211203160213-5a5e323a5c89
|
||||
github.com/anchore/stereoscope v0.0.0-20220110181730-c91cf94a3718
|
||||
// we are hinting brotli to latest due to warning when installing archiver v3:
|
||||
// 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/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/dustin/go-humanize v1.0.0
|
||||
github.com/facebookincubator/nvdtools v0.1.4
|
||||
@@ -53,7 +52,7 @@ require (
|
||||
github.com/x-cray/logrus-prefixed-formatter v0.5.2
|
||||
github.com/xeipuuv/gojsonschema v1.2.0
|
||||
golang.org/x/mod v0.4.2
|
||||
golang.org/x/net v0.0.0-20211111160137-58aab5ef257a
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2
|
||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
@@ -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/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf h1:DYssiUV1pBmKqzKsm4mqXx8artqC0Q8HgZsVI3lMsAg=
|
||||
github.com/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf/go.mod h1:FaODhIA06mxO1E6R32JE0TL1JWZZkmjRIAd4ULvHUKk=
|
||||
github.com/anchore/go-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/go.mod h1:6qH8c6U/3CBVvDDDBZnPSTbTINq3cIdADUYTaVf75EM=
|
||||
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 h1:VzprUTpc0vW0nnNKJfJieyH/TZ9UYAnTZs5/gHTdAe8=
|
||||
@@ -111,8 +109,8 @@ github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZV
|
||||
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E=
|
||||
github.com/anchore/packageurl-go v0.0.0-20210922164639-b3fa992ebd29 h1:K9LfnxwhqvihqU0+MF325FNy7fsKV9EGaUxdfR4gnWk=
|
||||
github.com/anchore/packageurl-go v0.0.0-20210922164639-b3fa992ebd29/go.mod h1:Oc1UkGaJwY6ND6vtAqPSlYrptKRJngHwkwB6W7l1uP0=
|
||||
github.com/anchore/stereoscope v0.0.0-20211203160213-5a5e323a5c89 h1:pcMFN7wjIQIxdZm8NA0R3JiRRFn5Eyx9mhagHOVS4Bw=
|
||||
github.com/anchore/stereoscope v0.0.0-20211203160213-5a5e323a5c89/go.mod h1:FNm1rtauEjkC3elA3jr3bJkU+/4QiovApwJdPnHQ9x0=
|
||||
github.com/anchore/stereoscope v0.0.0-20220110181730-c91cf94a3718 h1:46+DtmTaPlOCuY5KY3H6zazuz3+E/DSwc+ZpfPhyj50=
|
||||
github.com/anchore/stereoscope v0.0.0-20220110181730-c91cf94a3718/go.mod h1:OHhT0g7HQlELWJgZE80dJ0rCbMPIR+jIM8KNwN7ReKU=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
||||
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
||||
@@ -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/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/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/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
|
||||
@@ -205,8 +201,8 @@ github.com/containerd/containerd v1.5.0-beta.4/go.mod h1:GmdgZd2zA2GYIBZ0w09Zvgq
|
||||
github.com/containerd/containerd v1.5.0-rc.0/go.mod h1:V/IXoMqNGgBlabz3tHD2TWDoTJseu1FGOKuoA4nNb2s=
|
||||
github.com/containerd/containerd v1.5.1/go.mod h1:0DOxVqwDy2iZvrZp2JUx/E+hS0UNTVn7dJnIOwtYR4g=
|
||||
github.com/containerd/containerd v1.5.7/go.mod h1:gyvv6+ugqY25TiXxcZC3L5yOeYgEw0QMhscqVp1AR9c=
|
||||
github.com/containerd/containerd v1.5.8 h1:NmkCC1/QxyZFBny8JogwLpOy2f+VEbO/f6bV2Mqtwuw=
|
||||
github.com/containerd/containerd v1.5.8/go.mod h1:YdFSv5bTFLpG2HIYmfqDpSYYTDX+mc5qtSuYx1YUb/s=
|
||||
github.com/containerd/containerd v1.5.9 h1:rs6Xg1gtIxaeyG+Smsb/0xaSDu1VgFhOCKBXxMxbsF4=
|
||||
github.com/containerd/containerd v1.5.9/go.mod h1:fvQqCfadDGga5HZyn3j4+dx56qj2I9YwBrlSdalvJYQ=
|
||||
github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
|
||||
github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
|
||||
github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
|
||||
@@ -986,8 +982,9 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211111160137-58aab5ef257a h1:c83jeVQW0KGKNaKBRfelNYNHaev+qawl9yaA825s8XE=
|
||||
golang.org/x/net v0.0.0-20211111160137-58aab5ef257a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
|
||||
+2
-1
@@ -68,6 +68,7 @@ get_binaries() {
|
||||
darwin/arm64) BINARIES="syft" ;;
|
||||
darwin/amd64) BINARIES="syft" ;;
|
||||
linux/amd64) BINARIES="syft" ;;
|
||||
linux/arm64) 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"
|
||||
@@ -246,7 +247,7 @@ extract_from_dmg() {
|
||||
dmg_file=$1
|
||||
mount_point="/Volumes/tmp-dmg"
|
||||
hdiutil attach -quiet -nobrowse -mountpoint "${mount_point}" "${dmg_file}"
|
||||
cp -fR "${mount_point}/" ./
|
||||
cp -fR "${mount_point}/." ./
|
||||
hdiutil detach -quiet -force "${mount_point}"
|
||||
}
|
||||
http_download_curl() {
|
||||
|
||||
@@ -25,7 +25,7 @@ type packageSBOMImportAPI interface {
|
||||
func packageSbomModel(s sbom.SBOM) (*external.ImagePackageManifest, error) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
err := syftjson.Format().Presenter(s).Present(&buf)
|
||||
err := syftjson.Format().Encode(&buf, s)
|
||||
if err != nil {
|
||||
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.
|
||||
var model external.ImagePackageManifest
|
||||
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
|
||||
|
||||
@@ -105,8 +105,7 @@ func TestPackageSbomToModel(t *testing.T) {
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
pres := syftjson.Format().Presenter(s)
|
||||
if err := pres.Present(&buf); err != nil {
|
||||
if err := syftjson.Format().Encode(&buf, s); err != nil {
|
||||
t.Fatalf("unable to get expected json: %+v", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ type parser interface {
|
||||
// Application is the main syft application configuration.
|
||||
type Application struct {
|
||||
ConfigPath string `yaml:",omitempty" json:"configPath"` // the location where the application config was read from (either from -c or discovered while loading)
|
||||
Output string `yaml:"output" json:"output" mapstructure:"output"` // -o, the Presenter hint string to use for report formatting
|
||||
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
|
||||
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
|
||||
@@ -38,12 +38,13 @@ type Application struct {
|
||||
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"`
|
||||
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"`
|
||||
FileClassification fileClassification `yaml:"file-classification" json:"file-classification" mapstructure:"file-classification"`
|
||||
FileContents fileContents `yaml:"file-contents" json:"file-contents" mapstructure:"file-contents"`
|
||||
Secrets secrets `yaml:"secrets" json:"secrets" mapstructure:"secrets"`
|
||||
Registry registry `yaml:"registry" json:"registry" mapstructure:"registry"`
|
||||
Exclusions []string `yaml:"exclude" json:"exclude" mapstructure:"exclude"`
|
||||
}
|
||||
|
||||
// PowerUserCatalogerEnabledDefault switches all catalogers to be enabled when running power-user command
|
||||
|
||||
@@ -3,6 +3,8 @@ package config
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
@@ -12,6 +14,10 @@ type catalogerOptions struct {
|
||||
ScopeOpt source.Scope `yaml:"-" json:"-"`
|
||||
}
|
||||
|
||||
func (cfg catalogerOptions) loadDefaultValues(v *viper.Viper) {
|
||||
v.SetDefault("package.cataloger.enabled", true)
|
||||
}
|
||||
|
||||
func (cfg *catalogerOptions) parseConfigValues() error {
|
||||
scopeOption := source.ParseScope(cfg.Scope)
|
||||
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 (
|
||||
"fmt"
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ const (
|
||||
// ApplicationName is the non-capitalized name of the application (do not change this)
|
||||
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.
|
||||
JSONSchemaVersion = "2.0.1"
|
||||
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
|
||||
|
||||
import (
|
||||
|
||||
@@ -3,9 +3,7 @@ package file
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -22,8 +20,6 @@ const (
|
||||
GB
|
||||
)
|
||||
|
||||
const perFileReadLimit = 2 * GB
|
||||
|
||||
type errZipSlipDetected struct {
|
||||
Prefix 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)
|
||||
}
|
||||
|
||||
// limit the zip reader on each file read to prevent decompression bomb attacks
|
||||
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 {
|
||||
if err := safeCopy(tempFile, zippedFile); err != nil {
|
||||
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()}
|
||||
|
||||
return nil
|
||||
@@ -153,13 +138,7 @@ func ContentsFromZip(archivePath string, paths ...string) (map[string]string, er
|
||||
}
|
||||
|
||||
var buffer bytes.Buffer
|
||||
|
||||
// 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 {
|
||||
if err := safeCopy(&buffer, zippedFile); err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
// limit the zip reader on each file read to prevent decompression bomb attacks
|
||||
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 {
|
||||
if err := safeCopy(outputFile, zippedFile); err != nil {
|
||||
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
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package file
|
||||
|
||||
import (
|
||||
|
||||
@@ -12,7 +12,7 @@ func ExternalRefs(p pkg.Package) (externalRefs []model.ExternalRef) {
|
||||
for _, c := range p.CPEs {
|
||||
externalRefs = append(externalRefs, model.ExternalRef{
|
||||
ReferenceCategory: model.SecurityReferenceCategory,
|
||||
ReferenceLocator: c.BindToFmtString(),
|
||||
ReferenceLocator: pkg.CPEString(c),
|
||||
ReferenceType: model.Cpe23ExternalRefType,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ func Test_ExternalRefs(t *testing.T) {
|
||||
expected: []model.ExternalRef{
|
||||
{
|
||||
ReferenceCategory: model.SecurityReferenceCategory,
|
||||
ReferenceLocator: testCPE.BindToFmtString(),
|
||||
ReferenceLocator: pkg.CPEString(testCPE),
|
||||
ReferenceType: model.Cpe23ExternalRefType,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -2,14 +2,15 @@ package testutils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/go-presenter"
|
||||
"github.com/anchore/go-testutils"
|
||||
"github.com/anchore/stereoscope/pkg/filetree"
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
"github.com/anchore/syft/syft/distro"
|
||||
"github.com/anchore/syft/syft/format"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
"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
|
||||
|
||||
// grab the latest image contents and persist
|
||||
@@ -39,11 +40,11 @@ func AssertPresenterAgainstGoldenImageSnapshot(t *testing.T, pres presenter.Pres
|
||||
imagetest.UpdateGoldenFixtureImage(t, testImage)
|
||||
}
|
||||
|
||||
err := pres.Present(&buffer)
|
||||
err := format.Encode(&buffer, sbom)
|
||||
assert.NoError(t, err)
|
||||
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 {
|
||||
testutils.UpdateGoldenFileContents(t, actual)
|
||||
}
|
||||
@@ -51,6 +52,7 @@ func AssertPresenterAgainstGoldenImageSnapshot(t *testing.T, pres presenter.Pres
|
||||
var expected = testutils.GetGoldenFileContents(t)
|
||||
|
||||
// remove dynamic values, which should be tested independently
|
||||
redactors = append(redactors, carriageRedactor)
|
||||
for _, r := range redactors {
|
||||
actual = r(actual)
|
||||
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
|
||||
|
||||
err := pres.Present(&buffer)
|
||||
err := format.Encode(&buffer, sbom)
|
||||
assert.NoError(t, err)
|
||||
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 {
|
||||
testutils.UpdateGoldenFileContents(t, actual)
|
||||
}
|
||||
@@ -79,6 +81,7 @@ func AssertPresenterAgainstGoldenSnapshot(t *testing.T, pres presenter.Presenter
|
||||
var expected = testutils.GetGoldenFileContents(t)
|
||||
|
||||
// remove dynamic values, which should be tested independently
|
||||
redactors = append(redactors, carriageRedactor)
|
||||
for _, r := range redactors {
|
||||
actual = r(actual)
|
||||
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) {
|
||||
_, ref1, _ := img.SquashedTree().File("/somefile-1.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"
|
||||
)
|
||||
|
||||
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) {
|
||||
testutils.AssertPresenterAgainstGoldenSnapshot(t,
|
||||
Format().Presenter(testutils.DirectoryInput(t)),
|
||||
func TestCycloneDxDirectoryEncoder(t *testing.T) {
|
||||
testutils.AssertEncoderAgainstGoldenSnapshot(t,
|
||||
Format(),
|
||||
testutils.DirectoryInput(t),
|
||||
*updateCycloneDx,
|
||||
cycloneDxRedactor,
|
||||
)
|
||||
}
|
||||
|
||||
func TestCycloneDxImagePresenter(t *testing.T) {
|
||||
func TestCycloneDxImageEncoder(t *testing.T) {
|
||||
testImage := "image-simple"
|
||||
testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
|
||||
Format().Presenter(testutils.ImageInput(t, testImage)),
|
||||
testutils.AssertEncoderAgainstGoldenImageSnapshot(t,
|
||||
Format(),
|
||||
testutils.ImageInput(t, testImage),
|
||||
testImage,
|
||||
*updateCycloneDx,
|
||||
cycloneDxRedactor,
|
||||
|
||||
@@ -8,20 +8,22 @@ import (
|
||||
"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) {
|
||||
testutils.AssertPresenterAgainstGoldenSnapshot(t,
|
||||
Format().Presenter(testutils.DirectoryInput(t)),
|
||||
func TestCycloneDxDirectoryEncoder(t *testing.T) {
|
||||
testutils.AssertEncoderAgainstGoldenSnapshot(t,
|
||||
Format(),
|
||||
testutils.DirectoryInput(t),
|
||||
*updateCycloneDx,
|
||||
cycloneDxRedactor,
|
||||
)
|
||||
}
|
||||
|
||||
func TestCycloneDxImagePresenter(t *testing.T) {
|
||||
func TestCycloneDxImageEncoder(t *testing.T) {
|
||||
testImage := "image-simple"
|
||||
testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
|
||||
Format().Presenter(testutils.ImageInput(t, testImage)),
|
||||
testutils.AssertEncoderAgainstGoldenImageSnapshot(t,
|
||||
Format(),
|
||||
testutils.ImageInput(t, testImage),
|
||||
testImage,
|
||||
*updateCycloneDx,
|
||||
cycloneDxRedactor,
|
||||
|
||||
@@ -8,20 +8,22 @@ import (
|
||||
"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) {
|
||||
testutils.AssertPresenterAgainstGoldenSnapshot(t,
|
||||
Format().Presenter(testutils.DirectoryInput(t)),
|
||||
func TestSPDXJSONDirectoryEncoder(t *testing.T) {
|
||||
testutils.AssertEncoderAgainstGoldenSnapshot(t,
|
||||
Format(),
|
||||
testutils.DirectoryInput(t),
|
||||
*updateSpdxJson,
|
||||
spdxJsonRedactor,
|
||||
)
|
||||
}
|
||||
|
||||
func TestSPDXJSONImagePresenter(t *testing.T) {
|
||||
func TestSPDXJSONImageEncoder(t *testing.T) {
|
||||
testImage := "image-simple"
|
||||
testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
|
||||
Format().Presenter(testutils.ImageInput(t, testImage, testutils.FromSnapshot())),
|
||||
testutils.AssertEncoderAgainstGoldenImageSnapshot(t,
|
||||
Format(),
|
||||
testutils.ImageInput(t, testImage, testutils.FromSnapshot()),
|
||||
testImage,
|
||||
*updateSpdxJson,
|
||||
spdxJsonRedactor,
|
||||
|
||||
+4
-4
@@ -3,7 +3,7 @@
|
||||
"name": "/some/path",
|
||||
"spdxVersion": "SPDX-2.2",
|
||||
"creationInfo": {
|
||||
"created": "2021-12-01T15:08:29.469369Z",
|
||||
"created": "2021-12-20T19:12:47.869816Z",
|
||||
"creators": [
|
||||
"Organization: Anchore, Inc",
|
||||
"Tool: syft-[not provided]"
|
||||
@@ -11,10 +11,10 @@
|
||||
"licenseListVersion": "3.15"
|
||||
},
|
||||
"dataLicense": "CC0-1.0",
|
||||
"documentNamespace": "https://anchore.com/syft/dir/some/path-f4586501-2da6-4541-a8e9-232b32f25e9a",
|
||||
"documentNamespace": "https://anchore.com/syft/dir/some/path-4b896ded-7852-4e31-b764-136b53bdf346",
|
||||
"packages": [
|
||||
{
|
||||
"SPDXID": "SPDXRef-2a115ac97d018a0e",
|
||||
"SPDXID": "SPDXRef-1d97af55efe9512f",
|
||||
"name": "package-1",
|
||||
"licenseConcluded": "MIT",
|
||||
"downloadLocation": "NOASSERTION",
|
||||
@@ -36,7 +36,7 @@
|
||||
"versionInfo": "1.0.1"
|
||||
},
|
||||
{
|
||||
"SPDXID": "SPDXRef-5e920b2bece2c3ae",
|
||||
"SPDXID": "SPDXRef-ad3d1c4abd84bf75",
|
||||
"name": "package-2",
|
||||
"licenseConcluded": "NONE",
|
||||
"downloadLocation": "NOASSERTION",
|
||||
+4
-4
@@ -3,7 +3,7 @@
|
||||
"name": "user-image-input",
|
||||
"spdxVersion": "SPDX-2.2",
|
||||
"creationInfo": {
|
||||
"created": "2021-12-01T15:08:29.476498Z",
|
||||
"created": "2021-12-20T19:13:07.647486Z",
|
||||
"creators": [
|
||||
"Organization: Anchore, Inc",
|
||||
"Tool: syft-[not provided]"
|
||||
@@ -11,10 +11,10 @@
|
||||
"licenseListVersion": "3.15"
|
||||
},
|
||||
"dataLicense": "CC0-1.0",
|
||||
"documentNamespace": "https://anchore.com/syft/image/user-image-input-e3b7637c-9b2f-4005-a683-58e60f979082",
|
||||
"documentNamespace": "https://anchore.com/syft/image/user-image-input-174da656-1824-4bd3-8604-28919f8a65bc",
|
||||
"packages": [
|
||||
{
|
||||
"SPDXID": "SPDXRef-888661d4f0362f02",
|
||||
"SPDXID": "SPDXRef-d16127444133b5c1",
|
||||
"name": "package-1",
|
||||
"licenseConcluded": "MIT",
|
||||
"downloadLocation": "NOASSERTION",
|
||||
@@ -36,7 +36,7 @@
|
||||
"versionInfo": "1.0.1"
|
||||
},
|
||||
{
|
||||
"SPDXID": "SPDXRef-4068ff5e8926b305",
|
||||
"SPDXID": "SPDXRef-24907357f3705420",
|
||||
"name": "package-2",
|
||||
"licenseConcluded": "NONE",
|
||||
"downloadLocation": "NOASSERTION",
|
||||
BIN
Binary file not shown.
@@ -8,21 +8,23 @@ import (
|
||||
"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,
|
||||
Format().Presenter(testutils.DirectoryInput(t)),
|
||||
testutils.AssertEncoderAgainstGoldenSnapshot(t,
|
||||
Format(),
|
||||
testutils.DirectoryInput(t),
|
||||
*updateSpdxTagValue,
|
||||
spdxTagValueRedactor,
|
||||
)
|
||||
}
|
||||
|
||||
func TestSPDXTagValueImagePresenter(t *testing.T) {
|
||||
func TestSPDXTagValueImageEncoder(t *testing.T) {
|
||||
testImage := "image-simple"
|
||||
testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
|
||||
Format().Presenter(testutils.ImageInput(t, testImage, testutils.FromSnapshot())),
|
||||
testutils.AssertEncoderAgainstGoldenImageSnapshot(t,
|
||||
Format(),
|
||||
testutils.ImageInput(t, testImage, testutils.FromSnapshot()),
|
||||
testImage,
|
||||
*updateSpdxTagValue,
|
||||
spdxTagValueRedactor,
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
func encoder(output io.Writer, s sbom.SBOM) error {
|
||||
doc := ToFormatModel(s)
|
||||
doc := toFormatModel(s)
|
||||
|
||||
enc := json.NewEncoder(output)
|
||||
// prevent > and < from being escaped in the payload
|
||||
|
||||
@@ -16,19 +16,21 @@ import (
|
||||
"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) {
|
||||
testutils.AssertPresenterAgainstGoldenSnapshot(t,
|
||||
Format().Presenter(testutils.DirectoryInput(t)),
|
||||
func TestDirectoryEncoder(t *testing.T) {
|
||||
testutils.AssertEncoderAgainstGoldenSnapshot(t,
|
||||
Format(),
|
||||
testutils.DirectoryInput(t),
|
||||
*updateJson,
|
||||
)
|
||||
}
|
||||
|
||||
func TestImagePresenter(t *testing.T) {
|
||||
func TestImageEncoder(t *testing.T) {
|
||||
testImage := "image-simple"
|
||||
testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
|
||||
Format().Presenter(testutils.ImageInput(t, testImage, testutils.FromSnapshot())),
|
||||
testutils.AssertEncoderAgainstGoldenImageSnapshot(t,
|
||||
Format(),
|
||||
testutils.ImageInput(t, testImage, testutils.FromSnapshot()),
|
||||
testImage,
|
||||
*updateJson,
|
||||
)
|
||||
@@ -192,8 +194,9 @@ func TestEncodeFullJSONDocument(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
testutils.AssertPresenterAgainstGoldenSnapshot(t,
|
||||
Format().Presenter(s),
|
||||
testutils.AssertEncoderAgainstGoldenSnapshot(t,
|
||||
Format(),
|
||||
s,
|
||||
*updateJson,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/anchore/syft/syft/source"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
// Package represents a pkg.Package object specialized for JSON marshaling and unmarshalling.
|
||||
|
||||
+4
-4
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"artifacts": [
|
||||
{
|
||||
"id": "2a115ac97d018a0e",
|
||||
"id": "1d97af55efe9512f",
|
||||
"name": "package-1",
|
||||
"version": "1.0.1",
|
||||
"type": "python",
|
||||
@@ -36,7 +36,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "5e920b2bece2c3ae",
|
||||
"id": "ad3d1c4abd84bf75",
|
||||
"name": "package-2",
|
||||
"version": "2.0.1",
|
||||
"type": "deb",
|
||||
@@ -83,7 +83,7 @@
|
||||
}
|
||||
},
|
||||
"schema": {
|
||||
"version": "2.0.1",
|
||||
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-2.0.1.json"
|
||||
"version": "2.0.2",
|
||||
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-2.0.2.json"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"artifacts": [
|
||||
{
|
||||
"id": "962403cfb7be50d7",
|
||||
"id": "d9a7c58726ab4bef",
|
||||
"name": "package-1",
|
||||
"version": "1.0.1",
|
||||
"type": "python",
|
||||
@@ -31,7 +31,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "b11f44847bba0ed1",
|
||||
"id": "ac462e450060da2c",
|
||||
"name": "package-2",
|
||||
"version": "2.0.1",
|
||||
"type": "deb",
|
||||
@@ -62,8 +62,8 @@
|
||||
],
|
||||
"artifactRelationships": [
|
||||
{
|
||||
"parent": "962403cfb7be50d7",
|
||||
"child": "b11f44847bba0ed1",
|
||||
"parent": "",
|
||||
"child": "",
|
||||
"type": "ownership-by-file-overlap",
|
||||
"metadata": {
|
||||
"file": "path"
|
||||
@@ -179,7 +179,7 @@
|
||||
}
|
||||
},
|
||||
"schema": {
|
||||
"version": "2.0.1",
|
||||
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-2.0.1.json"
|
||||
"version": "2.0.2",
|
||||
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-2.0.2.json"
|
||||
}
|
||||
}
|
||||
|
||||
+11
-11
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"artifacts": [
|
||||
{
|
||||
"id": "888661d4f0362f02",
|
||||
"id": "d16127444133b5c1",
|
||||
"name": "package-1",
|
||||
"version": "1.0.1",
|
||||
"type": "python",
|
||||
@@ -9,7 +9,7 @@
|
||||
"locations": [
|
||||
{
|
||||
"path": "/somefile-1.txt",
|
||||
"layerID": "sha256:fb6beecb75b39f4bb813dbf177e501edd5ddb3e69bb45cedeb78c676ee1b7a59"
|
||||
"layerID": "sha256:16e64541f2ddf59a90391ce7bb8af90313f7d373f2105d88f3d3267b72e0ebab"
|
||||
}
|
||||
],
|
||||
"licenses": [
|
||||
@@ -32,7 +32,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "4068ff5e8926b305",
|
||||
"id": "24907357f3705420",
|
||||
"name": "package-2",
|
||||
"version": "2.0.1",
|
||||
"type": "deb",
|
||||
@@ -40,7 +40,7 @@
|
||||
"locations": [
|
||||
{
|
||||
"path": "/somefile-2.txt",
|
||||
"layerID": "sha256:319b588ce64253a87b533c8ed01cf0025e0eac98e7b516e12532957e1244fdec"
|
||||
"layerID": "sha256:de6c235f76ea24c8503ec08891445b5d6a8bdf8249117ed8d8b0b6fb3ebe4f67"
|
||||
}
|
||||
],
|
||||
"licenses": [],
|
||||
@@ -67,7 +67,7 @@
|
||||
"type": "image",
|
||||
"target": {
|
||||
"userInput": "user-image-input",
|
||||
"imageID": "sha256:2480160b55bec40c44d3b145c7b2c1c47160db8575c3dcae086d76b9370ae7ca",
|
||||
"imageID": "sha256:9624b89704d23fa5f61b427379d172dac91dc7a508c4d7dea7aed0e04a4cf39e",
|
||||
"manifestDigest": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368",
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"tags": [
|
||||
@@ -77,17 +77,17 @@
|
||||
"layers": [
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"digest": "sha256:fb6beecb75b39f4bb813dbf177e501edd5ddb3e69bb45cedeb78c676ee1b7a59",
|
||||
"digest": "sha256:16e64541f2ddf59a90391ce7bb8af90313f7d373f2105d88f3d3267b72e0ebab",
|
||||
"size": 22
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"digest": "sha256:319b588ce64253a87b533c8ed01cf0025e0eac98e7b516e12532957e1244fdec",
|
||||
"digest": "sha256:de6c235f76ea24c8503ec08891445b5d6a8bdf8249117ed8d8b0b6fb3ebe4f67",
|
||||
"size": 16
|
||||
}
|
||||
],
|
||||
"manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjo2NjcsImRpZ2VzdCI6InNoYTI1NjoyNDgwMTYwYjU1YmVjNDBjNDRkM2IxNDVjN2IyYzFjNDcxNjBkYjg1NzVjM2RjYWUwODZkNzZiOTM3MGFlN2NhIn0sImxheWVycyI6W3sibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjIwNDgsImRpZ2VzdCI6InNoYTI1NjpmYjZiZWVjYjc1YjM5ZjRiYjgxM2RiZjE3N2U1MDFlZGQ1ZGRiM2U2OWJiNDVjZWRlYjc4YzY3NmVlMWI3YTU5In0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6MjA0OCwiZGlnZXN0Ijoic2hhMjU2OjMxOWI1ODhjZTY0MjUzYTg3YjUzM2M4ZWQwMWNmMDAyNWUwZWFjOThlN2I1MTZlMTI1MzI5NTdlMTI0NGZkZWMifV19",
|
||||
"config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiV29ya2luZ0RpciI6Ii8iLCJPbkJ1aWxkIjpudWxsfSwiY3JlYXRlZCI6IjIwMjEtMTAtMDRUMTE6NDA6MDAuNjM4Mzk0NVoiLCJoaXN0b3J5IjpbeyJjcmVhdGVkIjoiMjAyMS0xMC0wNFQxMTo0MDowMC41OTA3MzE2WiIsImNyZWF0ZWRfYnkiOiJBREQgZmlsZS0xLnR4dCAvc29tZWZpbGUtMS50eHQgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn0seyJjcmVhdGVkIjoiMjAyMS0xMC0wNFQxMTo0MDowMC42MzgzOTQ1WiIsImNyZWF0ZWRfYnkiOiJBREQgZmlsZS0yLnR4dCAvc29tZWZpbGUtMi50eHQgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn1dLCJvcyI6ImxpbnV4Iiwicm9vdGZzIjp7InR5cGUiOiJsYXllcnMiLCJkaWZmX2lkcyI6WyJzaGEyNTY6ZmI2YmVlY2I3NWIzOWY0YmI4MTNkYmYxNzdlNTAxZWRkNWRkYjNlNjliYjQ1Y2VkZWI3OGM2NzZlZTFiN2E1OSIsInNoYTI1NjozMTliNTg4Y2U2NDI1M2E4N2I1MzNjOGVkMDFjZjAwMjVlMGVhYzk4ZTdiNTE2ZTEyNTMyOTU3ZTEyNDRmZGVjIl19fQ==",
|
||||
"manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjo2NjcsImRpZ2VzdCI6InNoYTI1Njo5NjI0Yjg5NzA0ZDIzZmE1ZjYxYjQyNzM3OWQxNzJkYWM5MWRjN2E1MDhjNGQ3ZGVhN2FlZDBlMDRhNGNmMzllIn0sImxheWVycyI6W3sibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjIwNDgsImRpZ2VzdCI6InNoYTI1NjoxNmU2NDU0MWYyZGRmNTlhOTAzOTFjZTdiYjhhZjkwMzEzZjdkMzczZjIxMDVkODhmM2QzMjY3YjcyZTBlYmFiIn0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6MjA0OCwiZGlnZXN0Ijoic2hhMjU2OmRlNmMyMzVmNzZlYTI0Yzg1MDNlYzA4ODkxNDQ1YjVkNmE4YmRmODI0OTExN2VkOGQ4YjBiNmZiM2ViZTRmNjcifV19",
|
||||
"config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiV29ya2luZ0RpciI6Ii8iLCJPbkJ1aWxkIjpudWxsfSwiY3JlYXRlZCI6IjIwMjEtMTItMDFUMTI6MTM6NDMuNDAxMzkxNFoiLCJoaXN0b3J5IjpbeyJjcmVhdGVkIjoiMjAyMS0xMi0wMVQxMjoxMzo0My4zNDM2MzQyWiIsImNyZWF0ZWRfYnkiOiJBREQgZmlsZS0xLnR4dCAvc29tZWZpbGUtMS50eHQgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn0seyJjcmVhdGVkIjoiMjAyMS0xMi0wMVQxMjoxMzo0My40MDEzOTE0WiIsImNyZWF0ZWRfYnkiOiJBREQgZmlsZS0yLnR4dCAvc29tZWZpbGUtMi50eHQgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn1dLCJvcyI6ImxpbnV4Iiwicm9vdGZzIjp7InR5cGUiOiJsYXllcnMiLCJkaWZmX2lkcyI6WyJzaGEyNTY6MTZlNjQ1NDFmMmRkZjU5YTkwMzkxY2U3YmI4YWY5MDMxM2Y3ZDM3M2YyMTA1ZDg4ZjNkMzI2N2I3MmUwZWJhYiIsInNoYTI1NjpkZTZjMjM1Zjc2ZWEyNGM4NTAzZWMwODg5MTQ0NWI1ZDZhOGJkZjgyNDkxMTdlZDhkOGIwYjZmYjNlYmU0ZjY3Il19fQ==",
|
||||
"repoDigests": []
|
||||
}
|
||||
},
|
||||
@@ -104,7 +104,7 @@
|
||||
}
|
||||
},
|
||||
"schema": {
|
||||
"version": "2.0.1",
|
||||
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-2.0.1.json"
|
||||
"version": "2.0.2",
|
||||
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-2.0.2.json"
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
@@ -19,8 +19,7 @@ import (
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
// TODO: this is exported for the use of the power-user command (temp)
|
||||
func ToFormatModel(s sbom.SBOM) model.Document {
|
||||
func toFormatModel(s sbom.SBOM) model.Document {
|
||||
src, err := toSourceModel(s.Source)
|
||||
if err != nil {
|
||||
log.Warnf("unable to create syft-json source object: %+v", err)
|
||||
@@ -143,7 +142,7 @@ func toPackageModels(catalog *pkg.Catalog) []model.Package {
|
||||
func toPackageModel(p pkg.Package) model.Package {
|
||||
var cpes = make([]string, len(p.CPEs))
|
||||
for i, c := range p.CPEs {
|
||||
cpes[i] = c.BindToFmtString()
|
||||
cpes[i] = pkg.CPEString(c)
|
||||
}
|
||||
|
||||
var licenses = make([]string, 0)
|
||||
|
||||
@@ -10,9 +10,10 @@ import (
|
||||
|
||||
var updateTableGoldenFiles = flag.Bool("update-table", false, "update the *.golden files for table format")
|
||||
|
||||
func TestTablePresenter(t *testing.T) {
|
||||
testutils.AssertPresenterAgainstGoldenSnapshot(t,
|
||||
Format().Presenter(testutils.DirectoryInput(t)),
|
||||
func TestTableEncoder(t *testing.T) {
|
||||
testutils.AssertEncoderAgainstGoldenSnapshot(t,
|
||||
Format(),
|
||||
testutils.DirectoryInput(t),
|
||||
*updateTableGoldenFiles,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,20 +7,22 @@ import (
|
||||
"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) {
|
||||
testutils.AssertPresenterAgainstGoldenSnapshot(t,
|
||||
Format().Presenter(testutils.DirectoryInput(t)),
|
||||
*updateTextPresenterGoldenFiles,
|
||||
func TestTextDirectoryEncoder(t *testing.T) {
|
||||
testutils.AssertEncoderAgainstGoldenSnapshot(t,
|
||||
Format(),
|
||||
testutils.DirectoryInput(t),
|
||||
*updateTextEncoderGoldenFiles,
|
||||
)
|
||||
}
|
||||
|
||||
func TestTextImagePresenter(t *testing.T) {
|
||||
func TestTextImageEncoder(t *testing.T) {
|
||||
testImage := "image-simple"
|
||||
testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
|
||||
Format().Presenter(testutils.ImageInput(t, testImage, testutils.FromSnapshot())),
|
||||
testutils.AssertEncoderAgainstGoldenImageSnapshot(t,
|
||||
Format(),
|
||||
testutils.ImageInput(t, testImage, testutils.FromSnapshot()),
|
||||
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 (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
)
|
||||
|
||||
// handleCatalogerPresenterReady is a UI function for processing the CatalogerFinished bus event, displaying the catalog
|
||||
// via the given presenter to stdout.
|
||||
func handleCatalogerPresenterReady(event partybus.Event, reportOutput io.Writer) error {
|
||||
// handleExit is a UI function for processing the Exit bus event,
|
||||
// and calling the given function to output the contents.
|
||||
func handleExit(event partybus.Event) error {
|
||||
// show the report to stdout
|
||||
pres, err := syftEventParsers.ParsePresenterReady(event)
|
||||
fn, err := syftEventParsers.ParseExit(event)
|
||||
if err != nil {
|
||||
return fmt.Errorf("bad CatalogerFinished event: %w", err)
|
||||
}
|
||||
|
||||
if err := pres.Present(reportOutput); err != nil {
|
||||
return fmt.Errorf("unable to show package catalog report: %w", err)
|
||||
if err := fn(); err != nil {
|
||||
return fmt.Errorf("unable to show package catalog report: %v", err)
|
||||
}
|
||||
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
|
||||
// processing an event before the ETUI exits (coordinated with a sync.WaitGroup)
|
||||
type ephemeralTerminalUI struct {
|
||||
unsubscribe func() error
|
||||
handler *ui.Handler
|
||||
waitGroup *sync.WaitGroup
|
||||
frame *frame.Frame
|
||||
logBuffer *bytes.Buffer
|
||||
uiOutput *os.File
|
||||
reportOutput io.Writer
|
||||
unsubscribe func() error
|
||||
handler *ui.Handler
|
||||
waitGroup *sync.WaitGroup
|
||||
frame *frame.Frame
|
||||
logBuffer *bytes.Buffer
|
||||
uiOutput *os.File
|
||||
}
|
||||
|
||||
// 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{
|
||||
handler: ui.NewHandler(),
|
||||
waitGroup: &sync.WaitGroup{},
|
||||
uiOutput: os.Stderr,
|
||||
reportOutput: reportWriter,
|
||||
handler: ui.NewHandler(),
|
||||
waitGroup: &sync.WaitGroup{},
|
||||
uiOutput: os.Stderr,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,12 +80,12 @@ func (h *ephemeralTerminalUI) Handle(event partybus.Event) error {
|
||||
log.Errorf("unable to show %s event: %+v", event.Type, err)
|
||||
}
|
||||
|
||||
case event.Type == syftEvent.PresenterReady:
|
||||
// we need to close the screen now since signaling the the presenter is ready means that we
|
||||
case event.Type == syftEvent.Exit:
|
||||
// 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
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
syftEvent "github.com/anchore/syft/syft/event"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
)
|
||||
|
||||
type loggerUI struct {
|
||||
unsubscribe func() error
|
||||
reportOutput io.Writer
|
||||
unsubscribe func() error
|
||||
}
|
||||
|
||||
// NewLoggerUI writes all events to the common application logger and writes the final report to the given writer.
|
||||
func NewLoggerUI(reportWriter io.Writer) UI {
|
||||
return &loggerUI{
|
||||
reportOutput: reportWriter,
|
||||
}
|
||||
func NewLoggerUI() UI {
|
||||
return &loggerUI{}
|
||||
}
|
||||
|
||||
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 {
|
||||
// ignore all events except for the final event
|
||||
if event.Type != syftEvent.PresenterReady {
|
||||
if event.Type != syftEvent.Exit {
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"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
|
||||
// are environmental problems (e.g. cannot write to the terminal). A writer is provided to capture the output of
|
||||
// the final SBOM report.
|
||||
func Select(verbose, quiet bool, reportWriter io.Writer) (uis []UI) {
|
||||
func Select(verbose, quiet bool) (uis []UI) {
|
||||
isStdoutATty := term.IsTerminal(int(os.Stdout.Fd()))
|
||||
isStderrATty := term.IsTerminal(int(os.Stderr.Fd()))
|
||||
notATerminal := !isStderrATty && !isStdoutATty
|
||||
|
||||
switch {
|
||||
case runtime.GOOS == "windows" || verbose || quiet || notATerminal || !isStderrATty:
|
||||
uis = append(uis, NewLoggerUI(reportWriter))
|
||||
uis = append(uis, NewLoggerUI())
|
||||
default:
|
||||
uis = append(uis, NewEphemeralTerminalUI(reportWriter), NewLoggerUI(reportWriter))
|
||||
uis = append(uis, NewEphemeralTerminalUI())
|
||||
}
|
||||
|
||||
return uis
|
||||
|
||||
@@ -3,15 +3,11 @@
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
// 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
|
||||
// is intended to be used and the UIs that follow are meant to be attempted only in a fallback posture when there
|
||||
// are environmental problems (e.g. cannot write to the terminal). A writer is provided to capture the output of
|
||||
// the final SBOM report.
|
||||
func Select(verbose, quiet bool, reportWriter io.Writer) (uis []UI) {
|
||||
return append(uis, NewLoggerUI(reportWriter))
|
||||
func Select(verbose, quiet bool) (uis []UI) {
|
||||
return append(uis, NewLoggerUI())
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -13,7 +13,7 @@ type Identifiable interface {
|
||||
ID() ID
|
||||
}
|
||||
|
||||
func IDFromHash(obj interface{}) (ID, error) {
|
||||
func IDByHash(obj interface{}) (ID, error) {
|
||||
f, err := hashstructure.Hash(obj, hashstructure.FormatV2, &hashstructure.HashOptions{
|
||||
ZeroNil: true,
|
||||
SlicesAsSets: true,
|
||||
|
||||
+2
-2
@@ -26,8 +26,8 @@ const (
|
||||
// FileIndexingStarted is a partybus event that occurs when the directory resolver begins indexing a filesystem
|
||||
FileIndexingStarted partybus.EventType = "syft-file-indexing-started-event"
|
||||
|
||||
// PresenterReady is a partybus event that occurs when an analysis result is ready for final presentation
|
||||
PresenterReady partybus.EventType = "syft-presenter-ready-event"
|
||||
// Exit is a partybus event that occurs when an analysis result is ready for final presentation
|
||||
Exit partybus.EventType = "syft-exit-event"
|
||||
|
||||
// ImportStarted is a partybus event that occurs when an SBOM upload process has begun
|
||||
ImportStarted partybus.EventType = "syft-import-started-event"
|
||||
|
||||
@@ -6,7 +6,6 @@ package parsers
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/anchore/go-presenter"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger"
|
||||
@@ -109,17 +108,17 @@ func ParseFileIndexingStarted(e partybus.Event) (string, progress.StagedProgress
|
||||
return path, prog, nil
|
||||
}
|
||||
|
||||
func ParsePresenterReady(e partybus.Event) (presenter.Presenter, error) {
|
||||
if err := checkEventType(e.Type, event.PresenterReady); err != nil {
|
||||
func ParseExit(e partybus.Event) (func() error, error) {
|
||||
if err := checkEventType(e.Type, event.Exit); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pres, ok := e.Value.(presenter.Presenter)
|
||||
fn, ok := e.Value.(func() error)
|
||||
if !ok {
|
||||
return nil, newPayloadErr(e.Type, "Value", e.Value)
|
||||
}
|
||||
|
||||
return pres, nil
|
||||
return fn, nil
|
||||
}
|
||||
|
||||
func ParseAppUpdateAvailable(e partybus.Event) (string, error) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/bmatcuk/doublestar/v2"
|
||||
"github.com/bmatcuk/doublestar/v4"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
)
|
||||
|
||||
|
||||
@@ -50,10 +50,3 @@ func (f Format) Validate(reader io.Reader) error {
|
||||
|
||||
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:
|
||||
|
||||
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
|
||||
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),
|
||||
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
|
||||
// (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.
|
||||
func CatalogPackages(src *source.Source, scope source.Scope) (*pkg.Catalog, []artifact.Relationship, *distro.Distro, error) {
|
||||
resolver, err := src.FileResolver(scope)
|
||||
func CatalogPackages(src *source.Source, cfg cataloger.Config) (*pkg.Catalog, []artifact.Relationship, *distro.Distro, error) {
|
||||
resolver, err := src.FileResolver(cfg.Search.Scope)
|
||||
if err != nil {
|
||||
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 {
|
||||
case source.ImageScheme:
|
||||
log.Info("cataloging image")
|
||||
catalogers = cataloger.ImageCatalogers()
|
||||
catalogers = cataloger.ImageCatalogers(cfg)
|
||||
case source.FileScheme:
|
||||
log.Info("cataloging file")
|
||||
catalogers = cataloger.AllCatalogers()
|
||||
catalogers = cataloger.AllCatalogers(cfg)
|
||||
case source.DirectoryScheme:
|
||||
log.Info("cataloging directory")
|
||||
catalogers = cataloger.DirectoryCatalogers()
|
||||
catalogers = cataloger.DirectoryCatalogers(cfg)
|
||||
default:
|
||||
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 {
|
||||
t.Run(strings.Join(test.expected, ","), func(t *testing.T) {
|
||||
var i interface{}
|
||||
i = test.metadata
|
||||
actual := i.(FileOwner).OwnedFiles()
|
||||
actual := test.metadata.OwnedFiles()
|
||||
for _, d := range deep.Equal(test.expected, actual) {
|
||||
t.Errorf("diff: %+v", d)
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ type CargoPackageMetadata struct {
|
||||
}
|
||||
|
||||
// Pkg returns the standard `pkg.Package` representation of the package referenced within the Cargo.lock metadata.
|
||||
func (p CargoPackageMetadata) Pkg() Package {
|
||||
return Package{
|
||||
func (p CargoPackageMetadata) Pkg() *Package {
|
||||
return &Package{
|
||||
Name: p.Name,
|
||||
Version: p.Version,
|
||||
Language: Rust,
|
||||
|
||||
+9
-6
@@ -4,12 +4,10 @@ import (
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/jinzhu/copier"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
)
|
||||
|
||||
// Catalog represents a collection of Packages.
|
||||
@@ -51,6 +49,7 @@ func (c *Catalog) Package(id artifact.ID) *Package {
|
||||
log.Warnf("unable to copy package id=%q name=%q: %+v", id, v.Name, err)
|
||||
return nil
|
||||
}
|
||||
p.id = v.id
|
||||
return &p
|
||||
}
|
||||
|
||||
@@ -75,8 +74,12 @@ func (c *Catalog) Add(p Package) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
// note: since we are capturing the ID, we cannot modify the package being added from this point forward
|
||||
id := p.ID()
|
||||
if id == "" {
|
||||
log.Warnf("found package with empty ID while adding to the catalog: %+v", p)
|
||||
p.SetID()
|
||||
id = p.ID()
|
||||
}
|
||||
|
||||
// store by package ID
|
||||
c.byID[id] = p
|
||||
@@ -142,7 +145,7 @@ func (c *Catalog) Sorted(types ...Type) (pkgs []Package) {
|
||||
sort.SliceStable(pkgs, func(i, j int) bool {
|
||||
if pkgs[i].Name == pkgs[j].Name {
|
||||
if pkgs[i].Version == pkgs[j].Version {
|
||||
if pkgs[i].Type == pkgs[j].Type {
|
||||
if pkgs[i].Type == pkgs[j].Type && len(pkgs[i].Locations) > 0 && len(pkgs[j].Locations) > 0 {
|
||||
return pkgs[i].Locations[0].String() < pkgs[j].Locations[0].String()
|
||||
}
|
||||
return pkgs[i].Type < pkgs[j].Type
|
||||
|
||||
+26
-27
@@ -10,23 +10,6 @@ import (
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
var catalogAddAndRemoveTestPkgs = []Package{
|
||||
{
|
||||
Locations: []source.Location{
|
||||
source.NewVirtualLocation("/a/path", "/another/path"),
|
||||
source.NewVirtualLocation("/b/path", "/bee/path"),
|
||||
},
|
||||
Type: RpmPkg,
|
||||
},
|
||||
{
|
||||
Locations: []source.Location{
|
||||
source.NewVirtualLocation("/c/path", "/another/path"),
|
||||
source.NewVirtualLocation("/d/path", "/another/path"),
|
||||
},
|
||||
Type: NpmPkg,
|
||||
},
|
||||
}
|
||||
|
||||
type expectedIndexes struct {
|
||||
byType map[Type]*strset.Set
|
||||
byPath map[string]*strset.Set
|
||||
@@ -34,18 +17,38 @@ type expectedIndexes struct {
|
||||
|
||||
func TestCatalogAddPopulatesIndex(t *testing.T) {
|
||||
|
||||
var pkgs = []Package{
|
||||
{
|
||||
Locations: []source.Location{
|
||||
source.NewVirtualLocation("/a/path", "/another/path"),
|
||||
source.NewVirtualLocation("/b/path", "/bee/path"),
|
||||
},
|
||||
Type: RpmPkg,
|
||||
},
|
||||
{
|
||||
Locations: []source.Location{
|
||||
source.NewVirtualLocation("/c/path", "/another/path"),
|
||||
source.NewVirtualLocation("/d/path", "/another/path"),
|
||||
},
|
||||
Type: NpmPkg,
|
||||
},
|
||||
}
|
||||
|
||||
for i := range pkgs {
|
||||
p := &pkgs[i]
|
||||
p.SetID()
|
||||
}
|
||||
|
||||
fixtureID := func(i int) string {
|
||||
return string(catalogAddAndRemoveTestPkgs[i].ID())
|
||||
return string(pkgs[i].ID())
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
pkgs []Package
|
||||
expectedIndexes expectedIndexes
|
||||
}{
|
||||
{
|
||||
name: "vanilla-add",
|
||||
pkgs: catalogAddAndRemoveTestPkgs,
|
||||
expectedIndexes: expectedIndexes{
|
||||
byType: map[Type]*strset.Set{
|
||||
RpmPkg: strset.New(fixtureID(0)),
|
||||
@@ -65,7 +68,7 @@ func TestCatalogAddPopulatesIndex(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
c := NewCatalog(test.pkgs...)
|
||||
c := NewCatalog(pkgs...)
|
||||
|
||||
assertIndexes(t, c, test.expectedIndexes)
|
||||
|
||||
@@ -75,9 +78,7 @@ func TestCatalogAddPopulatesIndex(t *testing.T) {
|
||||
|
||||
func assertIndexes(t *testing.T, c *Catalog, expectedIndexes expectedIndexes) {
|
||||
// assert path index
|
||||
if len(c.idsByPath) != len(expectedIndexes.byPath) {
|
||||
t.Errorf("unexpected path index length: %d != %d", len(c.idsByPath), len(expectedIndexes.byPath))
|
||||
}
|
||||
assert.Len(t, c.idsByPath, len(expectedIndexes.byPath), "unexpected path index length")
|
||||
for path, expectedIds := range expectedIndexes.byPath {
|
||||
actualIds := strset.New()
|
||||
for _, p := range c.PackagesByPath(path) {
|
||||
@@ -90,9 +91,7 @@ func assertIndexes(t *testing.T, c *Catalog, expectedIndexes expectedIndexes) {
|
||||
}
|
||||
|
||||
// assert type index
|
||||
if len(c.idsByType) != len(expectedIndexes.byType) {
|
||||
t.Errorf("unexpected type index length: %d != %d", len(c.idsByType), len(expectedIndexes.byType))
|
||||
}
|
||||
assert.Len(t, c.idsByType, len(expectedIndexes.byType), "unexpected type index length")
|
||||
for ty, expectedIds := range expectedIndexes.byType {
|
||||
actualIds := strset.New()
|
||||
for p := range c.Enumerate(ty) {
|
||||
|
||||
@@ -21,14 +21,25 @@ import (
|
||||
// integrity check
|
||||
var _ common.ParserFn = parseApkDB
|
||||
|
||||
func newApkDBPackage(d *pkg.ApkMetadata) *pkg.Package {
|
||||
return &pkg.Package{
|
||||
Name: d.Package,
|
||||
Version: d.Version,
|
||||
Licenses: strings.Split(d.License, " "),
|
||||
Type: pkg.ApkPkg,
|
||||
MetadataType: pkg.ApkMetadataType,
|
||||
Metadata: *d,
|
||||
}
|
||||
}
|
||||
|
||||
// parseApkDb parses individual packages from a given Alpine DB file. For more information on specific fields
|
||||
// see https://wiki.alpinelinux.org/wiki/Apk_spec .
|
||||
func parseApkDB(_ string, reader io.Reader) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
func parseApkDB(_ string, reader io.Reader) ([]*pkg.Package, []artifact.Relationship, error) {
|
||||
// larger capacity for the scanner.
|
||||
const maxScannerCapacity = 1024 * 1024
|
||||
// a new larger buffer for the scanner
|
||||
bufScan := make([]byte, maxScannerCapacity)
|
||||
packages := make([]pkg.Package, 0)
|
||||
packages := make([]*pkg.Package, 0)
|
||||
|
||||
scanner := bufio.NewScanner(reader)
|
||||
scanner.Buffer(bufScan, maxScannerCapacity)
|
||||
@@ -52,14 +63,7 @@ func parseApkDB(_ string, reader io.Reader) ([]pkg.Package, []artifact.Relations
|
||||
return nil, nil, err
|
||||
}
|
||||
if metadata != nil {
|
||||
packages = append(packages, pkg.Package{
|
||||
Name: metadata.Package,
|
||||
Version: metadata.Version,
|
||||
Licenses: strings.Split(metadata.License, " "),
|
||||
Type: pkg.ApkPkg,
|
||||
MetadataType: pkg.ApkMetadataType,
|
||||
Metadata: *metadata,
|
||||
})
|
||||
packages = append(packages, newApkDBPackage(metadata))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -647,11 +647,11 @@ func TestSinglePackageDetails(t *testing.T) {
|
||||
func TestMultiplePackages(t *testing.T) {
|
||||
tests := []struct {
|
||||
fixture string
|
||||
expected []pkg.Package
|
||||
expected []*pkg.Package
|
||||
}{
|
||||
{
|
||||
fixture: "test-fixtures/multiple",
|
||||
expected: []pkg.Package{
|
||||
expected: []*pkg.Package{
|
||||
{
|
||||
Name: "libc-utils",
|
||||
Version: "0.7.2-r0",
|
||||
|
||||
@@ -49,10 +49,10 @@ func Catalog(resolver source.FileResolver, theDistro *distro.Distro, catalogers
|
||||
|
||||
// perform analysis, accumulating errors for each failed analysis
|
||||
var errs error
|
||||
for _, theCataloger := range catalogers {
|
||||
for _, c := range catalogers {
|
||||
// find packages from the underlying raw data
|
||||
log.Debugf("cataloging with %q", theCataloger.Name())
|
||||
packages, relationships, err := theCataloger.Catalog(resolver)
|
||||
log.Debugf("cataloging with %q", c.Name())
|
||||
packages, relationships, err := c.Catalog(resolver)
|
||||
if err != nil {
|
||||
errs = multierror.Append(errs, err)
|
||||
continue
|
||||
@@ -64,10 +64,10 @@ func Catalog(resolver source.FileResolver, theDistro *distro.Distro, catalogers
|
||||
packagesDiscovered.N += int64(catalogedPackages)
|
||||
|
||||
for _, p := range packages {
|
||||
// generate CPEs
|
||||
// generate CPEs (note: this is excluded from package ID, so is safe to mutate)
|
||||
p.CPEs = cpe.Generate(p)
|
||||
|
||||
// generate PURL
|
||||
// generate PURL (note: this is excluded from package ID, so is safe to mutate)
|
||||
p.PURL = generatePackageURL(p, theDistro)
|
||||
|
||||
// create file-to-package relationships for files owned by the package
|
||||
|
||||
@@ -32,7 +32,7 @@ type Cataloger interface {
|
||||
}
|
||||
|
||||
// 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{
|
||||
ruby.NewGemSpecCataloger(),
|
||||
python.NewPythonPackageCataloger(),
|
||||
@@ -40,14 +40,14 @@ func ImageCatalogers() []Cataloger {
|
||||
javascript.NewJavascriptPackageCataloger(),
|
||||
deb.NewDpkgdbCataloger(),
|
||||
rpmdb.NewRpmdbCataloger(),
|
||||
java.NewJavaCataloger(),
|
||||
java.NewJavaCataloger(cfg.Java()),
|
||||
apkdb.NewApkdbCataloger(),
|
||||
golang.NewGoModuleBinaryCataloger(),
|
||||
}
|
||||
}
|
||||
|
||||
// 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{
|
||||
ruby.NewGemFileLockCataloger(),
|
||||
python.NewPythonIndexCataloger(),
|
||||
@@ -56,7 +56,7 @@ func DirectoryCatalogers() []Cataloger {
|
||||
javascript.NewJavascriptLockCataloger(),
|
||||
deb.NewDpkgdbCataloger(),
|
||||
rpmdb.NewRpmdbCataloger(),
|
||||
java.NewJavaCataloger(),
|
||||
java.NewJavaCataloger(cfg.Java()),
|
||||
apkdb.NewApkdbCataloger(),
|
||||
golang.NewGoModuleBinaryCataloger(),
|
||||
golang.NewGoModFileCataloger(),
|
||||
@@ -65,7 +65,7 @@ func DirectoryCatalogers() []Cataloger {
|
||||
}
|
||||
|
||||
// AllCatalogers returns all implemented catalogers
|
||||
func AllCatalogers() []Cataloger {
|
||||
func AllCatalogers(cfg Config) []Cataloger {
|
||||
return []Cataloger{
|
||||
ruby.NewGemFileLockCataloger(),
|
||||
ruby.NewGemSpecCataloger(),
|
||||
@@ -75,7 +75,7 @@ func AllCatalogers() []Cataloger {
|
||||
javascript.NewJavascriptPackageCataloger(),
|
||||
deb.NewDpkgdbCataloger(),
|
||||
rpmdb.NewRpmdbCataloger(),
|
||||
java.NewJavaCataloger(),
|
||||
java.NewJavaCataloger(cfg.Java()),
|
||||
apkdb.NewApkdbCataloger(),
|
||||
golang.NewGoModuleBinaryCataloger(),
|
||||
golang.NewGoModFileCataloger(),
|
||||
|
||||
@@ -34,7 +34,7 @@ cpeLoop:
|
||||
}
|
||||
|
||||
func disallowNonParseableCPEs(cpe pkg.CPE, _ pkg.Package) bool {
|
||||
v := cpe.BindToFmtString()
|
||||
v := pkg.CPEString(cpe)
|
||||
_, err := pkg.NewCPE(v)
|
||||
|
||||
cannotParse := err != nil
|
||||
|
||||
@@ -19,7 +19,6 @@ func newCPE(product, vendor, version, targetSW string) wfn.Attributes {
|
||||
cpe.Vendor = vendor
|
||||
cpe.Version = version
|
||||
cpe.TargetSW = targetSW
|
||||
|
||||
return cpe
|
||||
}
|
||||
|
||||
@@ -29,7 +28,6 @@ func newCPE(product, vendor, version, targetSW string) wfn.Attributes {
|
||||
func Generate(p pkg.Package) []pkg.CPE {
|
||||
vendors := candidateVendors(p)
|
||||
products := candidateProducts(p)
|
||||
|
||||
if len(products) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -44,7 +42,6 @@ func Generate(p pkg.Package) []pkg.CPE {
|
||||
continue
|
||||
}
|
||||
keys.Add(key)
|
||||
|
||||
// add a new entry...
|
||||
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:*:*:*:*:*:*:*",
|
||||
},
|
||||
},
|
||||
{
|
||||
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",
|
||||
p: pkg.Package{
|
||||
@@ -522,7 +554,7 @@ func TestGeneratePackageCPEs(t *testing.T) {
|
||||
expectedCpeSet := set.NewStringSet(test.expected...)
|
||||
actualCpeSet := set.NewStringSet()
|
||||
for _, a := range actual {
|
||||
actualCpeSet.Add(a.BindToFmtString())
|
||||
actualCpeSet.Add(pkg.CPEString(a))
|
||||
}
|
||||
|
||||
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
|
||||
// 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()
|
||||
}
|
||||
|
||||
|
||||
@@ -45,22 +45,23 @@ func (c *GenericCataloger) Catalog(resolver source.FileResolver) ([]pkg.Package,
|
||||
contentReader, err := resolver.FileContentsByLocation(location)
|
||||
if err != nil {
|
||||
// TODO: fail or log?
|
||||
return nil, nil, fmt.Errorf("unable to fetch contents for location=%v : %w", location, err)
|
||||
return nil, nil, fmt.Errorf("unable to fetch contents at location=%v: %w", location, err)
|
||||
}
|
||||
|
||||
discoveredPackages, discoveredRelationships, err := parser(location.RealPath, contentReader)
|
||||
internal.CloseAndLogError(contentReader, location.VirtualPath)
|
||||
if err != nil {
|
||||
// TODO: should we fail? or only log?
|
||||
log.Warnf("cataloger '%s' failed to parse entries (location=%+v): %+v", c.upstreamCataloger, location, err)
|
||||
log.Warnf("cataloger '%s' failed to parse entries at location=%+v: %+v", c.upstreamCataloger, location, err)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, p := range discoveredPackages {
|
||||
p.FoundBy = c.upstreamCataloger
|
||||
p.Locations = append(p.Locations, location)
|
||||
p.SetID()
|
||||
|
||||
packages = append(packages, p)
|
||||
packages = append(packages, *p)
|
||||
}
|
||||
|
||||
relationships = append(relationships, discoveredRelationships...)
|
||||
|
||||
@@ -13,12 +13,12 @@ import (
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
func parser(_ string, reader io.Reader) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
func parser(_ string, reader io.Reader) ([]*pkg.Package, []artifact.Relationship, error) {
|
||||
contents, err := ioutil.ReadAll(reader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return []pkg.Package{
|
||||
return []*pkg.Package{
|
||||
{
|
||||
Name: string(contents),
|
||||
},
|
||||
|
||||
@@ -8,4 +8,4 @@ import (
|
||||
)
|
||||
|
||||
// ParserFn standardizes a function signature for parser functions that accept the virtual file path (not usable for file reads) and contents and return any discovered packages from that file
|
||||
type ParserFn func(string, io.Reader) ([]pkg.Package, []artifact.Relationship, error)
|
||||
type ParserFn func(string, io.Reader) ([]*pkg.Package, []artifact.Relationship, error)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -68,6 +68,8 @@ func (c *Cataloger) Catalog(resolver source.FileResolver) ([]pkg.Package, []arti
|
||||
|
||||
// fetch additional data from the copyright file to derive the license information
|
||||
addLicenses(resolver, dbLocation, p)
|
||||
|
||||
p.SetID()
|
||||
}
|
||||
|
||||
allPackages = append(allPackages, pkgs...)
|
||||
|
||||
@@ -20,6 +20,16 @@ var (
|
||||
sourceRegexp = regexp.MustCompile(`(?P<name>\S+)( \((?P<version>.*)\))?`)
|
||||
)
|
||||
|
||||
func newDpkgPackage(d pkg.DpkgMetadata) pkg.Package {
|
||||
return pkg.Package{
|
||||
Name: d.Package,
|
||||
Version: d.Version,
|
||||
Type: pkg.DebPkg,
|
||||
MetadataType: pkg.DpkgMetadataType,
|
||||
Metadata: d,
|
||||
}
|
||||
}
|
||||
|
||||
// parseDpkgStatus is a parser function for Debian DB status contents, returning all Debian packages listed.
|
||||
func parseDpkgStatus(reader io.Reader) ([]pkg.Package, error) {
|
||||
buffedReader := bufio.NewReader(reader)
|
||||
@@ -37,13 +47,7 @@ func parseDpkgStatus(reader io.Reader) ([]pkg.Package, error) {
|
||||
}
|
||||
|
||||
if entry.Package != "" {
|
||||
packages = append(packages, pkg.Package{
|
||||
Name: entry.Package,
|
||||
Version: entry.Version,
|
||||
Type: pkg.DebPkg,
|
||||
MetadataType: pkg.DpkgMetadataType,
|
||||
Metadata: entry,
|
||||
})
|
||||
packages = append(packages, newDpkgPackage(entry))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,28 @@ const (
|
||||
|
||||
type exeOpener func(file io.ReadCloser) ([]exe, error)
|
||||
|
||||
func newGoBinaryPackage(name, version, h1Digest, goVersion, architecture string, location source.Location) pkg.Package {
|
||||
p := pkg.Package{
|
||||
Name: name,
|
||||
Version: version,
|
||||
Language: pkg.Go,
|
||||
Type: pkg.GoModulePkg,
|
||||
Locations: []source.Location{
|
||||
location,
|
||||
},
|
||||
MetadataType: pkg.GolangBinMetadataType,
|
||||
Metadata: pkg.GolangBinMetadata{
|
||||
GoCompiledVersion: goVersion,
|
||||
H1Digest: h1Digest,
|
||||
Architecture: architecture,
|
||||
},
|
||||
}
|
||||
|
||||
p.SetID()
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func parseGoBin(location source.Location, reader io.ReadCloser, opener exeOpener) (pkgs []pkg.Package, err error) {
|
||||
var exes []exe
|
||||
// it has been found that there are stdlib paths within openExe that can panic. We want to prevent this behavior
|
||||
@@ -54,21 +76,10 @@ func buildGoPkgInfo(location source.Location, mod, goVersion, arch string) []pkg
|
||||
}
|
||||
|
||||
if fields[0] == packageIdentifier || fields[0] == replaceIdentifier {
|
||||
pkgsSlice = append(pkgsSlice, pkg.Package{
|
||||
Name: fields[1],
|
||||
Version: fields[2],
|
||||
Language: pkg.Go,
|
||||
Type: pkg.GoModulePkg,
|
||||
Locations: []source.Location{
|
||||
location,
|
||||
},
|
||||
MetadataType: pkg.GolangBinMetadataType,
|
||||
Metadata: pkg.GolangBinMetadata{
|
||||
GoCompiledVersion: goVersion,
|
||||
H1Digest: fields[3],
|
||||
Architecture: arch,
|
||||
},
|
||||
})
|
||||
name := fields[1]
|
||||
version := fields[2]
|
||||
h1Digest := fields[3]
|
||||
pkgsSlice = append(pkgsSlice, newGoBinaryPackage(name, version, h1Digest, goVersion, arch, location))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -147,6 +147,10 @@ func TestBuildGoPkgInfo(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
for i := range test.expected {
|
||||
p := &test.expected[i]
|
||||
p.SetID()
|
||||
}
|
||||
location := source.Location{
|
||||
Coordinates: source.Coordinates{
|
||||
RealPath: "/a-path",
|
||||
|
||||
@@ -12,8 +12,8 @@ import (
|
||||
)
|
||||
|
||||
// parseGoMod takes a go.mod and lists all packages discovered.
|
||||
func parseGoMod(path string, reader io.Reader) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
packages := make(map[string]pkg.Package)
|
||||
func parseGoMod(path string, reader io.Reader) ([]*pkg.Package, []artifact.Relationship, error) {
|
||||
packages := make(map[string]*pkg.Package)
|
||||
|
||||
contents, err := ioutil.ReadAll(reader)
|
||||
if err != nil {
|
||||
@@ -26,7 +26,7 @@ func parseGoMod(path string, reader io.Reader) ([]pkg.Package, []artifact.Relati
|
||||
}
|
||||
|
||||
for _, m := range file.Require {
|
||||
packages[m.Mod.Path] = pkg.Package{
|
||||
packages[m.Mod.Path] = &pkg.Package{
|
||||
Name: m.Mod.Path,
|
||||
Version: m.Mod.Version,
|
||||
Language: pkg.Go,
|
||||
@@ -36,7 +36,7 @@ func parseGoMod(path string, reader io.Reader) ([]pkg.Package, []artifact.Relati
|
||||
|
||||
// remove any old packages and replace with new ones...
|
||||
for _, m := range file.Replace {
|
||||
packages[m.New.Path] = pkg.Package{
|
||||
packages[m.New.Path] = &pkg.Package{
|
||||
Name: m.New.Path,
|
||||
Version: m.New.Version,
|
||||
Language: pkg.Go,
|
||||
@@ -49,7 +49,7 @@ func parseGoMod(path string, reader io.Reader) ([]pkg.Package, []artifact.Relati
|
||||
delete(packages, m.Mod.Path)
|
||||
}
|
||||
|
||||
pkgsSlice := make([]pkg.Package, len(packages))
|
||||
pkgsSlice := make([]*pkg.Package, len(packages))
|
||||
idx := 0
|
||||
for _, p := range packages {
|
||||
pkgsSlice[idx] = p
|
||||
|
||||
@@ -87,7 +87,7 @@ func TestParseGoMod(t *testing.T) {
|
||||
continue
|
||||
}
|
||||
|
||||
diffs := deep.Equal(a, e)
|
||||
diffs := deep.Equal(a, &e)
|
||||
if len(diffs) > 0 {
|
||||
t.Errorf("diffs found for %q", a.Name)
|
||||
for _, d := range diffs {
|
||||
|
||||
@@ -113,7 +113,7 @@ func (a archiveFilename) extension() string {
|
||||
|
||||
func (a archiveFilename) pkgType() pkg.Type {
|
||||
switch strings.ToLower(a.extension()) {
|
||||
case "jar", "war", "ear":
|
||||
case "jar", "war", "ear", "lpkg", "par":
|
||||
return pkg.JavaPkg
|
||||
case "jpi", "hpi":
|
||||
return pkg.JenkinsPluginPkg
|
||||
|
||||
@@ -36,6 +36,13 @@ func TestExtractInfoFromJavaArchiveFilename(t *testing.T) {
|
||||
name: "pkg-extra-field-maven",
|
||||
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",
|
||||
version: "4.3.2-rc1",
|
||||
@@ -43,6 +50,13 @@ func TestExtractInfoFromJavaArchiveFilename(t *testing.T) {
|
||||
name: "pkg-extra-field-maven",
|
||||
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",
|
||||
version: "4.3.2-rc1",
|
||||
|
||||
@@ -6,9 +6,8 @@ import (
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
|
||||
"github.com/anchore/syft/internal/file"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/common"
|
||||
@@ -21,8 +20,17 @@ var archiveFormatGlobs = []string{
|
||||
"**/*.jar",
|
||||
"**/*.war",
|
||||
"**/*.ear",
|
||||
"**/*.par",
|
||||
"**/*.jpi",
|
||||
"**/*.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 {
|
||||
@@ -35,7 +43,7 @@ type archiveParser struct {
|
||||
}
|
||||
|
||||
// parseJavaArchive is a parser function for java archive contents, returning all Java libraries and nested archives.
|
||||
func parseJavaArchive(virtualPath string, reader io.Reader) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
func parseJavaArchive(virtualPath string, reader io.Reader) ([]*pkg.Package, []artifact.Relationship, error) {
|
||||
parser, cleanupFn, err := newJavaArchiveParser(virtualPath, reader, true)
|
||||
// note: even on error, we should always run cleanup functions
|
||||
defer cleanupFn()
|
||||
@@ -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
|
||||
// and parse nested archives or ignore them.
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
// fetch the last element of the virtual path
|
||||
virtualElements := strings.Split(virtualPath, ":")
|
||||
currentFilepath := virtualElements[len(virtualElements)-1]
|
||||
|
||||
return &archiveParser{
|
||||
fileManifest: fileManifest,
|
||||
virtualPath: virtualPath,
|
||||
@@ -81,8 +89,8 @@ func newJavaArchiveParser(virtualPath string, reader io.Reader, detectNested boo
|
||||
}
|
||||
|
||||
// parse the loaded archive and return all packages found.
|
||||
func (j *archiveParser) parse() ([]pkg.Package, []artifact.Relationship, error) {
|
||||
var pkgs []pkg.Package
|
||||
func (j *archiveParser) parse() ([]*pkg.Package, []artifact.Relationship, error) {
|
||||
var pkgs []*pkg.Package
|
||||
var relationships []artifact.Relationship
|
||||
|
||||
// find the parent package from the java manifest
|
||||
@@ -110,7 +118,7 @@ func (j *archiveParser) parse() ([]pkg.Package, []artifact.Relationship, error)
|
||||
|
||||
// lastly, add the parent package to the list (assuming the parent exists)
|
||||
if parentPkg != nil {
|
||||
pkgs = append([]pkg.Package{*parentPkg}, pkgs...)
|
||||
pkgs = append([]*pkg.Package{parentPkg}, pkgs...)
|
||||
}
|
||||
|
||||
return pkgs, relationships, nil
|
||||
@@ -158,12 +166,12 @@ func (j *archiveParser) discoverMainPackage() (*pkg.Package, error) {
|
||||
// parent package, returning all listed Java packages found for each pom
|
||||
// properties discovered and potentially updating the given parentPkg with new
|
||||
// data.
|
||||
func (j *archiveParser) discoverPkgsFromAllMavenFiles(parentPkg *pkg.Package) ([]pkg.Package, error) {
|
||||
func (j *archiveParser) discoverPkgsFromAllMavenFiles(parentPkg *pkg.Package) ([]*pkg.Package, error) {
|
||||
if parentPkg == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var pkgs []pkg.Package
|
||||
var pkgs []*pkg.Package
|
||||
|
||||
properties, err := pomPropertiesByParentPath(j.archivePath, j.fileManifest.GlobMatch(pomPropertiesGlob), j.virtualPath)
|
||||
if err != nil {
|
||||
@@ -183,41 +191,40 @@ func (j *archiveParser) discoverPkgsFromAllMavenFiles(parentPkg *pkg.Package) ([
|
||||
|
||||
pkgFromPom := newPackageFromMavenData(propertiesObj, pomProject, parentPkg, j.virtualPath)
|
||||
if pkgFromPom != nil {
|
||||
pkgs = append(pkgs, *pkgFromPom)
|
||||
pkgs = append(pkgs, pkgFromPom)
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
var pkgs []pkg.Package
|
||||
var relationships []artifact.Relationship
|
||||
func (j *archiveParser) discoverPkgsFromNestedArchives(parentPkg *pkg.Package) ([]*pkg.Package, []artifact.Relationship, error) {
|
||||
// we know that all java archives are zip formatted files, so we can use the shared zip helper
|
||||
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
|
||||
openers, err := file.ExtractFromZipToUniqueTempFile(j.archivePath, j.contentPath, j.fileManifest.GlobMatch(archiveFormatGlobs...)...)
|
||||
openers, err := file.ExtractFromZipToUniqueTempFile(archivePath, contentPath, fileManifest.GlobMatch(archiveFormatGlobs...)...)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("unable to extract files from zip: %w", err)
|
||||
}
|
||||
|
||||
// discover nested artifacts
|
||||
for archivePath, archiveOpener := range openers {
|
||||
archiveReadCloser, err := archiveOpener.Open()
|
||||
return discoverPkgsFromOpeners(virtualPath, openers, parentPkg)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return nil, nil, fmt.Errorf("unable to open archived file from tempdir: %w", err)
|
||||
}
|
||||
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)
|
||||
log.Warnf("unable to discover java packages from opener (%s): %+v", virtualPath, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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) {
|
||||
contentsOfMavenPropertiesFiles, err := file.ContentsFromZip(archivePath, extractPaths...)
|
||||
if err != nil {
|
||||
|
||||
@@ -6,18 +6,17 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/go-test/deep"
|
||||
"github.com/gookit/color"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func generateJavaBuildFixture(t *testing.T, fixturePath string) {
|
||||
@@ -227,7 +226,7 @@ func TestParseJar(t *testing.T) {
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@@ -257,7 +256,7 @@ func TestParseJar(t *testing.T) {
|
||||
var parent *pkg.Package
|
||||
for _, a := range actual {
|
||||
if strings.Contains(a.Name, "example-") {
|
||||
parent = &a
|
||||
parent = a
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,16 +282,14 @@ func TestParseJar(t *testing.T) {
|
||||
// ignore select fields (only works for the main section)
|
||||
for _, field := range test.ignoreExtras {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// write censored data back
|
||||
a.Metadata = metadata
|
||||
|
||||
diffs := deep.Equal(e, a)
|
||||
diffs := deep.Equal(&e, a)
|
||||
if len(diffs) > 0 {
|
||||
t.Errorf("diffs found for %q", a.Name)
|
||||
for _, d := range diffs {
|
||||
@@ -527,7 +524,7 @@ func TestParseNestedJar(t *testing.T) {
|
||||
|
||||
actualNameVersionPairSet := internal.NewStringSet()
|
||||
for _, a := range actual {
|
||||
key := makeKey(&a)
|
||||
key := makeKey(a)
|
||||
actualNameVersionPairSet.Add(key)
|
||||
if !expectedNameVersionPairSet.Contains(key) {
|
||||
t.Errorf("extra package: %s", a)
|
||||
@@ -545,7 +542,7 @@ func TestParseNestedJar(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, a := range actual {
|
||||
actualKey := makeKey(&a)
|
||||
actualKey := makeKey(a)
|
||||
|
||||
metadata := a.Metadata.(pkg.JavaMetadata)
|
||||
if actualKey == "spring-boot|0.0.1-SNAPSHOT" {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,11 +8,27 @@ import (
|
||||
)
|
||||
|
||||
// NewJavaCataloger returns a new Java archive cataloger object.
|
||||
func NewJavaCataloger() *common.GenericCataloger {
|
||||
func NewJavaCataloger(cfg Config) *common.GenericCataloger {
|
||||
globParsers := make(map[string]common.ParserFn)
|
||||
|
||||
// java archive formats
|
||||
for _, pattern := range archiveFormatGlobs {
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package java
|
||||
|
||||
type Config struct {
|
||||
SearchUnindexedArchives bool
|
||||
SearchIndexedArchives bool
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user