wip: inspect now prints various bundle fields in a nice json
This commit is contained in:
parent
e390874fef
commit
d348e46b26
1 changed files with 213 additions and 74 deletions
|
|
@ -1,18 +1,20 @@
|
|||
package inspect
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/v2/internal/ghinstance"
|
||||
"github.com/cli/cli/v2/internal/tableprinter"
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/api"
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact"
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci"
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/auth"
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/io"
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/verification"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
ghauth "github.com/cli/go-gh/v2/pkg/auth"
|
||||
"github.com/digitorus/timestamp"
|
||||
in_toto "github.com/in-toto/attestation/go/v1"
|
||||
"github.com/sigstore/sigstore-go/pkg/bundle"
|
||||
"github.com/sigstore/sigstore-go/pkg/fulcio/certificate"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/spf13/cobra"
|
||||
|
|
@ -28,18 +30,19 @@ func NewInspectCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command
|
|||
Long: heredoc.Docf(`
|
||||
### NOTE: This feature is currently in public preview, and subject to change.
|
||||
|
||||
Inspect a downloaded Sigstore bundle for a given artifact.
|
||||
Inspect a Sigstore bundle that has been downloaded to disk. See the %[1]sdownload%[1]s
|
||||
command.
|
||||
|
||||
The command requires either:
|
||||
* a relative path to a local artifact, or
|
||||
* a container image URI (e.g. %[1]soci://<my-OCI-image-URI>%[1]s)
|
||||
// The command requires either:
|
||||
// * a relative path to a local artifact, or
|
||||
// * a container image URI (e.g. %[1]soci://<my-OCI-image-URI>%[1]s)
|
||||
|
||||
Note that if you provide an OCI URI for the artifact you must already
|
||||
be authenticated with a container registry.
|
||||
// Note that if you provide an OCI URI for the artifact you must already
|
||||
// be authenticated with a container registry.
|
||||
|
||||
The command also requires the %[1]s--bundle%[1]s flag, which provides a file
|
||||
path to a previously downloaded Sigstore bundle. (See also the %[1]sdownload%[1]s
|
||||
command).
|
||||
// The command also requires the %[1]s--bundle%[1]s flag, which provides a file
|
||||
// path to a previously downloaded Sigstore bundle. (See also the %[1]sdownload%[1]s
|
||||
// command).
|
||||
|
||||
By default, the command will print information about the bundle in a table format.
|
||||
If the %[1]s--json-result%[1]s flag is provided, the command will print the
|
||||
|
|
@ -47,36 +50,36 @@ func NewInspectCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command
|
|||
`, "`"),
|
||||
Example: heredoc.Doc(`
|
||||
# Inspect a Sigstore bundle and print the results in table format
|
||||
$ gh attestation inspect <my-artifact> --bundle <path-to-bundle>
|
||||
$ gh attestation inspect <path-to-bundle>
|
||||
|
||||
# Inspect a Sigstore bundle and print the results in JSON format
|
||||
$ gh attestation inspect <my-artifact> --bundle <path-to-bundle> --json-result
|
||||
$ gh attestation inspect <path-to-bundle> --json-result
|
||||
|
||||
# Inspect a Sigsore bundle for an OCI artifact, and print the results in table format
|
||||
$ gh attestation inspect oci://<my-OCI-image> --bundle <path-to-bundle>
|
||||
// # Inspect a Sigsore bundle for an OCI artifact, and print the results in table format
|
||||
// $ gh attestation inspect oci://<my-OCI-image> --bundle <path-to-bundle>
|
||||
`),
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
// Create a logger for use throughout the inspect command
|
||||
opts.Logger = io.NewHandler(f.IOStreams)
|
||||
|
||||
// set the artifact path
|
||||
opts.ArtifactPath = args[0]
|
||||
opts.BundlePath = args[0]
|
||||
|
||||
// Clean file path options
|
||||
// opts.Clean()
|
||||
opts.Clean()
|
||||
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.OCIClient = oci.NewLiveClient()
|
||||
if opts.Hostname == "" {
|
||||
opts.Hostname, _ = ghauth.DefaultHost()
|
||||
}
|
||||
|
||||
if err := auth.IsHostSupported(opts.Hostname); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// opts.OCIClient = oci.NewLiveClient()
|
||||
// if opts.Hostname == "" {
|
||||
// opts.Hostname, _ = ghauth.DefaultHost()
|
||||
// }
|
||||
//
|
||||
// if err := auth.IsHostSupported(opts.Hostname); err != nil {
|
||||
// return err
|
||||
// }
|
||||
//
|
||||
if runF != nil {
|
||||
return runF(opts)
|
||||
}
|
||||
|
|
@ -84,7 +87,8 @@ func NewInspectCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command
|
|||
config := verification.SigstoreConfig{
|
||||
Logger: opts.Logger,
|
||||
}
|
||||
// Prepare for tenancy if detected
|
||||
|
||||
// fetch the correct trust domain so we can verify the bundle
|
||||
if ghauth.IsTenancy(opts.Hostname) {
|
||||
hc, err := f.HttpClient()
|
||||
if err != nil {
|
||||
|
|
@ -115,7 +119,7 @@ func NewInspectCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command
|
|||
}
|
||||
|
||||
inspectCmd.Flags().StringVarP(&opts.BundlePath, "bundle", "b", "", "Path to bundle on disk, either a single bundle in a JSON file or a JSON lines file with multiple bundles")
|
||||
inspectCmd.MarkFlagRequired("bundle") //nolint:errcheck
|
||||
// inspectCmd.MarkFlagRequired("bundle") //nolint:errcheck
|
||||
inspectCmd.Flags().StringVarP(&opts.Hostname, "hostname", "", "", "Configure host to use")
|
||||
cmdutil.StringEnumFlag(inspectCmd, &opts.DigestAlgorithm, "digest-alg", "d", "sha256", []string{"sha256", "sha512"}, "The algorithm used to compute a digest of the artifact")
|
||||
cmdutil.AddFormatFlags(inspectCmd, &opts.exporter)
|
||||
|
|
@ -123,66 +127,201 @@ func NewInspectCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command
|
|||
return inspectCmd
|
||||
}
|
||||
|
||||
func runInspect(opts *Options) error {
|
||||
artifact, err := artifact.NewDigestedArtifact(opts.OCIClient, opts.ArtifactPath, opts.DigestAlgorithm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to digest artifact: %s", err)
|
||||
}
|
||||
type BundleInspection struct {
|
||||
Certificate CertificateInspection
|
||||
Statement in_toto.Statement
|
||||
TransparencyLogEntries []TlogEntryInspection
|
||||
SignedTimestamps []time.Time
|
||||
}
|
||||
|
||||
opts.Logger.Printf("Verifying attestations for the artifact found at %s\n\n", artifact.URL)
|
||||
type CertificateInspection struct {
|
||||
certificate.Summary
|
||||
NotBefore time.Time
|
||||
NotAfter time.Time
|
||||
}
|
||||
|
||||
type TlogEntryInspection struct {
|
||||
IntegratedTime time.Time
|
||||
LogID string
|
||||
}
|
||||
|
||||
func runInspect(opts *Options) error {
|
||||
// artifact, err := artifact.NewDigestedArtifact(opts.OCIClient, opts.ArtifactPath, opts.DigestAlgorithm)
|
||||
// if err != nil {
|
||||
// return fmt.Errorf("failed to digest artifact: %s", err)
|
||||
// }
|
||||
//
|
||||
// opts.Logger.Printf("Verifying attestations for the artifact found at %s\n\n", artifact.URL)
|
||||
|
||||
attestations, err := verification.GetLocalAttestations(opts.BundlePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read attestations for subject: %s", artifact.DigestWithAlg())
|
||||
return fmt.Errorf("failed to read attestations")
|
||||
}
|
||||
|
||||
policy, err := buildPolicy(*artifact)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to build policy: %v", err)
|
||||
}
|
||||
inspectedBundles := []BundleInspection{}
|
||||
|
||||
res := opts.SigstoreVerifier.Verify(attestations, policy)
|
||||
if res.Error != nil {
|
||||
return fmt.Errorf("at least one attestation failed to verify against Sigstore: %v", res.Error)
|
||||
}
|
||||
for _, a := range attestations {
|
||||
inspectedBundle := BundleInspection{}
|
||||
|
||||
opts.Logger.VerbosePrint(opts.Logger.ColorScheme.Green(
|
||||
"Successfully verified all attestations against Sigstore!\n\n",
|
||||
))
|
||||
|
||||
// If the user provides the --format=json flag, print the results in JSON format
|
||||
if opts.exporter != nil {
|
||||
details, err := getAttestationDetails(opts.Tenant, res.VerifyResults)
|
||||
entity := a.Bundle
|
||||
verificationContent, err := entity.VerificationContent()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get attestation detail: %v", err)
|
||||
return fmt.Errorf("failed to fetch verification content: %w", err)
|
||||
}
|
||||
|
||||
// print the results to the terminal as an array of JSON objects
|
||||
if err = opts.exporter.Write(opts.Logger.IO, details); err != nil {
|
||||
return fmt.Errorf("failed to write JSON output")
|
||||
if leafCert := verificationContent.GetCertificate(); leafCert != nil {
|
||||
|
||||
certSummary, err := certificate.SummarizeCertificate(leafCert)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to summarize certificate: %w", err)
|
||||
}
|
||||
|
||||
inspectedCert := CertificateInspection{
|
||||
Summary: certSummary,
|
||||
NotBefore: leafCert.NotBefore,
|
||||
NotAfter: leafCert.NotAfter,
|
||||
}
|
||||
|
||||
inspectedBundle.Certificate = inspectedCert
|
||||
PrettyPrint(inspectedCert)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// otherwise, print results in a table
|
||||
details, err := getDetailsAsSlice(opts.Tenant, res.VerifyResults)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse attestation details: %v", err)
|
||||
}
|
||||
|
||||
headers := []string{"Repo Name", "Repo ID", "Org Name", "Org ID", "Workflow ID"}
|
||||
t := tableprinter.New(opts.Logger.IO, tableprinter.WithHeader(headers...))
|
||||
|
||||
for _, row := range details {
|
||||
for _, field := range row {
|
||||
t.AddField(field, tableprinter.WithTruncate(nil))
|
||||
sigContent, err := entity.SignatureContent()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch signature content: %w", err)
|
||||
}
|
||||
t.EndRow()
|
||||
|
||||
if envelope := sigContent.EnvelopeContent(); envelope != nil {
|
||||
stmt, err := envelope.Statement()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch envelope statement: %w", err)
|
||||
}
|
||||
|
||||
inspectedBundle.Statement = *stmt
|
||||
PrettyPrint(stmt)
|
||||
}
|
||||
|
||||
tlogTimestamps, err := dumpTlogs(entity)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to dump tlog: %w", err)
|
||||
}
|
||||
inspectedBundle.TransparencyLogEntries = tlogTimestamps
|
||||
PrettyPrint(tlogTimestamps)
|
||||
|
||||
signedTimestamps, err := dumpSignedTimestamps(entity)
|
||||
if err != nil {
|
||||
return fmt.Errorf("faield to dump tsa: %w", err)
|
||||
}
|
||||
inspectedBundle.SignedTimestamps = signedTimestamps
|
||||
PrettyPrint(signedTimestamps)
|
||||
|
||||
// collect timestamps
|
||||
|
||||
// fmt.Println(a.Bundle)
|
||||
inspectedBundles = append(inspectedBundles, inspectedBundle)
|
||||
}
|
||||
|
||||
if err = t.Render(); err != nil {
|
||||
return fmt.Errorf("failed to print output: %v", err)
|
||||
}
|
||||
// policy, err := buildPolicy(*artifact)
|
||||
// if err != nil {
|
||||
// return fmt.Errorf("failed to build policy: %v", err)
|
||||
// }
|
||||
//
|
||||
// res := opts.SigstoreVerifier.Verify(attestations, policy)
|
||||
// if res.Error != nil {
|
||||
// return fmt.Errorf("at least one attestation failed to verify against Sigstore: %v", res.Error)
|
||||
// }
|
||||
//
|
||||
// opts.Logger.VerbosePrint(opts.Logger.ColorScheme.Green(
|
||||
// "Successfully verified all attestations against Sigstore!\n\n",
|
||||
// ))
|
||||
//
|
||||
// If the user provides the --format=json flag, print the results in JSON format
|
||||
// if opts.exporter != nil {
|
||||
// details, err := getAttestationDetails(opts.Tenant, res.VerifyResults)
|
||||
// if err != nil {
|
||||
// return fmt.Errorf("failed to get attestation detail: %v", err)
|
||||
// }
|
||||
//
|
||||
// // print the results to the terminal as an array of JSON objects
|
||||
// if err = opts.exporter.Write(opts.Logger.IO, details); err != nil {
|
||||
// return fmt.Errorf("failed to write JSON output")
|
||||
// }
|
||||
// return nil
|
||||
// }
|
||||
//
|
||||
// // otherwise, print results in a table
|
||||
// details, err := getDetailsAsSlice(opts.Tenant, res.VerifyResults)
|
||||
// if err != nil {
|
||||
// return fmt.Errorf("failed to parse attestation details: %v", err)
|
||||
// }
|
||||
//
|
||||
// headers := []string{"Repo Name", "Repo ID", "Org Name", "Org ID", "Workflow ID"}
|
||||
// t := tableprinter.New(opts.Logger.IO, tableprinter.WithHeader(headers...))
|
||||
//
|
||||
// for _, row := range details {
|
||||
// for _, field := range row {
|
||||
// t.AddField(field, tableprinter.WithTruncate(nil))
|
||||
// }
|
||||
// t.EndRow()
|
||||
// }
|
||||
//
|
||||
// if err = t.Render(); err != nil {
|
||||
// return fmt.Errorf("failed to print output: %v", err)
|
||||
// }
|
||||
//
|
||||
|
||||
PrettyPrint(inspectedBundles)
|
||||
return nil
|
||||
}
|
||||
|
||||
func dumpTlogs(entity *bundle.Bundle) ([]TlogEntryInspection, error) {
|
||||
|
||||
inspectedTlogEntries := []TlogEntryInspection{}
|
||||
|
||||
entries, err := entity.TlogEntries()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
inspectedEntry := TlogEntryInspection{
|
||||
IntegratedTime: entry.IntegratedTime(),
|
||||
LogID: entry.LogKeyID(),
|
||||
}
|
||||
|
||||
inspectedTlogEntries = append(inspectedTlogEntries, inspectedEntry)
|
||||
}
|
||||
|
||||
return inspectedTlogEntries, nil
|
||||
}
|
||||
|
||||
func dumpSignedTimestamps(entity *bundle.Bundle) ([]time.Time, error) {
|
||||
timestamps := []time.Time{}
|
||||
|
||||
signedTimestamps, err := entity.Timestamps()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, signedTsBytes := range signedTimestamps {
|
||||
tsaTime, err := timestamp.ParseResponse(signedTsBytes)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
timestamps = append(timestamps, tsaTime.Time)
|
||||
}
|
||||
|
||||
return timestamps, nil
|
||||
}
|
||||
|
||||
func PrettyPrint(v interface{}) (err error) {
|
||||
b, err := json.MarshalIndent(v, "", " ")
|
||||
|
||||
if err == nil {
|
||||
fmt.Println(string(b))
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue