Skip to content

Commit 1dc2266

Browse files
sami-alajramitooky
andauthored
add attest custom command (#394)
* WIP - creating tests * add attest custom command * enable tests for attest custom --------- Co-authored-by: Steve Tooke <[email protected]>
1 parent 2371a23 commit 1dc2266

File tree

8 files changed

+381
-5
lines changed

8 files changed

+381
-5
lines changed

cmd/kosli/attest.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ func newAttestCmd(out io.Writer) *cobra.Command {
2424
newAttestJiraCmd(out),
2525
newAttestPRCmd(out),
2626
newAttestSonarCmd(out),
27+
newAttestCustomCmd(out),
2728
)
2829
return cmd
2930
}

cmd/kosli/attestArtifact.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ func (o *attestArtifactOptions) run(args []string) error {
167167
return err
168168
}
169169

170-
o.payload.Annotations, err = proccessAnnotations(o.annotations)
170+
o.payload.Annotations, err = processAnnotations(o.annotations)
171171
if err != nil {
172172
return err
173173
}

cmd/kosli/attestCustom.go

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"net/http"
7+
"os"
8+
9+
"github.com/kosli-dev/cli/internal/requests"
10+
"github.com/spf13/cobra"
11+
)
12+
13+
type CustomAttestationPayload struct {
14+
*CommonAttestationPayload
15+
TypeName string `json:"type_name"`
16+
AttestationData interface{} `json:"attestation_data"`
17+
}
18+
19+
type attestCustomOptions struct {
20+
*CommonAttestationOptions
21+
attestationDataFile string
22+
payload CustomAttestationPayload
23+
}
24+
25+
const attestCustomShortDesc = `Report a custom attestation to an artifact or a trail in a Kosli flow. `
26+
27+
const attestCustomLongDesc = attestCustomShortDesc + attestationBindingDesc + `
28+
29+
` + commitDescription
30+
31+
const attestCustomExample = `
32+
# report a custom attestation about a pre-built container image artifact (kosli finds the fingerprint):
33+
kosli attest custom yourDockerImageName \
34+
--artifact-type oci \
35+
--type customTypeName \
36+
--name yourAttestationName \
37+
--data yourCustomData \
38+
--flow yourFlowName \
39+
--trail yourTrailName \
40+
--api-token yourAPIToken \
41+
--org yourOrgName
42+
43+
# report a custom attestation about a pre-built docker artifact (you provide the fingerprint):
44+
kosli attest custom \
45+
--fingerprint yourDockerImageFingerprint \
46+
--type customTypeName \
47+
--name yourAttestationName \
48+
--data yourCustomData \
49+
--flow yourFlowName \
50+
--trail yourTrailName \
51+
--api-token yourAPIToken \
52+
--org yourOrgName
53+
54+
# report a custom attestation about a trail:
55+
kosli attest custom \
56+
--type customTypeName \
57+
--name yourAttestationName \
58+
--data yourCustomData \
59+
--flow yourFlowName \
60+
--trail yourTrailName \
61+
--api-token yourAPIToken \
62+
--org yourOrgName
63+
64+
# report a custom attestation about an artifact which has not been reported yet in a trail:
65+
kosli attest custom \
66+
--type customTypeName \
67+
--name yourTemplateArtifactName.yourAttestationName \
68+
--data yourCustomData \
69+
--flow yourFlowName \
70+
--trail yourTrailName \
71+
--commit yourArtifactGitCommit \
72+
--api-token yourAPIToken \
73+
--org yourOrgName
74+
75+
# report a custom attestation about a trail with an attachment:
76+
kosli attest custom \
77+
--type customTypeName \
78+
--name yourAttestationName \
79+
--data yourCustomData \
80+
--flow yourFlowName \
81+
--trail yourTrailName \
82+
--attachments yourAttachmentPathName \
83+
--api-token yourAPIToken \
84+
--org yourOrgName
85+
`
86+
87+
func newAttestCustomCmd(out io.Writer) *cobra.Command {
88+
o := &attestCustomOptions{
89+
CommonAttestationOptions: &CommonAttestationOptions{
90+
fingerprintOptions: &fingerprintOptions{},
91+
},
92+
payload: CustomAttestationPayload{
93+
CommonAttestationPayload: &CommonAttestationPayload{},
94+
},
95+
}
96+
cmd := &cobra.Command{
97+
// Args: cobra.MaximumNArgs(1), // See CustomMaximumNArgs() below
98+
Use: "custom [IMAGE-NAME | FILE-PATH | DIR-PATH]",
99+
Short: attestCustomShortDesc,
100+
Long: attestCustomLongDesc,
101+
Example: attestCustomExample,
102+
Hidden: true,
103+
PreRunE: func(cmd *cobra.Command, args []string) error {
104+
105+
err := CustomMaximumNArgs(1, args)
106+
if err != nil {
107+
return err
108+
}
109+
110+
err = RequireGlobalFlags(global, []string{"Org", "ApiToken"})
111+
if err != nil {
112+
return ErrorBeforePrintingUsage(cmd, err.Error())
113+
}
114+
115+
err = MuXRequiredFlags(cmd, []string{"fingerprint", "artifact-type"}, false)
116+
if err != nil {
117+
return err
118+
}
119+
120+
err = ValidateSliceValues(o.redactedCommitInfo, allowedCommitRedactionValues)
121+
if err != nil {
122+
return fmt.Errorf("%s for --redact-commit-info", err.Error())
123+
}
124+
125+
err = ValidateAttestationArtifactArg(args, o.fingerprintOptions.artifactType, o.payload.ArtifactFingerprint)
126+
if err != nil {
127+
return ErrorBeforePrintingUsage(cmd, err.Error())
128+
}
129+
130+
return ValidateRegistryFlags(cmd, o.fingerprintOptions)
131+
132+
},
133+
134+
RunE: func(cmd *cobra.Command, args []string) error {
135+
return o.run(args)
136+
},
137+
}
138+
139+
ci := WhichCI()
140+
addAttestationFlags(cmd, o.CommonAttestationOptions, o.payload.CommonAttestationPayload, ci)
141+
cmd.Flags().StringVar(&o.payload.TypeName, "type", "", attestationCustomTypeNameFlag)
142+
cmd.Flags().StringVar(&o.attestationDataFile, "attestation-data", "", attestationCustomDataFileFlag)
143+
144+
err := RequireFlags(cmd, []string{"type", "attestation-data", "flow", "trail", "name"})
145+
if err != nil {
146+
logger.Error("failed to configure required flags: %v", err)
147+
}
148+
149+
return cmd
150+
}
151+
152+
func (o *attestCustomOptions) run(args []string) error {
153+
url := fmt.Sprintf("%s/api/v2/attestations/%s/%s/trail/%s/custom", global.Host, global.Org, o.flowName, o.trailName)
154+
155+
err := o.CommonAttestationOptions.run(args, o.payload.CommonAttestationPayload)
156+
if err != nil {
157+
return err
158+
}
159+
160+
o.payload.AttestationData, err = LoadJsonData(o.attestationDataFile)
161+
if err != nil {
162+
return fmt.Errorf("failed to load attestation data. %s", err)
163+
}
164+
165+
form, cleanupNeeded, evidencePath, err := prepareAttestationForm(o.payload, o.attachments)
166+
if err != nil {
167+
return err
168+
}
169+
// if we created a tar package, remove it after uploading it
170+
if cleanupNeeded {
171+
defer os.Remove(evidencePath)
172+
}
173+
174+
reqParams := &requests.RequestParams{
175+
Method: http.MethodPost,
176+
URL: url,
177+
Form: form,
178+
DryRun: global.DryRun,
179+
Token: global.ApiToken,
180+
}
181+
_, err = kosliClient.Do(reqParams)
182+
if err == nil && !global.DryRun {
183+
logger.Info("custom:%s attestation '%s' is reported to trail: %s", o.payload.TypeName, o.payload.AttestationName, o.trailName)
184+
}
185+
return wrapAttestationError(err)
186+
}

