mirror of
https://github.com/anchore/syft.git
synced 2026-10-30 22:12:28 +01:00
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:
co-authored by
Alex Goodman
parent
ac8f72fdd1
commit
44e8ae2577
+12
-32
@@ -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)
|
||||
}
|
||||
|
||||
+134
-305
@@ -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,
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
return b.w.Write(p)
|
||||
}
|
||||
|
||||
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
|
||||
func commandExists(cmd string) bool {
|
||||
_, err := exec.LookPath(cmd)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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])")
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user