Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add attest custom command #394

Merged
merged 4 commits into from
Dec 20, 2024
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/attest.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ func newAttestCmd(out io.Writer) *cobra.Command {
newAttestJiraCmd(out),
newAttestPRCmd(out),
newAttestSonarCmd(out),
newAttestCustomCmd(out),
)
return cmd
}
2 changes: 1 addition & 1 deletion cmd/kosli/attestArtifact.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ func (o *attestArtifactOptions) run(args []string) error {
return err
}

o.payload.Annotations, err = proccessAnnotations(o.annotations)
o.payload.Annotations, err = processAnnotations(o.annotations)
if err != nil {
return err
}
Expand Down
186 changes: 186 additions & 0 deletions cmd/kosli/attestCustom.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package main

import (
"fmt"
"io"
"net/http"
"os"

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

type CustomAttestationPayload struct {
*CommonAttestationPayload
TypeName string `json:"type_name"`
AttestationData interface{} `json:"attestation_data"`
}

type attestCustomOptions struct {
*CommonAttestationOptions
attestationDataFile string
payload CustomAttestationPayload
}

const attestCustomShortDesc = `Report a custom attestation to an artifact or a trail in a Kosli flow. `

const attestCustomLongDesc = attestCustomShortDesc + attestationBindingDesc + `

` + commitDescription

const attestCustomExample = `
# report a custom attestation about a pre-built container image artifact (kosli finds the fingerprint):
kosli attest custom yourDockerImageName \
--artifact-type oci \
--type customTypeName \
--name yourAttestationName \
--data yourCustomData \
--flow yourFlowName \
--trail yourTrailName \
--api-token yourAPIToken \
--org yourOrgName

# report a custom attestation about a pre-built docker artifact (you provide the fingerprint):
kosli attest custom \
--fingerprint yourDockerImageFingerprint \
--type customTypeName \
--name yourAttestationName \
--data yourCustomData \
--flow yourFlowName \
--trail yourTrailName \
--api-token yourAPIToken \
--org yourOrgName

# report a custom attestation about a trail:
kosli attest custom \
--type customTypeName \
--name yourAttestationName \
--data yourCustomData \
--flow yourFlowName \
--trail yourTrailName \
--api-token yourAPIToken \
--org yourOrgName

# report a custom attestation about an artifact which has not been reported yet in a trail:
kosli attest custom \
--type customTypeName \
--name yourTemplateArtifactName.yourAttestationName \
--data yourCustomData \
--flow yourFlowName \
--trail yourTrailName \
--commit yourArtifactGitCommit \
--api-token yourAPIToken \
--org yourOrgName

# report a custom attestation about a trail with an attachment:
kosli attest custom \
--type customTypeName \
--name yourAttestationName \
--data yourCustomData \
--flow yourFlowName \
--trail yourTrailName \
--attachments yourAttachmentPathName \
--api-token yourAPIToken \
--org yourOrgName
`

func newAttestCustomCmd(out io.Writer) *cobra.Command {
o := &attestCustomOptions{
CommonAttestationOptions: &CommonAttestationOptions{
fingerprintOptions: &fingerprintOptions{},
},
payload: CustomAttestationPayload{
CommonAttestationPayload: &CommonAttestationPayload{},
},
}
cmd := &cobra.Command{
// Args: cobra.MaximumNArgs(1), // See CustomMaximumNArgs() below
Use: "custom [IMAGE-NAME | FILE-PATH | DIR-PATH]",
Short: attestCustomShortDesc,
Long: attestCustomLongDesc,
Example: attestCustomExample,
Hidden: true,
PreRunE: func(cmd *cobra.Command, args []string) error {

err := CustomMaximumNArgs(1, args)
if err != nil {
return err
}

err = RequireGlobalFlags(global, []string{"Org", "ApiToken"})
if err != nil {
return ErrorBeforePrintingUsage(cmd, err.Error())
}

err = MuXRequiredFlags(cmd, []string{"fingerprint", "artifact-type"}, false)
if err != nil {
return err
}

err = ValidateSliceValues(o.redactedCommitInfo, allowedCommitRedactionValues)
if err != nil {
return fmt.Errorf("%s for --redact-commit-info", err.Error())
}

err = ValidateAttestationArtifactArg(args, o.fingerprintOptions.artifactType, o.payload.ArtifactFingerprint)
if err != nil {
return ErrorBeforePrintingUsage(cmd, err.Error())
}

return ValidateRegistryFlags(cmd, o.fingerprintOptions)

},

RunE: func(cmd *cobra.Command, args []string) error {
return o.run(args)
},
}

ci := WhichCI()
addAttestationFlags(cmd, o.CommonAttestationOptions, o.payload.CommonAttestationPayload, ci)
cmd.Flags().StringVar(&o.payload.TypeName, "type", "", attestationCustomTypeNameFlag)
cmd.Flags().StringVar(&o.attestationDataFile, "attestation-data", "", attestationCustomDataFileFlag)

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

return cmd
}

func (o *attestCustomOptions) run(args []string) error {
url := fmt.Sprintf("%s/api/v2/attestations/%s/%s/trail/%s/custom", global.Host, global.Org, o.flowName, o.trailName)

err := o.CommonAttestationOptions.run(args, o.payload.CommonAttestationPayload)
if err != nil {
return err
}

o.payload.AttestationData, err = LoadJsonData(o.attestationDataFile)
if err != nil {
return fmt.Errorf("failed to load attestation data. %s", err)
}

form, cleanupNeeded, evidencePath, err := prepareAttestationForm(o.payload, o.attachments)
if err != nil {
return err
}
// if we created a tar package, remove it after uploading it
if cleanupNeeded {
defer os.Remove(evidencePath)
}

reqParams := &requests.RequestParams{
Method: http.MethodPost,
URL: url,
Form: form,
DryRun: global.DryRun,
Token: global.ApiToken,
}
_, err = kosliClient.Do(reqParams)
if err == nil && !global.DryRun {
logger.Info("custom:%s attestation '%s' is reported to trail: %s", o.payload.TypeName, o.payload.AttestationName, o.trailName)
}
return wrapAttestationError(err)
}
170 changes: 170 additions & 0 deletions cmd/kosli/attestCustom_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package main

import (
"fmt"
"testing"

"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 AttestCustomCommandTestSuite struct {
flowName string
trailName string
artifactFingerprint string
typeName string
schemaFilePath string
jqRules []string
attestationDataFile string
suite.Suite
defaultKosliArguments string
}

func (suite *AttestCustomCommandTestSuite) SetupTest() {
suite.flowName = "attest-custom"
suite.trailName = "test-321"
suite.artifactFingerprint = "7509e5bda0c762d2bac7f90d758b5b2263fa01ccbc542ab5e3df163be08e6ca9"
suite.typeName = "person"
suite.schemaFilePath = ""
suite.attestationDataFile = "testdata/person-type-data-example.json"
suite.jqRules = []string{}
global = &GlobalOpts{
ApiToken: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6ImNkNzg4OTg5In0.e8i_lA_QrEhFncb05Xw6E_tkCHU9QfcY4OLTVUCHffY",
Org: "docs-cmd-test-user",
Host: "http://localhost:8001",
}
suite.defaultKosliArguments = fmt.Sprintf(" --type %s --attestation-data %s --flow %s --trail %s --repo-root ../.. --host %s --org %s --api-token %s", suite.typeName, suite.attestationDataFile, suite.flowName, suite.trailName, global.Host, global.Org, global.ApiToken)

CreateCustomAttestationType(suite.typeName, suite.schemaFilePath, suite.jqRules, suite.T())
CreateFlow(suite.flowName, suite.T())
CreateArtifactOnTrail(suite.flowName, suite.trailName, "cli", suite.artifactFingerprint, "file1", suite.T())
}

func (suite *AttestCustomCommandTestSuite) TestAttestCustomCmd() {
tests := []cmdTestCase{
{
wantError: true,
name: "fails when more arguments are provided",
cmd: fmt.Sprintf("attest custom foo bar %s", suite.defaultKosliArguments),
golden: "Error: accepts at most 1 arg(s), received 2 [foo bar]\n",
},
{
wantError: true,
name: "fails when missing a required flag",
cmd: fmt.Sprintf("attest custom foo -t file %s", suite.defaultKosliArguments),
golden: "Error: required flag(s) \"name\" not set\n",
},
{
wantError: true,
name: "fails when artifact-name is provided and there is no --artifact-type",
cmd: fmt.Sprintf("attest custom wibble %s", suite.defaultKosliArguments),
golden: "Error: --artifact-type or --fingerprint must be specified when artifact name ('wibble') argument is supplied.\nUsage: kosli attest custom [IMAGE-NAME | FILE-PATH | DIR-PATH] [flags]\n",
},
{
wantError: true,
name: "fails when both --fingerprint and --artifact-type",
cmd: fmt.Sprintf("attest custom foo --fingerprint xxxx --artifact-type file --name bar --commit HEAD --origin-url http://example.com %s", suite.defaultKosliArguments),
golden: "Error: only one of --fingerprint, --artifact-type is allowed\n",
},
{
wantError: true,
name: "fails when --fingerprint is not valid",
cmd: fmt.Sprintf("attest custom --name foo --fingerprint xxxx --commit HEAD --origin-url http://example.com %s", suite.defaultKosliArguments),
golden: "Error: xxxx is not a valid SHA256 fingerprint. It should match the pattern ^([a-f0-9]{64})$\nUsage: kosli attest custom [IMAGE-NAME | FILE-PATH | DIR-PATH] [flags]\n",
},
{
wantError: true,
name: "attesting against an artifact that does not exist fails",
cmd: fmt.Sprintf("attest custom --fingerprint 3214e5bda0c762d2bac7f90d758b5b2263fa01ccbc542ab5e3df163be08e6ca9 --name foo --commit HEAD --origin-url http://example.com %s", suite.defaultKosliArguments),
golden: "Error: Artifact with fingerprint 3214e5bda0c762d2bac7f90d758b5b2263fa01ccbc542ab5e3df163be08e6ca9 does not exist in trail \"test-321\" of flow \"attest-custom\" belonging to organization \"docs-cmd-test-user\"\n",
},
{
wantError: true,
name: "fails when --name is passed as empty string",
cmd: fmt.Sprintf("attest custom --name \"\" --commit HEAD --origin-url http://example.com %s", suite.defaultKosliArguments),
golden: "Error: flag '--name' is required, but empty string was provided\n",
},
{
name: "can attest custom against an artifact using artifact-name and --fingerprint",
cmd: fmt.Sprintf("attest custom testdata/file1 %s --name foo --fingerprint %s", suite.defaultKosliArguments, suite.artifactFingerprint),
golden: "custom:person attestation 'foo' is reported to trail: test-321\n",
},
{
name: "can attest custom against an artifact using artifact name and --artifact-type",
cmd: fmt.Sprintf("attest custom testdata/file1 --artifact-type file --name foo --commit HEAD --origin-url http://example.com %s", suite.defaultKosliArguments),
golden: "custom:person attestation 'foo' is reported to trail: test-321\n",
},
{
name: "can attest custom against an artifact using artifact name and --artifact-type when --name does not exist in the trail template",
cmd: fmt.Sprintf("attest custom testdata/file1 --artifact-type file --name bar --commit HEAD --origin-url http://example.com %s", suite.defaultKosliArguments),
golden: "custom:person attestation 'bar' is reported to trail: test-321\n",
},
{
name: "can attest custom against an artifact using --fingerprint and no artifact-name",
cmd: fmt.Sprintf("attest custom --fingerprint %s --name foo --commit HEAD --origin-url http://example.com %s", suite.artifactFingerprint, suite.defaultKosliArguments),
golden: "custom:person attestation 'foo' is reported to trail: test-321\n",
},
{
name: "can attest custom against a trail",
cmd: fmt.Sprintf("attest custom --name bar --commit HEAD --origin-url http://example.com %s", suite.defaultKosliArguments),
golden: "custom:person attestation 'bar' is reported to trail: test-321\n",
},
{
name: "can attest custom against a trail when name is not found in the trail template",
cmd: fmt.Sprintf("attest custom --name additional --commit HEAD --origin-url http://example.com %s", suite.defaultKosliArguments),
golden: "custom:person attestation 'additional' is reported to trail: test-321\n",
},
{
name: "can attest custom against an artifact it is created using dot syntax in --name",
cmd: fmt.Sprintf("attest custom --name cli.foo --commit HEAD --origin-url http://example.com %s", suite.defaultKosliArguments),
golden: "custom:person attestation 'foo' is reported to trail: test-321\n",
},
{
name: "can attest custom attestation with attachment against a trail",
cmd: fmt.Sprintf("attest custom --name bar --attachments testdata/file1 %s", suite.defaultKosliArguments),
golden: "custom:person attestation 'bar' is reported to trail: test-321\n",
},
{
name: "can attest custom attestation with external-url against a trail",
cmd: fmt.Sprintf("attest custom --name bar --external-url foo=https://foo.com %s", suite.defaultKosliArguments),
golden: "custom:person attestation 'bar' is reported to trail: test-321\n",
},
{
name: "can attest custom attestation with external-url and external-fingerprint against a trail",
cmd: fmt.Sprintf("attest custom --name bar --external-url file=https://foo.com/file --external-fingerprint file=7509e5bda0c762d2bac7f90d758b5b2263fa01ccbc542ab5e3df163be08e6ca9 %s", suite.defaultKosliArguments),
golden: "custom:person attestation 'bar' is reported to trail: test-321\n",
},
{
wantError: true,
name: "fails when external-fingerprint has more items more than external-url",
cmd: fmt.Sprintf("attest custom --name bar --external-fingerprint file=7509e5bda0c762d2bac7f90d758b5b2263fa01ccbc542ab5e3df163be08e6ca9 %s", suite.defaultKosliArguments),
golden: "Error: --external-fingerprints have labels that don't have a URL in --external-url\n",
},
{
name: "can attest custom attestation with description against a trail",
cmd: fmt.Sprintf("attest custom --name bar --description 'foo bar foo' %s", suite.defaultKosliArguments),
golden: "custom:person attestation 'bar' is reported to trail: test-321\n",
},
{
name: "can attest with annotations against a trail",
cmd: fmt.Sprintf("attest custom --name bar --annotate foo=bar --annotate baz=\"data with spaces\" %s", suite.defaultKosliArguments),
golden: "custom:person attestation 'bar' is reported to trail: test-321\n",
},
{
wantError: true,
name: "fails when annotation is not valid",
cmd: fmt.Sprintf("attest custom --name bar --annotate foo.baz=bar %s", suite.defaultKosliArguments),
golden: "Error: --annotate flag should be in the format key=value. Invalid key: 'foo.baz'. Key can only contain [A-Za-z0-9_].\n",
},
}

runTestCmd(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 TestAttestCustomCommandTestSuite(t *testing.T) {
suite.Run(t, new(AttestCustomCommandTestSuite))
}
4 changes: 2 additions & 2 deletions cmd/kosli/attestation.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,11 @@ func (o *CommonAttestationOptions) run(args []string, payload *CommonAttestation
}

// process annotations
payload.Annotations, err = proccessAnnotations(o.annotations)
payload.Annotations, err = processAnnotations(o.annotations)
return err
}

func proccessAnnotations(annotations map[string]string) (map[string]string, error) {
func processAnnotations(annotations map[string]string) (map[string]string, error) {
for label := range annotations {
if !regexp.MustCompile(`^[A-Za-z0-9_]+$`).MatchString(label) {
return nil, fmt.Errorf("--annotate flag should be in the format key=value. Invalid key: '%s'. Key can only contain [A-Za-z0-9_].", label)
Expand Down
2 changes: 2 additions & 0 deletions cmd/kosli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,8 @@ The ^.kosli_ignore^ will be treated as part of the artifact like any other file,
attestationNameFlag = "The name of the attestation as declared in the flow or trail yaml template."
attestationCompliantFlag = "[defaulted] Whether the attestation is compliant or not. A boolean flag https://docs.kosli.com/faq/#boolean-flags"
attestationRepoRootFlag = "[defaulted] The directory where the source git repository is available. Only used if --commit is used."
attestationCustomTypeNameFlag = "The name of the custom attestation type."
attestationCustomDataFileFlag = "The filepath of a json file containing the custom attestation data."
uploadJunitResultsFlag = "[defaulted] Whether to upload the provided Junit results directory as an attachment to Kosli or not."
uploadSnykResultsFlag = "[defaulted] Whether to upload the provided Snyk results file as an attachment to Kosli or not."
attestationAssertFlag = "[optional] Exit with non-zero code if the attestation is non-compliant"
Expand Down
Loading
Loading