cmd/kosli/attestCustom_test.go

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/stretchr/testify/suite"
8+
)
9+
10+
// Define the suite, and absorb the built-in basic suite
11+
// functionality from testify - including a T() method which
12+
// returns the current testing context
13+
type AttestCustomCommandTestSuite struct {
14+
flowName string
15+
trailName string
16+
artifactFingerprint string
17+
typeName string
18+
schemaFilePath string
19+
jqRules []string
20+
attestationDataFile string
21+
suite.Suite
22+
defaultKosliArguments string
23+
}
24+
25+
func (suite *AttestCustomCommandTestSuite) SetupTest() {
26+
suite.flowName = "attest-custom"
27+
suite.trailName = "test-321"
28+
suite.artifactFingerprint = "7509e5bda0c762d2bac7f90d758b5b2263fa01ccbc542ab5e3df163be08e6ca9"
29+
suite.typeName = "person"
30+
suite.schemaFilePath = ""
31+
suite.attestationDataFile = "testdata/person-type-data-example.json"
32+
suite.jqRules = []string{}
33+
global = &GlobalOpts{
34+
ApiToken: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6ImNkNzg4OTg5In0.e8i_lA_QrEhFncb05Xw6E_tkCHU9QfcY4OLTVUCHffY",
35+
Org: "docs-cmd-test-user",
36+
Host: "http://localhost:8001",
37+
}
38+
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)
39+
40+
CreateCustomAttestationType(suite.typeName, suite.schemaFilePath, suite.jqRules, suite.T())
41+
CreateFlow(suite.flowName, suite.T())
42+
CreateArtifactOnTrail(suite.flowName, suite.trailName, "cli", suite.artifactFingerprint, "file1", suite.T())
43+
}
44+
45+
func (suite *AttestCustomCommandTestSuite) TestAttestCustomCmd() {
46+
tests := []cmdTestCase{
47+
{
48+
wantError: true,
49+
name: "fails when more arguments are provided",
50+
cmd: fmt.Sprintf("attest custom foo bar %s", suite.defaultKosliArguments),
51+
golden: "Error: accepts at most 1 arg(s), received 2 [foo bar]\n",
52+
},
53+
{
54+
wantError: true,
55+
name: "fails when missing a required flag",
56+
cmd: fmt.Sprintf("attest custom foo -t file %s", suite.defaultKosliArguments),
57+
golden: "Error: required flag(s) \"name\" not set\n",
58+
},
59+
{
60+
wantError: true,
61+
name: "fails when artifact-name is provided and there is no --artifact-type",
62+
cmd: fmt.Sprintf("attest custom wibble %s", suite.defaultKosliArguments),
63+
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",
64+
},
65+
{
66+
wantError: true,
67+
name: "fails when both --fingerprint and --artifact-type",
68+
cmd: fmt.Sprintf("attest custom foo --fingerprint xxxx --artifact-type file --name bar --commit HEAD --origin-url http://example.com %s", suite.defaultKosliArguments),
69+
golden: "Error: only one of --fingerprint, --artifact-type is allowed\n",
70+
},
71+
{
72+
wantError: true,
73+
name: "fails when --fingerprint is not valid",
74+
cmd: fmt.Sprintf("attest custom --name foo --fingerprint xxxx --commit HEAD --origin-url http://example.com %s", suite.defaultKosliArguments),
75+
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",
76+
},
77+
{
78+
wantError: true,
79+
name: "attesting against an artifact that does not exist fails",
80+
cmd: fmt.Sprintf("attest custom --fingerprint 3214e5bda0c762d2bac7f90d758b5b2263fa01ccbc542ab5e3df163be08e6ca9 --name foo --commit HEAD --origin-url http://example.com %s", suite.defaultKosliArguments),
81+
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",
82+
},
83+
{
84+
wantError: true,
85+
name: "fails when --name is passed as empty string",
86+
cmd: fmt.Sprintf("attest custom --name \"\" --commit HEAD --origin-url http://example.com %s", suite.defaultKosliArguments),
87+
golden: "Error: flag '--name' is required, but empty string was provided\n",
88+
},
89+
{
90+
name: "can attest custom against an artifact using artifact-name and --fingerprint",
91+
cmd: fmt.Sprintf("attest custom testdata/file1 %s --name foo --fingerprint %s", suite.defaultKosliArguments, suite.artifactFingerprint),
92+
golden: "custom:person attestation 'foo' is reported to trail: test-321\n",
93+
},
94+
{
95+
name: "can attest custom against an artifact using artifact name and --artifact-type",
96+
cmd: fmt.Sprintf("attest custom testdata/file1 --artifact-type file --name foo --commit HEAD --origin-url http://example.com %s", suite.defaultKosliArguments),
97+
golden: "custom:person attestation 'foo' is reported to trail: test-321\n",
98+
},
99+
{
100+
name: "can attest custom against an artifact using artifact name and --artifact-type when --name does not exist in the trail template",
101+
cmd: fmt.Sprintf("attest custom testdata/file1 --artifact-type file --name bar --commit HEAD --origin-url http://example.com %s", suite.defaultKosliArguments),
102+
golden: "custom:person attestation 'bar' is reported to trail: test-321\n",
103+
},
104+
{
105+
name: "can attest custom against an artifact using --fingerprint and no artifact-name",
106+
cmd: fmt.Sprintf("attest custom --fingerprint %s --name foo --commit HEAD --origin-url http://example.com %s", suite.artifactFingerprint, suite.defaultKosliArguments),
107+
golden: "custom:person attestation 'foo' is reported to trail: test-321\n",
108+
},
109+
{
110+
name: "can attest custom against a trail",
111+
cmd: fmt.Sprintf("attest custom --name bar --commit HEAD --origin-url http://example.com %s", suite.defaultKosliArguments),
112+
golden: "custom:person attestation 'bar' is reported to trail: test-321\n",
113+
},
114+
{
115+
name: "can attest custom against a trail when name is not found in the trail template",
116+
cmd: fmt.Sprintf("attest custom --name additional --commit HEAD --origin-url http://example.com %s", suite.defaultKosliArguments),
117+
golden: "custom:person attestation 'additional' is reported to trail: test-321\n",
118+
},
119+
{
120+
name: "can attest custom against an artifact it is created using dot syntax in --name",
121+
cmd: fmt.Sprintf("attest custom --name cli.foo --commit HEAD --origin-url http://example.com %s", suite.defaultKosliArguments),
122+
golden: "custom:person attestation 'foo' is reported to trail: test-321\n",
123+
},
124+
{
125+
name: "can attest custom attestation with attachment against a trail",
126+
cmd: fmt.Sprintf("attest custom --name bar --attachments testdata/file1 %s", suite.defaultKosliArguments),
127+
golden: "custom:person attestation 'bar' is reported to trail: test-321\n",
128+
},
129+
{
130+
name: "can attest custom attestation with external-url against a trail",
131+
cmd: fmt.Sprintf("attest custom --name bar --external-url foo=https://foo.com %s", suite.defaultKosliArguments),
132+
golden: "custom:person attestation 'bar' is reported to trail: test-321\n",
133+
},
134+
{
135+
name: "can attest custom attestation with external-url and external-fingerprint against a trail",
136+
cmd: fmt.Sprintf("attest custom --name bar --external-url file=https://foo.com/file --external-fingerprint file=7509e5bda0c762d2bac7f90d758b5b2263fa01ccbc542ab5e3df163be08e6ca9 %s", suite.defaultKosliArguments),
137+
golden: "custom:person attestation 'bar' is reported to trail: test-321\n",
138+
},
139+
{
140+
wantError: true,
141+
name: "fails when external-fingerprint has more items more than external-url",
142+
cmd: fmt.Sprintf("attest custom --name bar --external-fingerprint file=7509e5bda0c762d2bac7f90d758b5b2263fa01ccbc542ab5e3df163be08e6ca9 %s", suite.defaultKosliArguments),
143+
golden: "Error: --external-fingerprints have labels that don't have a URL in --external-url\n",
144+
},
145+
{
146+
name: "can attest custom attestation with description against a trail",
147+
cmd: fmt.Sprintf("attest custom --name bar --description 'foo bar foo' %s", suite.defaultKosliArguments),
148+
golden: "custom:person attestation 'bar' is reported to trail: test-321\n",
149+
},
150+
{
151+
name: "can attest with annotations against a trail",
152+
cmd: fmt.Sprintf("attest custom --name bar --annotate foo=bar --annotate baz=\"data with spaces\" %s", suite.defaultKosliArguments),
153+
golden: "custom:person attestation 'bar' is reported to trail: test-321\n",
154+
},
155+
{
156+
wantError: true,
157+
name: "fails when annotation is not valid",
158+
cmd: fmt.Sprintf("attest custom --name bar --annotate foo.baz=bar %s", suite.defaultKosliArguments),
159+
golden: "Error: --annotate flag should be in the format key=value. Invalid key: 'foo.baz'. Key can only contain [A-Za-z0-9_].\n",
160+
},
161+
}
162+
163+
runTestCmd(suite.T(), tests)
164+
}
165+
166+
// In order for 'go test' to run this suite, we need to create
167+
// a normal test function and pass our suite to suite.Run
168+
func TestAttestCustomCommandTestSuite(t *testing.T) {
169+
suite.Run(t, new(AttestCustomCommandTestSuite))
170+
}

