Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/kosli/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func newGetCmd(out io.Writer) *cobra.Command {
newGetTrailCmd(out),
newGetPolicyCmd(out),
newGetAttestationTypeCmd(out),
newGetAttestationCmd(out),
)
return cmd
}
184 changes: 184 additions & 0 deletions cmd/kosli/getAttestation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
package main

import (
"encoding/json"
"fmt"
"io"
"net/http"

"github.com/kosli-dev/cli/internal/output"
"github.com/kosli-dev/cli/internal/requests"
"github.com/spf13/cobra"
)

const getAttestationShortDesc = `Get attestation by name from a specified trail or artifact. `

const getAttestationLongDesc = getAttestationShortDesc + `
You can get an attestation from a trail or artifact using its name. The attestation name should be given
WITHOUT dot-notation.

To get an attestation from a trail, specify the trail name using the --trail flag.
To get an attestation from an artifact, specify the artifact fingerprint using the --fingerprint flag.

In both cases the flow must also be specified using the --flow flag.

If there are multiple attestations with the same name on the trail or artifact, a list of all will be returned.
`

const getAttestationExample = `
# get an attestation from a trail (requires the --trail flag)
kosli get attestation attestationName \
--flow flowName \
--trail trailName

# get an attestation from an artifact
kosli get attestation attestationName \
--flow flowName \
--fingerprint fingerprint
`

type getAttestationOptions struct {
output string
flow string
trail string
fingerprint string
}

type Attestation struct {
Name string `json:"attestation_name"`
Type string `json:"attestation_type"`
Compliance bool `json:"is_compliant"`
ArtifactFingerprint string `json:"artifact_fingerprint,omitempty"`
CreatedAt float64 `json:"created_at"`
GitCommitInfo *GitCommitInfo `json:"git_commit_info,omitempty"`
HtmlUrl string `json:"html_url"`
}

type GitCommitInfo struct {
Sha1 string `json:"sha1"`
Author string `json:"author"`
Message string `json:"message"`
Branch string `json:"branch"`
Url string `json:"url,omitempty"`
Timestamp float64 `json:"timestamp"`
}

