diff --git a/cmd/run.go b/cmd/run.go index ad19a30c..2efbf4b1 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -85,12 +85,26 @@ func runRun(ctx context.Context, ro options.RunOptions, args []string, signers . attestors = append(attestors, commandrun.New(commandrun.WithCommand(args), commandrun.WithTracing(ro.Tracing))) } - addtlAttestors, err := attestation.Attestors(ro.Attestations) - if err != nil { - return fmt.Errorf("failed to create attestors := %w", err) + for _, a := range ro.Attestations { + duplicate := false + for _, att := range attestors { + if a != att.Name() { + } else { + log.Warnf("Attestator %s already declared, skipping", a) + duplicate = true + break + } + } + + if !duplicate { + attestor, err := attestation.GetAttestor(a) + if err != nil { + return fmt.Errorf("failed to create attestor: %w", err) + } + attestors = append(attestors, attestor) + } } - attestors = append(attestors, addtlAttestors...) for _, attestor := range attestors { setters, ok := ro.AttestorOptSetters[attestor.Name()] if !ok { @@ -120,7 +134,6 @@ func runRun(ctx context.Context, ro options.RunOptions, args []string, signers . witness.RunWithAttestationOpts(attestation.WithWorkingDir(ro.WorkingDir), attestation.WithHashes(roHashes)), witness.RunWithTimestampers(timestampers...), ) - if err != nil { return err } diff --git a/cmd/run_test.go b/cmd/run_test.go index 03cd2db3..4871c9d2 100644 --- a/cmd/run_test.go +++ b/cmd/run_test.go @@ -20,15 +20,20 @@ import ( "crypto/rand" "crypto/rsa" "encoding/json" + "fmt" "os" "path/filepath" + "strings" "testing" "github.com/in-toto/go-witness/cryptoutil" "github.com/in-toto/go-witness/dsse" + "github.com/in-toto/go-witness/log" "github.com/in-toto/go-witness/signer" "github.com/in-toto/go-witness/signer/file" "github.com/in-toto/witness/options" + "github.com/sirupsen/logrus" + "github.com/sirupsen/logrus/hooks/test" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -171,3 +176,73 @@ func TestRunHashesOptions(t *testing.T) { }) } } + +func TestRunDuplicateAttestors(t *testing.T) { + tests := []struct { + name string + attestors []string + expectWarn int + }{ + { + name: "No duplicate attestors", + attestors: []string{"environment"}, + expectWarn: 0, + }, + { + name: "duplicate attestors", + attestors: []string{"environment", "environment"}, + expectWarn: 1, + }, + { + name: "duplicate attestor due to default", + attestors: []string{"product"}, + expectWarn: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fmt.Println(tt.name) + testLogger, hook := test.NewNullLogger() + log.SetLogger(testLogger) + + privatekey, err := rsa.GenerateKey(rand.Reader, keybits) + require.NoError(t, err) + signer := cryptoutil.NewRSASigner(privatekey, crypto.SHA256) + + workingDir := t.TempDir() + attestationPath := filepath.Join(workingDir, "outfile.txt") + runOptions := options.RunOptions{ + WorkingDir: workingDir, + Attestations: tt.attestors, + OutFilePath: attestationPath, + StepName: "teststep", + Tracing: false, + } + + args := []string{ + "bash", + "-c", + "echo 'test' > test.txt", + } + + err = runRun(context.Background(), runOptions, args, signer) + if tt.expectWarn > 0 { + c := 0 + for _, entry := range hook.AllEntries() { + fmt.Println(tt.name, "log:", entry.Message) + if entry.Level == logrus.WarnLevel && strings.Contains(entry.Message, "already declared, skipping") { + c++ + } + } + assert.Equal(t, tt.expectWarn, c) + } else { + require.NoError(t, err) + attestationBytes, err := os.ReadFile(attestationPath) + require.NoError(t, err) + env := dsse.Envelope{} + require.NoError(t, json.Unmarshal(attestationBytes, &env)) + } + }) + } +} diff --git a/docs/witness_run.md b/docs/witness_run.md new file mode 100644 index 00000000..831832d2 --- /dev/null +++ b/docs/witness_run.md @@ -0,0 +1,54 @@ +## witness run + +Runs the provided command and records attestations about the execution + +``` +witness run [cmd] [flags] +``` + +### Options + +``` + --archivista-server string URL of the Archivista server to store or retrieve attestations (default "https://archivista.testifysec.io") + -a, --attestations strings Attestations to record ('product' and 'material' are always recorded) (default [environment,git]) + --attestor-product-exclude-glob string Pattern to use when recording products. Files that match this pattern will be excluded as subjects on the attestation. + --attestor-product-include-glob string Pattern to use when recording products. Files that match this pattern will be included as subjects on the attestation. (default "*") + --enable-archivista Use Archivista to store or retrieve attestations + --hashes strings Hashes selected for digest calculation. Defaults to SHA256 (default [sha256]) + -h, --help help for run + -o, --outfile string File to which to write signed data. Defaults to stdout + --signer-file-cert-path string Path to the file containing the certificate for the private key + --signer-file-intermediate-paths strings Paths to files containing intermediates required to establish trust of the signer's certificate to a root + -k, --signer-file-key-path string Path to the file containing the private key + --signer-fulcio-oidc-client-id string OIDC client ID to use for authentication + --signer-fulcio-oidc-issuer string OIDC issuer to use for authentication + --signer-fulcio-oidc-redirect-url string OIDC redirect URL (Optional). The default oidc-redirect-url is 'http://localhost:0/auth/callback'. + --signer-fulcio-token string Raw token string to use for authentication to fulcio (cannot be used in conjunction with --fulcio-token-path) + --signer-fulcio-token-path string Path to the file containing a raw token to use for authentication to fulcio (cannot be used in conjunction with --fulcio-token) + --signer-fulcio-url string Fulcio address to sign with + --signer-spiffe-socket-path string Path to the SPIFFE Workload API Socket + --signer-vault-altnames strings Alt names to use for the generated certificate. All alt names must be allowed by the vault role policy + --signer-vault-commonname string Common name to use for the generated certificate. Must be allowed by the vault role policy + --signer-vault-namespace string Vault namespace to use + --signer-vault-pki-secrets-engine-path string Path to the Vault PKI Secrets Engine to use (default "pki") + --signer-vault-role string Name of the Vault role to generate the certificate for + --signer-vault-token string Token to use to connect to Vault + --signer-vault-ttl duration Time to live for the generated certificate. Defaults to the vault role policy's configured TTL if not provided + --signer-vault-url string Base url of the Vault instance to connect to + -s, --step string Name of the step being run + --timestamp-servers strings Timestamp Authority Servers to use when signing envelope + --trace Enable tracing for the command + -d, --workingdir string Directory from which commands will run +``` + +### Options inherited from parent commands + +``` + -c, --config string Path to the witness config file (default ".witness.yaml") + -l, --log-level string Level of logging to output (debug, info, warn, error) (default "info") +``` + +### SEE ALSO + +* [witness](witness.md) - Collect and verify attestations about your build environments + diff --git a/docs/witness_sign.md b/docs/witness_sign.md new file mode 100644 index 00000000..d91a689a --- /dev/null +++ b/docs/witness_sign.md @@ -0,0 +1,51 @@ +## witness sign + +Signs a file + +### Synopsis + +Signs a file with the provided key source and outputs the signed file to the specified destination + +``` +witness sign [file] [flags] +``` + +### Options + +``` + -t, --datatype string The URI reference to the type of data being signed. Defaults to the Witness policy type (default "https://witness.testifysec.com/policy/v0.1") + -h, --help help for sign + -f, --infile string Witness policy file to sign + -o, --outfile string File to write signed data. Defaults to stdout + --signer-file-cert-path string Path to the file containing the certificate for the private key + --signer-file-intermediate-paths strings Paths to files containing intermediates required to establish trust of the signer's certificate to a root + -k, --signer-file-key-path string Path to the file containing the private key + --signer-fulcio-oidc-client-id string OIDC client ID to use for authentication + --signer-fulcio-oidc-issuer string OIDC issuer to use for authentication + --signer-fulcio-oidc-redirect-url string OIDC redirect URL (Optional). The default oidc-redirect-url is 'http://localhost:0/auth/callback'. + --signer-fulcio-token string Raw token string to use for authentication to fulcio (cannot be used in conjunction with --fulcio-token-path) + --signer-fulcio-token-path string Path to the file containing a raw token to use for authentication to fulcio (cannot be used in conjunction with --fulcio-token) + --signer-fulcio-url string Fulcio address to sign with + --signer-spiffe-socket-path string Path to the SPIFFE Workload API Socket + --signer-vault-altnames strings Alt names to use for the generated certificate. All alt names must be allowed by the vault role policy + --signer-vault-commonname string Common name to use for the generated certificate. Must be allowed by the vault role policy + --signer-vault-namespace string Vault namespace to use + --signer-vault-pki-secrets-engine-path string Path to the Vault PKI Secrets Engine to use (default "pki") + --signer-vault-role string Name of the Vault role to generate the certificate for + --signer-vault-token string Token to use to connect to Vault + --signer-vault-ttl duration Time to live for the generated certificate. Defaults to the vault role policy's configured TTL if not provided + --signer-vault-url string Base url of the Vault instance to connect to + --timestamp-servers strings Timestamp Authority Servers to use when signing envelope +``` + +### Options inherited from parent commands + +``` + -c, --config string Path to the witness config file (default ".witness.yaml") + -l, --log-level string Level of logging to output (debug, info, warn, error) (default "info") +``` + +### SEE ALSO + +* [witness](witness.md) - Collect and verify attestations about your build environments + diff --git a/go.mod b/go.mod index ac0cc74f..8fcdfec6 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,13 @@ module github.com/in-toto/witness go 1.19 require ( - github.com/in-toto/go-witness v0.2.0 + github.com/in-toto/go-witness v0.2.1 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.15.0 github.com/stretchr/testify v1.8.4 - k8s.io/apimachinery v0.26.11 + k8s.io/apimachinery v0.26.12 ) require ( diff --git a/go.sum b/go.sum index ea2d955b..9bfc6685 100644 --- a/go.sum +++ b/go.sum @@ -220,8 +220,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1: github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/in-toto/archivista v0.2.0 h1:FViuHMVVETborvOqlmSYdROY8RmX3CO0V0MOhU/Rl20= github.com/in-toto/archivista v0.2.0/go.mod h1:qt9uN4TkHWUgR5A2wxRqQIBizSl32P2nI2AjESskkr0= -github.com/in-toto/go-witness v0.2.0 h1:lxp3+Kc4Der2C1jV9ZePjSCEHUr2NsB4sImXI5sZHu4= -github.com/in-toto/go-witness v0.2.0/go.mod h1:Jr6ZlYoVfTS3hjUSmJ10J8qiHjpF1cfSE4NLAIJpbLw= +github.com/in-toto/go-witness v0.2.1 h1:eAxMBWUPbz3oPU3lsfEYi/Kdj6weej2umm59bOXPJSU= +github.com/in-toto/go-witness v0.2.1/go.mod h1:xURJVj4QRD3xnzOJps7gT0pMCFPpAHcPqDC3EyuLuUE= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= @@ -749,8 +749,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/apimachinery v0.26.11 h1:w//840HHdwSRKqD15j9YX9HLlU6RPlfrvW0xEhLk2+0= -k8s.io/apimachinery v0.26.11/go.mod h1:2/HZp0l6coXtS26du1Bk36fCuAEr/lVs9Q9NbpBtd1Y= +k8s.io/apimachinery v0.26.12 h1:y+OgufxqLIZtyXIydRhjLBGzrYLF+qwiDdCFXYOjeN4= +k8s.io/apimachinery v0.26.12/go.mod h1:2/HZp0l6coXtS26du1Bk36fCuAEr/lVs9Q9NbpBtd1Y= k8s.io/klog/v2 v2.90.0 h1:VkTxIV/FjRXn1fgNNcKGM8cfmL1Z33ZjXRTVxKCoF5M= k8s.io/klog/v2 v2.90.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/utils v0.0.0-20230115233650-391b47cb4029 h1:L8zDtT4jrxj+TaQYD0k8KNlr556WaVQylDXswKmX+dE= diff --git a/options/run.go b/options/run.go index 3cccb3f7..0e7ab343 100644 --- a/options/run.go +++ b/options/run.go @@ -37,7 +37,7 @@ func (ro *RunOptions) AddFlags(cmd *cobra.Command) { ro.SignerOptions.AddFlags(cmd) ro.ArchivistaOptions.AddFlags(cmd) cmd.Flags().StringVarP(&ro.WorkingDir, "workingdir", "d", "", "Directory from which commands will run") - cmd.Flags().StringSliceVarP(&ro.Attestations, "attestations", "a", []string{"environment", "git"}, "Attestations to record") + cmd.Flags().StringSliceVarP(&ro.Attestations, "attestations", "a", []string{"environment", "git"}, "Attestations to record ('product' and 'material' are always recorded)") cmd.Flags().StringSliceVar(&ro.Hashes, "hashes", []string{"sha256"}, "Hashes selected for digest calculation. Defaults to SHA256") cmd.Flags().StringVarP(&ro.OutFilePath, "outfile", "o", "", "File to which to write signed data. Defaults to stdout") cmd.Flags().StringVarP(&ro.StepName, "step", "s", "", "Name of the step being run")