Compare commits

...
7 Commits
Author SHA1 Message Date
Alex Goodman b454160549 tidy gomod and gitignore (#2082)
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2023-08-31 14:50:32 +00:00
Alex Goodman 36d794febe fix quiet flag (#2081)
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2023-08-31 10:40:11 -04:00
William Murphy 51d38f8e59 fix: in some cases, try to use pom info to guess name and version to top level jar (#2080)
Otherwise, small renames like 'hudson-war-2.2.1.war' to 'hudson.war', would cause
syft to incorrectly catolog the archive.

Signed-off-by: Will Murphy <will.murphy@anchore.com>
2023-08-31 10:19:55 -04:00
William Murphy cfebae27f5 fix: don't panic on universal go binaries (#2078)
If crypto settings or arch cannot be determined, still attempt to catalog packages from
the build info, rather than panicking.

Signed-off-by: Will Murphy <will.murphy@anchore.com>
2023-08-30 08:37:50 -04:00
Keith ZantowandAlex Goodman 2b7a9d0be3 chore: update CLI to CLIO (#2001)
Signed-off-by: Keith Zantow <kzantow@gmail.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
2023-08-29 15:52:26 -04:00
b03e9c6868 Add registry certificate verification support (#1734)
* add registry certificate verification support

* replace stereoscope version

* modify go.mod

* pull in stereoscope update

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* rename registry cert options, add docs, and add test

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* update to account for changes in anchore/stereoscope#195

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix cli tests

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: lishituo <24578666@qq.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
2023-08-29 11:45:20 -04:00
witchcraze cedfa05e93 fix: CPE generation for django (#2068)
Signed-off-by: witchcraze <witchcraze@gmail.com>
2023-08-28 08:28:01 -04:00
106 changed files with 2081 additions and 3404 deletions
+25 -10
View File
@@ -1,17 +1,38 @@
# local development tailoring
go.work
go.work.sum
.tool-versions
# app configuration
/.syft.yaml
# tool and bin directories
.tmp/
bin/
/bin
/.bin
/build
CHANGELOG.md
VERSION
/test/results
/dist
/snapshot
.server/
# changelog generation
CHANGELOG.md
VERSION
# IDE configuration
.vscode/
.idea/
.server/
.history/
# test related
*.fingerprint
/test/results
coverage.txt
*.log
# probable archives
.images
*.tar
*.jar
*.war
@@ -19,13 +40,7 @@ VERSION
*.jpi
*.hpi
*.zip
.idea/
*.iml
*.log
.images
.tmp/
coverage.txt
bin/
# Binaries for programs and plugins
*.exe
+4 -4
View File
@@ -24,10 +24,10 @@ builds:
-w
-s
-extldflags '-static'
-X github.com/anchore/syft/internal/version.version={{.Version}}
-X github.com/anchore/syft/internal/version.gitCommit={{.Commit}}
-X github.com/anchore/syft/internal/version.buildDate={{.Date}}
-X github.com/anchore/syft/internal/version.gitDescription={{.Summary}}
-X main.version={{.Version}}
-X main.gitCommit={{.Commit}}
-X main.buildDate={{.Date}}
-X main.gitDescription={{.Summary}}
- id: darwin-build
dir: ./cmd/syft
+17 -1
View File
@@ -681,19 +681,35 @@ registry:
# SYFT_REGISTRY_INSECURE_USE_HTTP env var
insecure-use-http: false
# filepath to a CA certificate (or directory containing *.crt, *.cert, *.pem) used to generate the client certificate
# SYFT_REGISTRY_CA_CERT env var
ca-cert: ""
# credentials for specific registries
auth:
# the URL to the registry (e.g. "docker.io", "localhost:5000", etc.)
# SYFT_REGISTRY_AUTH_AUTHORITY env var
- authority: ""
# SYFT_REGISTRY_AUTH_USERNAME env var
username: ""
# SYFT_REGISTRY_AUTH_PASSWORD env var
password: ""
# note: token and username/password are mutually exclusive
# SYFT_REGISTRY_AUTH_TOKEN env var
token: ""
# - ... # note, more credentials can be provided via config file only
# filepath to the client certificate used for TLS authentication to the registry
# SYFT_REGISTRY_AUTH_TLS_CERT env var
tls-cert: ""
# filepath to the client key used for TLS authentication to the registry
# SYFT_REGISTRY_AUTH_TLS_KEY env var
tls-key: ""
# - ... # note, more credentials can be provided via config file only (not env vars)
# generate an attested SBOM
attest:
-66
View File
@@ -1,66 +0,0 @@
package cli
import (
"fmt"
"log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/anchore/syft/cmd/syft/cli/attest"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/config"
)
const (
attestExample = ` {{.appName}} {{.command}} --output [FORMAT] alpine:latest defaults to using images from a Docker daemon. If Docker is not present, the image is pulled directly from the registry
`
attestSchemeHelp = "\n" + indent + schemeHelpHeader + "\n" + imageSchemeHelp
attestHelp = attestExample + attestSchemeHelp
)
func Attest(v *viper.Viper, app *config.Application, ro *options.RootOptions, po *options.PackagesOptions, ao *options.AttestOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "attest --output [FORMAT] <IMAGE>",
Short: "Generate an SBOM as an attestation for the given [SOURCE] container image",
Long: "Generate a packaged-based Software Bill Of Materials (SBOM) from a container image as the predicate of an in-toto attestation that will be uploaded to the image registry",
Example: internal.Tprintf(attestHelp, map[string]interface{}{
"appName": internal.ApplicationName,
"command": "attest",
}),
Args: func(cmd *cobra.Command, args []string) error {
if err := app.LoadAllValues(v, ro.Config); err != nil {
return fmt.Errorf("unable to load configuration: %w", err)
}
newLogWrapper(app)
logApplicationConfig(app)
return validateArgs(cmd, args)
},
SilenceUsage: true,
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
if app.CheckForAppUpdate {
checkForApplicationUpdate()
// TODO: this is broke, the bus isn't available yet
}
return attest.Run(cmd.Context(), app, args)
},
}
// syft attest is an enhancement of the packages command, so it should have the same flags
err := po.AddFlags(cmd, v)
if err != nil {
log.Fatal(err)
}
// syft attest has its own options not included as part of the packages command
err = ao.AddFlags(cmd, v)
if err != nil {
log.Fatal(err)
}
return cmd
}
-261
View File
@@ -1,261 +0,0 @@
package attest
import (
"context"
"fmt"
"os"
"os/exec"
"strings"
"github.com/wagoodman/go-partybus"
"github.com/wagoodman/go-progress"
"golang.org/x/exp/slices"
"github.com/anchore/stereoscope"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/syft/cmd/syft/cli/eventloop"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/cmd/syft/cli/packages"
"github.com/anchore/syft/cmd/syft/internal/ui"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/config"
"github.com/anchore/syft/internal/file"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/event/monitor"
"github.com/anchore/syft/syft/formats/syftjson"
"github.com/anchore/syft/syft/formats/table"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
func Run(_ context.Context, app *config.Application, args []string) error {
err := ValidateOutputOptions(app)
if err != nil {
return err
}
// note: must be a container image
userInput := args[0]
_, err = exec.LookPath("cosign")
if err != nil {
// when cosign is not installed the error will be rendered like so:
// 2023/06/30 08:31:52 error during command execution: 'syft attest' requires cosign to be installed: exec: "cosign": executable file not found in $PATH
return fmt.Errorf("'syft attest' requires cosign to be installed: %w", err)
}
eventBus := partybus.NewBus()
stereoscope.SetBus(eventBus)
syft.SetBus(eventBus)
subscription := eventBus.Subscribe()
return eventloop.EventLoop(
execWorker(app, userInput),
eventloop.SetupSignals(),
subscription,
stereoscope.Cleanup,
ui.Select(options.IsVerbose(app), app.Quiet)...,
)
}
func buildSBOM(app *config.Application, userInput string, errs chan error) (*sbom.SBOM, error) {
cfg := source.DetectConfig{
DefaultImageSource: app.DefaultImagePullSource,
}
detection, err := source.Detect(userInput, cfg)
if err != nil {
return nil, fmt.Errorf("could not deteremine source: %w", err)
}
if detection.IsContainerImage() {
return nil, fmt.Errorf("attestations are only supported for oci images at this time")
}
var platform *image.Platform
if app.Platform != "" {
platform, err = image.NewPlatform(app.Platform)
if err != nil {
return nil, fmt.Errorf("invalid platform: %w", err)
}
}
hashers, err := file.Hashers(app.Source.File.Digests...)
if err != nil {
return nil, fmt.Errorf("invalid hash: %w", err)
}
src, err := detection.NewSource(
source.DetectionSourceConfig{
Alias: source.Alias{
Name: app.Source.Name,
Version: app.Source.Version,
},
RegistryOptions: app.Registry.ToOptions(),
Platform: platform,
Exclude: source.ExcludeConfig{
Paths: app.Exclusions,
},
DigestAlgorithms: hashers,
BasePath: app.BasePath,
},
)
if src != nil {
defer src.Close()
}
if err != nil {
return nil, fmt.Errorf("failed to construct source from user input %q: %w", userInput, err)
}
s, err := packages.GenerateSBOM(src, errs, app)
if err != nil {
return nil, err
}
if s == nil {
return nil, fmt.Errorf("no SBOM produced for %q", userInput)
}
return s, nil
}
//nolint:funlen
func execWorker(app *config.Application, userInput string) <-chan error {
errs := make(chan error)
go func() {
defer close(errs)
defer bus.Exit()
s, err := buildSBOM(app, userInput, errs)
if err != nil {
errs <- fmt.Errorf("unable to build SBOM: %w", err)
return
}
// note: ValidateOutputOptions ensures that there is no more than one output type
o := app.Outputs[0]
f, err := os.CreateTemp("", o)
if err != nil {
errs <- fmt.Errorf("unable to create temp file: %w", err)
return
}
defer os.Remove(f.Name())
writer, err := options.MakeSBOMWriter(app.Outputs, f.Name(), app.OutputTemplatePath)
if err != nil {
errs <- fmt.Errorf("unable to create SBOM writer: %w", err)
return
}
if err := writer.Write(*s); err != nil {
errs <- fmt.Errorf("unable to write SBOM to temp file: %w", err)
return
}
// TODO: what other validation here besides binary name?
cmd := "cosign"
if !commandExists(cmd) {
errs <- fmt.Errorf("unable to find cosign in PATH; make sure you have it installed")
return
}
// Select Cosign predicate type based on defined output type
// As orientation, check: https://github.com/sigstore/cosign/blob/main/pkg/cosign/attestation/attestation.go
var predicateType string
switch strings.ToLower(o) {
case "cyclonedx-json":
predicateType = "cyclonedx"
case "spdx-tag-value", "spdx-tv":
predicateType = "spdx"
case "spdx-json", "json":
predicateType = "spdxjson"
default:
predicateType = "custom"
}
args := []string{"attest", userInput, "--predicate", f.Name(), "--type", predicateType}
if app.Attest.Key != "" {
args = append(args, "--key", app.Attest.Key)
}
execCmd := exec.Command(cmd, args...)
execCmd.Env = os.Environ()
if app.Attest.Key != "" {
execCmd.Env = append(execCmd.Env, fmt.Sprintf("COSIGN_PASSWORD=%s", app.Attest.Password))
} else {
// no key provided, use cosign's keyless mode
execCmd.Env = append(execCmd.Env, "COSIGN_EXPERIMENTAL=1")
}
log.WithFields("cmd", strings.Join(execCmd.Args, " ")).Trace("creating attestation")
// bus adapter for ui to hook into stdout via an os pipe
r, w, err := os.Pipe()
if err != nil {
errs <- fmt.Errorf("unable to create os pipe: %w", err)
return
}
defer w.Close()
mon := progress.NewManual(-1)
bus.Publish(
partybus.Event{
Type: event.AttestationStarted,
Source: monitor.GenericTask{
Title: monitor.Title{
Default: "Create attestation",
WhileRunning: "Creating attestation",
OnSuccess: "Created attestation",
},
Context: "cosign",
},
Value: &monitor.ShellProgress{
Reader: r,
Progressable: mon,
},
},
)
execCmd.Stdout = w
execCmd.Stderr = w
// attest the SBOM
err = execCmd.Run()
if err != nil {
mon.SetError(err)
errs <- fmt.Errorf("unable to attest SBOM: %w", err)
return
}
mon.SetCompleted()
}()
return errs
}
func ValidateOutputOptions(app *config.Application) error {
err := packages.ValidateOutputOptions(app)
if err != nil {
return err
}
if len(app.Outputs) > 1 {
return fmt.Errorf("multiple SBOM format is not supported for attest at this time")
}
// cannot use table as default output format when using template output
if slices.Contains(app.Outputs, table.ID.String()) {
app.Outputs = []string{syftjson.ID.String()}
}
return nil
}
func commandExists(cmd string) bool {
_, err := exec.LookPath(cmd)
return err == nil
}
+81
View File
@@ -0,0 +1,81 @@
package cli
import (
"os"
cranecmd "github.com/google/go-containerregistry/cmd/crane/cmd"
"github.com/anchore/clio"
"github.com/anchore/stereoscope"
"github.com/anchore/syft/cmd/syft/cli/commands"
handler "github.com/anchore/syft/cmd/syft/cli/ui"
"github.com/anchore/syft/cmd/syft/internal/ui"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/redact"
)
// New constructs the `syft packages` command, aliases the root command to `syft packages`,
// and constructs the `syft power-user` command. It is also responsible for
// organizing flag usage and injecting the application config for each command.
// It also constructs the syft attest command and the syft version command.
// `RunE` is the earliest that the complete application configuration can be loaded.
func New(id clio.Identification) clio.Application {
clioCfg := clio.NewSetupConfig(id).
WithGlobalConfigFlag(). // add persistent -c <path> for reading an application config from
WithGlobalLoggingFlags(). // add persistent -v and -q flags tied to the logging config
WithConfigInRootHelp(). // --help on the root command renders the full application config in the help text
WithUIConstructor(
// select a UI based on the logging configuration and state of stdin (if stdin is a tty)
func(cfg clio.Config) ([]clio.UI, error) {
noUI := ui.None(cfg.Log.Quiet)
if !cfg.Log.AllowUI(os.Stdin) || cfg.Log.Quiet {
return []clio.UI{noUI}, nil
}
h := handler.New(handler.DefaultHandlerConfig())
return []clio.UI{
ui.New(h, false, cfg.Log.Quiet),
noUI,
}, nil
},
).
WithInitializers(
func(state *clio.State) error {
// clio is setting up and providing the bus, redact store, and logger to the application. Once loaded,
// we can hoist them into the internal packages for global use.
stereoscope.SetBus(state.Bus)
bus.Set(state.Bus)
redact.Set(state.RedactStore)
log.Set(state.Logger)
stereoscope.SetLogger(state.Logger)
return nil
},
).
WithPostRuns(func(state *clio.State, err error) {
stereoscope.Cleanup()
})
app := clio.New(*clioCfg)
// since root is aliased as the packages cmd we need to construct this command first
// we also need the command to have information about the `root` options because of this alias
packagesCmd := commands.Packages(app)
// rootCmd is currently an alias for the packages command
rootCmd := commands.Root(app, packagesCmd)
// add sub-commands
rootCmd.AddCommand(
packagesCmd,
commands.PowerUser(app),
commands.Attest(app),
commands.Convert(app),
clio.VersionCommand(id),
cranecmd.NewCmdAuthLogin(id.Name), // syft login uses the same command as crane
)
return app
}
-167
View File
@@ -1,167 +0,0 @@
package cli
import (
"fmt"
"strings"
cranecmd "github.com/google/go-containerregistry/cmd/crane/cmd"
"github.com/gookit/color"
logrusUpstream "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/wagoodman/go-partybus"
"github.com/anchore/go-logger/adapter/logrus"
"github.com/anchore/stereoscope"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/config"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/event"
)
const indent = " "
// New constructs the `syft packages` command, aliases the root command to `syft packages`,
// and constructs the `syft power-user` command. It is also responsible for
// organizing flag usage and injecting the application config for each command.
// It also constructs the syft attest command and the syft version command.
// Because of how the `cobra` library behaves, the application's configuration is initialized
// at this level. Values from the config should only be used after `app.LoadAllValues` has been called.
// Cobra does not have knowledge of the user provided flags until the `RunE` block of each command.
// `RunE` is the earliest that the complete application configuration can be loaded.
func New() (*cobra.Command, error) {
app := &config.Application{}
// allow for nested options to be specified via environment variables
// e.g. pod.context = APPNAME_POD_CONTEXT
v := viper.NewWithOptions(viper.EnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")))
// since root is aliased as the packages cmd we need to construct this command first
// we also need the command to have information about the `root` options because of this alias
ro := &options.RootOptions{}
po := &options.PackagesOptions{}
ao := &options.AttestOptions{}
packagesCmd := Packages(v, app, ro, po)
// root options are also passed to the attestCmd so that a user provided config location can be discovered
poweruserCmd := PowerUser(v, app, ro)
convertCmd := Convert(v, app, ro, po)
attestCmd := Attest(v, app, ro, po, ao)
// rootCmd is currently an alias for the packages command
rootCmd := &cobra.Command{
Use: fmt.Sprintf("%s [SOURCE]", internal.ApplicationName),
Short: packagesCmd.Short,
Long: packagesCmd.Long,
Args: packagesCmd.Args,
Example: packagesCmd.Example,
SilenceUsage: true,
SilenceErrors: true,
RunE: packagesCmd.RunE,
Version: version.FromBuild().Version,
}
rootCmd.SetVersionTemplate(fmt.Sprintf("%s {{.Version}}\n", internal.ApplicationName))
// start adding flags to all the commands
err := ro.AddFlags(rootCmd, v)
if err != nil {
return nil, err
}
// package flags need to be decorated onto the rootCmd so that rootCmd can function as a packages alias
err = po.AddFlags(rootCmd, v)
if err != nil {
return nil, err
}
// poweruser also uses the packagesCmd flags since it is a specialized version of the command
err = po.AddFlags(poweruserCmd, v)
if err != nil {
return nil, err
}
// commands to add to root
cmds := []*cobra.Command{
packagesCmd,
poweruserCmd,
convertCmd,
attestCmd,
Version(v, app),
cranecmd.NewCmdAuthLogin("syft"), // syft login uses the same command as crane
}
// Add sub-commands.
for _, cmd := range cmds {
rootCmd.AddCommand(cmd)
}
return rootCmd, err
}
func validateArgs(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
// in the case that no arguments are given we want to show the help text and return with a non-0 return code.
if err := cmd.Help(); err != nil {
return fmt.Errorf("unable to display help: %w", err)
}
return fmt.Errorf("an image/directory argument is required")
}
return cobra.MaximumNArgs(1)(cmd, args)
}
func checkForApplicationUpdate() {
log.Debugf("checking if a new version of %s is available", internal.ApplicationName)
isAvailable, newVersion, err := version.IsUpdateAvailable()
if err != nil {
// this should never stop the application
log.Errorf(err.Error())
}
if isAvailable {
log.Infof("new version of %s is available: %s (current version is %s)", internal.ApplicationName, newVersion, version.FromBuild().Version)
bus.Publish(partybus.Event{
Type: event.CLIAppUpdateAvailable,
Value: newVersion,
})
} else {
log.Debugf("no new %s update available", internal.ApplicationName)
}
}
func logApplicationConfig(app *config.Application) {
versionInfo := version.FromBuild()
log.Infof("%s version: %+v", internal.ApplicationName, versionInfo.Version)
log.Debugf("application config:\n%+v", color.Magenta.Sprint(app.String()))
}
func newLogWrapper(app *config.Application) {
cfg := logrus.Config{
EnableConsole: (app.Log.FileLocation == "" || app.Verbosity > 0) && !app.Quiet,
FileLocation: app.Log.FileLocation,
Level: app.Log.Level,
}
if app.Log.Structured {
cfg.Formatter = &logrusUpstream.JSONFormatter{
TimestampFormat: "2006-01-02 15:04:05",
DisableTimestamp: false,
DisableHTMLEscape: false,
PrettyPrint: false,
}
}
logWrapper, err := logrus.New(cfg)
if err != nil {
// this is kinda circular, but we can't return an error... ¯\_(ツ)_/¯
// I'm going to leave this here in case we one day have a different default logger other than the "discard" logger
log.Error("unable to initialize logger: %+v", err)
return
}
syft.SetLogger(logWrapper)
stereoscope.SetLogger(logWrapper.Nested("from-lib", "stereoscope"))
}
+258
View File
@@ -0,0 +1,258 @@
package commands
import (
"fmt"
"os"
"os/exec"
"strings"
"github.com/spf13/cobra"
"github.com/wagoodman/go-partybus"
"github.com/wagoodman/go-progress"
"github.com/anchore/clio"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/file"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/event/monitor"
"github.com/anchore/syft/syft/formats"
"github.com/anchore/syft/syft/formats/github"
"github.com/anchore/syft/syft/formats/syftjson"
"github.com/anchore/syft/syft/formats/table"
"github.com/anchore/syft/syft/formats/template"
"github.com/anchore/syft/syft/formats/text"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
const (
attestExample = ` {{.appName}} {{.command}} --output [FORMAT] alpine:latest defaults to using images from a Docker daemon. If Docker is not present, the image is pulled directly from the registry
`
attestSchemeHelp = "\n " + schemeHelpHeader + "\n" + imageSchemeHelp
attestHelp = attestExample + attestSchemeHelp
)
type attestOptions struct {
options.Config `yaml:",inline" mapstructure:",squash"`
options.SingleOutput `yaml:",inline" mapstructure:",squash"`
options.UpdateCheck `yaml:",inline" mapstructure:",squash"`
options.Catalog `yaml:",inline" mapstructure:",squash"`
options.Attest `yaml:",inline" mapstructure:",squash"`
}
func Attest(app clio.Application) *cobra.Command {
id := app.ID()
var allowableOutputs []string
for _, f := range formats.AllIDs() {
switch f {
case table.ID, text.ID, github.ID, template.ID:
continue
}
allowableOutputs = append(allowableOutputs, f.String())
}
opts := &attestOptions{
UpdateCheck: options.DefaultUpdateCheck(),
SingleOutput: options.SingleOutput{
AllowableOptions: allowableOutputs,
Output: syftjson.ID.String(),
},
Catalog: options.DefaultCatalog(),
}
return app.SetupCommand(&cobra.Command{
Use: "attest --output [FORMAT] <IMAGE>",
Short: "Generate an SBOM as an attestation for the given [SOURCE] container image",
Long: "Generate a packaged-based Software Bill Of Materials (SBOM) from a container image as the predicate of an in-toto attestation that will be uploaded to the image registry",
Example: internal.Tprintf(attestHelp, map[string]interface{}{
"appName": id.Name,
"command": "attest",
}),
Args: validatePackagesArgs,
PreRunE: applicationUpdateCheck(id, &opts.UpdateCheck),
RunE: func(cmd *cobra.Command, args []string) error {
return runAttest(id, opts, args[0])
},
}, opts)
}
//nolint:funlen
func runAttest(id clio.Identification, opts *attestOptions, userInput string) error {
_, err := exec.LookPath("cosign")
if err != nil {
// when cosign is not installed the error will be rendered like so:
// 2023/06/30 08:31:52 error during command execution: 'syft attest' requires cosign to be installed: exec: "cosign": executable file not found in $PATH
return fmt.Errorf("'syft attest' requires cosign to be installed: %w", err)
}
s, err := buildSBOM(id, &opts.Catalog, userInput)
if err != nil {
return fmt.Errorf("unable to build SBOM: %w", err)
}
o := opts.Output
f, err := os.CreateTemp("", o)
if err != nil {
return fmt.Errorf("unable to create temp file: %w", err)
}
defer os.Remove(f.Name())
writer, err := opts.SBOMWriter(f.Name())
if err != nil {
return fmt.Errorf("unable to create SBOM writer: %w", err)
}
if err := writer.Write(*s); err != nil {
return fmt.Errorf("unable to write SBOM to temp file: %w", err)
}
// TODO: what other validation here besides binary name?
cmd := "cosign"
if !commandExists(cmd) {
return fmt.Errorf("unable to find cosign in PATH; make sure you have it installed")
}
// Select Cosign predicate type based on defined output type
// As orientation, check: https://github.com/sigstore/cosign/blob/main/pkg/cosign/attestation/attestation.go
var predicateType string
switch strings.ToLower(o) {
case "cyclonedx-json":
predicateType = "cyclonedx"
case "spdx-tag-value", "spdx-tv":
predicateType = "spdx"
case "spdx-json", "json":
predicateType = "spdxjson"
default:
predicateType = "custom"
}
args := []string{"attest", userInput, "--predicate", f.Name(), "--type", predicateType}
if opts.Attest.Key != "" {
args = append(args, "--key", opts.Attest.Key.String())
}
execCmd := exec.Command(cmd, args...)
execCmd.Env = os.Environ()
if opts.Attest.Key != "" {
execCmd.Env = append(execCmd.Env, fmt.Sprintf("COSIGN_PASSWORD=%s", opts.Attest.Password))
} else {
// no key provided, use cosign's keyless mode
execCmd.Env = append(execCmd.Env, "COSIGN_EXPERIMENTAL=1")
}
log.WithFields("cmd", strings.Join(execCmd.Args, " ")).Trace("creating attestation")
// bus adapter for ui to hook into stdout via an os pipe
r, w, err := os.Pipe()
if err != nil {
return fmt.Errorf("unable to create os pipe: %w", err)
}
defer w.Close()
mon := progress.NewManual(-1)
bus.Publish(
partybus.Event{
Type: event.AttestationStarted,
Source: monitor.GenericTask{
Title: monitor.Title{
Default: "Create attestation",
WhileRunning: "Creating attestation",
OnSuccess: "Created attestation",
},
Context: "cosign",
},
Value: &monitor.ShellProgress{
Reader: r,
Progressable: mon,
},
},
)
execCmd.Stdout = w
execCmd.Stderr = w
// attest the SBOM
err = execCmd.Run()
if err != nil {
mon.SetError(err)
return fmt.Errorf("unable to attest SBOM: %w", err)
}
mon.SetCompleted()
return nil
}
func buildSBOM(id clio.Identification, opts *options.Catalog, userInput string) (*sbom.SBOM, error) {
cfg := source.DetectConfig{
DefaultImageSource: opts.DefaultImagePullSource,
}
detection, err := source.Detect(userInput, cfg)
if err != nil {
return nil, fmt.Errorf("could not deteremine source: %w", err)
}
if detection.IsContainerImage() {
return nil, fmt.Errorf("attestations are only supported for oci images at this time")
}
var platform *image.Platform
if opts.Platform != "" {
platform, err = image.NewPlatform(opts.Platform)
if err != nil {
return nil, fmt.Errorf("invalid platform: %w", err)
}
}
hashers, err := file.Hashers(opts.Source.File.Digests...)
if err != nil {
return nil, fmt.Errorf("invalid hash: %w", err)
}
src, err := detection.NewSource(
source.DetectionSourceConfig{
Alias: source.Alias{
Name: opts.Source.Name,
Version: opts.Source.Version,
},
RegistryOptions: opts.Registry.ToOptions(),
Platform: platform,
Exclude: source.ExcludeConfig{
Paths: opts.Exclusions,
},
DigestAlgorithms: hashers,
BasePath: opts.BasePath,
},
)
if src != nil {
defer src.Close()
}
if err != nil {
return nil, fmt.Errorf("failed to construct source from user input %q: %w", userInput, err)
}
s, err := generateSBOM(id, src, opts)
if err != nil {
return nil, err
}
if s == nil {
return nil, fmt.Errorf("no SBOM produced for %q", userInput)
}
return s, nil
}
func commandExists(cmd string) bool {
_, err := exec.LookPath(cmd)
return err == nil
}
+95
View File
@@ -0,0 +1,95 @@
package commands
import (
"fmt"
"io"
"os"
"github.com/spf13/cobra"
"github.com/anchore/clio"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/formats"
)
const (
convertExample = ` {{.appName}} {{.command}} img.syft.json -o spdx-json convert a syft SBOM to spdx-json, output goes to stdout
{{.appName}} {{.command}} img.syft.json -o cyclonedx-json=img.cdx.json convert a syft SBOM to CycloneDX, output is written to the file "img.cdx.json""
{{.appName}} {{.command}} - -o spdx-json convert an SBOM from STDIN to spdx-json
`
)
type ConvertOptions struct {
options.Config `yaml:",inline" mapstructure:",squash"`
options.MultiOutput `yaml:",inline" mapstructure:",squash"`
options.UpdateCheck `yaml:",inline" mapstructure:",squash"`
}
//nolint:dupl
func Convert(app clio.Application) *cobra.Command {
id := app.ID()
opts := &ConvertOptions{
UpdateCheck: options.DefaultUpdateCheck(),
}
return app.SetupCommand(&cobra.Command{
Use: "convert [SOURCE-SBOM] -o [FORMAT]",
Short: "Convert between SBOM formats",
Long: "[Experimental] Convert SBOM files to, and from, SPDX, CycloneDX and Syft's format. For more info about data loss between formats see https://github.com/anchore/syft#format-conversion-experimental",
Example: internal.Tprintf(convertExample, map[string]interface{}{
"appName": id.Name,
"command": "convert",
}),
Args: validateConvertArgs,
PreRunE: applicationUpdateCheck(id, &opts.UpdateCheck),
RunE: func(cmd *cobra.Command, args []string) error {
return RunConvert(opts, args[0])
},
}, opts)
}
func validateConvertArgs(cmd *cobra.Command, args []string) error {
return validateArgs(cmd, args, "an SBOM argument is required")
}
func RunConvert(opts *ConvertOptions, userInput string) error {
log.Warn("convert is an experimental feature, run `syft convert -h` for help")
writer, err := opts.SBOMWriter()
if err != nil {
return err
}
var reader io.ReadCloser
if userInput == "-" {
reader = os.Stdin
} else {
f, err := os.Open(userInput)
if err != nil {
return fmt.Errorf("failed to open SBOM file: %w", err)
}
defer func() {
_ = f.Close()
}()
reader = f
}
s, _, err := formats.Decode(reader)
if err != nil {
return fmt.Errorf("failed to decode SBOM: %w", err)
}
if s == nil {
return fmt.Errorf("no SBOM produced")
}
if err := writer.Write(*s); err != nil {
return fmt.Errorf("failed to write SBOM: %w", err)
}
return nil
}
+253
View File
@@ -0,0 +1,253 @@
package commands
import (
"fmt"
"github.com/hashicorp/go-multierror"
"github.com/spf13/cobra"
"github.com/anchore/clio"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/syft/cmd/syft/cli/eventloop"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/file"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/formats/template"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
const (
packagesExample = ` {{.appName}} {{.command}} alpine:latest a summary of discovered packages
{{.appName}} {{.command}} alpine:latest -o json show all possible cataloging details
{{.appName}} {{.command}} alpine:latest -o cyclonedx show a CycloneDX formatted SBOM
{{.appName}} {{.command}} alpine:latest -o cyclonedx-json show a CycloneDX JSON formatted SBOM
{{.appName}} {{.command}} alpine:latest -o spdx show a SPDX 2.3 Tag-Value formatted SBOM
{{.appName}} {{.command}} alpine:latest -o spdx@2.2 show a SPDX 2.2 Tag-Value formatted SBOM
{{.appName}} {{.command}} alpine:latest -o spdx-json show a SPDX 2.3 JSON formatted SBOM
{{.appName}} {{.command}} alpine:latest -o spdx-json@2.2 show a SPDX 2.2 JSON formatted SBOM
{{.appName}} {{.command}} alpine:latest -vv show verbose debug information
{{.appName}} {{.command}} alpine:latest -o template -t my_format.tmpl show a SBOM formatted according to given template file
Supports the following image sources:
{{.appName}} {{.command}} yourrepo/yourimage:tag defaults to using images from a Docker daemon. If Docker is not present, the image is pulled directly from the registry.
{{.appName}} {{.command}} path/to/a/file/or/dir a Docker tar, OCI tar, OCI directory, SIF container, or generic filesystem directory
`
schemeHelpHeader = "You can also explicitly specify the scheme to use:"
imageSchemeHelp = ` {{.appName}} {{.command}} docker:yourrepo/yourimage:tag explicitly use the Docker daemon
{{.appName}} {{.command}} podman:yourrepo/yourimage:tag explicitly use the Podman daemon
{{.appName}} {{.command}} registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required)
{{.appName}} {{.command}} docker-archive:path/to/yourimage.tar use a tarball from disk for archives created from "docker save"
{{.appName}} {{.command}} oci-archive:path/to/yourimage.tar use a tarball from disk for OCI archives (from Skopeo or otherwise)
{{.appName}} {{.command}} oci-dir:path/to/yourimage read directly from a path on disk for OCI layout directories (from Skopeo or otherwise)
{{.appName}} {{.command}} singularity:path/to/yourimage.sif read directly from a Singularity Image Format (SIF) container on disk
`
nonImageSchemeHelp = ` {{.appName}} {{.command}} dir:path/to/yourproject read directly from a path on disk (any directory)
{{.appName}} {{.command}} file:path/to/yourproject/file read directly from a path on disk (any single file)
`
packagesSchemeHelp = "\n " + schemeHelpHeader + "\n" + imageSchemeHelp + nonImageSchemeHelp
packagesHelp = packagesExample + packagesSchemeHelp
)
type packagesOptions struct {
options.Config `yaml:",inline" mapstructure:",squash"`
options.MultiOutput `yaml:",inline" mapstructure:",squash"`
options.UpdateCheck `yaml:",inline" mapstructure:",squash"`
options.Catalog `yaml:",inline" mapstructure:",squash"`
}
func defaultPackagesOptions() *packagesOptions {
return &packagesOptions{
MultiOutput: options.DefaultOutput(),
UpdateCheck: options.DefaultUpdateCheck(),
Catalog: options.DefaultCatalog(),
}
}
//nolint:dupl
func Packages(app clio.Application) *cobra.Command {
id := app.ID()
opts := defaultPackagesOptions()
return app.SetupCommand(&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",
Example: internal.Tprintf(packagesHelp, map[string]interface{}{
"appName": id.Name,
"command": "packages",
}),
Args: validatePackagesArgs,
PreRunE: applicationUpdateCheck(id, &opts.UpdateCheck),
RunE: func(cmd *cobra.Command, args []string) error {
return runPackages(id, opts, args[0])
},
}, opts)
}
func validatePackagesArgs(cmd *cobra.Command, args []string) error {
return validateArgs(cmd, args, "an image/directory argument is required")
}
func validateArgs(cmd *cobra.Command, args []string, error string) error {
if len(args) == 0 {
// in the case that no arguments are given we want to show the help text and return with a non-0 return code.
if err := cmd.Help(); err != nil {
return fmt.Errorf("unable to display help: %w", err)
}
return fmt.Errorf(error)
}
return cobra.MaximumNArgs(1)(cmd, args)
}
// nolint:funlen
func runPackages(id clio.Identification, opts *packagesOptions, userInput string) error {
err := validatePackageOutputOptions(&opts.MultiOutput)
if err != nil {
return err
}
writer, err := opts.SBOMWriter()
if err != nil {
return err
}
detection, err := source.Detect(
userInput,
source.DetectConfig{
DefaultImageSource: opts.DefaultImagePullSource,
},
)
if err != nil {
return fmt.Errorf("could not deteremine source: %w", err)
}
var platform *image.Platform
if opts.Platform != "" {
platform, err = image.NewPlatform(opts.Platform)
if err != nil {
return fmt.Errorf("invalid platform: %w", err)
}
}
hashers, err := file.Hashers(opts.Source.File.Digests...)
if err != nil {
return fmt.Errorf("invalid hash: %w", err)
}
src, err := detection.NewSource(
source.DetectionSourceConfig{
Alias: source.Alias{
Name: opts.Source.Name,
Version: opts.Source.Version,
},
RegistryOptions: opts.Registry.ToOptions(),
Platform: platform,
Exclude: source.ExcludeConfig{
Paths: opts.Exclusions,
},
DigestAlgorithms: hashers,
BasePath: opts.BasePath,
},
)
if err != nil {
return fmt.Errorf("failed to construct source from user input %q: %w", userInput, err)
}
defer func() {
if src != nil {
if err := src.Close(); err != nil {
log.Tracef("unable to close source: %+v", err)
}
}
}()
s, err := generateSBOM(id, src, &opts.Catalog)
if err != nil {
return err
}
if s == nil {
return fmt.Errorf("no SBOM produced for %q", userInput)
}
if err := writer.Write(*s); err != nil {
return fmt.Errorf("failed to write SBOM: %w", err)
}
return nil
}
func generateSBOM(id clio.Identification, src source.Source, opts *options.Catalog) (*sbom.SBOM, error) {
tasks, err := eventloop.Tasks(opts)
if err != nil {
return nil, err
}
s := sbom.SBOM{
Source: src.Describe(),
Descriptor: sbom.Descriptor{
Name: id.Name,
Version: id.Version,
Configuration: opts,
},
}
err = buildRelationships(&s, src, tasks)
return &s, err
}
func buildRelationships(s *sbom.SBOM, src source.Source, tasks []eventloop.Task) error {
var errs error
var relationships []<-chan artifact.Relationship
for _, task := range tasks {
c := make(chan artifact.Relationship)
relationships = append(relationships, c)
go func(task eventloop.Task) {
err := eventloop.RunTask(task, &s.Artifacts, src, c)
if err != nil {
errs = multierror.Append(errs, err)
}
}(task)
}
s.Relationships = append(s.Relationships, mergeRelationships(relationships...)...)
return errs
}
func mergeRelationships(cs ...<-chan artifact.Relationship) (relationships []artifact.Relationship) {
for _, c := range cs {
for n := range c {
relationships = append(relationships, n)
}
}
return relationships
}
func validatePackageOutputOptions(cfg *options.MultiOutput) error {
var usesTemplateOutput bool
for _, o := range cfg.Outputs {
if o == template.ID.String() {
usesTemplateOutput = true
break
}
}
if usesTemplateOutput && cfg.OutputTemplatePath == "" {
return fmt.Errorf(`must specify path to template file when using "template" output format`)
}
return nil
}
+154
View File
@@ -0,0 +1,154 @@
package commands
import (
"fmt"
"os"
"github.com/gookit/color"
"github.com/hashicorp/go-multierror"
"github.com/spf13/cobra"
"github.com/anchore/clio"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/syft/cmd/syft/cli/eventloop"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/formats/syftjson"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
const powerUserExample = ` {{.appName}} {{.command}} <image>
DEPRECATED - THIS COMMAND WILL BE REMOVED in v1.0.0
Only image sources are supported (e.g. docker: , podman: , docker-archive: , oci: , etc.), the directory source (dir:) is not supported, template outputs are not supported.
All behavior is controlled via application configuration and environment variables (see https://github.com/anchore/syft#configuration)
`
type powerUserOptions struct {
options.Config `yaml:",inline" mapstructure:",squash"`
options.OutputFile `yaml:",inline" mapstructure:",squash"`
options.UpdateCheck `yaml:",inline" mapstructure:",squash"`
options.Catalog `yaml:",inline" mapstructure:",squash"`
}
func PowerUser(app clio.Application) *cobra.Command {
id := app.ID()
pkgs := options.DefaultCatalog()
pkgs.Secrets.Cataloger.Enabled = true
pkgs.FileMetadata.Cataloger.Enabled = true
pkgs.FileContents.Cataloger.Enabled = true
pkgs.FileClassification.Cataloger.Enabled = true
opts := &powerUserOptions{
Catalog: pkgs,
}
return app.SetupCommand(&cobra.Command{
Use: "power-user [IMAGE]",
Short: "Run bulk operations on container images",
Example: internal.Tprintf(powerUserExample, map[string]interface{}{
"appName": id.Name,
"command": "power-user",
}),
Args: validatePackagesArgs,
Hidden: true,
PreRunE: applicationUpdateCheck(id, &opts.UpdateCheck),
RunE: func(cmd *cobra.Command, args []string) error {
return runPowerUser(id, opts, args[0])
},
}, opts)
}
//nolint:funlen
func runPowerUser(id clio.Identification, opts *powerUserOptions, userInput string) error {
writer, err := opts.SBOMWriter(syftjson.Format())
if err != nil {
return err
}
defer func() {
// inform user at end of run that command will be removed
deprecated := color.Style{color.Red, color.OpBold}.Sprint("DEPRECATED: This command will be removed in v1.0.0")
fmt.Fprintln(os.Stderr, deprecated)
}()
tasks, err := eventloop.Tasks(&opts.Catalog)
if err != nil {
return err
}
detection, err := source.Detect(
userInput,
source.DetectConfig{
DefaultImageSource: opts.DefaultImagePullSource,
},
)
if err != nil {
return fmt.Errorf("could not deteremine source: %w", err)
}
var platform *image.Platform
if opts.Platform != "" {
platform, err = image.NewPlatform(opts.Platform)
if err != nil {
return fmt.Errorf("invalid platform: %w", err)
}
}
src, err := detection.NewSource(
source.DetectionSourceConfig{
Alias: source.Alias{
Name: opts.Source.Name,
Version: opts.Source.Version,
},
RegistryOptions: opts.Registry.ToOptions(),
Platform: platform,
Exclude: source.ExcludeConfig{
Paths: opts.Exclusions,
},
DigestAlgorithms: nil,
BasePath: opts.BasePath,
},
)
if src != nil {
defer src.Close()
}
if err != nil {
return fmt.Errorf("failed to construct source from user input %q: %w", userInput, err)
}
s := sbom.SBOM{
Source: src.Describe(),
Descriptor: sbom.Descriptor{
Name: id.Name,
Version: id.Version,
Configuration: opts,
},
}
var errs error
var relationships []<-chan artifact.Relationship
for _, task := range tasks {
c := make(chan artifact.Relationship)
relationships = append(relationships, c)
go func(task eventloop.Task) {
err := eventloop.RunTask(task, &s.Artifacts, src, c)
errs = multierror.Append(errs, err)
}(task)
}
if errs != nil {
return errs
}
s.Relationships = append(s.Relationships, mergeRelationships(relationships...)...)
if err := writer.Write(s); err != nil {
return fmt.Errorf("failed to write sbom: %w", err)
}
return nil
}
+27
View File
@@ -0,0 +1,27 @@
package commands
import (
"fmt"
"github.com/spf13/cobra"
"github.com/anchore/clio"
)
func Root(app clio.Application, packagesCmd *cobra.Command) *cobra.Command {
id := app.ID()
opts := defaultPackagesOptions()
return app.SetupRootCommand(&cobra.Command{
Use: fmt.Sprintf("%s [SOURCE]", app.ID().Name),
Short: packagesCmd.Short,
Long: packagesCmd.Long,
Args: packagesCmd.Args,
Example: packagesCmd.Example,
PreRunE: applicationUpdateCheck(id, &opts.UpdateCheck),
RunE: func(cmd *cobra.Command, args []string) error {
return runPackages(id, opts, args[0])
},
}, opts)
}
+120
View File
@@ -0,0 +1,120 @@
package commands
import (
"fmt"
"io"
"net/http"
"strings"
"github.com/spf13/cobra"
"github.com/wagoodman/go-partybus"
"github.com/anchore/clio"
hashiVersion "github.com/anchore/go-version"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/cmd/syft/internal"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/event/parsers"
)
var latestAppVersionURL = struct {
host string
path string
}{
host: "https://toolbox-data.anchore.io",
path: "/syft/releases/latest/VERSION",
}
func applicationUpdateCheck(id clio.Identification, check *options.UpdateCheck) func(cmd *cobra.Command, args []string) error {
return func(cmd *cobra.Command, args []string) error {
if check.CheckForAppUpdate {
checkForApplicationUpdate(id)
}
return nil
}
}
func checkForApplicationUpdate(id clio.Identification) {
log.Debugf("checking if a new version of %s is available", id.Name)
isAvailable, newVersion, err := isUpdateAvailable(id.Version)
if err != nil {
// this should never stop the application
log.Errorf(err.Error())
}
if isAvailable {
log.Infof("new version of %s is available: %s (current version is %s)", id.Name, newVersion, id.Version)
bus.Publish(partybus.Event{
Type: event.CLIAppUpdateAvailable,
Value: parsers.UpdateCheck{
New: newVersion,
Current: id.Version,
},
})
} else {
log.Debugf("no new %s update available", id.Name)
}
}
// isUpdateAvailable indicates if there is a newer application version available, and if so, what the new version is.
func isUpdateAvailable(version string) (bool, string, error) {
if !isProductionBuild(version) {
// don't allow for non-production builds to check for a version.
return false, "", nil
}
currentVersion, err := hashiVersion.NewVersion(version)
if err != nil {
return false, "", fmt.Errorf("failed to parse current application version: %w", err)
}
latestVersion, err := fetchLatestApplicationVersion()
if err != nil {
return false, "", err
}
if latestVersion.GreaterThan(currentVersion) {
return true, latestVersion.String(), nil
}
return false, "", nil
}
func isProductionBuild(version string) bool {
if strings.Contains(version, "SNAPSHOT") || strings.Contains(version, internal.NotProvided) {
return false
}
return true
}
func fetchLatestApplicationVersion() (*hashiVersion.Version, error) {
req, err := http.NewRequest(http.MethodGet, latestAppVersionURL.host+latestAppVersionURL.path, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request for latest version: %w", err)
}
client := http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to fetch latest version: %w", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("HTTP %d on fetching latest version: %s", resp.StatusCode, resp.Status)
}
versionBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read latest version: %w", err)
}
versionStr := strings.TrimSuffix(string(versionBytes), "\n")
if len(versionStr) > 50 {
return nil, fmt.Errorf("version too long: %q", versionStr[:50])
}
return hashiVersion.NewVersion(versionStr)
}
@@ -1,4 +1,4 @@
package version
package commands
import (
"net/http"
@@ -6,6 +6,7 @@ import (
"testing"
hashiVersion "github.com/anchore/go-version"
"github.com/anchore/syft/cmd/syft/internal"
)
func TestIsUpdateAvailable(t *testing.T) {
@@ -74,7 +75,7 @@ func TestIsUpdateAvailable(t *testing.T) {
},
{
name: "NoBuildVersion",
buildVersion: valueNotProvided,
buildVersion: internal.NotProvided,
latestVersion: "1.0.0",
code: 200,
isAvailable: false,
@@ -105,7 +106,7 @@ func TestIsUpdateAvailable(t *testing.T) {
t.Run(test.name, func(t *testing.T) {
// setup mocks
// local...
version = test.buildVersion
version := test.buildVersion
// remote...
handler := http.NewServeMux()
handler.HandleFunc(latestAppVersionURL.path, func(w http.ResponseWriter, r *http.Request) {
@@ -116,7 +117,7 @@ func TestIsUpdateAvailable(t *testing.T) {
latestAppVersionURL.host = mockSrv.URL
defer mockSrv.Close()
isAvailable, newVersion, err := IsUpdateAvailable()
isAvailable, newVersion, err := isUpdateAvailable(version)
if err != nil && !test.err {
t.Fatalf("got error but expected none: %+v", err)
} else if err == nil && test.err {
-58
View File
@@ -1,58 +0,0 @@
package cli
import (
"fmt"
"log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/anchore/syft/cmd/syft/cli/convert"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/config"
)
const (
convertExample = ` {{.appName}} {{.command}} img.syft.json -o spdx-json convert a syft SBOM to spdx-json, output goes to stdout
{{.appName}} {{.command}} img.syft.json -o cyclonedx-json=img.cdx.json convert a syft SBOM to CycloneDX, output is written to the file "img.cdx.json""
{{.appName}} {{.command}} - -o spdx-json convert an SBOM from STDIN to spdx-json
`
)
//nolint:dupl
func Convert(v *viper.Viper, app *config.Application, ro *options.RootOptions, po *options.PackagesOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "convert [SOURCE-SBOM] -o [FORMAT]",
Short: "Convert between SBOM formats",
Long: "[Experimental] Convert SBOM files to, and from, SPDX, CycloneDX and Syft's format. For more info about data loss between formats see https://github.com/anchore/syft#format-conversion-experimental",
Example: internal.Tprintf(convertExample, map[string]interface{}{
"appName": internal.ApplicationName,
"command": "convert",
}),
Args: func(cmd *cobra.Command, args []string) error {
if err := app.LoadAllValues(v, ro.Config); err != nil {
return fmt.Errorf("invalid application config: %w", err)
}
newLogWrapper(app)
logApplicationConfig(app)
return validateArgs(cmd, args)
},
SilenceUsage: true,
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
if app.CheckForAppUpdate {
checkForApplicationUpdate()
// TODO: this is broke, the bus isn't available yet
}
return convert.Run(cmd.Context(), app, args)
},
}
err := po.AddFlags(cmd, v)
if err != nil {
log.Fatal(err)
}
return cmd
}
-85
View File
@@ -1,85 +0,0 @@
package convert
import (
"context"
"fmt"
"io"
"os"
"github.com/wagoodman/go-partybus"
"github.com/anchore/stereoscope"
"github.com/anchore/syft/cmd/syft/cli/eventloop"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/cmd/syft/internal/ui"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/config"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/formats"
"github.com/anchore/syft/syft/sbom"
)
func Run(_ context.Context, app *config.Application, args []string) error {
log.Warn("convert is an experimental feature, run `syft convert -h` for help")
writer, err := options.MakeSBOMWriter(app.Outputs, app.File, app.OutputTemplatePath)
if err != nil {
return err
}
// could be an image or a directory, with or without a scheme
userInput := args[0]
var reader io.ReadCloser
if userInput == "-" {
reader = os.Stdin
} else {
f, err := os.Open(userInput)
if err != nil {
return fmt.Errorf("failed to open SBOM file: %w", err)
}
defer func() {
_ = f.Close()
}()
reader = f
}
eventBus := partybus.NewBus()
stereoscope.SetBus(eventBus)
syft.SetBus(eventBus)
subscription := eventBus.Subscribe()
return eventloop.EventLoop(
execWorker(reader, writer),
eventloop.SetupSignals(),
subscription,
stereoscope.Cleanup,
ui.Select(options.IsVerbose(app), app.Quiet)...,
)
}
func execWorker(reader io.Reader, writer sbom.Writer) <-chan error {
errs := make(chan error)
go func() {
defer close(errs)
defer bus.Exit()
s, _, err := formats.Decode(reader)
if err != nil {
errs <- fmt.Errorf("failed to decode SBOM: %w", err)
return
}
if s == nil {
errs <- fmt.Errorf("no SBOM produced")
return
}
if err := writer.Write(*s); err != nil {
errs <- fmt.Errorf("failed to write SBOM: %w", err)
}
}()
return errs
}
-98
View File
@@ -1,98 +0,0 @@
package eventloop
import (
"errors"
"fmt"
"os"
"github.com/hashicorp/go-multierror"
"github.com/wagoodman/go-partybus"
"github.com/anchore/clio"
"github.com/anchore/syft/internal/log"
)
// EventLoop listens to worker errors (from execution path), worker events (from a partybus subscription), and
// signal interrupts. Is responsible for handling each event relative to a given UI an to coordinate eventing until
// an eventual graceful exit.
func EventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription *partybus.Subscription, cleanupFn func(), uxs ...clio.UI) error {
defer cleanupFn()
events := subscription.Events()
var err error
var ux clio.UI
if ux, err = setupUI(subscription, uxs...); err != nil {
return err
}
var retErr error
var forceTeardown bool
for {
if workerErrs == nil && events == nil {
break
}
select {
case err, isOpen := <-workerErrs:
if !isOpen {
workerErrs = nil
continue
}
if err != nil {
// capture the error from the worker and unsubscribe to complete a graceful shutdown
retErr = multierror.Append(retErr, err)
_ = subscription.Unsubscribe()
// the worker has exited, we may have been mid-handling events for the UI which should now be
// ignored, in which case forcing a teardown of the UI irregardless of the state is required.
forceTeardown = true
}
case e, isOpen := <-events:
if !isOpen {
events = nil
continue
}
if err := ux.Handle(e); err != nil {
if errors.Is(err, partybus.ErrUnsubscribe) {
events = nil
} else {
retErr = multierror.Append(retErr, err)
// TODO: should we unsubscribe? should we try to halt execution? or continue?
}
}
case <-signals:
// ignore further results from any event source and exit ASAP, but ensure that all cache is cleaned up.
// we ignore further errors since cleaning up the tmp directories will affect running catalogers that are
// reading/writing from/to their nested temp dirs. This is acceptable since we are bailing without result.
// TODO: potential future improvement would be to pass context into workers with a cancel function that is
// to the event loop. In this way we can have a more controlled shutdown even at the most nested levels
// of processing.
events = nil
workerErrs = nil
forceTeardown = true
}
}
if err := ux.Teardown(forceTeardown); err != nil {
retErr = multierror.Append(retErr, err)
}
return retErr
}
// setupUI takes one or more UIs that responds to events and takes a event bus unsubscribe function for use
// during teardown. With the given UIs, the first UI which the ui.Setup() function does not return an error
// will be utilized in execution. Providing a set of UIs allows for the caller to provide graceful fallbacks
// when there are environmental problem (e.g. unable to setup a TUI with the current TTY).
func setupUI(subscription *partybus.Subscription, uis ...clio.UI) (clio.UI, error) {
for _, ux := range uis {
if err := ux.Setup(subscription); err != nil {
log.Warnf("unable to setup given UI, falling back to alternative UI: %+v", err)
continue
}
return ux, nil
}
return nil, fmt.Errorf("unable to setup any UI")
}
-459
View File
@@ -1,459 +0,0 @@
package eventloop
import (
"fmt"
"os"
"syscall"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/wagoodman/go-partybus"
"github.com/anchore/clio"
"github.com/anchore/syft/syft/event"
)
var _ clio.UI = (*uiMock)(nil)
type uiMock struct {
t *testing.T
finalEvent partybus.Event
subscription partybus.Unsubscribable
mock.Mock
}
func (u *uiMock) Setup(unsubscribe partybus.Unsubscribable) error {
u.t.Helper()
u.t.Logf("UI Setup called")
u.subscription = unsubscribe
return u.Called(unsubscribe.Unsubscribe).Error(0)
}
func (u *uiMock) Handle(event partybus.Event) error {
u.t.Helper()
u.t.Logf("UI Handle called: %+v", event.Type)
if event == u.finalEvent {
assert.NoError(u.t, u.subscription.Unsubscribe())
}
return u.Called(event).Error(0)
}
func (u *uiMock) Teardown(_ bool) error {
u.t.Helper()
u.t.Logf("UI Teardown called")
return u.Called().Error(0)
}
func Test_EventLoop_gracefulExit(t *testing.T) {
test := func(t *testing.T) {
testBus := partybus.NewBus()
subscription := testBus.Subscribe()
t.Cleanup(testBus.Close)
finalEvent := partybus.Event{
Type: event.CLIExit,
}
worker := func() <-chan error {
ret := make(chan error)
go func() {
t.Log("worker running")
// send an empty item (which is ignored) ensuring we've entered the select statement,
// then close (a partial shutdown).
ret <- nil
t.Log("worker sent nothing")
close(ret)
t.Log("worker closed")
// do the other half of the shutdown
testBus.Publish(finalEvent)
t.Log("worker published final event")
}()
return ret
}
signaler := func() <-chan os.Signal {
return nil
}
ux := &uiMock{
t: t,
finalEvent: finalEvent,
}
// ensure the mock sees at least the final event
ux.On("Handle", finalEvent).Return(nil)
// ensure the mock sees basic setup/teardown events
ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
ux.On("Teardown").Return(nil)
var cleanupCalled bool
cleanupFn := func() {
t.Log("cleanup called")
cleanupCalled = true
}
assert.NoError(t,
EventLoop(
worker(),
signaler(),
subscription,
cleanupFn,
ux,
),
)
assert.True(t, cleanupCalled, "cleanup function not called")
ux.AssertExpectations(t)
}
// if there is a bug, then there is a risk of the event loop never returning
testWithTimeout(t, 5*time.Second, test)
}
func Test_EventLoop_workerError(t *testing.T) {
test := func(t *testing.T) {
testBus := partybus.NewBus()
subscription := testBus.Subscribe()
t.Cleanup(testBus.Close)
workerErr := fmt.Errorf("worker error")
worker := func() <-chan error {
ret := make(chan error)
go func() {
t.Log("worker running")
// send an empty item (which is ignored) ensuring we've entered the select statement,
// then close (a partial shutdown).
ret <- nil
t.Log("worker sent nothing")
ret <- workerErr
t.Log("worker sent error")
close(ret)
t.Log("worker closed")
// note: NO final event is fired
}()
return ret
}
signaler := func() <-chan os.Signal {
return nil
}
ux := &uiMock{
t: t,
}
// ensure the mock sees basic setup/teardown events
ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
ux.On("Teardown").Return(nil)
var cleanupCalled bool
cleanupFn := func() {
t.Log("cleanup called")
cleanupCalled = true
}
// ensure we see an error returned
assert.ErrorIs(t,
EventLoop(
worker(),
signaler(),
subscription,
cleanupFn,
ux,
),
workerErr,
"should have seen a worker error, but did not",
)
assert.True(t, cleanupCalled, "cleanup function not called")
ux.AssertExpectations(t)
}
// if there is a bug, then there is a risk of the event loop never returning
testWithTimeout(t, 5*time.Second, test)
}
func Test_EventLoop_unsubscribeError(t *testing.T) {
test := func(t *testing.T) {
testBus := partybus.NewBus()
subscription := testBus.Subscribe()
t.Cleanup(testBus.Close)
finalEvent := partybus.Event{
Type: event.CLIExit,
}
worker := func() <-chan error {
ret := make(chan error)
go func() {
t.Log("worker running")
// send an empty item (which is ignored) ensuring we've entered the select statement,
// then close (a partial shutdown).
ret <- nil
t.Log("worker sent nothing")
close(ret)
t.Log("worker closed")
// do the other half of the shutdown
testBus.Publish(finalEvent)
t.Log("worker published final event")
}()
return ret
}
signaler := func() <-chan os.Signal {
return nil
}
ux := &uiMock{
t: t,
finalEvent: finalEvent,
}
// ensure the mock sees at least the final event... note the unsubscribe error here
ux.On("Handle", finalEvent).Return(partybus.ErrUnsubscribe)
// ensure the mock sees basic setup/teardown events
ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
ux.On("Teardown").Return(nil)
var cleanupCalled bool
cleanupFn := func() {
t.Log("cleanup called")
cleanupCalled = true
}
// unsubscribe errors should be handled and ignored, not propagated. We are additionally asserting that
// this case is handled as a controlled shutdown (this test should not timeout)
assert.NoError(t,
EventLoop(
worker(),
signaler(),
subscription,
cleanupFn,
ux,
),
)
assert.True(t, cleanupCalled, "cleanup function not called")
ux.AssertExpectations(t)
}
// if there is a bug, then there is a risk of the event loop never returning
testWithTimeout(t, 5*time.Second, test)
}
func Test_EventLoop_handlerError(t *testing.T) {
test := func(t *testing.T) {
testBus := partybus.NewBus()
subscription := testBus.Subscribe()
t.Cleanup(testBus.Close)
finalEvent := partybus.Event{
Type: event.CLIExit,
Error: fmt.Errorf("an exit error occured"),
}
worker := func() <-chan error {
ret := make(chan error)
go func() {
t.Log("worker running")
// send an empty item (which is ignored) ensuring we've entered the select statement,
// then close (a partial shutdown).
ret <- nil
t.Log("worker sent nothing")
close(ret)
t.Log("worker closed")
// do the other half of the shutdown
testBus.Publish(finalEvent)
t.Log("worker published final event")
}()
return ret
}
signaler := func() <-chan os.Signal {
return nil
}
ux := &uiMock{
t: t,
finalEvent: finalEvent,
}
// ensure the mock sees at least the final event... note the event error is propagated
ux.On("Handle", finalEvent).Return(finalEvent.Error)
// ensure the mock sees basic setup/teardown events
ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
ux.On("Teardown").Return(nil)
var cleanupCalled bool
cleanupFn := func() {
t.Log("cleanup called")
cleanupCalled = true
}
// handle errors SHOULD propagate the event loop. We are additionally asserting that this case is
// handled as a controlled shutdown (this test should not timeout)
assert.ErrorIs(t,
EventLoop(
worker(),
signaler(),
subscription,
cleanupFn,
ux,
),
finalEvent.Error,
"should have seen a event error, but did not",
)
assert.True(t, cleanupCalled, "cleanup function not called")
ux.AssertExpectations(t)
}
// if there is a bug, then there is a risk of the event loop never returning
testWithTimeout(t, 5*time.Second, test)
}
func Test_EventLoop_signalsStopExecution(t *testing.T) {
test := func(t *testing.T) {
testBus := partybus.NewBus()
subscription := testBus.Subscribe()
t.Cleanup(testBus.Close)
worker := func() <-chan error {
// the worker will never return work and the event loop will always be waiting...
return make(chan error)
}
signaler := func() <-chan os.Signal {
ret := make(chan os.Signal)
go func() {
ret <- syscall.SIGINT
// note: we do NOT close the channel to ensure the event loop does not depend on that behavior to exit
}()
return ret
}
ux := &uiMock{
t: t,
}
// ensure the mock sees basic setup/teardown events
ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
ux.On("Teardown").Return(nil)
var cleanupCalled bool
cleanupFn := func() {
t.Log("cleanup called")
cleanupCalled = true
}
assert.NoError(t,
EventLoop(
worker(),
signaler(),
subscription,
cleanupFn,
ux,
),
)
assert.True(t, cleanupCalled, "cleanup function not called")
ux.AssertExpectations(t)
}
// if there is a bug, then there is a risk of the event loop never returning
testWithTimeout(t, 5*time.Second, test)
}
func Test_EventLoop_uiTeardownError(t *testing.T) {
test := func(t *testing.T) {
testBus := partybus.NewBus()
subscription := testBus.Subscribe()
t.Cleanup(testBus.Close)
finalEvent := partybus.Event{
Type: event.CLIExit,
}
worker := func() <-chan error {
ret := make(chan error)
go func() {
t.Log("worker running")
// send an empty item (which is ignored) ensuring we've entered the select statement,
// then close (a partial shutdown).
ret <- nil
t.Log("worker sent nothing")
close(ret)
t.Log("worker closed")
// do the other half of the shutdown
testBus.Publish(finalEvent)
t.Log("worker published final event")
}()
return ret
}
signaler := func() <-chan os.Signal {
return nil
}
ux := &uiMock{
t: t,
finalEvent: finalEvent,
}
teardownError := fmt.Errorf("sorry, dave, the UI doesn't want to be torn down")
// ensure the mock sees at least the final event... note the event error is propagated
ux.On("Handle", finalEvent).Return(nil)
// ensure the mock sees basic setup/teardown events
ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
ux.On("Teardown").Return(teardownError)
var cleanupCalled bool
cleanupFn := func() {
t.Log("cleanup called")
cleanupCalled = true
}
// ensure we see an error returned
assert.ErrorIs(t,
EventLoop(
worker(),
signaler(),
subscription,
cleanupFn,
ux,
),
teardownError,
"should have seen a UI teardown error, but did not",
)
assert.True(t, cleanupCalled, "cleanup function not called")
ux.AssertExpectations(t)
}
// if there is a bug, then there is a risk of the event loop never returning
testWithTimeout(t, 5*time.Second, test)
}
func testWithTimeout(t *testing.T, timeout time.Duration, test func(*testing.T)) {
done := make(chan bool)
go func() {
test(t)
done <- true
}()
select {
case <-time.After(timeout):
t.Fatal("test timed out")
case <-done:
}
}
-20
View File
@@ -1,20 +0,0 @@
package eventloop
import (
"os"
"os/signal"
"syscall"
)
func SetupSignals() <-chan os.Signal {
c := make(chan os.Signal, 1) // Note: A buffered channel is recommended for this; see https://golang.org/pkg/os/signal/#Notify
interruptions := []os.Signal{
syscall.SIGINT,
syscall.SIGTERM,
}
signal.Notify(c, interruptions...)
return c
}
+27 -26
View File
@@ -1,7 +1,7 @@
package eventloop
import (
"github.com/anchore/syft/internal/config"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/internal/file"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/artifact"
@@ -15,10 +15,10 @@ import (
type Task func(*sbom.Artifacts, source.Source) ([]artifact.Relationship, error)
func Tasks(app *config.Application) ([]Task, error) {
func Tasks(opts *options.Catalog) ([]Task, error) {
var tasks []Task
generators := []func(app *config.Application) (Task, error){
generators := []func(opts *options.Catalog) (Task, error){
generateCatalogPackagesTask,
generateCatalogFileMetadataTask,
generateCatalogFileDigestsTask,
@@ -27,7 +27,7 @@ func Tasks(app *config.Application) ([]Task, error) {
}
for _, generator := range generators {
task, err := generator(app)
task, err := generator(opts)
if err != nil {
return nil, err
}
@@ -40,13 +40,13 @@ func Tasks(app *config.Application) ([]Task, error) {
return tasks, nil
}
func generateCatalogPackagesTask(app *config.Application) (Task, error) {
if !app.Package.Cataloger.Enabled {
func generateCatalogPackagesTask(opts *options.Catalog) (Task, error) {
if !opts.Package.Cataloger.Enabled {
return nil, nil
}
task := func(results *sbom.Artifacts, src source.Source) ([]artifact.Relationship, error) {
packageCatalog, relationships, theDistro, err := syft.CatalogPackages(src, app.ToCatalogerConfig())
packageCatalog, relationships, theDistro, err := syft.CatalogPackages(src, opts.ToCatalogerConfig())
results.Packages = packageCatalog
results.LinuxDistribution = theDistro
@@ -57,15 +57,15 @@ func generateCatalogPackagesTask(app *config.Application) (Task, error) {
return task, nil
}
func generateCatalogFileMetadataTask(app *config.Application) (Task, error) {
if !app.FileMetadata.Cataloger.Enabled {
func generateCatalogFileMetadataTask(opts *options.Catalog) (Task, error) {
if !opts.FileMetadata.Cataloger.Enabled {
return nil, nil
}
metadataCataloger := filemetadata.NewCataloger()
task := func(results *sbom.Artifacts, src source.Source) ([]artifact.Relationship, error) {
resolver, err := src.FileResolver(app.FileMetadata.Cataloger.ScopeOpt)
resolver, err := src.FileResolver(opts.FileMetadata.Cataloger.GetScope())
if err != nil {
return nil, err
}
@@ -81,12 +81,12 @@ func generateCatalogFileMetadataTask(app *config.Application) (Task, error) {
return task, nil
}
func generateCatalogFileDigestsTask(app *config.Application) (Task, error) {
if !app.FileMetadata.Cataloger.Enabled {
func generateCatalogFileDigestsTask(opts *options.Catalog) (Task, error) {
if !opts.FileMetadata.Cataloger.Enabled {
return nil, nil
}
hashes, err := file.Hashers(app.FileMetadata.Digests...)
hashes, err := file.Hashers(opts.FileMetadata.Digests...)
if err != nil {
return nil, err
}
@@ -94,7 +94,7 @@ func generateCatalogFileDigestsTask(app *config.Application) (Task, error) {
digestsCataloger := filedigest.NewCataloger(hashes)
task := func(results *sbom.Artifacts, src source.Source) ([]artifact.Relationship, error) {
resolver, err := src.FileResolver(app.FileMetadata.Cataloger.ScopeOpt)
resolver, err := src.FileResolver(opts.FileMetadata.Cataloger.GetScope())
if err != nil {
return nil, err
}
@@ -110,23 +110,23 @@ func generateCatalogFileDigestsTask(app *config.Application) (Task, error) {
return task, nil
}
func generateCatalogSecretsTask(app *config.Application) (Task, error) {
if !app.Secrets.Cataloger.Enabled {
func generateCatalogSecretsTask(opts *options.Catalog) (Task, error) {
if !opts.Secrets.Cataloger.Enabled {
return nil, nil
}
patterns, err := secrets.GenerateSearchPatterns(secrets.DefaultSecretsPatterns, app.Secrets.AdditionalPatterns, app.Secrets.ExcludePatternNames)
patterns, err := secrets.GenerateSearchPatterns(secrets.DefaultSecretsPatterns, opts.Secrets.AdditionalPatterns, opts.Secrets.ExcludePatternNames)
if err != nil {
return nil, err
}
secretsCataloger, err := secrets.NewCataloger(patterns, app.Secrets.RevealValues, app.Secrets.SkipFilesAboveSize) //nolint:staticcheck
secretsCataloger, err := secrets.NewCataloger(patterns, opts.Secrets.RevealValues, opts.Secrets.SkipFilesAboveSize) //nolint:staticcheck
if err != nil {
return nil, err
}
task := func(results *sbom.Artifacts, src source.Source) ([]artifact.Relationship, error) {
resolver, err := src.FileResolver(app.Secrets.Cataloger.ScopeOpt)
resolver, err := src.FileResolver(opts.Secrets.Cataloger.GetScope())
if err != nil {
return nil, err
}
@@ -142,18 +142,18 @@ func generateCatalogSecretsTask(app *config.Application) (Task, error) {
return task, nil
}
func generateCatalogContentsTask(app *config.Application) (Task, error) {
if !app.FileContents.Cataloger.Enabled {
func generateCatalogContentsTask(opts *options.Catalog) (Task, error) {
if !opts.FileContents.Cataloger.Enabled {
return nil, nil
}
contentsCataloger, err := filecontent.NewCataloger(app.FileContents.Globs, app.FileContents.SkipFilesAboveSize) //nolint:staticcheck
contentsCataloger, err := filecontent.NewCataloger(opts.FileContents.Globs, opts.FileContents.SkipFilesAboveSize) //nolint:staticcheck
if err != nil {
return nil, err
}
task := func(results *sbom.Artifacts, src source.Source) ([]artifact.Relationship, error) {
resolver, err := src.FileResolver(app.FileContents.Cataloger.ScopeOpt)
resolver, err := src.FileResolver(opts.FileContents.Cataloger.GetScope())
if err != nil {
return nil, err
}
@@ -169,16 +169,17 @@ func generateCatalogContentsTask(app *config.Application) (Task, error) {
return task, nil
}
func RunTask(t Task, a *sbom.Artifacts, src source.Source, c chan<- artifact.Relationship, errs chan<- error) {
func RunTask(t Task, a *sbom.Artifacts, src source.Source, c chan<- artifact.Relationship) error {
defer close(c)
relationships, err := t(a, src)
if err != nil {
errs <- err
return
return err
}
for _, relationship := range relationships {
c <- relationship
}
return nil
}
+8 -17
View File
@@ -1,26 +1,17 @@
package options
import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
"github.com/anchore/clio"
)
type AttestOptions struct {
Key string
type Attest struct {
// IMPORTANT: do not show the attestation key/password in any YAML/JSON output (sensitive information)
Key secret `yaml:"key" json:"key" mapstructure:"key"`
Password secret `yaml:"password" json:"password" mapstructure:"password"`
}
var _ Interface = (*AttestOptions)(nil)
var _ clio.FlagAdder = (*Attest)(nil)
func (o AttestOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error {
cmd.Flags().StringVarP(&o.Key, "key", "k", "", "the key to use for the attestation")
return bindAttestConfigOptions(cmd.Flags(), v)
}
//nolint:revive
func bindAttestConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error {
if err := v.BindPFlag("attest.key", flags.Lookup("key")); err != nil {
return err
}
return nil
func (o Attest) AddFlags(flags clio.FlagSet) {
flags.StringVarP((*string)(&o.Key), "key", "k", "the key to use for the attestation")
}
+168
View File
@@ -0,0 +1,168 @@
package options
import (
"fmt"
"sort"
"strings"
"github.com/iancoleman/strcase"
"github.com/mitchellh/go-homedir"
"github.com/anchore/clio"
"github.com/anchore/fangs"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/pkg/cataloger"
golangCataloger "github.com/anchore/syft/syft/pkg/cataloger/golang"
"github.com/anchore/syft/syft/pkg/cataloger/kernel"
pythonCataloger "github.com/anchore/syft/syft/pkg/cataloger/python"
"github.com/anchore/syft/syft/source"
)
type Catalog struct {
Catalogers []string `yaml:"catalogers" json:"catalogers" mapstructure:"catalogers"`
Package pkg `yaml:"package" json:"package" mapstructure:"package"`
Golang golang `yaml:"golang" json:"golang" mapstructure:"golang"`
LinuxKernel linuxKernel `yaml:"linux-kernel" json:"linux-kernel" mapstructure:"linux-kernel"`
Python python `yaml:"python" json:"python" mapstructure:"python"`
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"`
Platform string `yaml:"platform" json:"platform" mapstructure:"platform"`
Name string `yaml:"name" json:"name" mapstructure:"name"`
Source sourceCfg `yaml:"source" json:"source" mapstructure:"source"`
Parallelism int `yaml:"parallelism" json:"parallelism" mapstructure:"parallelism"` // the number of catalog workers to run in parallel
DefaultImagePullSource string `yaml:"default-image-pull-source" json:"default-image-pull-source" mapstructure:"default-image-pull-source"` // specify default image pull source
BasePath string `yaml:"base-path" json:"base-path" mapstructure:"base-path"` // specify base path for all file paths
ExcludeBinaryOverlapByOwnership bool `yaml:"exclude-binary-overlap-by-ownership" json:"exclude-binary-overlap-by-ownership" mapstructure:"exclude-binary-overlap-by-ownership"` // exclude synthetic binary packages owned by os package files
}
var _ interface {
clio.FlagAdder
clio.PostLoader
} = (*Catalog)(nil)
func DefaultCatalog() Catalog {
return Catalog{
Package: defaultPkg(),
LinuxKernel: defaultLinuxKernel(),
FileMetadata: defaultFileMetadata(),
FileClassification: defaultFileClassification(),
FileContents: defaultFileContents(),
Secrets: defaultSecrets(),
Source: defaultSourceCfg(),
Parallelism: 1,
ExcludeBinaryOverlapByOwnership: true,
}
}
func (cfg *Catalog) AddFlags(flags clio.FlagSet) {
var validScopeValues []string
for _, scope := range source.AllScopes {
validScopeValues = append(validScopeValues, strcase.ToDelimited(string(scope), '-'))
}
flags.StringVarP(&cfg.Package.Cataloger.Scope, "scope", "s",
fmt.Sprintf("selection of layers to catalog, options=%v", validScopeValues))
flags.StringVarP(&cfg.Platform, "platform", "",
"an optional platform specifier for container image sources (e.g. 'linux/arm64', 'linux/arm64/v8', 'arm64', 'linux')")
flags.StringArrayVarP(&cfg.Exclusions, "exclude", "",
"exclude paths from being scanned using a glob expression")
flags.StringArrayVarP(&cfg.Catalogers, "catalogers", "",
"enable one or more package catalogers")
flags.StringVarP(&cfg.Source.Name, "name", "",
"set the name of the target being analyzed")
if pfp, ok := flags.(fangs.PFlagSetProvider); ok {
flagSet := pfp.PFlagSet()
flagSet.Lookup("name").Deprecated = "use: source-name"
}
flags.StringVarP(&cfg.Source.Name, "source-name", "",
"set the name of the target being analyzed")
flags.StringVarP(&cfg.Source.Version, "source-version", "",
"set the name of the target being analyzed")
flags.StringVarP(&cfg.BasePath, "base-path", "",
"base directory for scanning, no links will be followed above this directory, and all paths will be reported relative to this directory")
}
func (cfg *Catalog) PostLoad() error {
// parse options on this struct
var catalogers []string
for _, c := range cfg.Catalogers {
for _, f := range strings.Split(c, ",") {
catalogers = append(catalogers, strings.TrimSpace(f))
}
}
sort.Strings(catalogers)
cfg.Catalogers = catalogers
if err := checkDefaultSourceValues(cfg.DefaultImagePullSource); err != nil {
return err
}
if cfg.Name != "" {
log.Warnf("name parameter is deprecated. please use: source-name. name will be removed in a future version")
if cfg.Source.Name == "" {
cfg.Source.Name = cfg.Name
}
}
return nil
}
func (cfg Catalog) ToCatalogerConfig() cataloger.Config {
return cataloger.Config{
Search: cataloger.SearchConfig{
IncludeIndexedArchives: cfg.Package.SearchIndexedArchives,
IncludeUnindexedArchives: cfg.Package.SearchUnindexedArchives,
Scope: cfg.Package.Cataloger.GetScope(),
},
Catalogers: cfg.Catalogers,
Parallelism: cfg.Parallelism,
Golang: golangCataloger.NewGoCatalogerOpts().
WithSearchLocalModCacheLicenses(cfg.Golang.SearchLocalModCacheLicenses).
WithLocalModCacheDir(cfg.Golang.LocalModCacheDir).
WithSearchRemoteLicenses(cfg.Golang.SearchRemoteLicenses).
WithProxy(cfg.Golang.Proxy).
WithNoProxy(cfg.Golang.NoProxy),
LinuxKernel: kernel.LinuxCatalogerConfig{
CatalogModules: cfg.LinuxKernel.CatalogModules,
},
Python: pythonCataloger.CatalogerConfig{
GuessUnpinnedRequirements: cfg.Python.GuessUnpinnedRequirements,
},
ExcludeBinaryOverlapByOwnership: cfg.ExcludeBinaryOverlapByOwnership,
}
}
var validDefaultSourceValues = []string{"registry", "docker", "podman", ""}
func checkDefaultSourceValues(source string) error {
validValues := internal.NewStringSet(validDefaultSourceValues...)
if !validValues.Contains(source) {
validValuesString := strings.Join(validDefaultSourceValues, ", ")
return fmt.Errorf("%s is not a valid default source; please use one of the following: %s''", source, validValuesString)
}
return nil
}
func expandFilePath(file string) (string, error) {
if file != "" {
expandedPath, err := homedir.Expand(file)
if err != nil {
return "", fmt.Errorf("unable to expand file path=%q: %w", file, err)
}
file = expandedPath
}
return file, nil
}
+6
View File
@@ -0,0 +1,6 @@
package options
// Config holds a reference to the specific config file that was used to load application configuration
type Config struct {
ConfigFile string `yaml:"config" json:"config" mapstructure:"config"`
}
@@ -0,0 +1,17 @@
package options
import (
"github.com/anchore/syft/syft/source"
)
type fileClassification struct {
Cataloger scope `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"`
}
func defaultFileClassification() fileClassification {
return fileClassification{
Cataloger: scope{
Scope: source.SquashedScope.String(),
},
}
}
+21
View File
@@ -0,0 +1,21 @@
package options
import (
"github.com/anchore/syft/internal/file"
"github.com/anchore/syft/syft/source"
)
type fileContents struct {
Cataloger scope `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"`
SkipFilesAboveSize int64 `yaml:"skip-files-above-size" json:"skip-files-above-size" mapstructure:"skip-files-above-size"`
Globs []string `yaml:"globs" json:"globs" mapstructure:"globs"`
}
func defaultFileContents() fileContents {
return fileContents{
Cataloger: scope{
Scope: source.SquashedScope.String(),
},
SkipFilesAboveSize: 1 * file.MB,
}
}
+19
View File
@@ -0,0 +1,19 @@
package options
import (
"github.com/anchore/syft/syft/source"
)
type fileMetadata struct {
Cataloger scope `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"`
Digests []string `yaml:"digests" json:"digests" mapstructure:"digests"`
}
func defaultFileMetadata() fileMetadata {
return fileMetadata{
Cataloger: scope{
Scope: source.SquashedScope.String(),
},
Digests: []string{"sha256"},
}
}
-49
View File
@@ -1,49 +0,0 @@
package options
import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
const defaultFulcioURL = "https://fulcio.sigstore.dev"
// FulcioOptions is the wrapper for Fulcio related options.
type FulcioOptions struct {
URL string
IdentityToken string
InsecureSkipFulcioVerify bool
}
var _ Interface = (*FulcioOptions)(nil)
// AddFlags implements Interface
func (o *FulcioOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error {
// TODO: change this back to api.SigstorePublicServerURL after the v1 migration is complete.
cmd.Flags().StringVar(&o.URL, "fulcio-url", defaultFulcioURL,
"address of sigstore PKI server")
cmd.Flags().StringVar(&o.IdentityToken, "identity-token", "",
"identity token to use for certificate from fulcio")
cmd.Flags().BoolVar(&o.InsecureSkipFulcioVerify, "insecure-skip-verify", false,
"skip verifying fulcio certificat and the SCT (Signed Certificate Timestamp) (this should only be used for testing).")
return bindFulcioConfigOptions(cmd.Flags(), v)
}
//nolint:revive
func bindFulcioConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error {
if err := v.BindPFlag("attest.fulcio-url", flags.Lookup("fulcio-url")); err != nil {
return err
}
if err := v.BindPFlag("attest.fulcio-identity-token", flags.Lookup("identity-token")); err != nil {
return err
}
if err := v.BindPFlag("attest.insecure-skip-verify", flags.Lookup("insecure-skip-verify")); err != nil {
return err
}
return nil
}
@@ -1,6 +1,4 @@
package config
import "github.com/spf13/viper"
package options
type golang struct {
SearchLocalModCacheLicenses bool `json:"search-local-mod-cache-licenses" yaml:"search-local-mod-cache-licenses" mapstructure:"search-local-mod-cache-licenses"`
@@ -9,11 +7,3 @@ type golang struct {
Proxy string `json:"proxy" yaml:"proxy" mapstructure:"proxy"`
NoProxy string `json:"no-proxy" yaml:"no-proxy" mapstructure:"no-proxy"`
}
func (cfg golang) loadDefaultValues(v *viper.Viper) {
v.SetDefault("golang.search-local-mod-cache-licenses", false)
v.SetDefault("golang.local-mod-cache-dir", "")
v.SetDefault("golang.search-remote-licenses", false)
v.SetDefault("golang.proxy", "")
v.SetDefault("golang.no-proxy", "")
}
+11
View File
@@ -0,0 +1,11 @@
package options
type linuxKernel struct {
CatalogModules bool `json:"catalog-modules" yaml:"catalog-modules" mapstructure:"catalog-modules"`
}
func defaultLinuxKernel() linuxKernel {
return linuxKernel{
CatalogModules: true,
}
}
-49
View File
@@ -1,49 +0,0 @@
package options
import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
const DefaultOIDCIssuerURL = "https://oauth2.sigstore.dev/auth"
// OIDCOptions is the wrapper for OIDC related options.
type OIDCOptions struct {
Issuer string
ClientID string
RedirectURL string
}
var _ Interface = (*OIDCOptions)(nil)
// AddFlags implements Interface
func (o *OIDCOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error {
cmd.Flags().StringVar(&o.Issuer, "oidc-issuer", DefaultOIDCIssuerURL,
"OIDC provider to be used to issue ID token")
cmd.Flags().StringVar(&o.ClientID, "oidc-client-id", "sigstore",
"OIDC client ID for application")
cmd.Flags().StringVar(&o.RedirectURL, "oidc-redirect-url", "",
"OIDC redirect URL (Optional)")
return bindOIDCConfigOptions(cmd.Flags(), v)
}
//nolint:revive
func bindOIDCConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error {
if err := v.BindPFlag("attest.oidc-issuer", flags.Lookup("oidc-issuer")); err != nil {
return err
}
if err := v.BindPFlag("attest.oidc-client-id", flags.Lookup("oidc-client-id")); err != nil {
return err
}
if err := v.BindPFlag("attest.oidc-redirect-url", flags.Lookup("oidc-redirect-url")); err != nil {
return err
}
return nil
}
-11
View File
@@ -1,11 +0,0 @@
package options
import (
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
type Interface interface {
// AddFlags adds this options' flags to the cobra command.
AddFlags(cmd *cobra.Command, v *viper.Viper) error
}
+95
View File
@@ -0,0 +1,95 @@
package options
import (
"fmt"
"golang.org/x/exp/slices"
"github.com/anchore/clio"
"github.com/anchore/syft/syft/formats"
"github.com/anchore/syft/syft/formats/table"
"github.com/anchore/syft/syft/formats/template"
"github.com/anchore/syft/syft/sbom"
)
// MultiOutput has the standard output options syft accepts: multiple -o, --file, --template
type MultiOutput struct {
Outputs []string `yaml:"output" json:"output" mapstructure:"output"` // -o, the format to use for output
OutputFile `yaml:",inline" json:"" mapstructure:",squash"`
OutputTemplatePath string `yaml:"output-template-path" json:"output-template-path" mapstructure:"output-template-path"` // -t template file to use for output
}
var _ interface {
clio.FlagAdder
} = (*MultiOutput)(nil)
func DefaultOutput() MultiOutput {
return MultiOutput{
Outputs: []string{string(table.ID)},
}
}
func (o *MultiOutput) AddFlags(flags clio.FlagSet) {
flags.StringArrayVarP(&o.Outputs, "output", "o",
fmt.Sprintf("report output format, options=%v", formats.AllIDs()))
flags.StringVarP(&o.OutputTemplatePath, "template", "t",
"specify the path to a Go template file")
}
func (o *MultiOutput) SBOMWriter() (sbom.Writer, error) {
return makeSBOMWriter(o.Outputs, o.File, o.OutputTemplatePath)
}
// SingleOutput allows only 1 output to be specified, with a user able to set what options are allowed by setting AllowableOptions
type SingleOutput struct {
AllowableOptions []string `yaml:"-" json:"-" mapstructure:"-"`
Output string `yaml:"output" json:"output" mapstructure:"output"`
OutputTemplatePath string `yaml:"output-template-path" json:"output-template-path" mapstructure:"output-template-path"` // -t template file to use for output
}
var _ clio.FlagAdder = (*SingleOutput)(nil)
func (o *SingleOutput) AddFlags(flags clio.FlagSet) {
flags.StringVarP(&o.Output, "output", "o",
fmt.Sprintf("report output format, options=%v", o.AllowableOptions))
if slices.Contains(o.AllowableOptions, template.ID.String()) {
flags.StringVarP(&o.OutputTemplatePath, "template", "t",
"specify the path to a Go template file")
}
}
func (o *SingleOutput) SBOMWriter(file string) (sbom.Writer, error) {
return makeSBOMWriter([]string{o.Output}, file, o.OutputTemplatePath)
}
// OutputFile is only the --file argument
type OutputFile struct {
File string `yaml:"file" json:"file" mapstructure:"file"` // --file, the file to write report output to
}
var _ interface {
clio.FlagAdder
clio.PostLoader
} = (*OutputFile)(nil)
func (o *OutputFile) AddFlags(flags clio.FlagSet) {
flags.StringVarP(&o.File, "file", "",
"file to write the default report output to (default is STDOUT)")
}
func (o *OutputFile) PostLoad() error {
if o.File != "" {
file, err := expandFilePath(o.File)
if err != nil {
return err
}
o.File = file
}
return nil
}
func (o *OutputFile) SBOMWriter(f sbom.Format) (sbom.Writer, error) {
return makeSBOMWriterForFormat(f, o.File)
}
-118
View File
@@ -1,118 +0,0 @@
package options
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
"github.com/anchore/syft/syft/formats"
"github.com/anchore/syft/syft/formats/table"
"github.com/anchore/syft/syft/pkg/cataloger"
"github.com/anchore/syft/syft/source"
)
type PackagesOptions struct {
Scope string
Output []string
OutputTemplatePath string
File string
Platform string
Exclude []string
Catalogers []string
SourceName string
SourceVersion string
BasePath string
}
var _ Interface = (*PackagesOptions)(nil)
func (o *PackagesOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error {
cmd.Flags().StringVarP(&o.Scope, "scope", "s", cataloger.DefaultSearchConfig().Scope.String(),
fmt.Sprintf("selection of layers to catalog, options=%v", source.AllScopes))
cmd.Flags().StringArrayVarP(&o.Output, "output", "o", []string{string(table.ID)},
fmt.Sprintf("report output format, options=%v", formats.AllIDs()))
cmd.Flags().StringVarP(&o.File, "file", "", "",
"file to write the default report output to (default is STDOUT)")
cmd.Flags().StringVarP(&o.OutputTemplatePath, "template", "t", "",
"specify the path to a Go template file")
cmd.Flags().StringVarP(&o.Platform, "platform", "", "",
"an optional platform specifier for container image sources (e.g. 'linux/arm64', 'linux/arm64/v8', 'arm64', 'linux')")
cmd.Flags().StringArrayVarP(&o.Exclude, "exclude", "", nil,
"exclude paths from being scanned using a glob expression")
cmd.Flags().StringArrayVarP(&o.Catalogers, "catalogers", "", nil,
"enable one or more package catalogers")
cmd.Flags().StringVarP(&o.SourceName, "name", "", "",
"set the name of the target being analyzed")
cmd.Flags().Lookup("name").Deprecated = "use: source-name"
cmd.Flags().StringVarP(&o.SourceName, "source-name", "", "",
"set the name of the target being analyzed")
cmd.Flags().StringVarP(&o.SourceVersion, "source-version", "", "",
"set the name of the target being analyzed")
cmd.Flags().StringVarP(&o.BasePath, "base-path", "", "",
"base directory for scanning, no links will be followed above this directory, and all paths will be reported relative to this directory")
return bindPackageConfigOptions(cmd.Flags(), v)
}
//nolint:revive
func bindPackageConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error {
// Formatting & Input options //////////////////////////////////////////////
if err := v.BindPFlag("package.cataloger.scope", flags.Lookup("scope")); err != nil {
return err
}
if err := v.BindPFlag("file", flags.Lookup("file")); err != nil {
return err
}
if err := v.BindPFlag("exclude", flags.Lookup("exclude")); err != nil {
return err
}
if err := v.BindPFlag("catalogers", flags.Lookup("catalogers")); err != nil {
return err
}
if err := v.BindPFlag("name", flags.Lookup("name")); err != nil {
return err
}
if err := v.BindPFlag("source.name", flags.Lookup("source-name")); err != nil {
return err
}
if err := v.BindPFlag("source.version", flags.Lookup("source-version")); err != nil {
return err
}
if err := v.BindPFlag("output", flags.Lookup("output")); err != nil {
return err
}
if err := v.BindPFlag("output-template-path", flags.Lookup("template")); err != nil {
return err
}
if err := v.BindPFlag("platform", flags.Lookup("platform")); err != nil {
return err
}
if err := v.BindPFlag("base-path", flags.Lookup("base-path")); err != nil {
return err
}
return nil
}
+23
View File
@@ -0,0 +1,23 @@
package options
import (
"github.com/anchore/syft/syft/pkg/cataloger"
)
type pkg struct {
Cataloger scope `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 defaultPkg() pkg {
c := cataloger.DefaultSearchConfig()
return pkg{
SearchIndexedArchives: c.IncludeIndexedArchives,
SearchUnindexedArchives: c.IncludeUnindexedArchives,
Cataloger: scope{
Enabled: true,
Scope: c.Scope.String(),
},
}
}
@@ -1,13 +1,5 @@
package config
import (
"github.com/spf13/viper"
)
package options
type python struct {
GuessUnpinnedRequirements bool `json:"guess-unpinned-requirements" yaml:"guess-unpinned-requirements" mapstructure:"guess-unpinned-requirements"`
}
func (cfg python) loadDefaultValues(v *viper.Viper) {
v.SetDefault("python.guess-unpinned-requirements", false)
}
+84
View File
@@ -0,0 +1,84 @@
package options
import (
"os"
"github.com/anchore/clio"
"github.com/anchore/stereoscope/pkg/image"
)
type RegistryCredentials struct {
Authority string `yaml:"authority" json:"authority" mapstructure:"authority"`
// IMPORTANT: do not show any credential information, use secret type to automatically redact the values
Username secret `yaml:"username" json:"username" mapstructure:"username"`
Password secret `yaml:"password" json:"password" mapstructure:"password"`
Token secret `yaml:"token" json:"token" mapstructure:"token"`
TLSCert string `yaml:"tls-cert,omitempty" json:"tls-cert,omitempty" mapstructure:"tls-cert"`
TLSKey string `yaml:"tls-key,omitempty" json:"tls-key,omitempty" mapstructure:"tls-key"`
}
type registry struct {
InsecureSkipTLSVerify bool `yaml:"insecure-skip-tls-verify" json:"insecure-skip-tls-verify" mapstructure:"insecure-skip-tls-verify"`
InsecureUseHTTP bool `yaml:"insecure-use-http" json:"insecure-use-http" mapstructure:"insecure-use-http"`
Auth []RegistryCredentials `yaml:"auth" json:"auth" mapstructure:"auth"`
CACert string `yaml:"ca-cert" json:"ca-cert" mapstructure:"ca-cert"`
}
var _ clio.PostLoader = (*registry)(nil)
func (cfg *registry) PostLoad() error {
// there may be additional credentials provided by env var that should be appended to the set of credentials
authority, username, password, token, tlsCert, tlsKey :=
os.Getenv("SYFT_REGISTRY_AUTH_AUTHORITY"),
os.Getenv("SYFT_REGISTRY_AUTH_USERNAME"),
os.Getenv("SYFT_REGISTRY_AUTH_PASSWORD"),
os.Getenv("SYFT_REGISTRY_AUTH_TOKEN"),
os.Getenv("SYFT_REGISTRY_AUTH_TLS_CERT"),
os.Getenv("SYFT_REGISTRY_AUTH_TLS_KEY")
if hasNonEmptyCredentials(username, password, token, tlsCert, tlsKey) {
// note: we prepend the credentials such that the environment variables take precedence over on-disk configuration.
// since this PostLoad is called before the PostLoad on the Auth credentials list,
// all appropriate redactions will be added
cfg.Auth = append([]RegistryCredentials{
{
Authority: authority,
Username: secret(username),
Password: secret(password),
Token: secret(token),
TLSCert: tlsCert,
TLSKey: tlsKey,
},
}, cfg.Auth...)
}
return nil
}
func hasNonEmptyCredentials(username, password, token, tlsCert, tlsKey string) bool {
hasUserPass := username != "" && password != ""
hasToken := token != ""
hasTLSMaterial := tlsCert != "" && tlsKey != ""
return hasUserPass || hasToken || hasTLSMaterial
}
func (cfg *registry) ToOptions() *image.RegistryOptions {
var auth = make([]image.RegistryCredentials, len(cfg.Auth))
for i, a := range cfg.Auth {
auth[i] = image.RegistryCredentials{
Authority: a.Authority,
Username: a.Username.String(),
Password: a.Password.String(),
Token: a.Token.String(),
ClientCert: a.TLSCert,
ClientKey: a.TLSKey,
}
}
return &image.RegistryOptions{
InsecureSkipTLSVerify: cfg.InsecureSkipTLSVerify,
InsecureUseHTTP: cfg.InsecureUseHTTP,
Credentials: auth,
CAFileOrDir: cfg.CACert,
}
}
@@ -1,4 +1,4 @@
package config
package options
import (
"fmt"
@@ -11,48 +11,60 @@ import (
func TestHasNonEmptyCredentials(t *testing.T) {
tests := []struct {
username, password, token string
expected bool
username, password, token, cert, key string
expected bool
}{
{
"", "", "",
"", "", "", "", "",
false,
},
{
"user", "", "",
"user", "", "", "", "",
false,
},
{
"", "pass", "",
"", "pass", "", "", "",
false,
},
{
"", "pass", "tok",
"", "pass", "tok", "", "",
true,
},
{
"user", "", "tok",
"user", "", "tok", "", "",
true,
},
{
"", "", "tok",
"", "", "tok", "", "",
true,
},
{
"user", "pass", "tok",
"user", "pass", "tok", "", "",
true,
},
{
"user", "pass", "",
"user", "pass", "", "", "",
true,
},
{
"", "", "", "cert", "key",
true,
},
{
"", "", "", "cert", "",
false,
},
{
"", "", "", "", "key",
false,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%+v", test), func(t *testing.T) {
assert.Equal(t, test.expected, hasNonEmptyCredentials(test.username, test.password, test.token))
assert.Equal(t, test.expected, hasNonEmptyCredentials(test.username, test.password, test.token, test.cert, test.key))
})
}
}
@@ -102,6 +114,29 @@ func Test_registry_ToOptions(t *testing.T) {
Credentials: []image.RegistryCredentials{},
},
},
{
name: "provide all tls configuration",
input: registry{
CACert: "ca.crt",
InsecureSkipTLSVerify: true,
Auth: []RegistryCredentials{
{
TLSCert: "client.crt",
TLSKey: "client.key",
},
},
},
expected: image.RegistryOptions{
CAFileOrDir: "ca.crt",
InsecureSkipTLSVerify: true,
Credentials: []image.RegistryCredentials{
{
ClientCert: "client.crt",
ClientKey: "client.key",
},
},
},
},
}
for _, test := range tests {
-33
View File
@@ -1,33 +0,0 @@
package options
import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
const DefaultRekorURL = "https://rekor.sigstore.dev"
// RekorOptions is the wrapper for Rekor related options.
type RekorOptions struct {
URL string
}
var _ Interface = (*RekorOptions)(nil)
// AddFlags implements Interface
func (o *RekorOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error {
cmd.Flags().StringVar(&o.URL, "rekor-url", DefaultRekorURL,
"address of rekor STL server")
return bindRekorConfigOptions(cmd.Flags(), v)
}
//nolint:revive
func bindRekorConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error {
// TODO: config re-design
if err := v.BindPFlag("attest.rekor-url", flags.Lookup("rekor-url")); err != nil {
return err
}
return nil
}
-37
View File
@@ -1,37 +0,0 @@
package options
import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
type RootOptions struct {
Config string
Quiet bool
Verbose int
}
var _ Interface = (*RootOptions)(nil)
func (o *RootOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error {
cmd.PersistentFlags().StringVarP(&o.Config, "config", "c", "", "application config file")
cmd.PersistentFlags().CountVarP(&o.Verbose, "verbose", "v", "increase verbosity (-v = info, -vv = debug)")
cmd.PersistentFlags().BoolVarP(&o.Quiet, "quiet", "q", false, "suppress all logging output")
return bindRootConfigOptions(cmd.PersistentFlags(), v)
}
//nolint:revive
func bindRootConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error {
if err := v.BindPFlag("config", flags.Lookup("config")); err != nil {
return err
}
if err := v.BindPFlag("verbosity", flags.Lookup("verbose")); err != nil {
return err
}
if err := v.BindPFlag("quiet", flags.Lookup("quiet")); err != nil {
return err
}
return nil
}
+27
View File
@@ -0,0 +1,27 @@
package options
import (
"fmt"
"github.com/anchore/clio"
"github.com/anchore/syft/syft/source"
)
type scope struct {
Enabled bool `yaml:"enabled" json:"enabled" mapstructure:"enabled"`
Scope string `yaml:"scope" json:"scope" mapstructure:"scope"`
}
var _ clio.PostLoader = (*scope)(nil)
func (opt *scope) PostLoad() error {
s := opt.GetScope()
if s == source.UnknownScope {
return fmt.Errorf("bad scope value %v", opt.Scope)
}
return nil
}
func (opt scope) GetScope() source.Scope {
return source.ParseScope(opt.Scope)
}
+25
View File
@@ -0,0 +1,25 @@
package options
import (
"fmt"
"github.com/anchore/clio"
"github.com/anchore/syft/internal/redact"
)
type secret string
var _ interface {
fmt.Stringer
clio.PostLoader
} = (*secret)(nil)
// PostLoad needs to use a pointer receiver, even if it's not modifying the value
func (r *secret) PostLoad() error {
redact.Add(string(*r))
return nil
}
func (r secret) String() string {
return string(r)
}
@@ -1,29 +1,23 @@
package config
package options
import (
"github.com/spf13/viper"
"github.com/anchore/syft/internal/file"
"github.com/anchore/syft/syft/source"
)
type secrets struct {
Cataloger catalogerOptions `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"`
Cataloger scope `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"`
AdditionalPatterns map[string]string `yaml:"additional-patterns" json:"additional-patterns" mapstructure:"additional-patterns"`
ExcludePatternNames []string `yaml:"exclude-pattern-names" json:"exclude-pattern-names" mapstructure:"exclude-pattern-names"`
RevealValues bool `yaml:"reveal-values" json:"reveal-values" mapstructure:"reveal-values"`
SkipFilesAboveSize int64 `yaml:"skip-files-above-size" json:"skip-files-above-size" mapstructure:"skip-files-above-size"`
}
func (cfg secrets) loadDefaultValues(v *viper.Viper) {
v.SetDefault("secrets.cataloger.enabled", catalogerEnabledDefault)
v.SetDefault("secrets.cataloger.scope", source.AllLayersScope)
v.SetDefault("secrets.reveal-values", false)
v.SetDefault("secrets.skip-files-above-size", 1*file.MB)
v.SetDefault("secrets.additional-patterns", map[string]string{})
v.SetDefault("secrets.exclude-pattern-names", []string{})
}
func (cfg *secrets) parseConfigValues() error {
return cfg.Cataloger.parseConfigValues()
func defaultSecrets() secrets {
return secrets{
Cataloger: scope{
Scope: source.AllLayersScope.String(),
},
SkipFilesAboveSize: 1 * file.MB,
}
}
@@ -1,6 +1,4 @@
package config
import "github.com/spf13/viper"
package options
type sourceCfg struct {
Name string `json:"name" yaml:"name" mapstructure:"name"`
@@ -12,6 +10,10 @@ type fileSource struct {
Digests []string `json:"digests" yaml:"digests" mapstructure:"digests"`
}
func (cfg sourceCfg) loadDefaultValues(v *viper.Viper) {
v.SetDefault("source.file.digests", []string{"sha256"})
func defaultSourceCfg() sourceCfg {
return sourceCfg{
File: fileSource{
Digests: []string{"sha256"},
},
}
}
+11
View File
@@ -0,0 +1,11 @@
package options
type UpdateCheck struct {
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
}
func DefaultUpdateCheck() UpdateCheck {
return UpdateCheck{
CheckForAppUpdate: true,
}
}
-18
View File
@@ -1,18 +0,0 @@
package options
import (
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/config"
"github.com/anchore/syft/internal/log"
)
func IsVerbose(app *config.Application) (result bool) {
isPipedInput, err := internal.IsPipedInput()
if err != nil {
// since we can't tell if there was piped input we assume that there could be to disable the ETUI
log.Warnf("unable to determine if there is piped input: %+v", err)
return true
}
// verbosity should consider if there is piped input (in which case we should not show the ETUI)
return app.Verbosity > 0 || isPipedInput
}
-17
View File
@@ -1,17 +0,0 @@
package options
import (
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
type VersionOptions struct {
Output string
}
var _ Interface = (*VersionOptions)(nil)
func (o *VersionOptions) AddFlags(cmd *cobra.Command, _ *viper.Viper) error {
cmd.Flags().StringVarP(&o.Output, "output", "o", "text", "format to show version information (available=[text, json])")
return nil
}
+4 -4
View File
@@ -26,9 +26,9 @@ var _ interface {
sbom.Writer
} = (*sbomStreamWriter)(nil)
// MakeSBOMWriter creates a sbom.Writer for output or returns an error. this will either return a valid writer
// makeSBOMWriter 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 MakeSBOMWriter(outputs []string, defaultFile, templateFilePath string) (sbom.Writer, error) {
func makeSBOMWriter(outputs []string, defaultFile, templateFilePath string) (sbom.Writer, error) {
outputOptions, err := parseSBOMOutputFlags(outputs, defaultFile, templateFilePath)
if err != nil {
return nil, err
@@ -42,8 +42,8 @@ func MakeSBOMWriter(outputs []string, defaultFile, templateFilePath string) (sbo
return writer, nil
}
// MakeSBOMWriterForFormat creates a sbom.Writer for for the given format or returns an error.
func MakeSBOMWriterForFormat(format sbom.Format, path string) (sbom.Writer, error) {
// makeSBOMWriterForFormat creates a sbom.Writer for for the given format or returns an error.
func makeSBOMWriterForFormat(format sbom.Format, path string) (sbom.Writer, error) {
writer, err := newSBOMMultiWriter(newSBOMWriterDescription(format, path))
if err != nil {
return nil, err
+1 -1
View File
@@ -34,7 +34,7 @@ func Test_MakeSBOMWriter(t *testing.T) {
}
for _, tt := range tests {
_, err := MakeSBOMWriter(tt.outputs, "", "")
_, err := makeSBOMWriter(tt.outputs, "", "")
tt.wantErr(t, err)
}
}
-86
View File
@@ -1,86 +0,0 @@
package cli
import (
"fmt"
"log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/cmd/syft/cli/packages"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/config"
)
const (
packagesExample = ` {{.appName}} {{.command}} alpine:latest a summary of discovered packages
{{.appName}} {{.command}} alpine:latest -o json show all possible cataloging details
{{.appName}} {{.command}} alpine:latest -o cyclonedx show a CycloneDX formatted SBOM
{{.appName}} {{.command}} alpine:latest -o cyclonedx-json show a CycloneDX JSON formatted SBOM
{{.appName}} {{.command}} alpine:latest -o spdx show a SPDX 2.3 Tag-Value formatted SBOM
{{.appName}} {{.command}} alpine:latest -o spdx@2.2 show a SPDX 2.2 Tag-Value formatted SBOM
{{.appName}} {{.command}} alpine:latest -o spdx-json show a SPDX 2.3 JSON formatted SBOM
{{.appName}} {{.command}} alpine:latest -o spdx-json@2.2 show a SPDX 2.2 JSON formatted SBOM
{{.appName}} {{.command}} alpine:latest -vv show verbose debug information
{{.appName}} {{.command}} alpine:latest -o template -t my_format.tmpl show a SBOM formatted according to given template file
Supports the following image sources:
{{.appName}} {{.command}} yourrepo/yourimage:tag defaults to using images from a Docker daemon. If Docker is not present, the image is pulled directly from the registry.
{{.appName}} {{.command}} path/to/a/file/or/dir a Docker tar, OCI tar, OCI directory, SIF container, or generic filesystem directory
`
schemeHelpHeader = "You can also explicitly specify the scheme to use:"
imageSchemeHelp = ` {{.appName}} {{.command}} docker:yourrepo/yourimage:tag explicitly use the Docker daemon
{{.appName}} {{.command}} podman:yourrepo/yourimage:tag explicitly use the Podman daemon
{{.appName}} {{.command}} registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required)
{{.appName}} {{.command}} docker-archive:path/to/yourimage.tar use a tarball from disk for archives created from "docker save"
{{.appName}} {{.command}} oci-archive:path/to/yourimage.tar use a tarball from disk for OCI archives (from Skopeo or otherwise)
{{.appName}} {{.command}} oci-dir:path/to/yourimage read directly from a path on disk for OCI layout directories (from Skopeo or otherwise)
{{.appName}} {{.command}} singularity:path/to/yourimage.sif read directly from a Singularity Image Format (SIF) container on disk
`
nonImageSchemeHelp = ` {{.appName}} {{.command}} dir:path/to/yourproject read directly from a path on disk (any directory)
{{.appName}} {{.command}} file:path/to/yourproject/file read directly from a path on disk (any single file)
`
packagesSchemeHelp = "\n" + indent + schemeHelpHeader + "\n" + imageSchemeHelp + nonImageSchemeHelp
packagesHelp = packagesExample + packagesSchemeHelp
)
//nolint:dupl
func Packages(v *viper.Viper, app *config.Application, ro *options.RootOptions, po *options.PackagesOptions) *cobra.Command {
cmd := &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",
Example: internal.Tprintf(packagesHelp, map[string]interface{}{
"appName": internal.ApplicationName,
"command": "packages",
}),
Args: func(cmd *cobra.Command, args []string) error {
if err := app.LoadAllValues(v, ro.Config); err != nil {
return fmt.Errorf("invalid application config: %w", err)
}
// configure logging for command
newLogWrapper(app)
logApplicationConfig(app)
return validateArgs(cmd, args)
},
SilenceUsage: true,
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
if app.CheckForAppUpdate {
// TODO: this is broke, the bus isn't available yet
checkForApplicationUpdate()
}
return packages.Run(cmd.Context(), app, args)
},
}
err := po.AddFlags(cmd, v)
if err != nil {
log.Fatal(err)
}
return cmd
}
-192
View File
@@ -1,192 +0,0 @@
package packages
import (
"context"
"fmt"
"github.com/wagoodman/go-partybus"
"github.com/anchore/stereoscope"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/syft/cmd/syft/cli/eventloop"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/cmd/syft/internal/ui"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/config"
"github.com/anchore/syft/internal/file"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/formats/template"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
func Run(_ context.Context, app *config.Application, args []string) error {
err := ValidateOutputOptions(app)
if err != nil {
return err
}
writer, err := options.MakeSBOMWriter(app.Outputs, app.File, app.OutputTemplatePath)
if err != nil {
return err
}
// could be an image or a directory, with or without a scheme
userInput := args[0]
eventBus := partybus.NewBus()
stereoscope.SetBus(eventBus)
syft.SetBus(eventBus)
subscription := eventBus.Subscribe()
return eventloop.EventLoop(
execWorker(app, userInput, writer),
eventloop.SetupSignals(),
subscription,
stereoscope.Cleanup,
ui.Select(options.IsVerbose(app), app.Quiet)...,
)
}
// nolint:funlen
func execWorker(app *config.Application, userInput string, writer sbom.Writer) <-chan error {
errs := make(chan error)
go func() {
defer close(errs)
defer bus.Exit()
detection, err := source.Detect(
userInput,
source.DetectConfig{
DefaultImageSource: app.DefaultImagePullSource,
},
)
if err != nil {
errs <- fmt.Errorf("could not deteremine source: %w", err)
return
}
var platform *image.Platform
if app.Platform != "" {
platform, err = image.NewPlatform(app.Platform)
if err != nil {
errs <- fmt.Errorf("invalid platform: %w", err)
return
}
}
hashers, err := file.Hashers(app.Source.File.Digests...)
if err != nil {
errs <- fmt.Errorf("invalid hash: %w", err)
return
}
src, err := detection.NewSource(
source.DetectionSourceConfig{
Alias: source.Alias{
Name: app.Source.Name,
Version: app.Source.Version,
},
RegistryOptions: app.Registry.ToOptions(),
Platform: platform,
Exclude: source.ExcludeConfig{
Paths: app.Exclusions,
},
DigestAlgorithms: hashers,
BasePath: app.BasePath,
},
)
if err != nil {
errs <- fmt.Errorf("failed to construct source from user input %q: %w", userInput, err)
return
}
defer func() {
if src != nil {
if err := src.Close(); err != nil {
log.Tracef("unable to close source: %+v", err)
}
}
}()
s, err := GenerateSBOM(src, errs, app)
if err != nil {
errs <- err
return
}
if s == nil {
errs <- fmt.Errorf("no SBOM produced for %q", userInput)
return
}
if err := writer.Write(*s); err != nil {
errs <- fmt.Errorf("failed to write SBOM: %w", err)
return
}
}()
return errs
}
func GenerateSBOM(src source.Source, errs chan error, app *config.Application) (*sbom.SBOM, error) {
tasks, err := eventloop.Tasks(app)
if err != nil {
return nil, err
}
s := sbom.SBOM{
Source: src.Describe(),
Descriptor: sbom.Descriptor{
Name: internal.ApplicationName,
Version: version.FromBuild().Version,
Configuration: app,
},
}
buildRelationships(&s, src, tasks, errs)
return &s, nil
}
func buildRelationships(s *sbom.SBOM, src source.Source, tasks []eventloop.Task, errs chan error) {
var relationships []<-chan artifact.Relationship
for _, task := range tasks {
c := make(chan artifact.Relationship)
relationships = append(relationships, c)
go eventloop.RunTask(task, &s.Artifacts, src, c, errs)
}
s.Relationships = append(s.Relationships, MergeRelationships(relationships...)...)
}
func MergeRelationships(cs ...<-chan artifact.Relationship) (relationships []artifact.Relationship) {
for _, c := range cs {
for n := range c {
relationships = append(relationships, n)
}
}
return relationships
}
func ValidateOutputOptions(app *config.Application) error {
var usesTemplateOutput bool
for _, o := range app.Outputs {
if o == template.ID.String() {
usesTemplateOutput = true
break
}
}
if usesTemplateOutput && app.OutputTemplatePath == "" {
return fmt.Errorf(`must specify path to template file when using "template" output format`)
}
return nil
}
-51
View File
@@ -1,51 +0,0 @@
package cli
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/cmd/syft/cli/poweruser"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/config"
)
const powerUserExample = ` {{.appName}} {{.command}} <image>
DEPRECATED - THIS COMMAND WILL BE REMOVED in v1.0.0
Only image sources are supported (e.g. docker: , podman: , docker-archive: , oci: , etc.), the directory source (dir:) is not supported, template outputs are not supported.
All behavior is controlled via application configuration and environment variables (see https://github.com/anchore/syft#configuration)
`
func PowerUser(v *viper.Viper, app *config.Application, ro *options.RootOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "power-user [IMAGE]",
Short: "Run bulk operations on container images",
Example: internal.Tprintf(powerUserExample, map[string]interface{}{
"appName": internal.ApplicationName,
"command": "power-user",
}),
Args: func(cmd *cobra.Command, args []string) error {
if err := app.LoadAllValues(v, ro.Config); err != nil {
return fmt.Errorf("invalid application config: %w", err)
}
// configure logging for command
newLogWrapper(app)
logApplicationConfig(app)
return validateArgs(cmd, args)
},
Hidden: true,
SilenceUsage: true,
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
if app.CheckForAppUpdate {
checkForApplicationUpdate()
// TODO: this is broke, the bus isn't available yet
}
return poweruser.Run(cmd.Context(), app, args)
},
}
return cmd
}
-144
View File
@@ -1,144 +0,0 @@
package poweruser
import (
"context"
"fmt"
"os"
"github.com/gookit/color"
"github.com/wagoodman/go-partybus"
"github.com/anchore/stereoscope"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/syft/cmd/syft/cli/eventloop"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/cmd/syft/cli/packages"
"github.com/anchore/syft/cmd/syft/internal/ui"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/config"
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/formats/syftjson"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
func Run(_ context.Context, app *config.Application, args []string) error {
f := syftjson.Format()
writer, err := options.MakeSBOMWriterForFormat(f, app.File)
if err != nil {
return err
}
defer func() {
// inform user at end of run that command will be removed
deprecated := color.Style{color.Red, color.OpBold}.Sprint("DEPRECATED: This command will be removed in v1.0.0")
fmt.Fprintln(os.Stderr, deprecated)
}()
userInput := args[0]
eventBus := partybus.NewBus()
stereoscope.SetBus(eventBus)
syft.SetBus(eventBus)
subscription := eventBus.Subscribe()
return eventloop.EventLoop(
execWorker(app, userInput, writer),
eventloop.SetupSignals(),
subscription,
stereoscope.Cleanup,
ui.Select(options.IsVerbose(app), app.Quiet)...,
)
}
//nolint:funlen
func execWorker(app *config.Application, userInput string, writer sbom.Writer) <-chan error {
errs := make(chan error)
go func() {
defer close(errs)
defer bus.Exit()
app.Secrets.Cataloger.Enabled = true
app.FileMetadata.Cataloger.Enabled = true
app.FileContents.Cataloger.Enabled = true
app.FileClassification.Cataloger.Enabled = true
tasks, err := eventloop.Tasks(app)
if err != nil {
errs <- err
return
}
detection, err := source.Detect(
userInput,
source.DetectConfig{
DefaultImageSource: app.DefaultImagePullSource,
},
)
if err != nil {
errs <- fmt.Errorf("could not deteremine source: %w", err)
return
}
var platform *image.Platform
if app.Platform != "" {
platform, err = image.NewPlatform(app.Platform)
if err != nil {
errs <- fmt.Errorf("invalid platform: %w", err)
return
}
}
src, err := detection.NewSource(
source.DetectionSourceConfig{
Alias: source.Alias{
Name: app.Source.Name,
Version: app.Source.Version,
},
RegistryOptions: app.Registry.ToOptions(),
Platform: platform,
Exclude: source.ExcludeConfig{
Paths: app.Exclusions,
},
DigestAlgorithms: nil,
BasePath: app.BasePath,
},
)
if src != nil {
defer src.Close()
}
if err != nil {
errs <- fmt.Errorf("failed to construct source from user input %q: %w", userInput, err)
return
}
s := sbom.SBOM{
Source: src.Describe(),
Descriptor: sbom.Descriptor{
Name: internal.ApplicationName,
Version: version.FromBuild().Version,
Configuration: app,
},
}
var relationships []<-chan artifact.Relationship
for _, task := range tasks {
c := make(chan artifact.Relationship)
relationships = append(relationships, c)
go eventloop.RunTask(task, &s.Artifacts, src, c, errs)
}
s.Relationships = append(s.Relationships, packages.MergeRelationships(relationships...)...)
if err := writer.Write(s); err != nil {
errs <- fmt.Errorf("failed to write sbom: %w", err)
return
}
}()
return errs
}
-72
View File
@@ -1,72 +0,0 @@
package cli
import (
"encoding/json"
"fmt"
"log"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/config"
"github.com/anchore/syft/internal/version"
)
func Version(v *viper.Viper, _ *config.Application) *cobra.Command {
o := &options.VersionOptions{}
cmd := &cobra.Command{
Use: "version",
Short: "show the version",
RunE: func(cmd *cobra.Command, args []string) error {
return printVersion(o.Output)
},
}
err := o.AddFlags(cmd, v)
if err != nil {
log.Fatal(err)
}
return cmd
}
func printVersion(output string) error {
versionInfo := version.FromBuild()
switch output {
case "text":
fmt.Println("Application: ", internal.ApplicationName)
fmt.Println("Version: ", versionInfo.Version)
fmt.Println("JsonSchemaVersion: ", internal.JSONSchemaVersion)
fmt.Println("BuildDate: ", versionInfo.BuildDate)
fmt.Println("GitCommit: ", versionInfo.GitCommit)
fmt.Println("GitDescription: ", versionInfo.GitDescription)
fmt.Println("Platform: ", versionInfo.Platform)
fmt.Println("GoVersion: ", versionInfo.GoVersion)
fmt.Println("Compiler: ", versionInfo.Compiler)
case "json":
enc := json.NewEncoder(os.Stdout)
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
err := enc.Encode(&struct {
version.Version
Application string `json:"application"`
}{
Version: versionInfo,
Application: internal.ApplicationName,
})
if err != nil {
fmt.Printf("failed to show version information: %+v\n", err)
os.Exit(1)
}
default:
fmt.Printf("unsupported output format: %s\n", output)
os.Exit(1)
}
return nil
}
+5
View File
@@ -0,0 +1,5 @@
package internal
const (
NotProvided = "[not provided]"
)
-2
View File
@@ -33,8 +33,6 @@ func (n *NoUI) Handle(e partybus.Event) error {
case event.CLIReport, event.CLINotification:
// keep these for when the UI is terminated to show to the screen (or perform other events)
n.finalizeEvents = append(n.finalizeEvents, e)
case event.CLIExit:
return n.subscription.Unsubscribe()
}
return nil
}
+7 -3
View File
@@ -10,7 +10,6 @@ import (
"github.com/wagoodman/go-partybus"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/event/parsers"
)
@@ -119,13 +118,18 @@ func writeAppUpdate(writer io.Writer, events ...partybus.Event) error {
style := lipgloss.NewStyle().Foreground(lipgloss.Color("13")).Italic(true)
for _, e := range events {
newVersion, err := parsers.ParseCLIAppUpdateAvailable(e)
updateCheck, err := parsers.ParseCLIAppUpdateAvailable(e)
if err != nil {
log.WithFields("error", err).Warn("failed to parse app update notification")
continue
}
notice := fmt.Sprintf("A newer version of syft is available for download: %s (installed version is %s)", newVersion, version.FromBuild().Version)
if updateCheck.Current == updateCheck.New {
log.Tracef("update check event with identical versions: %s", updateCheck.Current)
continue
}
notice := fmt.Sprintf("A newer version of syft is available for download: %s (installed version is %s)", updateCheck.New, updateCheck.Current)
if _, err := fmt.Fprintln(writer, style.Render(notice)); err != nil {
// don't let this be fatal
@@ -9,6 +9,7 @@ import (
"github.com/wagoodman/go-partybus"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/event/parsers"
)
func Test_postUIEventWriter_write(t *testing.T) {
@@ -34,8 +35,11 @@ func Test_postUIEventWriter_write(t *testing.T) {
Value: "<notification 2>",
},
{
Type: event.CLIAppUpdateAvailable,
Value: "v0.33.0",
Type: event.CLIAppUpdateAvailable,
Value: parsers.UpdateCheck{
New: "v0.33.0",
Current: "[not provided]",
},
},
{
Type: event.CLINotification,
@@ -61,8 +65,11 @@ func Test_postUIEventWriter_write(t *testing.T) {
Value: "<notification 1>",
},
{
Type: event.CLIAppUpdateAvailable,
Value: "<app update>",
Type: event.CLIAppUpdateAvailable,
Value: parsers.UpdateCheck{
New: "<new version>",
Current: "<current version>",
},
},
{
Type: event.CLIReport,
+9 -14
View File
@@ -49,31 +49,23 @@ func (m *UI) Setup(subscription partybus.Unsubscribable) error {
}
m.subscription = subscription
m.program = tea.NewProgram(m, tea.WithOutput(os.Stderr), tea.WithInput(os.Stdin))
m.program = tea.NewProgram(m, tea.WithOutput(os.Stderr), tea.WithInput(os.Stdin), tea.WithoutSignalHandler())
m.running.Add(1)
go func() {
defer m.running.Done()
if _, err := m.program.Run(); err != nil {
log.Errorf("unable to start UI: %+v", err)
m.exit()
bus.ExitWithInterrupt()
}
}()
return nil
}
func (m *UI) exit() {
// stop the event loop
bus.Exit()
}
func (m *UI) Handle(e partybus.Event) error {
if m.program != nil {
m.program.Send(e)
if e.Type == event.CLIExit {
return m.subscription.Unsubscribe()
}
}
return nil
}
@@ -88,7 +80,9 @@ func (m *UI) Teardown(force bool) error {
// string from the worker (outside of the UI after teardown).
m.running.Wait()
} else {
m.program.Kill()
// it may be tempting to use Kill() however it has been found that this can cause the terminal to be left in
// a bad state (where Ctrl+C and other control characters no longer works for future processes in that terminal).
m.program.Quit()
}
// TODO: allow for writing out the full log output to the screen (only a partial log is shown currently)
@@ -107,7 +101,6 @@ func (m UI) RespondsTo() []partybus.EventType {
return append([]partybus.EventType{
event.CLIReport,
event.CLINotification,
event.CLIExit,
event.CLIAppUpdateAvailable,
}, m.handler.RespondsTo()...)
}
@@ -126,8 +119,10 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
// today we treat esc and ctrl+c the same, but in the future when the syft worker has a graceful way to
// cancel in-flight work via a context, we can wire up esc to this path with bus.Exit()
case "esc", "ctrl+c":
m.exit()
bus.ExitWithInterrupt()
return m, tea.Quit
}
@@ -135,7 +130,7 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
log.WithFields("component", "ui").Tracef("event: %q", msg.Type)
switch msg.Type {
case event.CLIReport, event.CLINotification, event.CLIExit, event.CLIAppUpdateAvailable:
case event.CLIReport, event.CLINotification, event.CLIAppUpdateAvailable:
// keep these for when the UI is terminated to show to the screen (or perform other events)
m.finalizeEvents = append(m.finalizeEvents, msg)
+23 -9
View File
@@ -1,20 +1,34 @@
package main
import (
"log"
_ "modernc.org/sqlite"
"github.com/anchore/clio"
"github.com/anchore/syft/cmd/syft/cli"
"github.com/anchore/syft/cmd/syft/internal"
)
// applicationName is the non-capitalized name of the application (do not change this)
const applicationName = "syft"
// all variables here are provided as build-time arguments, with clear default values
var (
version = internal.NotProvided
buildDate = internal.NotProvided
gitCommit = internal.NotProvided
gitDescription = internal.NotProvided
)
func main() {
cli, err := cli.New()
if err != nil {
log.Fatalf("error during command construction: %v", err)
}
app := cli.New(
clio.Identification{
Name: applicationName,
Version: version,
BuildDate: buildDate,
GitCommit: gitCommit,
GitDescription: gitDescription,
},
)
if err := cli.Execute(); err != nil {
log.Fatalf("error during command execution: %v", err)
}
app.Run()
}
+41 -48
View File
@@ -3,22 +3,50 @@ module github.com/anchore/syft
go 1.19
require (
github.com/CycloneDX/cyclonedx-go v0.7.1
github.com/Masterminds/semver v1.5.0
github.com/Masterminds/sprig/v3 v3.2.3
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
github.com/acobaugh/osrelease v0.1.0
github.com/adrg/xdg v0.4.0
github.com/adrg/xdg v0.4.0 // indirect
github.com/anchore/bubbly v0.0.0-20230801194016-acdb4981b461
github.com/anchore/clio v0.0.0-20230823172630-c42d666061af
github.com/anchore/fangs v0.0.0-20230818131516-2186b10924fe
github.com/anchore/go-logger v0.0.0-20230725134548-c21dafa1ec5a
github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb
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.1.1-0.20230104203445-02e0a6721501
github.com/anchore/stereoscope v0.0.0-20230829142608-334c2222e137
// 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/aquasecurity/go-pep440-version v0.0.0-20210121094942-22b2f8951d46
github.com/bmatcuk/doublestar/v4 v4.6.0
github.com/charmbracelet/bubbletea v0.24.2
github.com/charmbracelet/lipgloss v0.8.0
github.com/dave/jennifer v1.7.0
github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da
github.com/docker/distribution v2.8.2+incompatible
github.com/docker/docker v24.0.5+incompatible
github.com/dustin/go-humanize v1.0.1
github.com/facebookincubator/nvdtools v0.1.5
github.com/github/go-spdx/v2 v2.1.2
github.com/gkampitakis/go-snaps v0.4.7
github.com/go-git/go-billy/v5 v5.4.1
github.com/go-git/go-git/v5 v5.8.1
github.com/go-test/deep v1.1.0
github.com/google/go-cmp v0.5.9
github.com/google/go-containerregistry v0.16.1
github.com/google/licensecheck v0.3.1
github.com/google/uuid v1.3.1
github.com/gookit/color v1.5.4
github.com/hashicorp/go-multierror v1.1.1
github.com/iancoleman/strcase v0.3.0
github.com/invopop/jsonschema v0.7.0
github.com/jinzhu/copier v0.4.0
github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953
github.com/knqyf263/go-rpmdb v0.0.0-20230301153543-ba94b245509b
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mholt/archiver/v3 v3.5.1
github.com/microsoft/go-rustaudit v0.0.0-20220730194248-4b17361d90a5
@@ -26,58 +54,33 @@ require (
github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/mitchellh/mapstructure v1.5.0
github.com/olekukonko/tablewriter v0.0.5
github.com/opencontainers/go-digest v1.0.0
github.com/pelletier/go-toml v1.9.5
github.com/pkg/errors v0.9.1 // indirect
github.com/saferwall/pe v1.4.4
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d
github.com/sassoftware/go-rpmutils v0.2.0
// pinned to pull in 386 arch fix: https://github.com/scylladb/go-set/commit/cc7b2070d91ebf40d233207b633e28f5bd8f03a5
github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e
github.com/sergi/go-diff v1.3.1
github.com/sirupsen/logrus v1.9.3
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/spdx/tools-golang v0.5.3
github.com/spf13/afero v1.9.5
github.com/spf13/cobra v1.7.0
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.16.0
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.16.0 // indirect
github.com/stretchr/testify v1.8.4
github.com/vbatts/go-mtree v0.5.3
github.com/vifraa/gopom v1.0.0
github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651
github.com/wagoodman/go-progress v0.0.0-20230301185719-21920a456ad5
github.com/xeipuuv/gojsonschema v1.2.0
github.com/zyedidia/generic v1.2.2-0.20230320175451-4410d2372cb1
golang.org/x/crypto v0.12.0 // indirect
golang.org/x/exp v0.0.0-20230202163644-54bba9f4231b
golang.org/x/mod v0.12.0
golang.org/x/net v0.14.0
golang.org/x/term v0.11.0
gopkg.in/yaml.v2 v2.4.0
)
require (
github.com/CycloneDX/cyclonedx-go v0.7.1
github.com/Masterminds/semver v1.5.0
github.com/Masterminds/sprig/v3 v3.2.3
github.com/anchore/bubbly v0.0.0-20230801194016-acdb4981b461
github.com/anchore/clio v0.0.0-20230602170917-e747e60c4aa0
github.com/anchore/go-logger v0.0.0-20230531193951-db5ae83e7dbe
github.com/anchore/stereoscope v0.0.0-20230727211946-d1f3d766295e
github.com/aquasecurity/go-pep440-version v0.0.0-20210121094942-22b2f8951d46
github.com/charmbracelet/bubbletea v0.24.2
github.com/charmbracelet/lipgloss v0.8.0
github.com/dave/jennifer v1.7.0
github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da
github.com/docker/distribution v2.8.2+incompatible
github.com/docker/docker v24.0.5+incompatible
github.com/github/go-spdx/v2 v2.1.2
github.com/gkampitakis/go-snaps v0.4.7
github.com/go-git/go-billy/v5 v5.4.1
github.com/go-git/go-git/v5 v5.8.1
github.com/google/go-containerregistry v0.16.1
github.com/google/licensecheck v0.3.1
github.com/invopop/jsonschema v0.7.0
github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953
github.com/knqyf263/go-rpmdb v0.0.0-20230301153543-ba94b245509b
github.com/opencontainers/go-digest v1.0.0
github.com/saferwall/pe v1.4.4
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d
github.com/sassoftware/go-rpmutils v0.2.0
github.com/vbatts/go-mtree v0.5.3
github.com/zyedidia/generic v1.2.2-0.20230320175451-4410d2372cb1
golang.org/x/exp v0.0.0-20230202163644-54bba9f4231b
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.25.0
)
@@ -91,7 +94,6 @@ require (
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect
github.com/acomagu/bufpipe v1.0.4 // indirect
github.com/anchore/fangs v0.0.0-20230531202914-48a718c6b4ba // indirect
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 // indirect
github.com/aquasecurity/go-version v0.0.0-20210121072130-637058cfe492 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
@@ -163,7 +165,6 @@ require (
github.com/skeema/knownhosts v1.2.0 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
github.com/sylabs/sif/v2 v2.11.5 // indirect
github.com/sylabs/squashfs v0.6.1 // indirect
@@ -201,14 +202,6 @@ require (
modernc.org/token v1.0.1 // indirect
)
require (
// 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/pkg/errors v0.9.1 // indirect
golang.org/x/crypto v0.12.0 // indirect
)
retract (
v0.53.2
v0.53.1 // Published accidentally with incorrect license in depdencies
+10 -9
View File
@@ -90,12 +90,12 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/anchore/bubbly v0.0.0-20230801194016-acdb4981b461 h1:xGu4/uMWucwWV0YV3fpFIQZ6KVfS/Wfhmma8t0s0vRo=
github.com/anchore/bubbly v0.0.0-20230801194016-acdb4981b461/go.mod h1:Ger02eh5NpPm2IqkPAy396HU1KlK3BhOeCljDYXySSk=
github.com/anchore/clio v0.0.0-20230602170917-e747e60c4aa0 h1:g0UqRW60JDrf5fb40RUyIwwcfQ3nAJqGj4aUCVTwFE4=
github.com/anchore/clio v0.0.0-20230602170917-e747e60c4aa0/go.mod h1:0IQVIROfgRX4WZFMfgsbNZmMgLKqW/KgByyJDYvWiDE=
github.com/anchore/fangs v0.0.0-20230531202914-48a718c6b4ba h1:tJ186HK8e0Lf+hhNWX4fJrq14yj3mw8JQkkLhA0nFhE=
github.com/anchore/fangs v0.0.0-20230531202914-48a718c6b4ba/go.mod h1:E3zNHEz7mizIFGJhuX+Ga7AbCmEN5TfzVDxmOfj7XZw=
github.com/anchore/go-logger v0.0.0-20230531193951-db5ae83e7dbe h1:Df867YMmymdMG6z5IW8pR0/2CRpLIjYnaTXLp6j+s0k=
github.com/anchore/go-logger v0.0.0-20230531193951-db5ae83e7dbe/go.mod h1:ubLFmlsv8/DFUQrZwY5syT5/8Er3ugSr4rDFwHsE3hg=
github.com/anchore/clio v0.0.0-20230823172630-c42d666061af h1:dBVKZyMZeA0oZK0+aCCRoqxhxUvx/7xy/VEaLMMMnb0=
github.com/anchore/clio v0.0.0-20230823172630-c42d666061af/go.mod h1:XryJ3CIF1T7SbacQV+OPykfKKIbfXnBssYfpjy2peUg=
github.com/anchore/fangs v0.0.0-20230818131516-2186b10924fe h1:pVpLCGWdNeskAw7vGNdCAcGMezrNljHIqOc9HaOja5M=
github.com/anchore/fangs v0.0.0-20230818131516-2186b10924fe/go.mod h1:82EGoxZTfBXSW0/zollEP+Qs3wkiKmip5yBT5j+eZpY=
github.com/anchore/go-logger v0.0.0-20230725134548-c21dafa1ec5a h1:nJ2G8zWKASyVClGVgG7sfM5mwoZlZ2zYpIzN2OhjWkw=
github.com/anchore/go-logger v0.0.0-20230725134548-c21dafa1ec5a/go.mod h1:ubLFmlsv8/DFUQrZwY5syT5/8Er3ugSr4rDFwHsE3hg=
github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb h1:iDMnx6LIjtjZ46C0akqveX83WFzhpTD3eqOthawb5vU=
github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb/go.mod h1:DmTY2Mfcv38hsHbG78xMiTDdxFtkHpgYNVDPsF2TgHk=
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 h1:aM1rlcoLz8y5B2r4tTLMiVTrMtpfY0O8EScKJxaSaEc=
@@ -106,8 +106,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.1.1-0.20230104203445-02e0a6721501 h1:AV7qjwMcM4r8wFhJq3jLRztew3ywIyPTRapl2T1s9o8=
github.com/anchore/packageurl-go v0.1.1-0.20230104203445-02e0a6721501/go.mod h1:Blo6OgJNiYF41ufcgHKkbCKF2MDOMlrqhXv/ij6ocR4=
github.com/anchore/stereoscope v0.0.0-20230727211946-d1f3d766295e h1:S6IhYpsBCpvphlHA1tN0glSG/kjVvFzC6OJuU2qW5Pc=
github.com/anchore/stereoscope v0.0.0-20230727211946-d1f3d766295e/go.mod h1:0LsgHgXO4QFnk2hsYwtqd3fR18PIZXlFLIl2qb9tu3g=
github.com/anchore/stereoscope v0.0.0-20230829142608-334c2222e137 h1:ZuiAV3lYKbGPkZR42UpoKecp/9aFU38CZSjaxjXCMYc=
github.com/anchore/stereoscope v0.0.0-20230829142608-334c2222e137/go.mod h1:jkylrnuv++srUa2rsqCnG0n1ShD+0k2Xa6YtoNoRjAY=
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=
@@ -406,6 +406,8 @@ github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq
github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 h1:i462o439ZjprVSFSZLZxcsoAe592sZB1rci2Z8j4wdk=
github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA=
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
@@ -1226,7 +1228,6 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+8 -5
View File
@@ -3,21 +3,24 @@ package bus
import (
"github.com/wagoodman/go-partybus"
"github.com/anchore/syft/internal/log"
"github.com/anchore/clio"
"github.com/anchore/syft/internal/redact"
"github.com/anchore/syft/syft/event"
)
func Exit() {
Publish(partybus.Event{
Type: event.CLIExit,
})
Publish(clio.ExitEvent(false))
}
func ExitWithInterrupt() {
Publish(clio.ExitEvent(true))
}
func Report(report string) {
if len(report) == 0 {
return
}
report = log.Redactor.RedactString(report)
report = redact.Apply(report)
Publish(partybus.Event{
Type: event.CLIReport,
Value: report,
-337
View File
@@ -1,337 +0,0 @@
package config
import (
"errors"
"fmt"
"os"
"path"
"reflect"
"sort"
"strings"
"github.com/adrg/xdg"
"github.com/mitchellh/go-homedir"
"github.com/spf13/viper"
"gopkg.in/yaml.v2"
"github.com/anchore/go-logger"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/pkg/cataloger"
golangCataloger "github.com/anchore/syft/syft/pkg/cataloger/golang"
"github.com/anchore/syft/syft/pkg/cataloger/kernel"
pythonCataloger "github.com/anchore/syft/syft/pkg/cataloger/python"
)
var (
ErrApplicationConfigNotFound = fmt.Errorf("application config not found")
catalogerEnabledDefault = false
)
type defaultValueLoader interface {
loadDefaultValues(*viper.Viper)
}
type parser interface {
parseConfigValues() error
}
// Application is the main syft application configuration.
type Application struct {
// the location where the application config was read from (either from -c or discovered while loading); default .syft.yaml
ConfigPath string `yaml:"configPath,omitempty" json:"configPath" mapstructure:"config"`
Verbosity uint `yaml:"verbosity,omitempty" json:"verbosity" mapstructure:"verbosity"`
// -q, indicates to not show any status output to stderr (ETUI or logging UI)
Quiet bool `yaml:"quiet" json:"quiet" mapstructure:"quiet"`
Outputs []string `yaml:"output" json:"output" mapstructure:"output"` // -o, the format to use for output
OutputTemplatePath string `yaml:"output-template-path" json:"output-template-path" mapstructure:"output-template-path"` // -t template file to use for output
File string `yaml:"file" json:"file" mapstructure:"file"` // --file, the file to write report output to
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
Dev development `yaml:"dev" json:"dev" mapstructure:"dev"`
Log logging `yaml:"log" json:"log" mapstructure:"log"` // all logging-related options
Catalogers []string `yaml:"catalogers" json:"catalogers" mapstructure:"catalogers"`
Package pkg `yaml:"package" json:"package" mapstructure:"package"`
Golang golang `yaml:"golang" json:"golang" mapstructure:"golang"`
LinuxKernel linuxKernel `yaml:"linux-kernel" json:"linux-kernel" mapstructure:"linux-kernel"`
Python python `yaml:"python" json:"python" mapstructure:"python"`
Attest attest `yaml:"attest" json:"attest" mapstructure:"attest"`
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"`
Platform string `yaml:"platform" json:"platform" mapstructure:"platform"`
Name string `yaml:"name" json:"name" mapstructure:"name"`
Source sourceCfg `yaml:"source" json:"source" mapstructure:"source"`
Parallelism int `yaml:"parallelism" json:"parallelism" mapstructure:"parallelism"` // the number of catalog workers to run in parallel
DefaultImagePullSource string `yaml:"default-image-pull-source" json:"default-image-pull-source" mapstructure:"default-image-pull-source"` // specify default image pull source
BasePath string `yaml:"base-path" json:"base-path" mapstructure:"base-path"` // specify base path for all file paths
ExcludeBinaryOverlapByOwnership bool `yaml:"exclude-binary-overlap-by-ownership" json:"exclude-binary-overlap-by-ownership" mapstructure:"exclude-binary-overlap-by-ownership"` // exclude synthetic binary packages owned by os package files
}
func (cfg Application) ToCatalogerConfig() cataloger.Config {
return cataloger.Config{
Search: cataloger.SearchConfig{
IncludeIndexedArchives: cfg.Package.SearchIndexedArchives,
IncludeUnindexedArchives: cfg.Package.SearchUnindexedArchives,
Scope: cfg.Package.Cataloger.ScopeOpt,
},
Catalogers: cfg.Catalogers,
Parallelism: cfg.Parallelism,
ExcludeBinaryOverlapByOwnership: cfg.ExcludeBinaryOverlapByOwnership,
Golang: golangCataloger.NewGoCatalogerOpts().
WithSearchLocalModCacheLicenses(cfg.Golang.SearchLocalModCacheLicenses).
WithLocalModCacheDir(cfg.Golang.LocalModCacheDir).
WithSearchRemoteLicenses(cfg.Golang.SearchRemoteLicenses).
WithProxy(cfg.Golang.Proxy).
WithNoProxy(cfg.Golang.NoProxy),
LinuxKernel: kernel.LinuxCatalogerConfig{
CatalogModules: cfg.LinuxKernel.CatalogModules,
},
Python: pythonCataloger.CatalogerConfig{
GuessUnpinnedRequirements: cfg.Python.GuessUnpinnedRequirements,
},
}
}
func (cfg *Application) LoadAllValues(v *viper.Viper, configPath string) error {
// priority order: viper.Set, flag, env, config, kv, defaults
// flags have already been loaded into viper by command construction
// check if user specified config; otherwise read all possible paths
if err := loadConfig(v, configPath); err != nil {
var notFound *viper.ConfigFileNotFoundError
if errors.As(err, &notFound) {
log.Debugf("no config file found, using defaults")
} else {
return fmt.Errorf("unable to load config: %w", err)
}
}
// load default config values into viper
loadDefaultValues(v)
// load environment variables
v.SetEnvPrefix(internal.ApplicationName)
v.AllowEmptyEnv(true)
v.AutomaticEnv()
// unmarshal fully populated viper object onto config
err := v.Unmarshal(cfg)
if err != nil {
return err
}
// Convert all populated config options to their internal application values ex: scope string => scopeOpt source.Scope
return cfg.parseConfigValues()
}
func (cfg *Application) parseConfigValues() error {
// parse options on this struct
var catalogers []string
for _, c := range cfg.Catalogers {
for _, f := range strings.Split(c, ",") {
catalogers = append(catalogers, strings.TrimSpace(f))
}
}
sort.Strings(catalogers)
cfg.Catalogers = catalogers
// parse application config options
for _, optionFn := range []func() error{
cfg.parseLogLevelOption,
cfg.parseFile,
} {
if err := optionFn(); err != nil {
return err
}
}
if err := checkDefaultSourceValues(cfg.DefaultImagePullSource); err != nil {
return err
}
if cfg.Name != "" {
log.Warnf("name parameter is deprecated. please use: source-name. name will be removed in a future version")
if cfg.Source.Name == "" {
cfg.Source.Name = cfg.Name
}
}
// check for valid default source options
// parse nested config options
// for each field in the configuration struct, see if the field implements the parser interface
// note: the app config is a pointer, so we need to grab the elements explicitly (to traverse the address)
value := reflect.ValueOf(cfg).Elem()
for i := 0; i < value.NumField(); i++ {
// note: since the interface method of parser is a pointer receiver we need to get the value of the field as a pointer.
if parsable, ok := value.Field(i).Addr().Interface().(parser); ok {
// the field implements parser, call it
if err := parsable.parseConfigValues(); err != nil {
return err
}
}
}
return nil
}
func (cfg *Application) parseLogLevelOption() error {
switch {
case cfg.Quiet:
// TODO: this is bad: quiet option trumps all other logging options (such as to a file on disk)
// we should be able to quiet the console logging and leave file logging alone...
// ... this will be an enhancement for later
cfg.Log.Level = logger.DisabledLevel
case cfg.Verbosity > 0:
cfg.Log.Level = logger.LevelFromVerbosity(int(cfg.Verbosity), logger.WarnLevel, logger.InfoLevel, logger.DebugLevel, logger.TraceLevel)
case cfg.Log.Level != "":
var err error
cfg.Log.Level, err = logger.LevelFromString(string(cfg.Log.Level))
if err != nil {
return err
}
if logger.IsVerbose(cfg.Log.Level) {
cfg.Verbosity = 1
}
default:
cfg.Log.Level = logger.WarnLevel
}
return nil
}
func (cfg *Application) parseFile() error {
if cfg.File != "" {
expandedPath, err := homedir.Expand(cfg.File)
if err != nil {
return fmt.Errorf("unable to expand file path=%q: %w", cfg.File, err)
}
cfg.File = expandedPath
}
return nil
}
// init loads the default configuration values into the viper instance (before the config values are read and parsed).
func loadDefaultValues(v *viper.Viper) {
// set the default values for primitive fields in this struct
v.SetDefault("quiet", false)
v.SetDefault("check-for-app-update", true)
v.SetDefault("catalogers", nil)
v.SetDefault("parallelism", 1)
v.SetDefault("default-image-pull-source", "")
v.SetDefault("exclude-binary-overlap-by-ownership", true)
// for each field in the configuration struct, see if the field implements the defaultValueLoader interface and invoke it if it does
value := reflect.ValueOf(Application{})
for i := 0; i < value.NumField(); i++ {
// note: the defaultValueLoader method receiver is NOT a pointer receiver.
if loadable, ok := value.Field(i).Interface().(defaultValueLoader); ok {
// the field implements defaultValueLoader, call it
loadable.loadDefaultValues(v)
}
}
}
func (cfg Application) String() string {
// yaml is pretty human friendly (at least when compared to json)
appaStr, err := yaml.Marshal(&cfg)
if err != nil {
return err.Error()
}
return string(appaStr)
}
// nolint:funlen
func loadConfig(v *viper.Viper, configPath string) error {
var err error
// use explicitly the given user config
if configPath != "" {
v.SetConfigFile(configPath)
if err := v.ReadInConfig(); err != nil {
return fmt.Errorf("unable to read application config=%q : %w", configPath, err)
}
v.Set("config", v.ConfigFileUsed())
// don't fall through to other options if the config path was explicitly provided
return nil
}
// start searching for valid configs in order...
// 1. look for .<appname>.yaml (in the current directory)
confFilePath := "." + internal.ApplicationName
// TODO: Remove this before v1.0.0
// See syft #1634
v.AddConfigPath(".")
v.SetConfigName(confFilePath)
// check if config.yaml exists in the current directory
// DEPRECATED: this will be removed in v1.0.0
if _, err := os.Stat("config.yaml"); err == nil {
log.Warn("DEPRECATED: ./config.yaml as a configuration file is deprecated and will be removed as an option in v1.0.0, please rename to .syft.yaml")
}
if _, err := os.Stat(confFilePath + ".yaml"); err == nil {
if err = v.ReadInConfig(); err == nil {
v.Set("config", v.ConfigFileUsed())
return nil
} else if !errors.As(err, &viper.ConfigFileNotFoundError{}) {
return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err)
}
}
// 2. look for .<appname>/config.yaml (in the current directory)
v.AddConfigPath("." + internal.ApplicationName)
v.SetConfigName("config")
if err = v.ReadInConfig(); err == nil {
v.Set("config", v.ConfigFileUsed())
return nil
} else if !errors.As(err, &viper.ConfigFileNotFoundError{}) {
return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err)
}
// 3. look for ~/.<appname>.yaml
home, err := homedir.Dir()
if err == nil {
v.AddConfigPath(home)
v.SetConfigName("." + internal.ApplicationName)
if err = v.ReadInConfig(); err == nil {
v.Set("config", v.ConfigFileUsed())
return nil
} else if !errors.As(err, &viper.ConfigFileNotFoundError{}) {
return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err)
}
}
// 4. look for <appname>/config.yaml in xdg locations (starting with xdg home config dir, then moving upwards)
v.SetConfigName("config")
configPath = path.Join(xdg.ConfigHome, internal.ApplicationName)
v.AddConfigPath(configPath)
for _, dir := range xdg.ConfigDirs {
v.AddConfigPath(path.Join(dir, internal.ApplicationName))
}
if err = v.ReadInConfig(); err == nil {
v.Set("config", v.ConfigFileUsed())
return nil
} else if !errors.As(err, &viper.ConfigFileNotFoundError{}) {
return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err)
}
return nil
}
var validDefaultSourceValues = []string{"registry", "docker", "podman", ""}
func checkDefaultSourceValues(source string) error {
validValues := internal.NewStringSet(validDefaultSourceValues...)
if !validValues.Contains(source) {
validValuesString := strings.Join(validDefaultSourceValues, ", ")
return fmt.Errorf("%s is not a valid default source; please use one of the following: %s''", source, validValuesString)
}
return nil
}
-129
View File
@@ -1,129 +0,0 @@
package config
import (
"fmt"
"os"
"path"
"testing"
"github.com/adrg/xdg"
"github.com/mitchellh/go-homedir"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TODO: set negative case when config.yaml is no longer a valid option
func TestApplicationConfig(t *testing.T) {
// disable homedir package cache for testing
originalCacheOpt := homedir.DisableCache
homedir.DisableCache = true
t.Cleanup(func() {
homedir.DisableCache = originalCacheOpt
})
// ensure we have no side effects for xdg package for future tests
originalXDG := os.Getenv("XDG_CONFIG_HOME")
t.Cleanup(func() {
// note: we're not using t.Setenv since the effect we're trying to eliminate is within the xdg package
require.NoError(t, os.Setenv("XDG_CONFIG_HOME", originalXDG))
xdg.Reload()
})
// config is picked up at desired configuration paths
// VALID: .syft.yaml, .syft/config.yaml, ~/.syft.yaml, <XDG_CONFIG_HOME>/syft/config.yaml
// DEPRECATED: config.yaml is currently supported by
tests := []struct {
name string
setup func(t *testing.T) string
assertions func(t *testing.T, app *Application)
cleanup func()
}{
{
name: "explicit config",
setup: func(t *testing.T) string {
return "./test-fixtures/.syft.yaml"
}, // no-op for explicit config
assertions: func(t *testing.T, app *Application) {
assert.Equal(t, "test-explicit-config", app.File)
},
},
{
name: "current working directory named config",
setup: func(t *testing.T) string {
err := os.Chdir("./test-fixtures/config-wd-file") // change application cwd to test-fixtures
require.NoError(t, err)
return ""
},
assertions: func(t *testing.T, app *Application) {
assert.Equal(t, "test-wd-named-config", app.File)
},
},
{
name: "current working directory syft dir config",
setup: func(t *testing.T) string {
err := os.Chdir("./test-fixtures/config-dir-test") // change application cwd to test-fixtures
require.NoError(t, err)
return ""
},
assertions: func(t *testing.T, app *Application) {
assert.Equal(t, "test-dir-config", app.File)
},
},
{
name: "home directory file config",
setup: func(t *testing.T) string {
// Because Setenv affects the whole process, it cannot be used in parallel tests or
// tests with parallel ancestors: see separate XDG test for consequence of this
t.Setenv("HOME", "./test-fixtures/config-home-test/config-file")
return ""
},
assertions: func(t *testing.T, app *Application) {
assert.Equal(t, "test-home-config", app.File)
},
},
{
name: "XDG file config",
setup: func(t *testing.T) string {
wd, err := os.Getwd()
require.NoError(t, err)
configDir := path.Join(wd, "./test-fixtures/config-home-test") // set HOME to testdata
// note: this explicitly has multiple XDG paths, make certain we use the first VALID one (not the first one)
t.Setenv("XDG_CONFIG_DIRS", fmt.Sprintf("/another/foo/bar:%s", configDir))
xdg.Reload()
return ""
},
assertions: func(t *testing.T, app *Application) {
assert.Equal(t, "test-home-XDG-config", app.File)
},
cleanup: func() {
require.NoError(t, os.Unsetenv("XDG_CONFIG_DIRS"))
xdg.Reload()
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if test.cleanup != nil {
t.Cleanup(test.cleanup)
}
wd, err := os.Getwd()
require.NoError(t, err)
defer os.Chdir(wd) // reset working directory after test
application := &Application{}
viperInstance := viper.New()
// this will override home in case you are running this test locally and DO have a syft config
// in your home directory... now it will be ignored. Same for XDG_CONFIG_DIRS.
t.Setenv("HOME", "/foo/bar")
t.Setenv("XDG_CONFIG_DIRS", "/foo/bar")
xdg.Reload()
configPath := test.setup(t)
err = application.LoadAllValues(viperInstance, configPath)
require.NoError(t, err)
test.assertions(t, application)
})
}
}
-14
View File
@@ -1,14 +0,0 @@
package config
import "github.com/spf13/viper"
type attest struct {
// IMPORTANT: do not show the attestation key/password in any YAML/JSON output (sensitive information)
Key string `yaml:"-" json:"-" mapstructure:"key"`
Password string `yaml:"-" json:"-" mapstructure:"password"`
}
func (cfg attest) loadDefaultValues(v *viper.Viper) {
v.SetDefault("attest.key", "")
v.SetDefault("attest.password", "")
}
-29
View File
@@ -1,29 +0,0 @@
package config
import (
"fmt"
"github.com/spf13/viper"
"github.com/anchore/syft/syft/source"
)
type catalogerOptions struct {
Enabled bool `yaml:"enabled" json:"enabled" mapstructure:"enabled"`
Scope string `yaml:"scope" json:"scope" mapstructure:"scope"`
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 {
return fmt.Errorf("bad scope value %q", cfg.Scope)
}
cfg.ScopeOpt = scopeOption
return nil
}
-13
View File
@@ -1,13 +0,0 @@
package config
import "github.com/spf13/viper"
type development struct {
ProfileCPU bool `yaml:"profile-cpu" json:"profile-cpu" mapstructure:"profile-cpu"`
ProfileMem bool `yaml:"profile-mem" json:"profile-mem" mapstructure:"profile-mem"`
}
func (cfg development) loadDefaultValues(v *viper.Viper) {
v.SetDefault("dev.profile-cpu", false)
v.SetDefault("dev.profile-mem", false)
}
-20
View File
@@ -1,20 +0,0 @@
package config
import (
"github.com/spf13/viper"
"github.com/anchore/syft/syft/source"
)
type fileClassification struct {
Cataloger catalogerOptions `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"`
}
func (cfg fileClassification) loadDefaultValues(v *viper.Viper) {
v.SetDefault("file-classification.cataloger.enabled", catalogerEnabledDefault)
v.SetDefault("file-classification.cataloger.scope", source.SquashedScope)
}
func (cfg *fileClassification) parseConfigValues() error {
return cfg.Cataloger.parseConfigValues()
}
-25
View File
@@ -1,25 +0,0 @@
package config
import (
"github.com/spf13/viper"
"github.com/anchore/syft/internal/file"
"github.com/anchore/syft/syft/source"
)
type fileContents struct {
Cataloger catalogerOptions `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"`
SkipFilesAboveSize int64 `yaml:"skip-files-above-size" json:"skip-files-above-size" mapstructure:"skip-files-above-size"`
Globs []string `yaml:"globs" json:"globs" mapstructure:"globs"`
}
func (cfg fileContents) loadDefaultValues(v *viper.Viper) {
v.SetDefault("file-contents.cataloger.enabled", catalogerEnabledDefault)
v.SetDefault("file-contents.cataloger.scope", source.SquashedScope)
v.SetDefault("file-contents.skip-files-above-size", 1*file.MB)
v.SetDefault("file-contents.globs", []string{})
}
func (cfg *fileContents) parseConfigValues() error {
return cfg.Cataloger.parseConfigValues()
}
-22
View File
@@ -1,22 +0,0 @@
package config
import (
"github.com/spf13/viper"
"github.com/anchore/syft/syft/source"
)
type FileMetadata struct {
Cataloger catalogerOptions `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"`
Digests []string `yaml:"digests" json:"digests" mapstructure:"digests"`
}
func (cfg FileMetadata) loadDefaultValues(v *viper.Viper) {
v.SetDefault("file-metadata.cataloger.enabled", catalogerEnabledDefault)
v.SetDefault("file-metadata.cataloger.scope", source.SquashedScope)
v.SetDefault("file-metadata.digests", []string{"sha256"})
}
func (cfg *FileMetadata) parseConfigValues() error {
return cfg.Cataloger.parseConfigValues()
}
-11
View File
@@ -1,11 +0,0 @@
package config
import "github.com/spf13/viper"
type linuxKernel struct {
CatalogModules bool `json:"catalog-modules" yaml:"catalog-modules" mapstructure:"catalog-modules"`
}
func (cfg linuxKernel) loadDefaultValues(v *viper.Viper) {
v.SetDefault("linux-kernel.catalog-modules", true)
}
-34
View File
@@ -1,34 +0,0 @@
package config
import (
"fmt"
"github.com/mitchellh/go-homedir"
"github.com/spf13/viper"
"github.com/anchore/go-logger"
)
// logging contains all logging-related configuration options available to the user via the application config.
type logging struct {
Structured bool `yaml:"structured" json:"structured" mapstructure:"structured"` // show all log entries as JSON formatted strings
Level logger.Level `yaml:"level" json:"level" mapstructure:"level"` // the log level string hint
FileLocation string `yaml:"file" json:"file-location" mapstructure:"file"` // the file path to write logs to
}
func (cfg *logging) parseConfigValues() error {
if cfg.FileLocation != "" {
expandedPath, err := homedir.Expand(cfg.FileLocation)
if err != nil {
return fmt.Errorf("unable to expand log file path=%q: %w", cfg.FileLocation, err)
}
cfg.FileLocation = expandedPath
}
return nil
}
func (cfg logging) loadDefaultValues(v *viper.Viper) {
v.SetDefault("log.structured", false)
v.SetDefault("log.file", "")
v.SetDefault("log.level", string(logger.WarnLevel))
}
-24
View File
@@ -1,24 +0,0 @@
package config
import (
"github.com/spf13/viper"
"github.com/anchore/syft/syft/pkg/cataloger"
)
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()
}
-75
View File
@@ -1,75 +0,0 @@
package config
import (
"os"
"github.com/spf13/viper"
"github.com/anchore/stereoscope/pkg/image"
)
type RegistryCredentials struct {
Authority string `yaml:"authority" json:"authority" mapstructure:"authority"`
// IMPORTANT: do not show the username in any YAML/JSON output (sensitive information)
Username string `yaml:"-" json:"-" mapstructure:"username"`
// IMPORTANT: do not show the password in any YAML/JSON output (sensitive information)
Password string `yaml:"-" json:"-" mapstructure:"password"`
// IMPORTANT: do not show the token in any YAML/JSON output (sensitive information)
Token string `yaml:"-" json:"-" mapstructure:"token"`
}
type registry struct {
InsecureSkipTLSVerify bool `yaml:"insecure-skip-tls-verify" json:"insecure-skip-tls-verify" mapstructure:"insecure-skip-tls-verify"`
InsecureUseHTTP bool `yaml:"insecure-use-http" json:"insecure-use-http" mapstructure:"insecure-use-http"`
Auth []RegistryCredentials `yaml:"auth" json:"auth" mapstructure:"auth"`
}
func (cfg registry) loadDefaultValues(v *viper.Viper) {
v.SetDefault("registry.insecure-skip-tls-verify", false)
v.SetDefault("registry.insecure-use-http", false)
v.SetDefault("registry.auth", []RegistryCredentials{})
}
//nolint:unparam
func (cfg *registry) parseConfigValues() error {
// there may be additional credentials provided by env var that should be appended to the set of credentials
authority, username, password, token :=
os.Getenv("SYFT_REGISTRY_AUTH_AUTHORITY"),
os.Getenv("SYFT_REGISTRY_AUTH_USERNAME"),
os.Getenv("SYFT_REGISTRY_AUTH_PASSWORD"),
os.Getenv("SYFT_REGISTRY_AUTH_TOKEN")
if hasNonEmptyCredentials(username, password, token) {
// note: we prepend the credentials such that the environment variables take precedence over on-disk configuration.
cfg.Auth = append([]RegistryCredentials{
{
Authority: authority,
Username: username,
Password: password,
Token: token,
},
}, cfg.Auth...)
}
return nil
}
func hasNonEmptyCredentials(username, password, token string) bool {
return password != "" && username != "" || token != ""
}
func (cfg *registry) ToOptions() *image.RegistryOptions {
var auth = make([]image.RegistryCredentials, len(cfg.Auth))
for i, a := range cfg.Auth {
auth[i] = image.RegistryCredentials{
Authority: a.Authority,
Username: a.Username,
Password: a.Password,
Token: a.Token,
}
}
return &image.RegistryOptions{
InsecureSkipTLSVerify: cfg.InsecureSkipTLSVerify,
InsecureUseHTTP: cfg.InsecureUseHTTP,
Credentials: auth,
}
}
-7
View File
@@ -1,7 +0,0 @@
# same as --file; write output report to a file (default is to write to stdout)
file: "test-explicit-config"
package:
cataloger:
scope: "squashed"
# same as --scope; limit the scope of the cataloger to only the specified types
@@ -1,5 +0,0 @@
# same as --file; write output report to a file (default is to write to stdout)
file: "test-dir-config"
package:
cataloger:
scope: "squashed"
@@ -1,5 +0,0 @@
# same as --file; write output report to a file (default is to write to stdout)
file: "test-home-config"
package:
cataloger:
scope: "squashed"
@@ -1,5 +0,0 @@
# same as --file; write output report to a file (default is to write to stdout)
file: "test-home-XDG-config"
package:
cataloger:
scope: "squashed"
@@ -1,5 +0,0 @@
# same as --file; write output report to a file (default is to write to stdout)
file: "test-wd-named-config"
package:
cataloger:
scope: "squashed"
-3
View File
@@ -1,9 +1,6 @@
package internal
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 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 = "10.0.1"
+12 -13
View File
@@ -7,29 +7,28 @@ import (
"github.com/anchore/go-logger"
"github.com/anchore/go-logger/adapter/discard"
"github.com/anchore/go-logger/adapter/redact"
red "github.com/anchore/syft/internal/redact"
)
var (
// log is the singleton used to facilitate logging internally within
log = discard.New()
store = redact.NewStore()
Redactor = store.(redact.Redactor)
)
// log is the singleton used to facilitate logging internally within
var log = discard.New()
func Set(l logger.Logger) {
log = redact.New(l, store)
// though the application will automatically have a redaction logger, library consumers may not be doing this.
// for this reason we additionally ensure there is a redaction logger configured for any logger passed. The
// source of truth for redaction values is still in the internal redact package. If the passed logger is already
// redacted, then this is a no-op.
store := red.Get()
if store != nil {
l = redact.New(l, store)
}
log = l
}
func Get() logger.Logger {
return log
}
func Redact(values ...string) {
store.Add(values...)
}
// Errorf takes a formatted template string and template arguments for the error logging level.
func Errorf(format string, args ...interface{}) {
log.Errorf(format, args...)
+36
View File
@@ -0,0 +1,36 @@
package redact
import "github.com/anchore/go-logger/adapter/redact"
var store redact.Store
func Set(s redact.Store) {
if store != nil {
// if someone is trying to set a redaction store and we already have one then something is wrong. The store
// that we're replacing might already have values in it, so we should never replace it.
panic("replace existing redaction store (probably unintentional)")
}
store = s
}
func Get() redact.Store {
return store
}
func Add(vs ...string) {
if store == nil {
// if someone is trying to add values that should never be output and we don't have a store then something is wrong.
// we should never accidentally output values that should be redacted, thus we panic here.
panic("cannot add redactions without a store")
}
store.Add(vs...)
}
func Apply(value string) string {
if store == nil {
// if someone is trying to add values that should never be output and we don't have a store then something is wrong.
// we should never accidentally output values that should be redacted, thus we panic here.
panic("cannot apply redactions without a store")
}
return store.RedactString(value)
}
-54
View File
@@ -1,54 +0,0 @@
/*
Package version contains all build time metadata (version, build time, git commit, etc).
*/
package version
import (
"fmt"
"runtime"
"strings"
"github.com/anchore/syft/internal"
)
const valueNotProvided = "[not provided]"
// all variables here are provided as build-time arguments, with clear default values
var version = valueNotProvided
var gitCommit = valueNotProvided
var gitDescription = valueNotProvided
var buildDate = valueNotProvided
var platform = fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)
// Version defines the application version details (generally from build information)
type Version struct {
Version string `json:"version"` // application semantic version
JSONSchemaVersion string `json:"jsonSchemaVersion"` // application semantic JSON schema version
GitCommit string `json:"gitCommit"` // git SHA at build-time
GitDescription string `json:"gitDescription"` // output of 'git describe --dirty --always --tags'
BuildDate string `json:"buildDate"` // date of the build
GoVersion string `json:"goVersion"` // go runtime version at build-time
Compiler string `json:"compiler"` // compiler used at build-time
Platform string `json:"platform"` // GOOS and GOARCH at build-time
}
func (v Version) IsProductionBuild() bool {
if strings.Contains(v.Version, "SNAPSHOT") || strings.Contains(v.Version, valueNotProvided) {
return false
}
return true
}
// FromBuild provides all version details
func FromBuild() Version {
return Version{
Version: version,
JSONSchemaVersion: internal.JSONSchemaVersion,
GitCommit: gitCommit,
GitDescription: gitDescription,
BuildDate: buildDate,
GoVersion: runtime.Version(),
Compiler: runtime.Compiler,
Platform: platform,
}
}
-73
View File
@@ -1,73 +0,0 @@
package version
import (
"fmt"
"io"
"net/http"
"strings"
hashiVersion "github.com/anchore/go-version"
"github.com/anchore/syft/internal"
)
var latestAppVersionURL = struct {
host string
path string
}{
host: "https://toolbox-data.anchore.io",
path: fmt.Sprintf("/%s/releases/latest/VERSION", internal.ApplicationName),
}
// IsUpdateAvailable indicates if there is a newer application version available, and if so, what the new version is.
func IsUpdateAvailable() (bool, string, error) {
currentBuildInfo := FromBuild()
if !currentBuildInfo.IsProductionBuild() {
// don't allow for non-production builds to check for a version.
return false, "", nil
}
currentVersion, err := hashiVersion.NewVersion(currentBuildInfo.Version)
if err != nil {
return false, "", fmt.Errorf("failed to parse current application version: %w", err)
}
latestVersion, err := fetchLatestApplicationVersion()
if err != nil {
return false, "", err
}
if latestVersion.GreaterThan(currentVersion) {
return true, latestVersion.String(), nil
}
return false, "", nil
}
func fetchLatestApplicationVersion() (*hashiVersion.Version, error) {
req, err := http.NewRequest(http.MethodGet, latestAppVersionURL.host+latestAppVersionURL.path, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request for latest version: %w", err)
}
client := http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to fetch latest version: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("HTTP %d on fetching latest version: %s", resp.StatusCode, resp.Status)
}
versionBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read latest version: %w", err)
}
versionStr := strings.TrimSuffix(string(versionBytes), "\n")
if len(versionStr) > 50 {
return nil, fmt.Errorf("version too long: %q", versionStr[:50])
}
return hashiVersion.NewVersion(versionStr)
}
+1 -6
View File
@@ -6,12 +6,10 @@ package event
import (
"github.com/wagoodman/go-partybus"
"github.com/anchore/syft/internal"
)
const (
typePrefix = internal.ApplicationName
typePrefix = "syft"
cliTypePrefix = typePrefix + "-cli"
// Events from the syft library
@@ -48,7 +46,4 @@ const (
// CLINotification is a partybus event that occurs when auxiliary information is ready for presentation to stderr
CLINotification partybus.EventType = cliTypePrefix + "-notification"
// CLIExit is a partybus event that occurs when an analysis result is ready for final presentation
CLIExit partybus.EventType = cliTypePrefix + "-exit-event"
)
+9 -17
View File
@@ -144,30 +144,22 @@ func ParseAttestationStartedEvent(e partybus.Event) (io.Reader, progress.Progres
// CLI event types
func ParseCLIExit(e partybus.Event) (func() error, error) {
if err := checkEventType(e.Type, event.CLIExit); err != nil {
type UpdateCheck struct {
New string
Current string
}
func ParseCLIAppUpdateAvailable(e partybus.Event) (*UpdateCheck, error) {
if err := checkEventType(e.Type, event.CLIAppUpdateAvailable); err != nil {
return nil, err
}
fn, ok := e.Value.(func() error)
updateCheck, ok := e.Value.(UpdateCheck)
if !ok {
return nil, newPayloadErr(e.Type, "Value", e.Value)
}
return fn, nil
}
func ParseCLIAppUpdateAvailable(e partybus.Event) (string, error) {
if err := checkEventType(e.Type, event.CLIAppUpdateAvailable); err != nil {
return "", err
}
newVersion, ok := e.Value.(string)
if !ok {
return "", newPayloadErr(e.Type, "Value", e.Value)
}
return newVersion, nil
return &updateCheck, nil
}
func ParseCLIReport(e partybus.Event) (string, string, error) {
@@ -7,7 +7,6 @@ import (
"github.com/google/uuid"
"golang.org/x/exp/slices"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/cpe"
@@ -24,7 +23,7 @@ func ToFormatModel(s sbom.SBOM) *cyclonedx.BOM {
// https://github.com/CycloneDX/specification/blob/master/schema/bom-1.3-strict.schema.json#L36
// "pattern": "^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
cdxBOM.SerialNumber = uuid.New().URN()
cdxBOM.Metadata = toBomDescriptor(internal.ApplicationName, s.Descriptor.Version, s.Source)
cdxBOM.Metadata = toBomDescriptor(s.Descriptor.Name, s.Descriptor.Version, s.Source)
packages := s.Artifacts.Packages.Sorted()
components := make([]cyclonedx.Component, len(packages))
@@ -8,7 +8,7 @@ import (
"github.com/google/uuid"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
@@ -18,12 +18,12 @@ const (
inputFile = "file"
)
func DocumentNameAndNamespace(src source.Description) (string, string) {
func DocumentNameAndNamespace(src source.Description, desc sbom.Descriptor) (string, string) {
name := DocumentName(src)
return name, DocumentNamespace(name, src)
return name, DocumentNamespace(name, src, desc)
}
func DocumentNamespace(name string, src source.Description) string {
func DocumentNamespace(name string, src source.Description, desc sbom.Descriptor) string {
name = cleanName(name)
input := "unknown-source-type"
switch src.Metadata.(type) {
@@ -44,7 +44,7 @@ func DocumentNamespace(name string, src source.Description) string {
u := url.URL{
Scheme: "https",
Host: "anchore.com",
Path: path.Join(internal.ApplicationName, identifier),
Path: path.Join(desc.Name, identifier),
}
return u.String()
@@ -8,6 +8,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/anchore/syft/syft/internal/sourcemetadata"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
@@ -55,9 +56,12 @@ func Test_documentNamespace(t *testing.T) {
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actual := DocumentNamespace(test.inputName, test.src)
actual := DocumentNamespace(test.inputName, test.src, sbom.Descriptor{
Name: "syft",
})
// note: since the namespace ends with a UUID we check the prefix
assert.True(t, strings.HasPrefix(actual, test.expected), fmt.Sprintf("actual namespace %q", actual))
assert.True(t, strings.HasPrefix(actual, test.expected), fmt.Sprintf("expected prefix: '%s' got: '%s'", test.expected, actual))
// track each scheme tested (passed or not)
tracker.Tested(t, test.src.Metadata)
@@ -44,7 +44,7 @@ const (
//
//nolint:funlen
func ToFormatModel(s sbom.SBOM) *spdx.Document {
name, namespace := DocumentNameAndNamespace(s.Source)
name, namespace := DocumentNameAndNamespace(s.Source, s.Descriptor)
packages := toPackages(s.Artifacts.Packages, s)
@@ -136,7 +136,7 @@ func ToFormatModel(s sbom.SBOM) *spdx.Document {
CreatorType: "Organization",
},
{
Creator: internal.ApplicationName + "-" + s.Descriptor.Version,
Creator: s.Descriptor.Name + "-" + s.Descriptor.Version,
CreatorType: "Tool",
},
},
+1 -2
View File
@@ -8,7 +8,6 @@ import (
"github.com/mholt/archiver/v3"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
@@ -26,7 +25,7 @@ func toGithubModel(s *sbom.SBOM) DependencySnapshot {
Version: 0,
// TODO allow property input to specify the Job, Sha, and Ref
Detector: DetectorMetadata{
Name: internal.ApplicationName,
Name: s.Descriptor.Name,
URL: "https://github.com/anchore/syft",
Version: v,
},
+3
View File
@@ -18,6 +18,9 @@ import (
func sbomFixture() sbom.SBOM {
s := sbom.SBOM{
Descriptor: sbom.Descriptor{
Name: "syft",
},
Source: source.Description{
Metadata: source.StereoscopeImageSourceMetadata{
UserInput: "ubuntu:18.04",
+11 -7
View File
@@ -4,7 +4,7 @@ import (
"bytes"
"testing"
"github.com/sergi/go-diff/diffmatchpatch"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -64,12 +64,16 @@ func AssertEncoderAgainstGoldenSnapshot(t *testing.T, cfg EncoderSnapshotTestCon
if cfg.IsJSON {
require.JSONEq(t, string(expected), string(actual))
} else if !bytes.Equal(expected, actual) {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(string(expected), string(actual), true)
t.Logf("len: %d\nexpected: %s", len(expected), expected)
t.Logf("len: %d\nactual: %s", len(actual), actual)
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
} else {
requireEqual(t, expected, actual)
}
}
func requireEqual(t *testing.T, expected any, actual any) {
if diff := cmp.Diff(expected, actual); diff != "" {
t.Logf("expected: %s", expected)
t.Logf("actual: %s", actual)
t.Fatalf("mismatched output: %s", diff)
}
}
+1 -1
View File
@@ -115,7 +115,7 @@ func redactor(values ...string) testutils.Redactor {
`Created: .*`: "Created: redacted",
// each SBOM reports a unique documentNamespace when generated, this is not useful for snapshot testing
`DocumentNamespace: https://anchore.com/syft/.*`: "DocumentNamespace: redacted",
`DocumentNamespace: https://anchore.com/.*`: "DocumentNamespace: redacted",
// the license list will be updated periodically, the value here should not be directly tested in snapshot tests
`LicenseListVersion: .*`: "LicenseListVersion: redacted",
@@ -183,6 +183,11 @@ var defaultCandidateAdditions = buildCandidateLookup(
candidateKey{PkgName: "pip"},
candidateAddition{AdditionalVendors: []string{"pypa"}},
},
{
pkg.PythonPkg,
candidateKey{PkgName: "Django"},
candidateAddition{AdditionalVendors: []string{"djangoproject"}},
},
// Alpine packages
{
pkg.ApkPkg,
@@ -896,6 +896,14 @@ func TestCandidateVendor(t *testing.T) {
},
expected: []string{"apache"},
},
{
name: "Django",
p: pkg.Package{
Name: "Django",
Type: pkg.PythonPkg,
},
expected: []string{"djangoproject" /* <-- known good names | default guess --> */, "Django"},
},
}
for _, test := range tests {
+3 -40
View File
@@ -16,7 +16,6 @@ import (
"golang.org/x/mod/module"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
@@ -57,11 +56,11 @@ func (c *goBinaryCataloger) parseGoBinary(resolver file.Resolver, _ *generic.Env
return nil, nil, err
}
mods, archs := scanFile(unionReader, reader.RealPath)
mods := scanFile(unionReader, reader.RealPath)
internal.CloseAndLogError(reader.ReadCloser, reader.RealPath)
for i, mod := range mods {
pkgs = append(pkgs, c.buildGoPkgInfo(resolver, reader.Location, mod, archs[i])...)
for _, mod := range mods {
pkgs = append(pkgs, c.buildGoPkgInfo(resolver, reader.Location, mod, mod.arch)...)
}
return pkgs, nil, nil
}
@@ -151,42 +150,6 @@ func extractVersionFromLDFlags(ldflags string) (majorVersion string, fullVersion
return "", ""
}
// getArchs finds a binary architecture by two ways:
// 1) reading build info from binaries compiled by go1.18+
// 2) reading file headers from binaries compiled by < go1.18
func getArchs(readers []io.ReaderAt, builds []*extendedBuildInfo) []string {
if len(readers) != len(builds) {
log.Trace("golang cataloger: bin parsing: number of builds and readers doesn't match")
return nil
}
if len(readers) == 0 || len(builds) == 0 {
log.Tracef("golang cataloger: bin parsing: %d readers and %d build info items", len(readers), len(builds))
return nil
}
archs := make([]string, len(builds))
for i, build := range builds {
archs[i] = getGOARCH(build.Settings)
}
// if architecture was found via build settings return
if archs[0] != "" {
return archs
}
for i, r := range readers {
a, err := getGOARCHFromBin(r)
if err != nil {
log.Tracef("golang cataloger: bin parsing: getting arch from binary: %v", err)
continue
}
archs[i] = a
}
return archs
}
func getGOARCH(settings []debug.BuildSetting) string {
for _, s := range settings {
if s.Key == GOARCH {
@@ -156,18 +156,12 @@ func TestBuildGoPkgInfo(t *testing.T) {
tests := []struct {
name string
mod *extendedBuildInfo
arch string
expected []pkg.Package
}{
{
name: "parse an empty mod",
mod: nil,
expected: []pkg.Package(nil),
},
{
name: "package without name",
mod: &extendedBuildInfo{
&debug.BuildInfo{
BuildInfo: &debug.BuildInfo{
Deps: []*debug.Module{
{
Path: "github.com/adrg/xdg",
@@ -177,7 +171,9 @@ func TestBuildGoPkgInfo(t *testing.T) {
Version: "v0.2.1",
},
},
}, nil,
},
cryptoSettings: nil,
arch: "",
},
expected: []pkg.Package{
{
@@ -200,14 +196,13 @@ func TestBuildGoPkgInfo(t *testing.T) {
},
{
name: "buildGoPkgInfo parses a blank mod and returns no packages",
mod: &extendedBuildInfo{&debug.BuildInfo{}, nil},
mod: &extendedBuildInfo{&debug.BuildInfo{}, nil, ""},
expected: []pkg.Package(nil),
},
{
name: "parse a mod without main module",
arch: archDetails,
mod: &extendedBuildInfo{
&debug.BuildInfo{
BuildInfo: &debug.BuildInfo{
GoVersion: goCompiledVersion,
Settings: []debug.BuildSetting{
{Key: "GOARCH", Value: archDetails},
@@ -221,7 +216,9 @@ func TestBuildGoPkgInfo(t *testing.T) {
Sum: "h1:VSVdnH7cQ7V+B33qSJHTCRlNgra1607Q8PzEmnvb2Ic=",
},
},
}, nil,
},
cryptoSettings: nil,
arch: archDetails,
},
expected: []pkg.Package{
{
@@ -249,9 +246,8 @@ func TestBuildGoPkgInfo(t *testing.T) {
},
{
name: "parse a mod with path but no main module",
arch: archDetails,
mod: &extendedBuildInfo{
&debug.BuildInfo{
BuildInfo: &debug.BuildInfo{
GoVersion: goCompiledVersion,
Settings: []debug.BuildSetting{
{Key: "GOARCH", Value: archDetails},
@@ -259,7 +255,9 @@ func TestBuildGoPkgInfo(t *testing.T) {
{Key: "GOAMD64", Value: "v1"},
},
Path: "github.com/a/b/c",
}, []string{"boringcrypto + fips"},
},
cryptoSettings: []string{"boringcrypto + fips"},
arch: archDetails,
},
expected: []pkg.Package{
{
@@ -294,9 +292,8 @@ func TestBuildGoPkgInfo(t *testing.T) {
},
{
name: "parse a mod without packages",
arch: archDetails,
mod: &extendedBuildInfo{
&debug.BuildInfo{
BuildInfo: &debug.BuildInfo{
GoVersion: goCompiledVersion,
Main: debug.Module{Path: "github.com/anchore/syft", Version: "(devel)"},
Settings: []debug.BuildSetting{
@@ -304,15 +301,16 @@ func TestBuildGoPkgInfo(t *testing.T) {
{Key: "GOOS", Value: "darwin"},
{Key: "GOAMD64", Value: "v1"},
},
}, nil,
},
cryptoSettings: nil,
arch: archDetails,
},
expected: []pkg.Package{unmodifiedMain},
},
{
name: "parse main mod and replace devel pseudo version and ldflags exists (but contains no version)",
arch: archDetails,
mod: &extendedBuildInfo{
&debug.BuildInfo{
BuildInfo: &debug.BuildInfo{
GoVersion: goCompiledVersion,
Main: debug.Module{Path: "github.com/anchore/syft", Version: "(devel)"},
Settings: []debug.BuildSetting{
@@ -323,7 +321,9 @@ func TestBuildGoPkgInfo(t *testing.T) {
{Key: "vcs.time", Value: "2022-10-14T19:54:57Z"},
{Key: "-ldflags", Value: `build -ldflags="-w -s -extldflags '-static' -X blah=foobar`},
},
}, nil,
},
cryptoSettings: nil,
arch: archDetails,
},
expected: []pkg.Package{
{
@@ -359,9 +359,8 @@ func TestBuildGoPkgInfo(t *testing.T) {
},
{
name: "parse main mod and replace devel version with one from ldflags with vcs. build settings",
arch: archDetails,
mod: &extendedBuildInfo{
&debug.BuildInfo{
BuildInfo: &debug.BuildInfo{
GoVersion: goCompiledVersion,
Main: debug.Module{Path: "github.com/anchore/syft", Version: "(devel)"},
Settings: []debug.BuildSetting{
@@ -372,7 +371,9 @@ func TestBuildGoPkgInfo(t *testing.T) {
{Key: "vcs.time", Value: "2022-10-14T19:54:57Z"},
{Key: "-ldflags", Value: `build -ldflags="-w -s -extldflags '-static' -X github.com/anchore/syft/internal/version.version=0.79.0`},
},
}, nil,
},
cryptoSettings: nil,
arch: archDetails,
},
expected: []pkg.Package{
{
@@ -408,9 +409,8 @@ func TestBuildGoPkgInfo(t *testing.T) {
},
{
name: "parse main mod and replace devel version with one from ldflags without any vcs. build settings",
arch: archDetails,
mod: &extendedBuildInfo{
&debug.BuildInfo{
BuildInfo: &debug.BuildInfo{
GoVersion: goCompiledVersion,
Main: debug.Module{Path: "github.com/anchore/syft", Version: "(devel)"},
Settings: []debug.BuildSetting{
@@ -419,7 +419,9 @@ func TestBuildGoPkgInfo(t *testing.T) {
{Key: "GOAMD64", Value: "v1"},
{Key: "-ldflags", Value: `build -ldflags="-w -s -extldflags '-static' -X github.com/anchore/syft/internal/version.version=0.79.0`},
},
}, nil,
},
cryptoSettings: nil,
arch: archDetails,
},
expected: []pkg.Package{
{
@@ -453,9 +455,8 @@ func TestBuildGoPkgInfo(t *testing.T) {
},
{
name: "parse main mod and replace devel version with one from ldflags main.version without any vcs. build settings",
arch: archDetails,
mod: &extendedBuildInfo{
&debug.BuildInfo{
BuildInfo: &debug.BuildInfo{
GoVersion: goCompiledVersion,
Main: debug.Module{Path: "github.com/anchore/syft", Version: "(devel)"},
Settings: []debug.BuildSetting{
@@ -464,7 +465,9 @@ func TestBuildGoPkgInfo(t *testing.T) {
{Key: "GOAMD64", Value: "v1"},
{Key: "-ldflags", Value: `build -ldflags="-w -s -extldflags '-static' -X main.version=0.79.0`},
},
}, nil,
},
cryptoSettings: nil,
arch: archDetails,
},
expected: []pkg.Package{
{
@@ -498,9 +501,8 @@ func TestBuildGoPkgInfo(t *testing.T) {
},
{
name: "parse main mod and replace devel version with one from ldflags main.Version without any vcs. build settings",
arch: archDetails,
mod: &extendedBuildInfo{
&debug.BuildInfo{
BuildInfo: &debug.BuildInfo{
GoVersion: goCompiledVersion,
Main: debug.Module{Path: "github.com/anchore/syft", Version: "(devel)"},
Settings: []debug.BuildSetting{
@@ -509,7 +511,9 @@ func TestBuildGoPkgInfo(t *testing.T) {
{Key: "GOAMD64", Value: "v1"},
{Key: "-ldflags", Value: `build -ldflags="-w -s -extldflags '-static' -X main.Version=0.79.0`},
},
}, nil,
},
cryptoSettings: nil,
arch: archDetails,
},
expected: []pkg.Package{
{
@@ -543,9 +547,8 @@ func TestBuildGoPkgInfo(t *testing.T) {
},
{
name: "parse main mod and replace devel version with a pseudo version",
arch: archDetails,
mod: &extendedBuildInfo{
&debug.BuildInfo{
BuildInfo: &debug.BuildInfo{
GoVersion: goCompiledVersion,
Main: debug.Module{Path: "github.com/anchore/syft", Version: "(devel)"},
Settings: []debug.BuildSetting{
@@ -555,7 +558,9 @@ func TestBuildGoPkgInfo(t *testing.T) {
{Key: "vcs.revision", Value: "41bc6bb410352845f22766e27dd48ba93aa825a4"},
{Key: "vcs.time", Value: "2022-10-14T19:54:57Z"},
},
}, nil,
},
cryptoSettings: nil,
arch: archDetails,
},
expected: []pkg.Package{
{
@@ -590,9 +595,8 @@ func TestBuildGoPkgInfo(t *testing.T) {
},
{
name: "parse a populated mod string and returns packages but no source info",
arch: archDetails,
mod: &extendedBuildInfo{
&debug.BuildInfo{
BuildInfo: &debug.BuildInfo{
GoVersion: goCompiledVersion,
Main: debug.Module{Path: "github.com/anchore/syft", Version: "(devel)"},
Settings: []debug.BuildSetting{
@@ -612,7 +616,9 @@ func TestBuildGoPkgInfo(t *testing.T) {
Sum: "h1:DYssiUV1pBmKqzKsm4mqXx8artqC0Q8HgZsVI3lMsAg=",
},
},
}, nil,
},
cryptoSettings: nil,
arch: archDetails,
},
expected: []pkg.Package{
{
@@ -664,9 +670,8 @@ func TestBuildGoPkgInfo(t *testing.T) {
},
{
name: "parse a populated mod string and returns packages when a replace directive exists",
arch: archDetails,
mod: &extendedBuildInfo{
&debug.BuildInfo{
BuildInfo: &debug.BuildInfo{
GoVersion: goCompiledVersion,
Main: debug.Module{Path: "github.com/anchore/syft", Version: "(devel)"},
Settings: []debug.BuildSetting{
@@ -691,7 +696,9 @@ func TestBuildGoPkgInfo(t *testing.T) {
},
},
},
}, nil,
},
cryptoSettings: nil,
arch: archDetails,
},
expected: []pkg.Package{
{
@@ -756,7 +763,7 @@ func TestBuildGoPkgInfo(t *testing.T) {
)
c := goBinaryCataloger{}
pkgs := c.buildGoPkgInfo(fileresolver.Empty{}, location, test.mod, test.arch)
pkgs := c.buildGoPkgInfo(fileresolver.Empty{}, location, test.mod, test.mod.arch)
assert.Equal(t, test.expected, pkgs)
})
}
+17 -8
View File
@@ -15,16 +15,17 @@ import (
type extendedBuildInfo struct {
*debug.BuildInfo
cryptoSettings []string
arch string
}
// scanFile scans file to try to report the Go and module versions.
func scanFile(reader unionreader.UnionReader, filename string) ([]*extendedBuildInfo, []string) {
func scanFile(reader unionreader.UnionReader, filename string) []*extendedBuildInfo {
// NOTE: multiple readers are returned to cover universal binaries, which are files
// with more than one binary
readers, err := unionreader.GetReaders(reader)
if err != nil {
log.WithFields("error", err).Warnf("failed to open a golang binary")
return nil, nil
return nil
}
var builds []*extendedBuildInfo
@@ -34,6 +35,7 @@ func scanFile(reader unionreader.UnionReader, filename string) ([]*extendedBuild
log.WithFields("file", filename, "error", err).Trace("unable to read golang buildinfo")
continue
}
// it's possible the reader just isn't a go binary, in which case just skip it
if bi == nil {
continue
}
@@ -41,15 +43,22 @@ func scanFile(reader unionreader.UnionReader, filename string) ([]*extendedBuild
v, err := getCryptoInformation(r)
if err != nil {
log.WithFields("file", filename, "error", err).Trace("unable to read golang version info")
continue
// don't skip this build info.
// we can still catalog packages, even if we can't get the crypto information
}
arch := getGOARCH(bi.Settings)
if arch == "" {
arch, err = getGOARCHFromBin(r)
if err != nil {
log.WithFields("file", filename, "error", err).Trace("unable to read golang arch info")
// don't skip this build info.
// we can still catalog packages, even if we can't get the arch information
}
}
builds = append(builds, &extendedBuildInfo{bi, v})
builds = append(builds, &extendedBuildInfo{bi, v, arch})
}
archs := getArchs(readers, builds)
return builds, archs
return builds
}
func getCryptoInformation(reader io.ReaderAt) ([]string, error) {
+48 -3
View File
@@ -149,7 +149,6 @@ func (j *archiveParser) parse() ([]pkg.Package, []artifact.Relationship, error)
// discoverMainPackage parses the root Java manifest used as the parent package to all discovered nested packages.
func (j *archiveParser) discoverMainPackage() (*pkg.Package, error) {
// search and parse java manifest files
// TODO: do we want to prefer or check for pom files over manifest here?
manifestMatches := j.fileManifest.GlobMatch(manifestGlob)
if len(manifestMatches) > 1 {
return nil, fmt.Errorf("found multiple manifests in the jar: %+v", manifestMatches)
@@ -186,9 +185,24 @@ func (j *archiveParser) discoverMainPackage() (*pkg.Package, error) {
// we use j.location because we want to associate the license declaration with where we discovered the contents in the manifest
licenses := pkg.NewLicensesFromLocation(j.location, selectLicenses(manifest)...)
/*
We should name and version from, in this order:
1. pom.properties if we find exactly 1
2. pom.xml if we find exactly 1
3. manifest
4. filename
*/
name, version := j.guessMainPackageNameAndVersionFromPomInfo()
if name == "" {
name = selectName(manifest, j.fileInfo)
}
if version == "" {
version = selectVersion(manifest, j.fileInfo)
}
return &pkg.Package{
Name: selectName(manifest, j.fileInfo),
Version: selectVersion(manifest, j.fileInfo),
// TODO: maybe select name should just have a pom properties in it?
Name: name,
Version: version,
Language: pkg.Java,
Licenses: pkg.NewLicenseSet(licenses...),
Locations: file.NewLocationSet(
@@ -204,6 +218,37 @@ func (j *archiveParser) discoverMainPackage() (*pkg.Package, error) {
}, nil
}
func (j *archiveParser) guessMainPackageNameAndVersionFromPomInfo() (string, string) {
pomPropertyMatches := j.fileManifest.GlobMatch(pomPropertiesGlob)
pomMatches := j.fileManifest.GlobMatch(pomXMLGlob)
var pomPropertiesObject pkg.PomProperties
var pomProjectObject pkg.PomProject
if len(pomPropertyMatches) == 1 || len(pomMatches) == 1 {
// we have exactly 1 pom.properties or pom.xml in the archive; assume it represents the
// package we're scanning if the names seem like a plausible match
properties, _ := pomPropertiesByParentPath(j.archivePath, j.location, pomPropertyMatches)
projects, _ := pomProjectByParentPath(j.archivePath, j.location, pomMatches)
for parentPath, propertiesObj := range properties {
if propertiesObj.ArtifactID != "" && j.fileInfo.name != "" && strings.HasPrefix(propertiesObj.ArtifactID, j.fileInfo.name) {
pomPropertiesObject = propertiesObj
if proj, exists := projects[parentPath]; exists {
pomProjectObject = proj
}
}
}
}
name := pomPropertiesObject.ArtifactID
if name == "" {
name = pomProjectObject.ArtifactID
}
version := pomPropertiesObject.Version
if version == "" {
version = pomProjectObject.Version
}
return name, version
}
// discoverPkgsFromAllMavenFiles parses Maven POM properties/xml for a given
// parent package, returning all listed Java packages found for each pom
// properties discovered and potentially updating the given parentPkg with new

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