cmd/kosli/attestation.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,11 @@ func (o *CommonAttestationOptions) run(args []string, payload *CommonAttestation
9191
}
9292

9393
// process annotations
94-
payload.Annotations, err = proccessAnnotations(o.annotations)
94+
payload.Annotations, err = processAnnotations(o.annotations)
9595
return err
9696
}
9797

98-
func proccessAnnotations(annotations map[string]string) (map[string]string, error) {
98+
func processAnnotations(annotations map[string]string) (map[string]string, error) {
9999
for label := range annotations {
100100
if !regexp.MustCompile(`^[A-Za-z0-9_]+$`).MatchString(label) {
101101
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)

cmd/kosli/root.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,8 @@ The ^.kosli_ignore^ will be treated as part of the artifact like any other file,
205205
attestationNameFlag = "The name of the attestation as declared in the flow or trail yaml template."
206206
attestationCompliantFlag = "[defaulted] Whether the attestation is compliant or not. A boolean flag https://docs.kosli.com/faq/#boolean-flags"
207207
attestationRepoRootFlag = "[defaulted] The directory where the source git repository is available. Only used if --commit is used."
208+
attestationCustomTypeNameFlag = "The name of the custom attestation type."
209+
attestationCustomDataFileFlag = "The filepath of a json file containing the custom attestation data."
208210
uploadJunitResultsFlag = "[defaulted] Whether to upload the provided Junit results directory as an attachment to Kosli or not."
209211
uploadSnykResultsFlag = "[defaulted] Whether to upload the provided Snyk results file as an attachment to Kosli or not."
210212
attestationAssertFlag = "[optional] Exit with non-zero code if the attestation is non-compliant"

0 commit comments

Comments
 (0)