mirror of
https://github.com/anchore/syft
synced 2024-11-10 06:14:16 +00:00
510 - SBOM attestation stdout (#785)
add syft attest command to produce an attestation as application/vnd.in-toto+json to standard out using on disk PKI Signed-off-by: Christopher Phillips <christopher.phillips@anchore.com>
This commit is contained in:
parent
738b3b60a5
commit
256e85bc12
20 changed files with 2698 additions and 145 deletions
13
.github/workflows/validations.yaml
vendored
13
.github/workflows/validations.yaml
vendored
|
@ -348,9 +348,16 @@ jobs:
|
|||
restore-keys: |
|
||||
${{ runner.os }}-go-${{ env.GO_VERSION }}-
|
||||
|
||||
- name: (cache-miss) Bootstrap go dependencies
|
||||
if: steps.go-cache.outputs.cache-hit != 'true'
|
||||
run: make bootstrap-go
|
||||
- name: Restore tool cache
|
||||
id: tool-cache
|
||||
uses: actions/cache@v2.1.3
|
||||
with:
|
||||
path: ${{ github.workspace }}/.tmp
|
||||
key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }}
|
||||
|
||||
- name: (cache-miss) Bootstrap all project dependencies
|
||||
if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true'
|
||||
run: make bootstrap
|
||||
|
||||
- name: Build key for tar cache
|
||||
run: make cli-fingerprint
|
||||
|
|
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -36,4 +36,8 @@ bin/
|
|||
# macOS Finder metadata
|
||||
.DS_STORE
|
||||
|
||||
*.profile
|
||||
*.profile
|
||||
|
||||
# attestation
|
||||
cosign.key
|
||||
cosign.pub
|
||||
|
|
3
Makefile
3
Makefile
|
@ -3,7 +3,7 @@ TEMPDIR = ./.tmp
|
|||
RESULTSDIR = test/results
|
||||
COVER_REPORT = $(RESULTSDIR)/unit-coverage-details.txt
|
||||
COVER_TOTAL = $(RESULTSDIR)/unit-coverage-summary.txt
|
||||
LINTCMD = $(TEMPDIR)/golangci-lint run --tests=false --timeout=2m --config .golangci.yaml
|
||||
LINTCMD = $(TEMPDIR)/golangci-lint run --tests=false --timeout=4m --config .golangci.yaml
|
||||
RELEASE_CMD=$(TEMPDIR)/goreleaser release --rm-dist
|
||||
SNAPSHOT_CMD=$(RELEASE_CMD) --skip-publish --snapshot
|
||||
VERSION=$(shell git describe --dirty --always --tags)
|
||||
|
@ -111,6 +111,7 @@ bootstrap-tools: $(TEMPDIR)
|
|||
curl -sSfL https://raw.githubusercontent.com/anchore/chronicle/main/install.sh | sh -s -- -b $(TEMPDIR)/ v0.3.0
|
||||
.github/scripts/goreleaser-install.sh -d -b $(TEMPDIR)/ v1.4.1
|
||||
GOBIN="$(shell realpath $(TEMPDIR))" go install github.com/neilpa/yajsv@v1.4.0
|
||||
GOBIN="$(shell realpath $(TEMPDIR))" go install github.com/sigstore/cosign/cmd/cosign@v1.5.1
|
||||
|
||||
.PHONY: bootstrap-go
|
||||
bootstrap-go:
|
||||
|
|
43
README.md
43
README.md
|
@ -21,6 +21,7 @@ A CLI tool and Go library for generating a Software Bill of Materials (SBOM) fro
|
|||
|
||||
## Features
|
||||
- Catalog container images and filesystems to discover packages and libraries.
|
||||
- Generate in-toto attestations where an SBOM is included as the payload.
|
||||
- Supports packages and libraries from various ecosystems (APK, DEB, RPM, Ruby Bundles, Python Wheel/Egg/requirements.txt, JavaScript NPM/Yarn, Java JAR/EAR/WAR/PAR/SAR, Jenkins plugins JPI/HPI, Go modules, PHP Composer)
|
||||
- Linux distribution identification (supports Alpine, BusyBox, CentOS/RedHat, Debian/Ubuntu flavored distributions)
|
||||
- Supports Docker and OCI image formats
|
||||
|
@ -68,7 +69,8 @@ nix-shell -p syft
|
|||
|
||||
## Getting started
|
||||
|
||||
To generate an SBOM for a Docker or OCI image:
|
||||
#### SBOM
|
||||
To generate an SBOM for an OCI image:
|
||||
```
|
||||
syft <image>
|
||||
```
|
||||
|
@ -85,6 +87,17 @@ To include software from all image layers in the SBOM, regardless of its presenc
|
|||
syft packages <image> --scope all-layers
|
||||
```
|
||||
|
||||
#### SBOM Attestation
|
||||
To generate an attested SBOM for an OCI image as the predicate of an in-toto attestation
|
||||
```
|
||||
syft attest --output [FORMAT] --key [KEY] [SOURCE] [flags]
|
||||
```
|
||||
|
||||
The above output is in the form of the [DSSE envelope](https://github.com/secure-systems-lab/dsse/blob/master/envelope.md#dsse-envelope).
|
||||
The payload is a base64 encoded `in-toto` statement with the SBOM as the predicate, the payload type is `application/vnd.in-toto+json`, and the signatures array is populated
|
||||
with the contents needed for public key verification. For details on workflows using this command see [here](#adding-an-sbom-to-an-image-as-an-attestation-using-syft).
|
||||
|
||||
|
||||
### Supported sources
|
||||
|
||||
Syft can generate a SBOM from a variety of sources:
|
||||
|
@ -396,6 +409,16 @@ registry:
|
|||
token: ""
|
||||
# - ... # note, more credentials can be provided via config file only
|
||||
|
||||
# generate an attested SBOM
|
||||
attest:
|
||||
# path to the private key file to use for attestation
|
||||
# SYFT_ATTEST_KEY env var
|
||||
key: "cosign.key"
|
||||
|
||||
# password to decrypt to given private key
|
||||
# SYFT_ATTEST_PASSWORD env var, additionally responds to COSIGN_PASSWORD
|
||||
password: ""
|
||||
|
||||
log:
|
||||
# use structured logging
|
||||
# same as SYFT_LOG_STRUCTURED env var
|
||||
|
@ -432,30 +455,32 @@ anchore:
|
|||
dockerfile: ""
|
||||
```
|
||||
|
||||
### Adding an SBOM to an image as an attestation
|
||||
### Adding an SBOM to an image as an attestation using Syft
|
||||
|
||||
`syft attest --output [FORMAT] --key [KEY] [SOURCE] [flags]`
|
||||
|
||||
SBOMs themselves can serve as input to different analysis tools. The Anchore organization offers the vulnerability scanner
|
||||
[grype](https://github.com/anchore/grype) as one such tool.
|
||||
One of the foundational approaches to "trust" between tools is for producers to use the artifacts generated by syft as attestations to their images.
|
||||
The SBOM output of syft can be used with the [cosign](https://github.com/sigstore/cosign) tool to generate an attestation that is attached to a signed image.
|
||||
The DSSE output of `syft attest` can be used with the [cosign](https://github.com/sigstore/cosign) tool to attach an attestation to an image.
|
||||
|
||||
#### Example attest
|
||||
Note for the following example replace `test/image:latest` with an image you own. You should also have push access to
|
||||
Note for the following example replace `docker.io/image:latest` with an image you own. You should also have push access to
|
||||
its remote reference. Replace $MY_PRIVATE_KEY with a private key you own or have generated with cosign.
|
||||
|
||||
```bash
|
||||
cosign sign --key $MY_PRIVATE_KEY test/image:latest
|
||||
syft test/image:latest -o json > test_latest_sbom.json
|
||||
cosign attest --predicate test_latest_sbom.json --key $MY_PRIVATE_KEY test/image:latest
|
||||
syft attest --key $MY_PRIVATE_KEY docker.io/image:latest > image_latest_sbom_attestation.json
|
||||
cosign attach attestation --attestation image_latest_sbom_attestation.json docker.io/image:latest
|
||||
```
|
||||
|
||||
Verify the new attestation exists on your image
|
||||
```bash
|
||||
cosign verify-attestation -key $MY_PUBLIC_KEY test/image:latest | jq '.payload |= @base64d | .payload | fromjson | .predicate.Data | fromjson | .'
|
||||
cosign verify-attestation -key $MY_PUBLIC_KEY docker.io/image:latest | jq '.payload | @base64d | .payload | fromjson | .predicate'
|
||||
```
|
||||
|
||||
You should see this output along with the attached SBOM.
|
||||
```
|
||||
Verification for test/image:latest --
|
||||
Verification for docker.io/image:latest --
|
||||
The following checks were performed on each of these signatures:
|
||||
- The cosign claims were validated
|
||||
- The signatures were verified against the specified public key
|
||||
|
|
280
cmd/attest.go
Normal file
280
cmd/attest.go
Normal file
|
@ -0,0 +1,280 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/stereoscope"
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"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/format"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/in-toto/in-toto-golang/in_toto"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/pkg/profile"
|
||||
"github.com/sigstore/cosign/cmd/cosign/cli/sign"
|
||||
"github.com/sigstore/cosign/pkg/cosign"
|
||||
"github.com/sigstore/cosign/pkg/cosign/attestation"
|
||||
"github.com/sigstore/sigstore/pkg/signature/dsse"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
|
||||
signatureoptions "github.com/sigstore/sigstore/pkg/signature/options"
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
`
|
||||
attestSchemeHelp = "\n" + indent + schemeHelpHeader + "\n" + imageSchemeHelp
|
||||
|
||||
attestHelp = attestExample + attestSchemeHelp
|
||||
|
||||
intotoJSONDsseType = `application/vnd.in-toto+json`
|
||||
)
|
||||
|
||||
var attestFormats = []format.Option{format.SPDXJSONOption, format.CycloneDxJSONOption, format.JSONOption}
|
||||
|
||||
var (
|
||||
attestCmd = &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",
|
||||
Example: internal.Tprintf(attestHelp, map[string]interface{}{
|
||||
"appName": internal.ApplicationName,
|
||||
"command": "attest",
|
||||
}),
|
||||
Args: validateInputArgs,
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
PreRunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
if appConfig.Dev.ProfileCPU && appConfig.Dev.ProfileMem {
|
||||
return fmt.Errorf("cannot profile CPU and memory simultaneously")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if appConfig.Dev.ProfileCPU {
|
||||
defer profile.Start(profile.CPUProfile).Stop()
|
||||
} else if appConfig.Dev.ProfileMem {
|
||||
defer profile.Start(profile.MemProfile).Stop()
|
||||
}
|
||||
|
||||
return attestExec(cmd.Context(), cmd, args)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func fetchPassword(_ bool) (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 appConfig.Attest.Password != "":
|
||||
return []byte(appConfig.Attest.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")
|
||||
}
|
||||
|
||||
func selectPassFunc(keypath 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 = fetchPassword
|
||||
}
|
||||
|
||||
return fn, nil
|
||||
}
|
||||
|
||||
func attestExec(ctx context.Context, _ *cobra.Command, args []string) error {
|
||||
// can only be an image for attestation or OCI DIR
|
||||
userInput := args[0]
|
||||
fs := afero.NewOsFs()
|
||||
parsedScheme, _, _, err := source.DetectScheme(fs, image.DetectSource, userInput)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if parsedScheme != source.ImageScheme {
|
||||
return fmt.Errorf("attest command can only be used with image sources but discovered %q when given %q", parsedScheme, userInput)
|
||||
}
|
||||
|
||||
if len(appConfig.Output) > 1 {
|
||||
return fmt.Errorf("unable to generate attestation for more than one output")
|
||||
}
|
||||
|
||||
output := format.ParseOption(appConfig.Output[0])
|
||||
predicateType := assertPredicateType(output)
|
||||
if predicateType == "" {
|
||||
return fmt.Errorf("could not produce attestation predicate for given format: %q. Available formats: %+v", output, attestFormats)
|
||||
}
|
||||
|
||||
passFunc, err := selectPassFunc(appConfig.Attest.Key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ko := sign.KeyOpts{
|
||||
KeyRef: appConfig.Attest.Key,
|
||||
PassFunc: passFunc,
|
||||
}
|
||||
|
||||
sv, err := sign.SignerFromKeyOpts(ctx, "", ko)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer sv.Close()
|
||||
|
||||
return eventLoop(
|
||||
attestationExecWorker(userInput, output, predicateType, sv),
|
||||
setupSignals(),
|
||||
eventSubscription,
|
||||
stereoscope.Cleanup,
|
||||
ui.Select(isVerbose(), appConfig.Quiet)...,
|
||||
)
|
||||
}
|
||||
|
||||
func attestationExecWorker(userInput string, output format.Option, predicateType string, sv *sign.SignerVerifier) <-chan error {
|
||||
errs := make(chan error)
|
||||
go func() {
|
||||
defer close(errs)
|
||||
|
||||
s, src, err := generateSBOM(userInput, errs)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
|
||||
sbomBytes, err := syft.Encode(*s, output)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
|
||||
err = generateAttestation(sbomBytes, src, sv, predicateType)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
}()
|
||||
return errs
|
||||
}
|
||||
|
||||
func assertPredicateType(output format.Option) string {
|
||||
switch output {
|
||||
case format.SPDXJSONOption:
|
||||
return in_toto.PredicateSPDX
|
||||
// Tentative see https://github.com/in-toto/attestation/issues/82
|
||||
case format.CycloneDxJSONOption:
|
||||
return "https://cyclonedx.org/bom"
|
||||
case format.JSONOption:
|
||||
return "https://syft.dev/bom"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func generateAttestation(predicate []byte, src *source.Source, sv *sign.SignerVerifier, predicateType string) error {
|
||||
h, err := v1.NewHash(src.Image.Metadata.ManifestDigest)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not hash manifest digest for image")
|
||||
}
|
||||
|
||||
wrapped := dsse.WrapSigner(sv, intotoJSONDsseType)
|
||||
|
||||
sh, err := attestation.GenerateStatement(attestation.GenerateOpts{
|
||||
Predicate: bytes.NewBuffer(predicate),
|
||||
Type: predicateType,
|
||||
Digest: h.Hex,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(sh)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
signedPayload, err := wrapped.SignMessage(bytes.NewReader(payload), signatureoptions.WithContext(context.Background()))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to sign SBOM")
|
||||
}
|
||||
|
||||
bus.Publish(partybus.Event{
|
||||
Type: event.Exit,
|
||||
Value: func() error {
|
||||
_, err := os.Stdout.Write(signedPayload)
|
||||
return err
|
||||
},
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
setAttestFlags(attestCmd.Flags())
|
||||
if err := bindAttestConfigOptions(attestCmd.Flags()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
rootCmd.AddCommand(attestCmd)
|
||||
}
|
||||
|
||||
func setAttestFlags(flags *pflag.FlagSet) {
|
||||
// key options
|
||||
flags.StringP("key", "", "cosign.key",
|
||||
"path to the private key file to use for attestation",
|
||||
)
|
||||
|
||||
// in-toto attestations only support JSON predicates, so not all SBOM formats that syft can output are supported
|
||||
flags.StringP(
|
||||
"output", "o", string(format.JSONOption),
|
||||
fmt.Sprintf("the SBOM format encapsulated within the attestation, available options=%v", attestFormats),
|
||||
)
|
||||
}
|
||||
|
||||
func bindAttestConfigOptions(flags *pflag.FlagSet) error {
|
||||
// note: output is not included since this configuration option is shared between multiple subcommands
|
||||
|
||||
if err := viper.BindPFlag("attest.key", flags.Lookup("key")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
21
cmd/cmd.go
21
cmd/cmd.go
|
@ -6,6 +6,8 @@ import (
|
|||
"os"
|
||||
"sort"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/anchore/stereoscope"
|
||||
"github.com/anchore/syft/internal/config"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
|
@ -57,6 +59,7 @@ func initCmdAliasBindings() {
|
|||
config.PowerUserCatalogerEnabledDefault()
|
||||
}
|
||||
|
||||
// set bindings based on the packages alias
|
||||
switch activeCmd {
|
||||
case packagesCmd, rootCmd:
|
||||
// note: we need to lazily bind config options since they are shared between both the root command
|
||||
|
@ -68,6 +71,16 @@ func initCmdAliasBindings() {
|
|||
if err = bindPackagesConfigOptions(activeCmd.Flags()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
case attestCmd:
|
||||
// the --output option is an independently defined flag, but a shared config option
|
||||
if err = bindSharedOutputConfigOption(attestCmd.Flags()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// even though the root command or packages command is NOT being run, we still need default bindings
|
||||
// such that application config parsing passes.
|
||||
if err = bindExclusivePackagesConfigOptions(packagesCmd.Flags()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
default:
|
||||
// even though the root command or packages command is NOT being run, we still need default bindings
|
||||
// such that application config parsing passes.
|
||||
|
@ -77,6 +90,14 @@ func initCmdAliasBindings() {
|
|||
}
|
||||
}
|
||||
|
||||
func bindSharedOutputConfigOption(flags *pflag.FlagSet) error {
|
||||
if err := viper.BindPFlag("output", flags.Lookup("output")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initAppConfig() {
|
||||
cfg, err := config.LoadApplicationConfig(viper.GetViper(), persistentOpts)
|
||||
if err != nil {
|
||||
|
|
120
cmd/packages.go
120
cmd/packages.go
|
@ -28,25 +28,31 @@ import (
|
|||
|
||||
const (
|
||||
packagesExample = ` {{.appName}} {{.command}} alpine:latest a summary of discovered packages
|
||||
{{.appName}} {{.command}} alpine:latest -o json show all possible cataloging details
|
||||
{{.appName}} {{.command}} alpine:latest -o cyclonedx show a CycloneDX formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -o spdx show a SPDX 2.2 tag-value formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -o spdx-json show a SPDX 2.2 JSON formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -vv show verbose debug information
|
||||
{{.appName}} {{.command}} alpine:latest -o json show all possible cataloging details
|
||||
{{.appName}} {{.command}} alpine:latest -o cyclonedx show a CycloneDX formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -o cyclonedx-json show a CycloneDX JSON formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -o spdx show a SPDX 2.2 Tag-Value formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -o spdx-json show a SPDX 2.2 JSON formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -vv show verbose debug information
|
||||
|
||||
Supports the following image sources:
|
||||
{{.appName}} {{.command}} yourrepo/yourimage:tag defaults to using images from a Docker daemon. If Docker is not present, the image is pulled directly from the registry.
|
||||
{{.appName}} {{.command}} path/to/a/file/or/dir a Docker tar, OCI tar, OCI directory, or generic filesystem directory
|
||||
`
|
||||
|
||||
You can also explicitly specify the scheme to use:
|
||||
{{.appName}} {{.command}} docker:yourrepo/yourimage:tag explicitly use the Docker daemon
|
||||
schemeHelpHeader = "You can also explicitly specify the scheme to use:"
|
||||
imageSchemeHelp = ` {{.appName}} {{.command}} docker:yourrepo/yourimage:tag explicitly use the Docker daemon
|
||||
{{.appName}} {{.command}} registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required)
|
||||
{{.appName}} {{.command}} docker-archive:path/to/yourimage.tar use a tarball from disk for archives created from "docker save"
|
||||
{{.appName}} {{.command}} oci-archive:path/to/yourimage.tar use a tarball from disk for OCI archives (from Skopeo or otherwise)
|
||||
{{.appName}} {{.command}} oci-dir:path/to/yourimage read directly from a path on disk for OCI layout directories (from Skopeo or otherwise)
|
||||
{{.appName}} {{.command}} dir:path/to/yourproject read directly from a path on disk (any directory)
|
||||
{{.appName}} {{.command}} file:path/to/yourproject/file read directly from a path on disk (any single file)
|
||||
{{.appName}} {{.command}} registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required)
|
||||
`
|
||||
nonImageSchemeHelp = ` {{.appName}} {{.command}} dir:path/to/yourproject read directly from a path on disk (any directory)
|
||||
{{.appName}} {{.command}} file:path/to/yourproject/file read directly from a path on disk (any single file)
|
||||
`
|
||||
packagesSchemeHelp = "\n" + indent + schemeHelpHeader + "\n" + imageSchemeHelp + nonImageSchemeHelp
|
||||
|
||||
packagesHelp = packagesExample + packagesSchemeHelp
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -54,7 +60,7 @@ var (
|
|||
Use: "packages [SOURCE]",
|
||||
Short: "Generate a package SBOM",
|
||||
Long: "Generate a packaged-based Software Bill Of Materials (SBOM) from container images and filesystems",
|
||||
Example: internal.Tprintf(packagesExample, map[string]interface{}{
|
||||
Example: internal.Tprintf(packagesHelp, map[string]interface{}{
|
||||
"appName": internal.ApplicationName,
|
||||
"command": "packages",
|
||||
}),
|
||||
|
@ -88,7 +94,6 @@ func init() {
|
|||
|
||||
func setPackageFlags(flags *pflag.FlagSet) {
|
||||
// Formatting & Input options //////////////////////////////////////////////
|
||||
|
||||
flags.StringP(
|
||||
"scope", "s", cataloger.DefaultSearchConfig().Scope.String(),
|
||||
fmt.Sprintf("selection of layers to catalog, options=%v", source.AllScopes))
|
||||
|
@ -140,15 +145,23 @@ func setPackageFlags(flags *pflag.FlagSet) {
|
|||
)
|
||||
}
|
||||
|
||||
// NOTE(alex): Write a helper for the binding operation, which can be used to perform the binding but also double check that the intended effect was had or else return an error. Another thought is to somehow provide zero-valued defaults for all values in our config struct (maybe with reflection?). There may be a mechanism that already exists in viper that protects against this that I'm not aware of. ref: https://github.com/anchore/syft/pull/805#discussion_r801931192
|
||||
func bindPackagesConfigOptions(flags *pflag.FlagSet) error {
|
||||
// Formatting & Input options //////////////////////////////////////////////
|
||||
|
||||
if err := viper.BindPFlag("package.cataloger.scope", flags.Lookup("scope")); err != nil {
|
||||
if err := bindExclusivePackagesConfigOptions(flags); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := bindSharedOutputConfigOption(flags); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := viper.BindPFlag("output", flags.Lookup("output")); err != nil {
|
||||
// NOTE(alex): Write a helper for the binding operation, which can be used to perform the binding but also double check that the intended effect was had or else return an error. Another thought is to somehow provide zero-valued defaults for all values in our config struct (maybe with reflection?). There may be a mechanism that already exists in viper that protects against this that I'm not aware of. ref: https://github.com/anchore/syft/pull/805#discussion_r801931192
|
||||
func bindExclusivePackagesConfigOptions(flags *pflag.FlagSet) error {
|
||||
// Formatting & Input options //////////////////////////////////////////////
|
||||
|
||||
// note: output is not included since this configuration option is shared between multiple subcommands
|
||||
|
||||
if err := viper.BindPFlag("package.cataloger.scope", flags.Lookup("scope")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -236,46 +249,61 @@ func isVerbose() (result bool) {
|
|||
return appConfig.CliOptions.Verbosity > 0 || isPipedInput
|
||||
}
|
||||
|
||||
func generateSBOM(userInput string, errs chan error) (*sbom.SBOM, *source.Source, error) {
|
||||
tasks, err := tasks()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
src, cleanup, err := source.New(userInput, appConfig.Registry.ToOptions(), appConfig.Exclusions)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to construct source from user input %q: %w", userInput, err)
|
||||
}
|
||||
if cleanup != nil {
|
||||
defer cleanup()
|
||||
}
|
||||
|
||||
s := sbom.SBOM{
|
||||
Source: src.Metadata,
|
||||
Descriptor: sbom.Descriptor{
|
||||
Name: internal.ApplicationName,
|
||||
Version: version.FromBuild().Version,
|
||||
Configuration: appConfig,
|
||||
},
|
||||
}
|
||||
|
||||
buildRelationships(&s, src, tasks, errs)
|
||||
|
||||
return &s, src, nil
|
||||
}
|
||||
|
||||
func buildRelationships(s *sbom.SBOM, src *source.Source, tasks []task, errs chan error) {
|
||||
var relationships []<-chan artifact.Relationship
|
||||
for _, task := range tasks {
|
||||
c := make(chan artifact.Relationship)
|
||||
relationships = append(relationships, c)
|
||||
go runTask(task, &s.Artifacts, src, c, errs)
|
||||
}
|
||||
|
||||
s.Relationships = append(s.Relationships, mergeRelationships(relationships...)...)
|
||||
}
|
||||
|
||||
func packagesExecWorker(userInput string, writer sbom.Writer) <-chan error {
|
||||
errs := make(chan error)
|
||||
go func() {
|
||||
defer close(errs)
|
||||
|
||||
tasks, err := tasks()
|
||||
s, src, err := generateSBOM(userInput, errs)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
|
||||
src, cleanup, err := source.New(userInput, appConfig.Registry.ToOptions(), appConfig.Exclusions)
|
||||
if err != nil {
|
||||
errs <- fmt.Errorf("failed to construct source from user input %q: %w", userInput, err)
|
||||
return
|
||||
if s == nil {
|
||||
panic("nil sbomb returned with no error")
|
||||
}
|
||||
if cleanup != nil {
|
||||
defer cleanup()
|
||||
}
|
||||
|
||||
s := sbom.SBOM{
|
||||
Source: src.Metadata,
|
||||
Descriptor: sbom.Descriptor{
|
||||
Name: internal.ApplicationName,
|
||||
Version: version.FromBuild().Version,
|
||||
Configuration: appConfig,
|
||||
},
|
||||
}
|
||||
|
||||
var relationships []<-chan artifact.Relationship
|
||||
for _, task := range tasks {
|
||||
c := make(chan artifact.Relationship)
|
||||
relationships = append(relationships, c)
|
||||
|
||||
go runTask(task, &s.Artifacts, src, c, errs)
|
||||
}
|
||||
s.Relationships = append(s.Relationships, mergeRelationships(relationships...)...)
|
||||
|
||||
if appConfig.Anchore.Host != "" {
|
||||
if err := runPackageSbomUpload(src, s); err != nil {
|
||||
if err := runPackageSbomUpload(src, *s); err != nil {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
|
@ -283,7 +311,7 @@ func packagesExecWorker(userInput string, writer sbom.Writer) <-chan error {
|
|||
|
||||
bus.Publish(partybus.Event{
|
||||
Type: event.Exit,
|
||||
Value: func() error { return writer.Write(s) },
|
||||
Value: func() error { return writer.Write(*s) },
|
||||
})
|
||||
}()
|
||||
return errs
|
||||
|
|
|
@ -27,6 +27,8 @@ var rootCmd = &cobra.Command{
|
|||
Version: version.FromBuild().Version,
|
||||
}
|
||||
|
||||
const indent = " "
|
||||
|
||||
func init() {
|
||||
// set universal flags
|
||||
rootCmd.PersistentFlags().StringVarP(&persistentOpts.ConfigPath, "config", "c", "", "application config file")
|
||||
|
|
281
go.mod
281
go.mod
|
@ -20,27 +20,29 @@ require (
|
|||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/facebookincubator/nvdtools v0.1.4
|
||||
github.com/go-test/deep v1.0.8
|
||||
github.com/google/go-cmp v0.5.6
|
||||
github.com/google/uuid v1.2.0
|
||||
github.com/gookit/color v1.2.7
|
||||
github.com/hashicorp/go-multierror v1.1.0
|
||||
github.com/google/go-cmp v0.5.7
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/gookit/color v1.4.2
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/in-toto/in-toto-golang v0.3.4-0.20211211042327-af1f9fb822bf
|
||||
github.com/jinzhu/copier v0.3.2
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/mholt/archiver/v3 v3.5.1
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2
|
||||
github.com/mitchellh/mapstructure v1.4.1
|
||||
github.com/olekukonko/tablewriter v0.0.4
|
||||
github.com/pelletier/go-toml v1.9.3
|
||||
github.com/mitchellh/mapstructure v1.4.3
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
github.com/pelletier/go-toml v1.9.4
|
||||
github.com/pkg/profile v1.5.0
|
||||
github.com/scylladb/go-set v1.0.2
|
||||
github.com/sergi/go-diff v1.1.0
|
||||
github.com/sergi/go-diff v1.2.0
|
||||
github.com/sigstore/sigstore v1.1.1-0.20220217212907-e48ca03a5ba7
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
github.com/spdx/tools-golang v0.2.0
|
||||
github.com/spf13/afero v1.6.0
|
||||
github.com/spf13/cobra v1.2.1
|
||||
github.com/spf13/afero v1.8.0
|
||||
github.com/spf13/cobra v1.3.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/spf13/viper v1.8.1
|
||||
github.com/spf13/viper v1.10.1
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/vifraa/gopom v0.1.0
|
||||
github.com/wagoodman/go-partybus v0.0.0-20210627031916-db1f5573bbc5
|
||||
|
@ -48,19 +50,228 @@ require (
|
|||
github.com/wagoodman/jotframe v0.0.0-20211129225309-56b0d0a4aebb
|
||||
github.com/x-cray/logrus-prefixed-formatter v0.5.2
|
||||
github.com/xeipuuv/gojsonschema v1.2.0
|
||||
golang.org/x/mod v0.4.2
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2
|
||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b
|
||||
golang.org/x/mod v0.5.1
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
require github.com/sigstore/cosign v1.5.2-0.20220222220736-ec79daf63c24
|
||||
|
||||
require (
|
||||
bitbucket.org/creachadair/shell v0.0.6 // indirect
|
||||
cloud.google.com/go v0.100.2 // indirect
|
||||
cloud.google.com/go/compute v1.3.0 // indirect
|
||||
cloud.google.com/go/iam v0.1.1 // indirect
|
||||
cloud.google.com/go/kms v1.3.0 // indirect
|
||||
cloud.google.com/go/storage v1.21.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go v61.5.0+incompatible // indirect
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
|
||||
github.com/Azure/go-autorest/autorest v0.11.24 // indirect
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.18 // indirect
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 // indirect
|
||||
github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 // indirect
|
||||
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
|
||||
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
|
||||
github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect
|
||||
github.com/Azure/go-autorest/logger v0.2.1 // indirect
|
||||
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
|
||||
github.com/PaesslerAG/gval v1.0.0 // indirect
|
||||
github.com/PaesslerAG/jsonpath v0.1.1 // indirect
|
||||
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||
github.com/ReneKroon/ttlcache/v2 v2.11.0 // indirect
|
||||
github.com/ThalesIgnite/crypto11 v1.2.5 // indirect
|
||||
github.com/armon/go-metrics v0.3.10 // indirect
|
||||
github.com/armon/go-radix v1.0.0 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
|
||||
github.com/aws/aws-sdk-go v1.43.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.13.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.13.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.8.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.10.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.2.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ecr v1.14.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.11.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.7.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.9.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.14.0 // indirect
|
||||
github.com/aws/smithy-go v1.10.0 // indirect
|
||||
github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20220216180153-3d7835abdf40 // 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/v3 v3.2.2 // 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/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/coreos/go-oidc/v3 v3.1.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.1 // indirect
|
||||
github.com/cyberphone/json-canonicalization v0.0.0-20210823021906-dc406ceaf94b // indirect
|
||||
github.com/dimchansky/utfbom v1.1.1 // indirect
|
||||
github.com/envoyproxy/go-control-plane v0.10.1 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v0.6.2 // indirect
|
||||
github.com/fatih/color v1.13.0 // indirect
|
||||
github.com/form3tech-oss/jwt-go v3.2.5+incompatible // indirect
|
||||
github.com/fullstorydev/grpcurl v1.8.2 // indirect
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
github.com/go-chi/chi v4.1.2+incompatible // indirect
|
||||
github.com/go-logr/logr v1.2.2 // indirect
|
||||
github.com/go-openapi/analysis v0.20.1 // indirect
|
||||
github.com/go-openapi/errors v0.20.1 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.19.6 // indirect
|
||||
github.com/go-openapi/loads v0.21.0 // indirect
|
||||
github.com/go-openapi/runtime v0.23.0 // indirect
|
||||
github.com/go-openapi/spec v0.20.4 // indirect
|
||||
github.com/go-openapi/strfmt v0.21.2 // indirect
|
||||
github.com/go-openapi/swag v0.21.1 // indirect
|
||||
github.com/go-openapi/validate v0.20.3 // indirect
|
||||
github.com/go-piv/piv-go v1.9.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.10.0 // indirect
|
||||
github.com/go-stack/stack v1.8.0 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.2.0 // 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/google/btree v1.0.1 // indirect
|
||||
github.com/google/certificate-transparency-go v1.1.2 // indirect
|
||||
github.com/google/go-github/v42 v42.0.0 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/trillian v1.4.0 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.1.1 // 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/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-hclog v1.1.0 // indirect
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||
github.com/hashicorp/go-plugin v1.4.3 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.0 // indirect
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
|
||||
github.com/hashicorp/go-secure-stdlib/mlock v0.1.2 // indirect
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.2 // indirect
|
||||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
|
||||
github.com/hashicorp/go-sockaddr v1.0.2 // indirect
|
||||
github.com/hashicorp/go-uuid v1.0.2 // indirect
|
||||
github.com/hashicorp/go-version v1.4.0 // indirect
|
||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||
github.com/hashicorp/vault/api v1.3.1 // indirect
|
||||
github.com/hashicorp/vault/sdk v0.3.0 // indirect
|
||||
github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 // indirect
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
github.com/jedisct1/go-minisign v0.0.0-20210703085342-c1f07ee84431 // indirect
|
||||
github.com/jhump/protoreflect v1.9.0 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/jonboulle/clockwork v0.2.2 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
|
||||
github.com/miekg/pkcs11 v1.1.1 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-testing-interface v1.14.1 // 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/oklog/run v1.1.0 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||
github.com/pierrec/lz4 v2.6.1+incompatible // indirect
|
||||
github.com/prometheus/client_golang v1.11.0 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.32.1 // indirect
|
||||
github.com/prometheus/procfs v0.7.3 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/ryanuber/go-glob v1.0.0 // indirect
|
||||
github.com/sassoftware/relic v0.0.0-20210427151427-dfb082b79b74 // indirect
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.3.0 // indirect
|
||||
github.com/segmentio/ksuid v1.0.4 // indirect
|
||||
github.com/shibumi/go-pathspec v1.3.0 // indirect
|
||||
github.com/sigstore/fulcio v0.1.2-0.20220114150912-86a2036f9bc7 // indirect
|
||||
github.com/sigstore/rekor v0.4.1-0.20220114213500-23f583409af3 // indirect
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect
|
||||
github.com/soheilhy/cmux v0.1.5 // indirect
|
||||
github.com/spiffe/go-spiffe/v2 v2.0.0-beta.11 // indirect
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // 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/theupdateframework/go-tuf v0.0.0-20220211205608-f0c3294f63b9 // indirect
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect
|
||||
github.com/urfave/cli v1.22.5 // indirect
|
||||
github.com/xanzy/go-gitlab v0.55.1 // 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.5.1 // indirect
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.1 // indirect
|
||||
go.etcd.io/etcd/client/v2 v2.305.1 // indirect
|
||||
go.etcd.io/etcd/client/v3 v3.5.0 // indirect
|
||||
go.etcd.io/etcd/etcdctl/v3 v3.5.0 // indirect
|
||||
go.etcd.io/etcd/etcdutl/v3 v3.5.0 // indirect
|
||||
go.etcd.io/etcd/pkg/v3 v3.5.0 // indirect
|
||||
go.etcd.io/etcd/raft/v3 v3.5.0 // indirect
|
||||
go.etcd.io/etcd/server/v3 v3.5.0 // indirect
|
||||
go.etcd.io/etcd/tests/v3 v3.5.0 // indirect
|
||||
go.etcd.io/etcd/v3 v3.5.0 // indirect
|
||||
go.mongodb.org/mongo-driver v1.7.5 // indirect
|
||||
go.opencensus.io v0.23.0 // indirect
|
||||
go.opentelemetry.io/contrib v1.3.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0 // indirect
|
||||
go.opentelemetry.io/otel v0.20.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp v0.20.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v0.20.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v0.20.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/export/metric v0.20.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v0.20.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v0.20.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v0.12.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.7.0 // indirect
|
||||
go.uber.org/zap v1.20.0 // indirect
|
||||
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect
|
||||
golang.org/x/tools v0.1.8 // indirect
|
||||
google.golang.org/api v0.70.0 // indirect
|
||||
gopkg.in/cheggaaa/pb.v1 v1.0.28 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // 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.3 // indirect
|
||||
k8s.io/apimachinery v0.23.3 // indirect
|
||||
k8s.io/client-go v0.23.3 // indirect
|
||||
k8s.io/klog/v2 v2.40.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20220124234850-424119656bbf // indirect
|
||||
k8s.io/utils v0.0.0-20220127004650-9b3446523e65 // indirect
|
||||
knative.dev/pkg v0.0.0-20220202132633-df430fa0dd96 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect
|
||||
sigs.k8s.io/release-utils v0.4.1-0.20220207182343-6dadf2228617 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect
|
||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.5.1 // indirect
|
||||
// we are hinting brotli to latest due to warning when installing archiver v3:
|
||||
// go: warning: github.com/andybalholm/brotli@v1.0.1: retracted by module author: occasional panics and data corruption
|
||||
github.com/andybalholm/brotli v1.0.4 // indirect
|
||||
github.com/containerd/containerd v1.5.9 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.10.0 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.10.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/docker/cli v20.10.12+incompatible // indirect
|
||||
github.com/docker/distribution v2.7.1+incompatible // indirect
|
||||
|
@ -68,49 +279,49 @@ require (
|
|||
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/fsnotify/fsnotify v1.4.9 // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.0 // indirect
|
||||
github.com/go-restruct/restruct v1.2.0-alpha // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/golang/snappy v0.0.3 // indirect
|
||||
github.com/google/go-containerregistry v0.7.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.0.0 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/go-containerregistry v0.8.1-0.20220209165246-a44adc326839
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/klauspost/compress v1.13.6 // indirect
|
||||
github.com/klauspost/compress v1.14.2 // indirect
|
||||
github.com/klauspost/pgzip v1.2.5 // indirect
|
||||
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 // indirect
|
||||
github.com/magiconair/properties v1.8.5 // indirect
|
||||
github.com/mattn/go-colorable v0.0.9 // indirect
|
||||
github.com/mattn/go-isatty v0.0.6 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/nwaples/rardecode v1.1.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.2 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.2 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/spf13/cast v1.3.1 // indirect
|
||||
github.com/spf13/cast v1.4.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/stretchr/objx v0.2.0 // indirect
|
||||
github.com/stretchr/objx v0.3.0 // indirect
|
||||
github.com/subosito/gotenv v1.2.0 // indirect
|
||||
github.com/ulikunitz/xz v0.5.9 // indirect
|
||||
github.com/ulikunitz/xz v0.5.10 // indirect
|
||||
github.com/vbatts/tar-split v0.11.2 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // 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
|
||||
golang.org/x/crypto v0.0.0-20220210151621-f4118a5b28e2 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220213190939-1e6e3497d506 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
|
||||
golang.org/x/sys v0.0.0-20211110154304-99a53858aa08 // indirect
|
||||
golang.org/x/text v0.3.6 // indirect
|
||||
golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20211111162719-482062a4217b // indirect
|
||||
google.golang.org/grpc v1.42.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c // indirect
|
||||
google.golang.org/grpc v1.44.0 // indirect
|
||||
google.golang.org/protobuf v1.27.1 // indirect
|
||||
gopkg.in/ini.v1 v1.62.0 // indirect
|
||||
gopkg.in/ini.v1 v1.66.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
)
|
||||
|
|
|
@ -45,6 +45,7 @@ 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"`
|
||||
}
|
||||
|
||||
// PowerUserCatalogerEnabledDefault switches all catalogers to be enabled when running power-user command
|
||||
|
|
29
internal/config/attest.go
Normal file
29
internal/config/attest.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type attest struct {
|
||||
Key string `yaml:"key" json:"key" mapstructure:"key"`
|
||||
// IMPORTANT: do not show the password in any YAML/JSON output (sensitive information)
|
||||
Password string `yaml:"-" json:"-" mapstructure:"password"`
|
||||
}
|
||||
|
||||
//nolint:unparam
|
||||
func (cfg *attest) parseConfigValues() error {
|
||||
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.password", "")
|
||||
}
|
|
@ -18,3 +18,9 @@ func IsPipedInput() (bool, error) {
|
|||
// if there *may* be bytes that will show up on stdin that should be used for the analysis source.
|
||||
return fi.Mode()&os.ModeNamedPipe != 0, nil
|
||||
}
|
||||
|
||||
// IsTerminal returns true if there is a terminal present.
|
||||
func IsTerminal() bool {
|
||||
stat, _ := os.Stdin.Stat()
|
||||
return (stat.Mode() & os.ModeCharDevice) != 0
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ func Encode(s sbom.SBOM, option format.Option) ([]byte, error) {
|
|||
if f == nil {
|
||||
return nil, fmt.Errorf("unsupported format: %+v", option)
|
||||
}
|
||||
|
||||
buff := bytes.Buffer{}
|
||||
|
||||
if err := f.Encode(&buff, s); err != nil {
|
||||
|
|
|
@ -29,7 +29,7 @@ var AllSchemes = []Scheme{
|
|||
FileScheme,
|
||||
}
|
||||
|
||||
func detectScheme(fs afero.Fs, imageDetector sourceDetector, userInput string) (Scheme, image.Source, string, error) {
|
||||
func DetectScheme(fs afero.Fs, imageDetector sourceDetector, userInput string) (Scheme, image.Source, string, error) {
|
||||
switch {
|
||||
case strings.HasPrefix(userInput, "dir:"):
|
||||
dirLocation, err := homedir.Expand(strings.TrimPrefix(userInput, "dir:"))
|
||||
|
|
|
@ -287,7 +287,7 @@ func TestDetectScheme(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
actualScheme, actualSource, actualLocation, err := detectScheme(fs, imageDetector, test.userInput)
|
||||
actualScheme, actualSource, actualLocation, err := DetectScheme(fs, imageDetector, test.userInput)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err : %+v", err)
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ type sourceDetector func(string) (image.Source, string, error)
|
|||
// New produces a Source based on userInput like dir: or image:tag
|
||||
func New(userInput string, registryOptions *image.RegistryOptions, exclusions []string) (*Source, func(), error) {
|
||||
fs := afero.NewOsFs()
|
||||
parsedScheme, imageSource, location, err := detectScheme(fs, image.DetectSource, userInput)
|
||||
parsedScheme, imageSource, location, err := DetectScheme(fs, image.DetectSource, userInput)
|
||||
if err != nil {
|
||||
return &Source{}, func() {}, fmt.Errorf("unable to parse input=%q: %w", userInput, err)
|
||||
}
|
||||
|
|
62
test/cli/attest_cmd_test.go
Normal file
62
test/cli/attest_cmd_test.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAttestCmd(t *testing.T) {
|
||||
coverageImage := "docker-archive:" + getFixtureImage(t, "image-pkg-coverage")
|
||||
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", coverageImage},
|
||||
assertions: []traitAssertion{
|
||||
assertSuccessfulReturnCode,
|
||||
// assertVerifyAttestation(coverageImage), Follow up on this assertion with verify blog or ephemperal registry
|
||||
},
|
||||
pw: "test",
|
||||
},
|
||||
{
|
||||
name: "can encode syft.json as the predicate given a blank password",
|
||||
args: []string{"attest", "-o", "json", coverageImage},
|
||||
assertions: []traitAssertion{
|
||||
assertSuccessfulReturnCode,
|
||||
// assertVerifyAttestation(coverageImage), Follow up on this assertion with verify blog or ephemperal registry
|
||||
},
|
||||
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, " "))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -3,6 +3,8 @@ package cli
|
|||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
@ -116,6 +118,46 @@ func assertSuccessfulReturnCode(tb testing.TB, _, _ string, rc int) {
|
|||
}
|
||||
}
|
||||
|
||||
func assertVerifyAttestation(coverageImage string) traitAssertion {
|
||||
return func(tb testing.TB, stdout, _ string, _ int) {
|
||||
tb.Helper()
|
||||
cosignPath := filepath.Join(repoRoot(tb), ".tmp/cosign")
|
||||
err := os.WriteFile("attestation.json", []byte(stdout), 0664)
|
||||
if err != nil {
|
||||
tb.Errorf("could not write attestation to disk")
|
||||
}
|
||||
defer os.Remove("attestation.json")
|
||||
attachCmd := exec.Command(
|
||||
cosignPath,
|
||||
"attach",
|
||||
"attestation",
|
||||
"--attestation",
|
||||
"attestation.json",
|
||||
coverageImage, // TODO which remote image to use?
|
||||
)
|
||||
|
||||
stdout, stderr := runCommand(attachCmd, nil)
|
||||
if attachCmd.ProcessState.ExitCode() != 0 {
|
||||
tb.Log("STDOUT", stdout)
|
||||
tb.Log("STDERR", stderr)
|
||||
tb.Fatalf("could not attach image")
|
||||
}
|
||||
|
||||
verifyCmd := exec.Command(
|
||||
cosignPath,
|
||||
"verify-attestation",
|
||||
coverageImage, // TODO which remote image to use?
|
||||
)
|
||||
|
||||
stdout, stderr = runCommand(verifyCmd, nil)
|
||||
if attachCmd.ProcessState.ExitCode() != 0 {
|
||||
tb.Log("STDOUT", stdout)
|
||||
tb.Log("STDERR", stderr)
|
||||
tb.Fatalf("could not verify attestation")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func assertFileExists(file string) traitAssertion {
|
||||
return func(tb testing.TB, _, _ string, _ int) {
|
||||
tb.Helper()
|
||||
|
|
|
@ -14,6 +14,39 @@ import (
|
|||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
)
|
||||
|
||||
func setupPKI(t *testing.T, pw string) func() {
|
||||
err := os.Setenv("COSIGN_PASSWORD", pw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cosignPath := filepath.Join(repoRoot(t), ".tmp/cosign")
|
||||
cmd := exec.Command(cosignPath, "generate-key-pair")
|
||||
stdout, stderr := runCommand(cmd, nil)
|
||||
if cmd.ProcessState.ExitCode() != 0 {
|
||||
t.Log("STDOUT", stdout)
|
||||
t.Log("STDERR", stderr)
|
||||
t.Fatalf("could not generate keypair")
|
||||
}
|
||||
|
||||
return func() {
|
||||
err := os.Unsetenv("COSIGN_PASSWORD")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = os.Remove("cosign.key")
|
||||
if err != nil {
|
||||
t.Fatalf("could not cleanup cosign.key")
|
||||
}
|
||||
|
||||
err = os.Remove("cosign.pub")
|
||||
if err != nil {
|
||||
t.Fatalf("could not cleanup cosign.key")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getFixtureImage(t testing.TB, fixtureImageName string) string {
|
||||
t.Logf("obtaining fixture image for %s", fixtureImageName)
|
||||
imagetest.GetFixtureImage(t, "docker-archive", fixtureImageName)
|
||||
|
|
Loading…
Reference in a new issue