func newGetAttestationCmd(out io.Writer) *cobra.Command {
o := new(getAttestationOptions)
cmd := &cobra.Command{
Use: "attestation ATTESTATION-NAME",
Short: getAttestationShortDesc,
Long: getAttestationLongDesc,
Example: getAttestationExample,
Args: cobra.ExactArgs(1),
PreRunE: func(cmd *cobra.Command, args []string) error {
err := RequireGlobalFlags(global, []string{"Org", "ApiToken"})
if err != nil {
return ErrorBeforePrintingUsage(cmd, err.Error())
}
err = MuXRequiredFlags(cmd, []string{"trail", "fingerprint"}, true)
if err != nil {
return err
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
return o.run(out, args)
},
}

cmd.Flags().StringVarP(&o.output, "output", "o", "table", outputFlag)
cmd.Flags().StringVarP(&o.flow, "flow", "f", "", flowNameFlag)
cmd.Flags().StringVarP(&o.trail, "trail", "t", "", getAttestationTrailFlag)
cmd.Flags().StringVarP(&o.fingerprint, "fingerprint", "F", "", getAttestationFingerprintFlag)

err := RequireFlags(cmd, []string{"flow"})
if err != nil {
logger.Error("failed to configure required flags: %v", err)
}

return cmd
}

func (o *getAttestationOptions) run(out io.Writer, args []string) error {
var url string
baseUrl := fmt.Sprintf("%s/api/v2/attestations/%s/%s", global.Host, global.Org, o.flow)
if o.trail != "" {
url = fmt.Sprintf("%s/trail/%s", baseUrl, o.trail)
}

if o.fingerprint != "" {
url = fmt.Sprintf("%s/artifact/%s", baseUrl, o.fingerprint)
}

url = fmt.Sprintf("%s/%s", url, args[0])

reqParams := &requests.RequestParams{
Method: http.MethodGet,
URL: url,
Token: global.ApiToken,
}

response, err := kosliClient.Do(reqParams)
if err != nil {
return err
}

return output.FormattedPrint(response.Body, o.output, out, 0,
map[string]output.FormatOutputFunc{
"table": printAttestationsAsTable,
"json": output.PrintJson,
})
}

func printAttestationsAsTable(raw string, out io.Writer, pageNumber int) error {
var attestations []Attestation
err := json.Unmarshal([]byte(raw), &attestations)
if err != nil {
return err
}

if len(attestations) == 0 {
logger.Info("No attestations found.")
return nil
}

separator := ""
for _, attestation := range attestations {
rows := []string{}
rows = append(rows, fmt.Sprintf("Name:\t%s", attestation.Name))
rows = append(rows, fmt.Sprintf("Type:\t%s", attestation.Type))
rows = append(rows, fmt.Sprintf("Compliance:\t%t", attestation.Compliance))

createdAt, err := formattedTimestamp(attestation.CreatedAt, false)
if err != nil {
return err
}
rows = append(rows, fmt.Sprintf("Created at:\t%s", createdAt))

if attestation.ArtifactFingerprint != "" {
rows = append(rows, fmt.Sprintf("Artifact fingerprint:\t%s", attestation.ArtifactFingerprint))
}
if attestation.GitCommitInfo != nil {
rows = append(rows, "Git Commit Info:")
rows = append(rows, fmt.Sprintf(" Sha1:\t%s", attestation.GitCommitInfo.Sha1))
rows = append(rows, fmt.Sprintf(" Author:\t%s", attestation.GitCommitInfo.Author))
rows = append(rows, fmt.Sprintf(" Branch:\t%s", attestation.GitCommitInfo.Branch))
rows = append(rows, fmt.Sprintf(" Commit URL:\t%s", attestation.GitCommitInfo.Url))
timestamp, err := formattedTimestamp(attestation.GitCommitInfo.Timestamp, false)
if err != nil {
return err
}
rows = append(rows, fmt.Sprintf(" Timestamp:\t%s", timestamp))
}

if attestation.HtmlUrl != "" {
rows = append(rows, fmt.Sprintf("Attestation URL:\t%s", attestation.HtmlUrl))
}

fmt.Print(separator)
separator = "\n"
tabFormattedPrint(out, []string{}, rows)
}
return nil
}
120 changes: 120 additions & 0 deletions cmd/kosli/getAttestation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package main

import (
"fmt"
"testing"

"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)

// Define the suite, and absorb the built-in basic suite
// functionality from testify - including a T() method which
// returns the current testing context
type GetAttestationCommandTestSuite struct {
suite.Suite
defaultKosliArguments string
flowName string
artifactName string
artifactPath string
fingerprint string
trailName string
}

func (suite *GetAttestationCommandTestSuite) SetupTest() {
suite.flowName = "get-attestation"
suite.artifactName = "arti"
suite.artifactPath = "testdata/folder1/hello.txt"
suite.trailName = "cli-build-1"
global = &GlobalOpts{
ApiToken: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6ImNkNzg4OTg5In0.e8i_lA_QrEhFncb05Xw6E_tkCHU9QfcY4OLTVUCHffY",
Org: "docs-cmd-test-user-shared",
Host: "http://localhost:8001",
}
suite.defaultKosliArguments = fmt.Sprintf(" --host %s --org %s --api-token %s", global.Host, global.Org, global.ApiToken)

CreateFlowWithTemplate(suite.flowName, "testdata/valid_template.yml", suite.Suite.T())
BeginTrail(suite.trailName, suite.flowName, "", suite.Suite.T())
fingerprintOptions := &fingerprintOptions{
artifactType: "file",
}
var err error
suite.fingerprint, err = GetSha256Digest(suite.artifactPath, fingerprintOptions, logger)
require.NoError(suite.Suite.T(), err)
CreateArtifactOnTrail(suite.flowName, suite.trailName, "cli", suite.fingerprint, suite.artifactName, suite.Suite.T())
CreateGenericArtifactAttestation(suite.flowName, suite.trailName, suite.fingerprint, "first-artifact-attestation", suite.Suite.T())
CreateGenericTrailAttestation(suite.flowName, suite.trailName, "first-trail-attestation", suite.Suite.T())
CreateGenericArtifactAttestation(suite.flowName, suite.trailName, suite.fingerprint, "second-artifact-attestation", suite.Suite.T())
CreateGenericTrailAttestation(suite.flowName, suite.trailName, "second-trail-attestation", suite.Suite.T())
}

func (suite *GetAttestationCommandTestSuite) TestGetAttestationCmd() {
tests := []cmdTestCase{
{
wantError: false,
name: "if no attestation found, say so",
cmd: fmt.Sprintf(`get attestation non-existent-attestation --flow %s --trail %s %s`, suite.flowName, suite.trailName, suite.defaultKosliArguments),
golden: "No attestations found.\n",
},
{
wantError: false,
name: "if no attestation found return empty list in json format",
cmd: fmt.Sprintf(`get attestation non-existent-attestation --flow %s --trail %s %s --output json`, suite.flowName, suite.trailName, suite.defaultKosliArguments),
golden: "[]\n",
},
{
wantError: true,
name: "providing more than one argument fails",
cmd: fmt.Sprintf(`get attestation first-attestation second-attestation --flow %s --trail %s %s`, suite.flowName, suite.trailName, suite.defaultKosliArguments),
golden: "Error: accepts 1 arg(s), received 2\n",
},
{
wantError: true,
name: "missing --flow fails",
cmd: fmt.Sprintf(`get attestation first-artifact-attestation --trail %s %s`, suite.trailName, suite.defaultKosliArguments),
golden: "Error: required flag(s) \"flow\" not set\n",
},
{
wantError: true,
name: "missing --api-token fails",
cmd: fmt.Sprintf(`get attestation first-artifact-attestation --flow %s --org orgX`, suite.flowName),
golden: "Error: --api-token is not set\nUsage: kosli get attestation ATTESTATION-NAME [flags]\n",
},
{
name: "getting an existing trail attestation works",
cmd: fmt.Sprintf(`get attestation first-trail-attestation --flow %s --trail %s %s`, suite.flowName, suite.trailName, suite.defaultKosliArguments),
},
{
name: "getting an existing trail attestation with --output json works",
cmd: fmt.Sprintf(`get attestation first-trail-attestation --flow %s --trail %s --output json %s`, suite.flowName, suite.trailName, suite.defaultKosliArguments),
},
{
name: "getting an existing artifact attestation works",
cmd: fmt.Sprintf(`get attestation first-artifact-attestation --flow %s --fingerprint %s %s`, suite.flowName, suite.fingerprint, suite.defaultKosliArguments),
},
{
name: "getting an existing artifact attestation with --output json works",
cmd: fmt.Sprintf(`get attestation first-artifact-attestation --flow %s --fingerprint %s --output json %s`, suite.flowName, suite.fingerprint, suite.defaultKosliArguments),
},
{
wantError: true,
name: "missing both trail and fingerprint fails",
cmd: fmt.Sprintf(`get attestation first-artifact-attestation --flow %s %s`, suite.flowName, suite.defaultKosliArguments),
golden: "Error: at least one of --trail, --fingerprint is required\n",
},
{
wantError: true,
name: "providing both trail and fingerprint fails",
cmd: fmt.Sprintf(`get attestation first-artifact-attestation --flow %s --trail %s --fingerprint %s %s`, suite.flowName, suite.trailName, suite.fingerprint, suite.defaultKosliArguments),
golden: "Error: only one of --trail, --fingerprint is allowed\n",
},
}

runTestCmd(suite.Suite.T(), tests)
}

// In order for 'go test' to run this suite, we need to create
// a normal test function and pass our suite to suite.Run
func TestGetAttestationCommandTestSuite(t *testing.T) {
suite.Run(t, new(GetAttestationCommandTestSuite))
}
2 changes: 2 additions & 0 deletions cmd/kosli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,8 @@ The ^.kosli_ignore^ will be treated as part of the artifact like any other file,
attestationTypeJqFlag = "[optional] The attestation type evaluation JQ rules."
envNameFlag = "The Kosli environment name to assert the artifact against."
pathsWatchFlag = "[optional] Watch the filesystem for changes and report snapshots of artifacts running in specific filesystem paths to Kosli."
getAttestationFingerprintFlag = "[conditional] The fingerprint of the artifact for the attestation. Cannot be used together with --trail."
getAttestationTrailFlag = "[conditional] The name of the Kosli trailfor the attestation. Cannot be used together with --fingerprint."
)

var global *GlobalOpts
Expand Down
48 changes: 48 additions & 0 deletions cmd/kosli/testHelpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"strings"
"testing"

"github.com/kosli-dev/cli/internal/gitview"
shellwords "github.com/mattn/go-shellwords"
"github.com/pkg/errors"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -415,3 +416,50 @@ func CreatePolicy(org, policyName string, t *testing.T) {
err := o.run([]string{policyName, "testdata/policy-files/test-policy.yml"})
require.NoError(t, err, "policy should be created without error")
}

func CreateGenericArtifactAttestation(flowName, trailName, fingerprint, attestationName string, t *testing.T) {
t.Helper()
o := &attestGenericOptions{
CommonAttestationOptions: &CommonAttestationOptions{
flowName: flowName,
trailName: trailName,
fingerprintOptions: &fingerprintOptions{},
attestationNameTemplate: attestationName,
},
payload: GenericAttestationPayload{
CommonAttestationPayload: &CommonAttestationPayload{
ArtifactFingerprint: fingerprint,
},
Compliant: true,
},
}
err := o.run([]string{})
require.NoError(t, err, "generic artifact attestation should be created without error")
}

func CreateGenericTrailAttestation(flowName, trailName, attestationName string, t *testing.T) {
t.Helper()

o := &attestGenericOptions{
CommonAttestationOptions: &CommonAttestationOptions{
flowName: flowName,
trailName: trailName,
fingerprintOptions: &fingerprintOptions{},
attestationNameTemplate: attestationName,
},
payload: GenericAttestationPayload{
CommonAttestationPayload: &CommonAttestationPayload{
Commit: &gitview.BasicCommitInfo{
Sha1: "0fc1ba9876f91b215679f3649b8668085d820ab5",
Message: "test commit",
Author: "test author",
Timestamp: 1234567890,
Branch: "test branch",
},
},
Compliant: true,
},
}
err := o.run([]string{})
require.NoError(t, err, "generic artifact attestation should be created without error")
}