fix: update attestation code to remove library dependencies and shellout for keyless flow (#1442)

Co-authored-by: Alex Goodman <alex.goodman@anchore.com>
This commit is contained in:
Christopher Angelo Phillips 2023-01-12 12:22:05 -05:00 committed by GitHub
parent ac8f72fdd1
commit 44e8ae2577
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 346 additions and 2026 deletions

View file

@ -367,6 +367,8 @@ syft convert sbom.syft.json -o cyclonedx-json=sbom.cdx.json # convert it to Cyc
### Keyless support
Syft supports generating attestations using cosign's [keyless](https://github.com/sigstore/cosign/blob/main/KEYLESS.md) signatures.
Note: users need to have >= v1.12.0 of cosign installed for this command to function
To use this feature with a format like CycloneDX json simply run:
```
syft attest --output cyclonedx-json <IMAGE WITH OCI WRITE ACCESS>

View file

@ -4,7 +4,6 @@ import (
"fmt"
"log"
sigopts "github.com/sigstore/cosign/cmd/cosign/cli/options"
"github.com/spf13/cobra"
"github.com/spf13/viper"
@ -15,33 +14,27 @@ import (
)
const (
attestExample = ` {{.appName}} {{.command}} --output [FORMAT] --key [KEY] alpine:latest
Supports the following image sources:
{{.appName}} {{.command}} --key [KEY] 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}} --key [KEY] path/to/a/file/or/dir only for OCI tar or OCI directory
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
attestHelp = attestExample + attestSchemeHelp
)
func Attest(v *viper.Viper, app *config.Application, ro *options.RootOptions) *cobra.Command {
ao := options.AttestOptions{}
//nolint:dupl
func Attest(v *viper.Viper, app *config.Application, ro *options.RootOptions, po *options.PackagesOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "attest --output [FORMAT] --key [KEY] [SOURCE]",
Short: "Generate a package 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",
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 {
// run to unmarshal viper object onto app config
// the viper object correctly
if err := app.LoadAllValues(v, ro.Config); err != nil {
return fmt.Errorf("invalid application config: %w", err)
return fmt.Errorf("unable to load configuration: %w", err)
}
// configure logging for command
newLogWrapper(app)
logApplicationConfig(app)
return validateArgs(cmd, args)
@ -49,29 +42,16 @@ func Attest(v *viper.Viper, app *config.Application, ro *options.RootOptions) *c
SilenceUsage: true,
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
// this MUST be called first to make sure app config decodes
// the viper object correctly
if app.CheckForAppUpdate {
checkForApplicationUpdate()
}
// build cosign key options for attestation
ko := sigopts.KeyOpts{
KeyRef: app.Attest.KeyRef,
FulcioURL: app.Attest.FulcioURL,
IDToken: app.Attest.FulcioIdentityToken,
InsecureSkipFulcioVerify: app.Attest.InsecureSkipFulcioVerify,
RekorURL: app.Attest.RekorURL,
OIDCIssuer: app.Attest.OIDCIssuer,
OIDCClientID: app.Attest.OIDCClientID,
OIDCRedirectURL: app.Attest.OIDCRedirectURL,
}
return attest.Run(cmd.Context(), app, ko, args)
return attest.Run(cmd.Context(), app, args)
},
}
err := ao.AddFlags(cmd, v)
// syft attest is an enhancment of the packages command, so it should have the same flags
err := po.AddFlags(cmd, v)
if err != nil {
log.Fatal(err)
}

View file

@ -1,105 +1,60 @@
package attest
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/in-toto/in-toto-golang/in_toto"
sigopts "github.com/sigstore/cosign/cmd/cosign/cli/options"
"github.com/sigstore/cosign/cmd/cosign/cli/rekor"
"github.com/sigstore/cosign/cmd/cosign/cli/sign"
"github.com/sigstore/cosign/pkg/cosign"
"github.com/sigstore/cosign/pkg/cosign/attestation"
cbundle "github.com/sigstore/cosign/pkg/cosign/bundle"
"github.com/sigstore/cosign/pkg/oci/mutate"
ociremote "github.com/sigstore/cosign/pkg/oci/remote"
"github.com/sigstore/cosign/pkg/oci/static"
sigs "github.com/sigstore/cosign/pkg/signature"
"github.com/sigstore/cosign/pkg/types"
"github.com/sigstore/rekor/pkg/generated/client"
"github.com/sigstore/rekor/pkg/generated/models"
"github.com/sigstore/sigstore/pkg/signature/dsse"
signatureoptions "github.com/sigstore/sigstore/pkg/signature/options"
"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/internal/bus"
"github.com/anchore/syft/internal/config"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/ui"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/formats/cyclonedxjson"
"github.com/anchore/syft/syft/formats/spdxjson"
"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/formats/template"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
var (
allowedAttestFormats = []sbom.FormatID{
syftjson.ID,
spdxjson.ID,
cyclonedxjson.ID,
}
intotoJSONDsseType = `application/vnd.in-toto+json`
)
func Run(ctx context.Context, app *config.Application, ko sigopts.KeyOpts, args []string) error {
// We cannot generate an attestation for more than one output
if len(app.Outputs) > 1 {
return fmt.Errorf("unable to generate attestation for more than one output")
}
// can only be an image for attestation or OCI DIR
userInput := args[0]
si, err := parseImageSource(userInput, app)
func Run(ctx context.Context, app *config.Application, args []string) error {
err := ValidateOutputOptions(app)
if err != nil {
return err
}
output := parseAttestationOutput(app.Outputs)
format := syft.FormatByName(output)
// user typo or unknown outputs provided
if format == nil {
format = syft.FormatByID(syftjson.ID) // default attestation format
}
predicateType := formatPredicateType(format)
if predicateType == "" {
return fmt.Errorf(
"could not produce attestation predicate for given format: %q. Available formats: %+v",
options.FormatAliases(format.ID()),
options.FormatAliases(allowedAttestFormats...),
)
writer, err := options.MakeWriter(app.Outputs, app.File, app.OutputTemplatePath)
if err != nil {
return fmt.Errorf("unable to write to report destination: %w", err)
}
if app.Attest.KeyRef != "" {
passFunc, err := selectPassFunc(app.Attest.KeyRef, app.Attest.Password)
if err != nil {
return err
defer func() {
if err := writer.Close(); err != nil {
fmt.Printf("unable to close report destination: %+v", err)
}
}()
ko.PassFunc = passFunc
}
sv, err := sign.SignerFromKeyOpts(ctx, "", "", ko)
// could be an image or a directory, with or without a scheme
// TODO: validate that source is image
userInput := args[0]
si, err := source.ParseInputWithName(userInput, app.Platform, true, app.Name)
if err != nil {
return err
return fmt.Errorf("could not generate source input for packages command: %w", err)
}
if si.Scheme != source.ImageScheme {
return fmt.Errorf("attestations are only supported for oci images at this time")
}
defer sv.Close()
eventBus := partybus.NewBus()
stereoscope.SetBus(eventBus)
@ -107,7 +62,7 @@ func Run(ctx context.Context, app *config.Application, ko sigopts.KeyOpts, args
subscription := eventBus.Subscribe()
return eventloop.EventLoop(
execWorker(app, *si, format, predicateType, sv),
execWorker(app, *si, writer),
eventloop.SetupSignals(),
subscription,
stereoscope.Cleanup,
@ -115,284 +70,158 @@ func Run(ctx context.Context, app *config.Application, ko sigopts.KeyOpts, args
)
}
func parseAttestationOutput(outputs []string) (format string) {
if len(outputs) == 0 {
outputs = append(outputs, string(syftjson.ID))
func buildSBOM(app *config.Application, si source.Input, writer sbom.Writer, errs chan error) ([]byte, error) {
src, cleanup, err := source.New(si, app.Registry.ToOptions(), app.Exclusions)
if cleanup != nil {
defer cleanup()
}
return outputs[0]
}
func parseImageSource(userInput string, app *config.Application) (s *source.Input, err error) {
si, err := source.ParseInputWithName(userInput, app.Platform, false, app.Name)
if err != nil {
return nil, fmt.Errorf("could not generate source input for attest command: %w", err)
return nil, fmt.Errorf("failed to construct source from user input %q: %w", si.UserInput, err)
}
switch si.Scheme {
case source.ImageScheme, source.UnknownScheme:
// at this point we know that it cannot be dir: or file: schemes;
// we will assume that the unknown scheme could represent an image;
si.Scheme = source.ImageScheme
default:
return nil, fmt.Errorf("attest command can only be used with image sources but discovered %q when given %q", si.Scheme, userInput)
s, err := packages.GenerateSBOM(src, errs, app)
if err != nil {
return nil, err
}
// if the original detection was from the local daemon we want to short circuit
// that and attempt to generate the image source from its current registry source instead
switch si.ImageSource {
case image.UnknownSource, image.OciRegistrySource:
si.ImageSource = image.OciRegistrySource
case image.SingularitySource:
default:
return nil, fmt.Errorf("attest command can only be used with image sources fetch directly from the registry, but discovered an image source of %q when given %q", si.ImageSource, userInput)
if s == nil {
return nil, fmt.Errorf("no SBOM produced for %q", si.UserInput)
}
return si, nil
// note: only works for single format no multi writer support
sBytes, err := writer.Bytes(*s)
if err != nil {
return nil, fmt.Errorf("unable to build SBOM bytes: %w", err)
}
return sBytes, nil
}
func execWorker(app *config.Application, sourceInput source.Input, format sbom.Format, predicateType string, sv *sign.SignerVerifier) <-chan error {
func execWorker(app *config.Application, si source.Input, writer sbom.Writer) <-chan error {
errs := make(chan error)
go func() {
defer close(errs)
src, cleanup, err := source.NewFromRegistry(sourceInput, app.Registry.ToOptions(), app.Exclusions)
if cleanup != nil {
defer cleanup()
}
sBytes, err := buildSBOM(app, si, writer, errs)
if err != nil {
errs <- fmt.Errorf("failed to construct source from user input %q: %w", sourceInput.UserInput, err)
errs <- fmt.Errorf("unable to build SBOM: %w", err)
return
}
s, err := packages.GenerateSBOM(src, errs, app)
if err != nil {
errs <- err
return
}
// TODO: add multi writer support
for _, o := range app.Outputs {
f, err := os.CreateTemp("", o)
if err != nil {
errs <- fmt.Errorf("unable to create temp file: %w", err)
return
}
sbomBytes, err := syft.Encode(*s, format)
if err != nil {
errs <- err
return
}
defer f.Close()
defer os.Remove(f.Name())
signedPayload, err := generateAttestation(sourceInput, sbomBytes, src, sv, predicateType)
if err != nil {
errs <- err
return
}
if _, err := f.Write(sBytes); err != nil {
errs <- fmt.Errorf("unable to write SBOM to temp file: %w", err)
return
}
err = publishAttestation(app, signedPayload, src, sv)
if err != nil {
errs <- 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
}
args := []string{"attest", si.UserInput, "--type", "custom", "--predicate", f.Name()}
execCmd := exec.Command(cmd, args...)
execCmd.Env = os.Environ()
execCmd.Env = append(execCmd.Env, "COSIGN_EXPERIMENTAL=1")
// 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()
b := &busWriter{r: r, w: w, mon: &progress.Manual{N: -1}}
execCmd.Stdout = b
execCmd.Stderr = b
defer b.mon.SetCompleted()
// attest the SBOM
err = execCmd.Run()
if err != nil {
b.mon.Err = err
errs <- fmt.Errorf("unable to attest SBOM: %w", err)
return
}
}
bus.Publish(partybus.Event{
Type: event.Exit,
Value: func() error {
return nil
},
Type: event.Exit,
Value: func() error { return nil },
})
}()
return errs
}
func generateAttestation(si source.Input, predicate []byte, src *source.Source, sv *sign.SignerVerifier, predicateType string) ([]byte, error) {
var h v1.Hash
switch si.ImageSource {
case image.OciRegistrySource:
switch len(src.Image.Metadata.RepoDigests) {
case 0:
return nil, fmt.Errorf("cannot generate attestation since no repo digests were found; make sure you're passing an OCI registry source for the attest command")
case 1:
d, err := name.NewDigest(src.Image.Metadata.RepoDigests[0])
if err != nil {
return nil, err
}
h, err = v1.NewHash(d.Identifier())
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("cannot generate attestation since multiple repo digests were found for the image: %+v", src.Image.Metadata.RepoDigests)
}
case image.SingularitySource:
var err error
h, err = v1.NewHash(src.Image.Metadata.ID)
if err != nil {
return nil, err
func ValidateOutputOptions(app *config.Application) error {
var usesTemplateOutput bool
for _, o := range app.Outputs {
if o == template.ID.String() {
usesTemplateOutput = true
break
}
}
sh, err := attestation.GenerateStatement(attestation.GenerateOpts{
Predicate: bytes.NewBuffer(predicate),
Type: predicateType,
Digest: h.Hex,
})
if err != nil {
return nil, err
if usesTemplateOutput && app.OutputTemplatePath == "" {
return fmt.Errorf(`must specify path to template file when using "template" output format`)
}
payload, err := json.Marshal(sh)
if err != nil {
return nil, err
if len(app.Outputs) > 1 {
return fmt.Errorf("multiple SBOM format is not supported for attest at this time")
}
wrapped := dsse.WrapSigner(sv, intotoJSONDsseType)
return wrapped.SignMessage(bytes.NewReader(payload), signatureoptions.WithContext(context.Background()))
}
// publishAttestation publishes signedPayload to the location specified by the user.
func publishAttestation(app *config.Application, signedPayload []byte, src *source.Source, sv *sign.SignerVerifier) error {
switch {
// We want to give the option to not upload the generated attestation
// if passed or if the user is using local PKI
case app.Attest.NoUpload || app.Attest.KeyRef != "":
if app.File != "" {
return os.WriteFile(app.File, signedPayload, 0600)
}
_, err := os.Stdout.Write(signedPayload)
return err
default:
ref, err := name.ParseReference(src.Metadata.ImageMetadata.UserInput)
if err != nil {
return err
}
digest, err := ociremote.ResolveDigest(ref)
if err != nil {
return err
}
return uploadAttestation(app, signedPayload, digest, sv)
// 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()}
}
}
func trackUploadAttestation() (*progress.Stage, *progress.Manual) {
stage := &progress.Stage{}
prog := &progress.Manual{}
bus.Publish(partybus.Event{
Type: event.UploadAttestation,
Value: progress.StagedProgressable(&struct {
progress.Stager
progress.Progressable
}{
Stager: stage,
Progressable: prog,
}),
})
return stage, prog
}
// uploads signed SBOM payload to Rekor transparency log along with key information;
// returns a bundle for attestation annotations
// rekor bundle includes a signed payload and rekor timestamp;
// the bundle is then wrapped onto an OCI signed entity and uploaded to
// the user's image's OCI registry repository as *.att
func uploadAttestation(app *config.Application, signedPayload []byte, digest name.Digest, sv *sign.SignerVerifier) error {
// add application/vnd.dsse.envelope.v1+json as media type for other applications to decode attestation
opts := []static.Option{static.WithLayerMediaType(types.DssePayloadType)}
if sv.Cert != nil {
opts = append(opts, static.WithCertChain(sv.Cert, sv.Chain))
}
stage, prog := trackUploadAttestation()
defer prog.SetCompleted() // just in case we return early
prog.Total = 2
stage.Current = "uploading signing information to transparency log"
// uploads payload to Rekor transparency log along with key information;
// returns bundle for attesation annotations
// rekor bundle includes a signed payload and rekor timestamp;
// the bundle is then wrapped onto an OCI signed entity and uploaded to
// the user's image's OCI registry repository as *.att
bundle, err := uploadToTlog(context.TODO(), sv, app.Attest.RekorURL, func(r *client.Rekor, b []byte) (*models.LogEntryAnon, error) {
return cosign.TLogUploadInTotoAttestation(context.TODO(), r, signedPayload, b)
})
if err != nil {
return err
}
prog.N = 1
stage.Current = "uploading attestation to OCI registry"
// add bundle OCI attestation that is uploaded to
opts = append(opts, static.WithBundle(bundle))
sig, err := static.NewAttestation(signedPayload, opts...)
if err != nil {
return err
}
se, err := ociremote.SignedEntity(digest)
if err != nil {
return err
}
newSE, err := mutate.AttachAttestationToEntity(se, sig)
if err != nil {
return err
}
// Publish the attestations associated with this entity
err = ociremote.WriteAttestations(digest.Repository, newSE)
if err != nil {
return err
}
prog.SetCompleted()
return nil
}
func formatPredicateType(format sbom.Format) string {
switch format.ID() {
case spdxjson.ID:
return in_toto.PredicateSPDX
case cyclonedxjson.ID:
return in_toto.PredicateCycloneDX
case syftjson.ID:
return "https://syft.dev/bom"
default:
return ""
}
type busWriter struct {
w *os.File
r *os.File
hasWritten bool
mon *progress.Manual
}
type tlogUploadFn func(*client.Rekor, []byte) (*models.LogEntryAnon, error)
func uploadToTlog(ctx context.Context, sv *sign.SignerVerifier, rekorURL string, upload tlogUploadFn) (*cbundle.RekorBundle, error) {
var rekorBytes []byte
// Upload the cert or the public key, depending on what we have
if sv.Cert != nil {
rekorBytes = sv.Cert
} else {
pemBytes, err := sigs.PublicKeyPem(sv, signatureoptions.WithContext(ctx))
if err != nil {
return nil, err
}
rekorBytes = pemBytes
func (b *busWriter) Write(p []byte) (n int, err error) {
if !b.hasWritten {
b.hasWritten = true
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: b.r,
Manual: b.mon,
},
},
)
}
rekorClient, err := rekor.NewClient(rekorURL)
if err != nil {
return nil, err
}
entry, err := upload(rekorClient, rekorBytes)
if err != nil {
return nil, err
}
if entry.LogIndex != nil {
log.Debugf("transparency log entry created with index: %v", *entry.LogIndex)
}
return cbundle.EntryToBundle(entry), nil
return b.w.Write(p)
}
func commandExists(cmd string) bool {
_, err := exec.LookPath(cmd)
return err == nil
}

View file

@ -1,57 +0,0 @@
package attest
import (
"errors"
"fmt"
"io"
"os"
"strings"
"github.com/sigstore/cosign/pkg/cosign"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
)
func selectPassFunc(keypath, password string) (cosign.PassFunc, error) {
keyContents, err := os.ReadFile(keypath)
if err != nil {
return nil, err
}
var fn cosign.PassFunc = func(bool) (b []byte, err error) { return nil, nil }
_, err = cosign.LoadPrivateKey(keyContents, nil)
if err != nil {
fn = func(bool) (b []byte, err error) {
return fetchPassword(password)
}
}
return fn, nil
}
func fetchPassword(password string) (b []byte, err error) {
potentiallyPipedInput, err := internal.IsPipedInput()
if err != nil {
log.Warnf("unable to determine if there is piped input: %+v", err)
}
switch {
case password != "":
return []byte(password), nil
case potentiallyPipedInput:
// handle piped in passwords
pwBytes, err := io.ReadAll(os.Stdin)
if err != nil {
return nil, fmt.Errorf("unable to get password from stdin: %w", err)
}
// be resilient to input that may have newline characters (in case someone is using echo without -n)
cleanPw := strings.TrimRight(string(pwBytes), "\n")
return []byte(cleanPw), nil
case internal.IsTerminal():
return cosign.GetPassFromTerm(false)
}
return nil, errors.New("no method available to fetch password")
}

View file

@ -26,8 +26,10 @@ import (
const indent = " "
// New constructs the `syft packages` command, aliases the root command to `syft packages`,
// and constructs the `syft power-user` and `syft attest` commands. It is also responsible for
// 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.
@ -46,9 +48,9 @@ func New() (*cobra.Command, error) {
packagesCmd := Packages(v, app, ro, po)
// root options are also passed to the attestCmd so that a user provided config location can be discovered
attestCmd := Attest(v, app, ro)
poweruserCmd := PowerUser(v, app, ro)
convertCmd := Convert(v, app, ro, po)
attestCmd := Attest(v, app, ro, po)
// rootCmd is currently an alias for the packages command
rootCmd := &cobra.Command{
@ -73,11 +75,7 @@ func New() (*cobra.Command, error) {
if err != nil {
return nil, err
}
// attest also uses flags from the packagesCmd since it generates an sbom
err = po.AddFlags(attestCmd, 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 {
@ -87,13 +85,11 @@ func New() (*cobra.Command, error) {
// commands to add to root
cmds := []*cobra.Command{
packagesCmd,
attestCmd,
poweruserCmd,
convertCmd,
poweruserCmd,
poweruserCmd,
Completion(),
attestCmd,
Version(v, app),
cranecmd.NewCmdAuthLogin("syft"),
cranecmd.NewCmdAuthLogin("syft"), // syft login uses the same command as crane
}
// Add sub-commands.

View file

@ -1,54 +0,0 @@
package cli
import (
"os"
"github.com/spf13/cobra"
)
func Completion() *cobra.Command {
cmd := &cobra.Command{
Use: "completion [bash|zsh|fish]",
Short: "Generate a shell completion for Syft (listing local docker images)",
Long: `To load completions (docker image list):
Bash:
$ source <(syft completion bash)
# To load completions for each session, execute once:
Linux:
$ syft completion bash > /etc/bash_completion.d/syft
MacOS:
$ syft completion bash > /usr/local/etc/bash_completion.d/syft
Zsh:
# If shell completion is not already enabled in your environment you will need
# to enable it. You can execute the following once:
$ echo "autoload -U compinit; compinit" >> ~/.zshrc
# To load completions for each session, execute once:
$ syft completion zsh > "${fpath[1]}/_syft"
# You will need to start a new shell for this setup to take effect.
Fish:
$ syft completion fish | source
# To load completions for each session, execute once:
$ syft completion fish > ~/.config/fish/completions/syft.fish
`,
DisableFlagsInUseLine: true,
ValidArgs: []string{"bash", "zsh", "fish"},
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
RunE: func(cmd *cobra.Command, args []string) error {
var err error
switch args[0] {
case "bash":
err = cmd.Root().GenBashCompletion(os.Stdout)
case "zsh":
err = cmd.Root().GenZshCompletion(os.Stdout)
case "fish":
err = cmd.Root().GenFishCompletion(os.Stdout, true)
}
if err != nil {
return err
}
return nil
},
}
return cmd
}

View file

@ -1,77 +0,0 @@
package options
import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
const defaultKeyFileName = "cosign.key"
type AttestOptions struct {
Key string
Cert string
CertChain string
NoUpload bool
Force bool
Recursive bool
Rekor RekorOptions
Fulcio FulcioOptions
OIDC OIDCOptions
}
var _ Interface = (*AttestOptions)(nil)
func (o *AttestOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error {
if err := o.Rekor.AddFlags(cmd, v); err != nil {
return err
}
if err := o.Fulcio.AddFlags(cmd, v); err != nil {
return err
}
if err := o.OIDC.AddFlags(cmd, v); err != nil {
return err
}
cmd.Flags().StringVarP(&o.Key, "key", "", defaultKeyFileName,
"path to the private key file to use for attestation")
cmd.Flags().StringVarP(&o.Cert, "cert", "", "",
"path to the x.509 certificate in PEM format to include in the OCI Signature")
cmd.Flags().BoolVarP(&o.NoUpload, "no-upload", "", false,
"do not upload the generated attestation")
cmd.Flags().BoolVarP(&o.Force, "force", "", false,
"skip warnings and confirmations")
cmd.Flags().BoolVarP(&o.Recursive, "recursive", "", false,
"if a multi-arch image is specified, additionally sign each discrete image")
return bindAttestationConfigOptions(cmd.Flags(), v)
}
func bindAttestationConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error {
if err := v.BindPFlag("attest.key", flags.Lookup("key")); err != nil {
return err
}
if err := v.BindPFlag("attest.cert", flags.Lookup("cert")); err != nil {
return err
}
if err := v.BindPFlag("attest.no-upload", flags.Lookup("no-upload")); err != nil {
return err
}
if err := v.BindPFlag("attest.force", flags.Lookup("force")); err != nil {
return err
}
if err := v.BindPFlag("attest.recursive", flags.Lookup("recursive")); err != nil {
return err
}
return nil
}

View file

@ -9,7 +9,7 @@ type VersionOptions struct {
Output string
}
var _ Interface = (*PackagesOptions)(nil)
var _ Interface = (*VersionOptions)(nil)
func (o *VersionOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error {
cmd.Flags().StringVarP(&o.Output, "output", "o", "text", "format to show version information (available=[text, json])")

View file

@ -24,7 +24,7 @@ import (
)
func Run(ctx context.Context, app *config.Application, args []string) error {
err := validateOutputOptions(app)
err := ValidateOutputOptions(app)
if err != nil {
return err
}
@ -134,7 +134,7 @@ func MergeRelationships(cs ...<-chan artifact.Relationship) (relationships []art
return relationships
}
func validateOutputOptions(app *config.Application) error {
func ValidateOutputOptions(app *config.Application) error {
var usesTemplateOutput bool
for _, o := range app.Outputs {
if o == template.ID.String() {

189
go.mod
View file

@ -3,7 +3,6 @@ module github.com/anchore/syft
go 1.18
require (
github.com/CycloneDX/cyclonedx-go v0.7.1-0.20221222100750-41a1ac565cce
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
github.com/acobaugh/osrelease v0.1.0
github.com/adrg/xdg v0.3.3
@ -50,268 +49,90 @@ require (
)
require (
github.com/CycloneDX/cyclonedx-go v0.7.1-0.20221222100750-41a1ac565cce
github.com/Masterminds/sprig/v3 v3.2.2
github.com/anchore/go-logger v0.0.0-20220728155337-03b66a5207d8
github.com/anchore/stereoscope v0.0.0-20221208011002-c5ff155d72f1
github.com/docker/docker v20.10.17+incompatible
github.com/google/go-containerregistry v0.11.0
github.com/in-toto/in-toto-golang v0.4.1-0.20221018183522-731d0640b65f
github.com/invopop/jsonschema v0.7.0
github.com/knqyf263/go-rpmdb v0.0.0-20221030135625-4082a22221ce
github.com/opencontainers/go-digest v1.0.0
github.com/sassoftware/go-rpmutils v0.2.0
github.com/sigstore/cosign v1.13.1
github.com/sigstore/rekor v0.12.1-0.20220915152154-4bb6f441c1b2
github.com/sigstore/sigstore v1.4.4
github.com/vbatts/go-mtree v0.5.0
golang.org/x/exp v0.0.0-20220823124025-807a23277127
gopkg.in/yaml.v3 v3.0.1
)
require (
bitbucket.org/creachadair/shell v0.0.7 // indirect
cloud.google.com/go/compute v1.10.0 // indirect
github.com/AliyunContainerService/ack-ram-tool/pkg/credentials/alibabacloudsdkgo/helper v0.2.0 // indirect
github.com/Azure/azure-sdk-for-go v66.0.0+incompatible // indirect
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
github.com/Azure/go-autorest/autorest v0.11.28 // indirect
github.com/Azure/go-autorest/autorest/adal v0.9.21 // indirect
github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 // indirect
github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
github.com/Azure/go-autorest/logger v0.2.1 // indirect
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/DataDog/zstd v1.4.5 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.1.1 // indirect
github.com/Microsoft/go-winio v0.5.2 // indirect
github.com/ThalesIgnite/crypto11 v1.2.5 // indirect
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 // indirect
github.com/alibabacloud-go/cr-20160607 v1.0.1 // indirect
github.com/alibabacloud-go/cr-20181201 v1.0.10 // indirect
github.com/alibabacloud-go/darabonba-openapi v0.1.18 // indirect
github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68 // indirect
github.com/alibabacloud-go/endpoint-util v1.1.1 // indirect
github.com/alibabacloud-go/openapi-util v0.0.11 // indirect
github.com/alibabacloud-go/tea v1.1.18 // indirect
github.com/alibabacloud-go/tea-utils v1.4.4 // indirect
github.com/alibabacloud-go/tea-xml v1.1.2 // indirect
github.com/aliyun/credentials-go v1.2.3 // indirect
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
github.com/aws/aws-sdk-go-v2 v1.16.16 // indirect
github.com/aws/aws-sdk-go-v2/config v1.17.8 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.12.21 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.23 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.24 // indirect
github.com/aws/aws-sdk-go-v2/service/ecr v1.15.0 // indirect
github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.12.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.17 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.11.23 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.6 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.16.19 // indirect
github.com/aws/smithy-go v1.13.3 // indirect
github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20220517224237-e6f29200ae04 // indirect
github.com/benbjohnson/clock v1.1.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bgentry/speakeasy v0.1.0 // indirect
github.com/blang/semver v3.5.1+incompatible // indirect
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/chrismellard/docker-credential-acr-env v0.0.0-20220119192733-fe33c00cee21 // indirect
github.com/clbanning/mxj/v2 v2.5.6 // indirect
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 // indirect
github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490 // indirect
github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be // indirect
github.com/containerd/containerd v1.6.12 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.12.0 // indirect
github.com/coreos/go-oidc/v3 v3.4.0 // indirect
github.com/coreos/go-semver v0.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/cyberphone/json-canonicalization v0.0.0-20210823021906-dc406ceaf94b // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dimchansky/utfbom v1.1.1 // indirect
github.com/docker/cli v20.10.17+incompatible // indirect
github.com/docker/distribution v2.8.1+incompatible // indirect
github.com/docker/docker-credential-helpers v0.6.4 // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1 // indirect
github.com/envoyproxy/protoc-gen-validate v0.6.2 // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/fullstorydev/grpcurl v1.8.7 // indirect
github.com/gabriel-vasile/mimetype v1.4.0 // indirect
github.com/go-chi/chi v4.1.2+incompatible // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/analysis v0.21.4 // indirect
github.com/go-openapi/errors v0.20.3 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect
github.com/go-openapi/loads v0.21.2 // indirect
github.com/go-openapi/runtime v0.24.2 // indirect
github.com/go-openapi/spec v0.20.7 // indirect
github.com/go-openapi/strfmt v0.21.3 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/go-openapi/validate v0.22.0 // indirect
github.com/go-piv/piv-go v1.10.0 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.11.0 // indirect
github.com/go-restruct/restruct v1.2.0-alpha // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang-jwt/jwt/v4 v4.4.2 // indirect
github.com/golang/glog v1.0.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/certificate-transparency-go v1.1.3 // indirect
github.com/google/go-github/v45 v45.2.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/trillian v1.5.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.0 // indirect
github.com/googleapis/gnostic v0.5.5 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/huandu/xstrings v1.3.2 // indirect
github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/jedisct1/go-minisign v0.0.0-20211028175153-1c139d1cc84b // indirect
github.com/jhump/protoreflect v1.13.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jonboulle/clockwork v0.3.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/klauspost/compress v1.15.9 // indirect
github.com/klauspost/pgzip v1.2.5 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/letsencrypt/boulder v0.0.0-20220929215747-76583552c2be // indirect
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/miekg/pkcs11 v1.1.1 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mozillazg/docker-credential-acr-helper v0.3.0 // indirect
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
github.com/nwaples/rardecode v1.1.0 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
github.com/pierrec/lz4/v4 v4.1.15 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.13.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sassoftware/relic v0.0.0-20210427151427-dfb082b79b74 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.4.0 // indirect
github.com/segmentio/ksuid v1.0.4 // indirect
github.com/shibumi/go-pathspec v1.3.0 // indirect
github.com/rogpeppe/go-internal v1.8.0 // indirect
github.com/shopspring/decimal v1.2.0 // indirect
github.com/sigstore/fulcio v0.6.0 // indirect
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect
github.com/soheilhy/cmux v0.1.5 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spiffe/go-spiffe/v2 v2.1.1 // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/subosito/gotenv v1.4.1 // indirect
github.com/sylabs/sif/v2 v2.8.1 // indirect
github.com/sylabs/squashfs v0.6.1 // indirect
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect
github.com/tent/canonical-json-go v0.0.0-20130607151641-96e4ba3a7613 // indirect
github.com/thales-e-security/pool v0.0.2 // indirect
github.com/therootcompany/xz v1.0.1 // indirect
github.com/theupdateframework/go-tuf v0.5.2-0.20220930112810-3890c1e7ace4 // indirect
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect
github.com/tjfoc/gmsm v1.3.2 // indirect
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect
github.com/transparency-dev/merkle v0.0.1 // indirect
github.com/ulikunitz/xz v0.5.10 // indirect
github.com/urfave/cli v1.22.7 // indirect
github.com/vbatts/tar-split v0.11.2 // indirect
github.com/xanzy/go-gitlab v0.73.1 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
github.com/zeebo/errs v1.2.2 // indirect
go.etcd.io/bbolt v1.3.6 // indirect
go.etcd.io/etcd/api/v3 v3.6.0-alpha.0 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.6.0-alpha.0 // indirect
go.etcd.io/etcd/client/v2 v2.306.0-alpha.0 // indirect
go.etcd.io/etcd/client/v3 v3.6.0-alpha.0 // indirect
go.etcd.io/etcd/etcdctl/v3 v3.6.0-alpha.0 // indirect
go.etcd.io/etcd/etcdutl/v3 v3.6.0-alpha.0 // indirect
go.etcd.io/etcd/pkg/v3 v3.6.0-alpha.0 // indirect
go.etcd.io/etcd/raft/v3 v3.6.0-alpha.0 // indirect
go.etcd.io/etcd/server/v3 v3.6.0-alpha.0 // indirect
go.etcd.io/etcd/tests/v3 v3.6.0-alpha.0 // indirect
go.etcd.io/etcd/v3 v3.6.0-alpha.0 // indirect
go.mongodb.org/mongo-driver v1.10.0 // indirect
go.opencensus.io v0.23.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.28.0 // indirect
go.opentelemetry.io/otel v1.7.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.7.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.7.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.7.0 // indirect
go.opentelemetry.io/otel/sdk v1.7.0 // indirect
go.opentelemetry.io/otel/trace v1.7.0 // indirect
go.opentelemetry.io/proto/otlp v0.16.0 // indirect
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/multierr v1.8.0 // indirect
go.uber.org/zap v1.23.0 // indirect
golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1 // indirect
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0 // indirect
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect
golang.org/x/text v0.3.8-0.20211004125949-5bd84dd9b33b // indirect
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af // indirect
golang.org/x/tools v0.1.12 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/api v0.99.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e // indirect
google.golang.org/grpc v1.50.1 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/cheggaaa/pb.v1 v1.0.28 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
k8s.io/api v0.23.5 // indirect
k8s.io/apimachinery v0.23.5 // indirect
k8s.io/client-go v0.23.5 // indirect
k8s.io/klog/v2 v2.60.1-0.20220317184644-43cc75f9ae89 // indirect
k8s.io/kube-openapi v0.0.0-20220124234850-424119656bbf // indirect
k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect
lukechampine.com/uint128 v1.1.1 // indirect
modernc.org/cc/v3 v3.36.0 // indirect
modernc.org/ccgo/v3 v3.16.6 // indirect
@ -322,10 +143,6 @@ require (
modernc.org/sqlite v1.17.3 // indirect
modernc.org/strutil v1.1.1 // indirect
modernc.org/token v1.0.0 // indirect
sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect
sigs.k8s.io/release-utils v0.7.3 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)
require (

972
go.sum

File diff suppressed because it is too large Load diff

View file

@ -53,7 +53,6 @@ type Application struct {
Secrets secrets `yaml:"secrets" json:"secrets" mapstructure:"secrets"`
Registry registry `yaml:"registry" json:"registry" mapstructure:"registry"`
Exclusions []string `yaml:"exclude" json:"exclude" mapstructure:"exclude"`
Attest attest `yaml:"attest" json:"attest" mapstructure:"attest"`
Platform string `yaml:"platform" json:"platform" mapstructure:"platform"`
Name string `yaml:"name" json:"name" mapstructure:"name"`
Parallelism int `yaml:"parallelism" json:"parallelism" mapstructure:"parallelism"` // the number of catalog workers to run in parallel

View file

@ -1,56 +0,0 @@
package config
import (
"fmt"
"os"
"github.com/mitchellh/go-homedir"
"github.com/sigstore/cosign/cmd/cosign/cli/options"
"github.com/spf13/viper"
)
// IMPORTANT: do not show the password in any YAML/JSON output (sensitive information)
type attest struct {
KeyRef string `yaml:"key" json:"key" mapstructure:"key"` // same as --key, file path to the private key
Cert string `yaml:"cert" json:"cert" mapstructure:"cert"`
NoUpload bool `yaml:"no_upload" json:"noUpload" mapstructure:"no_upload"`
Force bool `yaml:"force" json:"force" mapstructure:"force"`
Recursive bool `yaml:"recursive" json:"recursive" mapstructure:"recursive"`
Replace bool `yaml:"replace" json:"replace" mapstructure:"replace"`
Password string `yaml:"-" json:"-" mapstructure:"password"` // password for the private key
FulcioURL string `yaml:"fulcio_url" json:"fulcioUrl" mapstructure:"fulcio_url"`
FulcioIdentityToken string `yaml:"fulcio_identity_token" json:"fulcio_identity_token" mapstructure:"fulcio_identity_token"`
InsecureSkipFulcioVerify bool `yaml:"insecure_skip_verify" json:"insecure_skip_verify" mapstructure:"insecure_skip_verify"`
RekorURL string `yaml:"rekor_url" json:"rekorUrl" mapstructure:"rekor_url"`
OIDCIssuer string `yaml:"oidc_issuer" json:"oidcIssuer" mapstructure:"oidc_issuer"`
OIDCClientID string `yaml:"oidc_client_id" json:"oidcClientId" mapstructure:"oidc_client_id"`
OIDCRedirectURL string `yaml:"oidc_redirect_url" json:"OIDCRedirectURL" mapstructure:"oidc_redirect_url"`
}
func (cfg *attest) parseConfigValues() error {
if cfg.KeyRef != "" {
expandedPath, err := homedir.Expand(cfg.KeyRef)
if err != nil {
return fmt.Errorf("unable to expand key path=%q: %w", cfg.KeyRef, err)
}
cfg.KeyRef = expandedPath
}
if cfg.Password == "" {
// we allow for configuration via syft config/env vars and additionally interop with known cosign config env vars
if pw, ok := os.LookupEnv("COSIGN_PASSWORD"); ok {
cfg.Password = pw
}
}
return nil
}
func (cfg attest) loadDefaultValues(v *viper.Viper) {
v.SetDefault("attest.key", "")
v.SetDefault("attest.password", "")
v.SetDefault("attest.fulcio_url", options.DefaultFulcioURL)
v.SetDefault("attest.rekor_url", options.DefaultRekorURL)
v.SetDefault("attest.oidc_issuer", options.DefaultOIDCIssuerURL)
v.SetDefault("attest.oidc_client_id", "sigstore")
}

View file

@ -32,6 +32,6 @@ const (
// ImportStarted is a partybus event that occurs when an SBOM upload process has begun
ImportStarted partybus.EventType = "syft-import-started-event"
// UploadAttestation is a partybus event that occurs when syft uploads an attestation to an OCI registry (+ any transparency log)
UploadAttestation partybus.EventType = "syft-upload-attestation"
// AttestationStarted is a partybus event that occurs when starting an SBOM attestation process
AttestationStarted partybus.EventType = "syft-attestation-started-event"
)

View file

@ -0,0 +1,23 @@
package monitor
import (
"io"
"github.com/wagoodman/go-progress"
)
type ShellProgress struct {
io.Reader
*progress.Manual
}
type Title struct {
Default string
WhileRunning string
OnSuccess string
}
type GenericTask struct {
Title Title
Context string
}

View file

@ -5,11 +5,13 @@ package parsers
import (
"fmt"
"io"
"github.com/wagoodman/go-partybus"
"github.com/wagoodman/go-progress"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/event/monitor"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg/cataloger"
)
@ -153,15 +155,20 @@ func ParseImportStarted(e partybus.Event) (string, progress.StagedProgressable,
return host, prog, nil
}
func ParseUploadAttestation(e partybus.Event) (progress.StagedProgressable, error) {
if err := checkEventType(e.Type, event.UploadAttestation); err != nil {
return nil, err
func ParseAttestationStartedEvent(e partybus.Event) (io.Reader, progress.Progressable, *monitor.GenericTask, error) {
if err := checkEventType(e.Type, event.AttestationStarted); err != nil {
return nil, nil, nil, err
}
prog, ok := e.Value.(progress.StagedProgressable)
source, ok := e.Source.(monitor.GenericTask)
if !ok {
return nil, newPayloadErr(e.Type, "Value", e.Value)
return nil, nil, nil, newPayloadErr(e.Type, "Source", e.Source)
}
return prog, nil
sp, ok := e.Value.(*monitor.ShellProgress)
if !ok {
return nil, nil, nil, newPayloadErr(e.Type, "Value", e.Value)
}
return sp.Reader, sp.Manual, &source, nil
}

View file

@ -99,6 +99,18 @@ func (m *multiWriter) Write(s SBOM) (errs error) {
return errs
}
// Bytes returns the bytes of the SBOM that would be written
func (m *multiWriter) Bytes(s SBOM) (bytes []byte, err error) {
for _, w := range m.writers {
b, err := w.Bytes(s)
if err != nil {
return nil, err
}
bytes = append(bytes, b...)
}
return bytes, nil
}
// Close closes all writers
func (m *multiWriter) Close() (errs error) {
for _, w := range m.writers {

View file

@ -1,6 +1,7 @@
package sbom
import (
"bytes"
"io"
)
@ -16,6 +17,16 @@ func (w *streamWriter) Write(s SBOM) error {
return w.format.Encode(w.out, s)
}
// Bytes returns the bytes of the SBOM that would be written
func (w *streamWriter) Bytes(s SBOM) ([]byte, error) {
var buffer bytes.Buffer
err := w.format.Encode(&buffer, s)
if err != nil {
return nil, err
}
return buffer.Bytes(), nil
}
// Close any resources, such as open files
func (w *streamWriter) Close() error {
if w.close != nil {

View file

@ -7,6 +7,9 @@ type Writer interface {
// Write writes the provided SBOM
Write(SBOM) error
// Bytes returns the bytes of the SBOM that would be written
Bytes(SBOM) ([]byte, error)
// Closer a resource cleanup hook which will be called after SBOM
// is written or if an error occurs before Write is called
io.Closer

View file

@ -1,68 +0,0 @@
package cli
import (
"strings"
"testing"
)
func TestAttestCmd(t *testing.T) {
img := "registry:busybox:latest"
tests := []struct {
name string
args []string
env map[string]string
assertions []traitAssertion
pw string
}{
{
name: "no-args-shows-help",
args: []string{"attest"},
assertions: []traitAssertion{
assertInOutput("an image/directory argument is required"), // specific error that should be shown
assertInOutput("from a container image as the predicate of an in-toto attestation"), // excerpt from help description
assertFailingReturnCode,
},
pw: "",
},
{
name: "can encode syft.json as the predicate given a password",
args: []string{"attest", "-o", "json", "--key", "cosign.key", img},
assertions: []traitAssertion{
assertSuccessfulReturnCode,
},
pw: "test",
},
{
name: "can encode syft.json as the predicate given a blank password",
args: []string{"attest", "-o", "json", "--key", "cosign.key", img},
assertions: []traitAssertion{
assertSuccessfulReturnCode,
},
pw: "",
},
{
name: "can encode syft.json as the predicate given a user format typo",
args: []string{"attest", "-o", "spdx-jsonx", "--key", "cosign.key", img},
assertions: []traitAssertion{
assertSuccessfulReturnCode,
},
pw: "",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
cleanup := setupPKI(t, test.pw)
defer cleanup()
cmd, stdout, stderr := runSyft(t, test.env, test.args...)
for _, traitFn := range test.assertions {
traitFn(t, stdout, stderr, cmd.ProcessState.ExitCode())
}
if t.Failed() {
t.Log("STDOUT:\n", stdout)
t.Log("STDERR:\n", stderr)
t.Log("COMMAND:", strings.Join(cmd.Args, " "))
}
})
}
}

View file

@ -1,150 +0,0 @@
package cli
import (
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCosignWorkflow(t *testing.T) {
// found under test-fixtures/registry/Makefile
img := "localhost:5000/attest:latest"
attestationFile := "attestation.json"
tests := []struct {
name string
syftArgs []string
cosignAttachArgs []string
cosignVerifyArgs []string
env map[string]string
assertions []traitAssertion
setup func(*testing.T)
cleanup func()
}{
{
name: "cosign verify syft attest",
syftArgs: []string{
"attest",
"-o",
"json",
"--key",
"cosign.key",
img,
},
// cosign attach attestation
cosignAttachArgs: []string{
"attach",
"attestation",
"--attestation",
attestationFile,
img,
},
// cosign verify-attestation
cosignVerifyArgs: []string{
"verify-attestation",
"-key",
"cosign.pub",
img,
},
assertions: []traitAssertion{
assertSuccessfulReturnCode,
},
setup: func(t *testing.T) {
cwd, err := os.Getwd()
require.NoErrorf(t, err, "unable to get cwd: %+v", err)
// get working directory for local registry
fixturesPath := filepath.Join(cwd, "test-fixtures", "registry")
makeTask := filepath.Join(fixturesPath, "Makefile")
t.Logf("Generating Fixture from 'make %s'", makeTask)
cmd := exec.Command("make")
cmd.Dir = fixturesPath
runAndShow(t, cmd)
var done = make(chan struct{})
defer close(done)
for interval := range testRetryIntervals(done) {
resp, err := http.Get("http://127.0.0.1:5000/v2/")
if err != nil {
t.Logf("waiting for registry err=%+v", err)
} else {
if resp.StatusCode == http.StatusOK {
break
}
t.Logf("waiting for registry code=%+v", resp.StatusCode)
}
time.Sleep(interval)
}
cmd = exec.Command("make", "push")
cmd.Dir = fixturesPath
runAndShow(t, cmd)
},
cleanup: func() {
cwd, err := os.Getwd()
assert.NoErrorf(t, err, "unable to get cwd: %+v", err)
fixturesPath := filepath.Join(cwd, "test-fixtures", "registry")
makeTask := filepath.Join(fixturesPath, "Makefile")
t.Logf("Generating Fixture from 'make %s'", makeTask)
// delete attestation file
os.Remove(attestationFile)
cmd := exec.Command("make", "stop")
cmd.Dir = fixturesPath
runAndShow(t, cmd)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Cleanup(tt.cleanup)
tt.setup(t)
pkiCleanup := setupPKI(t, "") // blank password
defer pkiCleanup()
// attest
cmd, stdout, stderr := runSyft(t, tt.env, tt.syftArgs...)
for _, traitFn := range tt.assertions {
traitFn(t, stdout, stderr, cmd.ProcessState.ExitCode())
}
checkCmdFailure(t, stdout, stderr, cmd)
require.NoError(t, os.WriteFile(attestationFile, []byte(stdout), 0666))
// attach
cmd, stdout, stderr = runCosign(t, tt.env, tt.cosignAttachArgs...)
for _, traitFn := range tt.assertions {
traitFn(t, stdout, stderr, cmd.ProcessState.ExitCode())
}
checkCmdFailure(t, stdout, stderr, cmd)
// attest
cmd, stdout, stderr = runCosign(t, tt.env, tt.cosignAttachArgs...)
for _, traitFn := range tt.assertions {
traitFn(t, stdout, stderr, cmd.ProcessState.ExitCode())
}
checkCmdFailure(t, stdout, stderr, cmd)
})
}
}
func checkCmdFailure(t testing.TB, stdout, stderr string, cmd *exec.Cmd) {
require.Falsef(t, t.Failed(), "%s %s trait assertion failed", cmd.Path, strings.Join(cmd.Args, " "))
if t.Failed() {
t.Log("STDOUT:\n", stdout)
t.Log("STDERR:\n", stderr)
t.Log("COMMAND:", strings.Join(cmd.Args, " "))
}
}

View file

@ -1,7 +1,10 @@
package ui
import (
"bufio"
"container/list"
"context"
"errors"
"fmt"
"io"
"strings"
@ -25,6 +28,7 @@ import (
const maxBarWidth = 50
const statusSet = components.SpinnerDotSet
const completedStatus = "✔"
const failedStatus = "✘"
const tileFormat = color.Bold
const interval = 150 * time.Millisecond
@ -226,48 +230,6 @@ func FetchImageHandler(ctx context.Context, fr *frame.Frame, event partybus.Even
return err
}
func UploadAttestationHandler(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error {
prog, err := syftEventParsers.ParseUploadAttestation(event)
if err != nil {
return fmt.Errorf("bad %s event: %w", event.Type, err)
}
line, err := fr.Append()
if err != nil {
return err
}
wg.Add(1)
formatter, spinner := startProcess()
stream := progress.Stream(ctx, prog, interval)
title := tileFormat.Sprint("Uploading attestation")
formatFn := func(p progress.Progress) {
progStr, err := formatter.Format(p)
spin := color.Magenta.Sprint(spinner.Next())
if err != nil {
_, _ = io.WriteString(line, fmt.Sprintf("Error: %+v", err))
} else {
auxInfo := auxInfoFormat.Sprintf("[%s]", prog.Stage())
_, _ = io.WriteString(line, fmt.Sprintf(statusTitleTemplate+"%s %s", spin, title, progStr, auxInfo))
}
}
go func() {
defer wg.Done()
formatFn(progress.Progress{})
for p := range stream {
formatFn(p)
}
spin := color.Green.Sprint(completedStatus)
title = tileFormat.Sprint("Uploaded attestation")
_, _ = io.WriteString(line, fmt.Sprintf(statusTitleTemplate, spin, title))
}()
return err
}
// ReadImageHandler periodically writes a the image read/parse/build-tree status in the form of a progress bar.
func ReadImageHandler(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error {
_, prog, err := stereoEventParsers.ParseReadImage(event)
@ -570,3 +532,110 @@ func ImportStartedHandler(ctx context.Context, fr *frame.Frame, event partybus.E
}()
return err
}
// AttestationStartedHandler takes bytes from a event.ShellOutput and publishes them to the frame.
//
//nolint:funlen,gocognit
func AttestationStartedHandler(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error {
reader, prog, taskInfo, err := syftEventParsers.ParseAttestationStartedEvent(event)
if err != nil {
return fmt.Errorf("bad %s event: %w", event.Type, err)
}
titleLine, err := fr.Append()
if err != nil {
return err
}
wg.Add(2)
_, spinner := startProcess()
title := tileFormat.Sprintf(taskInfo.Title.WhileRunning)
s := bufio.NewScanner(reader)
l := list.New()
formatFn := func() {
auxInfo := auxInfoFormat.Sprintf("[running %s]", taskInfo.Context)
spin := color.Magenta.Sprint(spinner.Next())
_, _ = io.WriteString(titleLine, fmt.Sprintf(statusTitleTemplate+"%s", spin, title, auxInfo))
}
formatFn()
var failed bool
formatComplete := func(aux string) {
spin := color.Green.Sprint(completedStatus)
if failed {
spin = color.Red.Sprint(failedStatus)
aux = prog.Error().Error()
} else {
title = tileFormat.Sprintf(taskInfo.Title.OnSuccess)
}
auxInfo := auxInfoFormat.Sprintf("[%s]", aux)
_, _ = io.WriteString(titleLine, fmt.Sprintf(statusTitleTemplate+"%s", spin, title, auxInfo))
}
endWg := &sync.WaitGroup{}
endWg.Add(1)
go func() {
defer wg.Done()
defer endWg.Done()
stream := progress.Stream(ctx, prog, interval)
for range stream {
formatFn()
}
err := prog.Error()
if err != nil && !errors.Is(err, io.EOF) {
failed = true
}
}()
go func() {
defer wg.Done()
var tlogEntry string
// only show the last 5 lines of the shell output
for s.Scan() {
line, _ := fr.Append()
if l.Len() > 5 {
elem := l.Front()
line := elem.Value.(*frame.Line)
err = line.Remove()
if err != nil {
return
}
l.Remove(elem)
}
l.PushBack(line)
text := s.Text()
if strings.Contains(text, "tlog entry created with index") {
tlogEntry = text
}
_, err = line.Write([]byte(fmt.Sprintf(" %s %s", auxInfoFormat.Sprintf("░░"), text)))
if err != nil {
return
}
}
endWg.Wait()
if !failed {
// roll up logs into completed status (only if successful)
for e := l.Back(); e != nil; e = e.Prev() {
line := e.Value.(*frame.Line)
err = line.Remove()
if err != nil {
return
}
}
}
formatComplete(tlogEntry)
}()
return nil
}

View file

@ -31,13 +31,13 @@ func (r *Handler) RespondsTo(event partybus.Event) bool {
case stereoscopeEvent.PullDockerImage,
stereoscopeEvent.ReadImage,
stereoscopeEvent.FetchImage,
syftEvent.UploadAttestation,
syftEvent.PackageCatalogerStarted,
syftEvent.SecretsCatalogerStarted,
syftEvent.FileDigestsCatalogerStarted,
syftEvent.FileMetadataCatalogerStarted,
syftEvent.FileIndexingStarted,
syftEvent.ImportStarted:
syftEvent.ImportStarted,
syftEvent.AttestationStarted:
return true
default:
return false
@ -56,9 +56,6 @@ func (r *Handler) Handle(ctx context.Context, fr *frame.Frame, event partybus.Ev
case stereoscopeEvent.FetchImage:
return FetchImageHandler(ctx, fr, event, wg)
case syftEvent.UploadAttestation:
return UploadAttestationHandler(ctx, fr, event, wg)
case syftEvent.PackageCatalogerStarted:
return PackageCatalogerStartedHandler(ctx, fr, event, wg)
@ -76,6 +73,9 @@ func (r *Handler) Handle(ctx context.Context, fr *frame.Frame, event partybus.Ev
case syftEvent.ImportStarted:
return ImportStartedHandler(ctx, fr, event, wg)
case syftEvent.AttestationStarted:
return AttestationStartedHandler(ctx, fr, event, wg)
}
return nil
}