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/attest.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func newAttestCmd(out io.Writer) *cobra.Command {
newAttestSonarCmd(out),
newAttestCustomCmd(out),
newAttestOverrideCmd(out),
newAttestDecisionCmd(out),
)
return cmd
}
187 changes: 187 additions & 0 deletions cmd/kosli/attestDecision.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package main

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

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

type DecisionAttestationData struct {
Compliant bool `json:"is_compliant"`
}

type DecisionAttestationPayload struct {
*CommonAttestationPayload
TypeName string `json:"type_name"`
Control string `json:"control"`
AttestationData DecisionAttestationData `json:"attestation_data"`
}

type attestDecisionOptions struct {
*CommonAttestationOptions
payload DecisionAttestationPayload
}

const attestDecisionShortDesc = `[BETA] Record a compliance decision against a control in a Kosli trail. `

const attestDecisionLongDesc = attestDecisionShortDesc + `
Use this command to record the outcome of evaluating a control as part of your delivery
pipeline — whether it was satisfied or not — attached to a specific trail with an optional artifact.
This decision is the evidence that a governance requirement was assessed.
` + attestationBindingDesc + `

` + commitDescription

const attestDecisionExample = `
# record a compliant decision against a trail:
kosli attest decision \
--name yourAttestationName \
--flow yourFlowName \
--trail yourTrailName \
--control RCTL-043 \
--compliant=true \
--api-token yourAPIToken \
--org yourOrgName

# record a non-compliant decision against a trail:
kosli attest decision \
--name yourAttestationName \
--flow yourFlowName \
--trail yourTrailName \
--control RCTL-043 \
--compliant=false \
--api-token yourAPIToken \
--org yourOrgName

# record a decision linked to a specific artifact (by fingerprint):
kosli attest decision \
--name yourAttestationName \
--flow yourFlowName \
--trail yourTrailName \
--control RCTL-043 \
--compliant=true \
--fingerprint yourArtifactFingerprint \
--api-token yourAPIToken \
--org yourOrgName

# record a decision with an evidence attachment:
kosli attest decision \
--name yourAttestationName \
--flow yourFlowName \
--trail yourTrailName \
--control RCTL-043 \
--compliant=true \
--attachments eval-report.json \
--api-token yourAPIToken \
--org yourOrgName
`

func newAttestDecisionCmd(out io.Writer) *cobra.Command {
o := &attestDecisionOptions{
CommonAttestationOptions: &CommonAttestationOptions{
fingerprintOptions: &fingerprintOptions{},
},
payload: DecisionAttestationPayload{
CommonAttestationPayload: &CommonAttestationPayload{},
TypeName: "decision",
},
}
cmd := &cobra.Command{
Use: "decision [IMAGE-NAME | FILE-PATH | DIR-PATH]",
Short: attestDecisionShortDesc,
Long: attestDecisionLongDesc,
Example: attestDecisionExample,
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())
}

if !cmd.Flags().Changed("compliant") {
return fmt.Errorf(`required flag(s) "compliant" not set`)
}

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 {
o.repoURLExplicit = cmd.Flags().Changed("repo-url")
return o.run(args)
},
}

ci := WhichCI()
addAttestationFlags(cmd, o.CommonAttestationOptions, o.payload.CommonAttestationPayload, ci)
cmd.Flags().StringVar(&o.payload.Control, "control", "", attestationDecisionControlFlag)
cmd.Flags().BoolVarP(&o.payload.AttestationData.Compliant, "compliant", "C", false, attestationCompliantFlag)
Comment thread
pbeckham marked this conversation as resolved.

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

return cmd
}

func (o *attestDecisionOptions) run(args []string) error {
url, err := url.JoinPath(global.Host, "api/v2/attestations", global.Org, o.flowName, "trail", o.trailName, "system")
Comment thread
pbeckham marked this conversation as resolved.
if err != nil {
return err
}

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

form, cleanupNeeded, evidencePath, err := prepareAttestationForm(o.payload, o.attachments)
if err != nil {
return err
}
if cleanupNeeded {
defer func() {
if err := os.Remove(evidencePath); err != nil {
logger.Warn("failed to remove evidence file: %v", err)
}
}()
}

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("decision attestation '%s' is reported to trail: %s", o.payload.AttestationName, o.trailName)
}
return wrapAttestationError(err)
}
132 changes: 132 additions & 0 deletions cmd/kosli/attestDecision_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package main

import (
"fmt"
"testing"

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

type AttestDecisionCommandTestSuite struct {
flowName string
trailName string
artifactFingerprint string
suite.Suite
defaultKosliArguments string
}

func (suite *AttestDecisionCommandTestSuite) SetupTest() {
suite.flowName = "attest-decision"
suite.trailName = "test-123"
suite.artifactFingerprint = "7509e5bda0c762d2bac7f90d758b5b2263fa01ccbc542ab5e3df163be08e6ca9"
global = &GlobalOpts{
ApiToken: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6ImNkNzg4OTg5In0.e8i_lA_QrEhFncb05Xw6E_tkCHU9QfcY4OLTVUCHffY",
Org: "docs-cmd-test-user",
Host: "http://localhost:8001",
}
suite.defaultKosliArguments = fmt.Sprintf(" --flow %s --trail %s --repo-root ../.. --host %s --org %s --api-token %s", suite.flowName, suite.trailName, global.Host, global.Org, global.ApiToken)
CreateControl(global.Org, "RCTL-043", "Test Control", suite.T())
CreateFlowWithTemplate(suite.flowName, "testdata/valid_template.yml", suite.T())
BeginTrail(suite.trailName, suite.flowName, "", suite.T())
CreateArtifactOnTrail(suite.flowName, suite.trailName, "cli", suite.artifactFingerprint, "file1", suite.T())
}

func (suite *AttestDecisionCommandTestSuite) TestAttestDecisionCmd() {
tests := []cmdTestCase{
{
wantError: true,
name: "fails when more arguments are provided",
cmd: fmt.Sprintf("attest decision foo bar %s --control RCTL-043 --compliant=true", suite.defaultKosliArguments),
golden: "Error: accepts at most 1 arg(s), received 2 [foo bar]\n",
},
{
wantError: true,
name: "fails when missing --name flag",
cmd: fmt.Sprintf("attest decision %s --control RCTL-043 --compliant=true", suite.defaultKosliArguments),
golden: "Error: required flag(s) \"name\" not set\n",
},
{
wantError: true,
name: "fails when missing --control flag",
cmd: fmt.Sprintf("attest decision --name foo %s --compliant=true", suite.defaultKosliArguments),
golden: "Error: required flag(s) \"control\" not set\n",
},
{
wantError: true,
name: "fails when --compliant is not set",
cmd: fmt.Sprintf("attest decision --name foo --control RCTL-043 %s", suite.defaultKosliArguments),
golden: "Error: required flag(s) \"compliant\" not set\n",
},
{
wantError: true,
name: "fails when both --fingerprint and --artifact-type are set",
cmd: fmt.Sprintf("attest decision testdata/file1 --fingerprint xxxx --artifact-type file --name foo --control RCTL-043 --compliant=true %s", suite.defaultKosliArguments),
golden: "Error: only one of --fingerprint, --artifact-type is allowed\n",
},
{
wantError: true,
name: "fails when --fingerprint is not a valid SHA256",
cmd: fmt.Sprintf("attest decision --name foo --fingerprint xxxx --control RCTL-043 --compliant=true %s", suite.defaultKosliArguments),
golden: "Error: xxxx is not a valid SHA256 fingerprint. It should match the pattern ^([a-f0-9]{64})$\nUsage: kosli attest decision [IMAGE-NAME | FILE-PATH | DIR-PATH] [flags]\n",
},
{
wantError: true,
name: "fails when --name is passed as empty string",
cmd: fmt.Sprintf("attest decision --name \"\" --control RCTL-043 --compliant=true %s", suite.defaultKosliArguments),
golden: "Error: flag '--name' is required, but empty string was provided\n",
},
{
name: "can record a compliant decision against a trail",
cmd: fmt.Sprintf("attest decision --name foo --control RCTL-043 --compliant=true %s", suite.defaultKosliArguments),
golden: "decision attestation 'foo' is reported to trail: test-123\n",
},
{
name: "can record a non-compliant decision against a trail",
cmd: fmt.Sprintf("attest decision --name foo --control RCTL-043 --compliant=false %s", suite.defaultKosliArguments),
golden: "decision attestation 'foo' is reported to trail: test-123\n",
},
{
name: "can record a decision linked to a specific artifact by fingerprint",
cmd: fmt.Sprintf("attest decision --fingerprint 7509e5bda0c762d2bac7f90d758b5b2263fa01ccbc542ab5e3df163be08e6ca9 --name foo --control RCTL-043 --compliant=true %s", suite.defaultKosliArguments),
golden: "decision attestation 'foo' is reported to trail: test-123\n",
},
{
name: "can record a decision with an attachment",
cmd: fmt.Sprintf("attest decision --name foo --control RCTL-043 --compliant=true --attachments testdata/file1 %s", suite.defaultKosliArguments),
golden: "decision attestation 'foo' is reported to trail: test-123\n",
},
{
name: "can record a decision with description",
cmd: fmt.Sprintf("attest decision --name foo --control RCTL-043 --compliant=true --description 'evaluation passed' %s", suite.defaultKosliArguments),
golden: "decision attestation 'foo' is reported to trail: test-123\n",
},
{
name: "can record a decision with annotations",
cmd: fmt.Sprintf("attest decision --name foo --control RCTL-043 --compliant=true --annotate key=value %s", suite.defaultKosliArguments),
golden: "decision attestation 'foo' is reported to trail: test-123\n",
},
{
name: "can record a decision with user data",
cmd: fmt.Sprintf("attest decision --name foo --control RCTL-043 --compliant=true --user-data testdata/person-type-data-example.json %s", suite.defaultKosliArguments),
golden: "decision attestation 'foo' is reported to trail: test-123\n",
},
{
wantError: true,
name: "fails when annotation key is invalid",
cmd: fmt.Sprintf("attest decision --name foo --control RCTL-043 --compliant=true --annotate foo.bar=baz %s", suite.defaultKosliArguments),
golden: "Error: --annotate flag should be in the format key=value. Invalid key: 'foo.bar'. Key can only contain [A-Za-z0-9_]\n",
},
{
wantError: true,
name: "fails when --name has invalid dot format",
cmd: fmt.Sprintf("attest decision --name .foo --control RCTL-043 --compliant=true %s", suite.defaultKosliArguments),
golden: "Error: failed to parse attestation name: invalid attestation name format: .foo\n",
},
}

runTestCmd(suite.T(), tests)
}

func TestAttestDecisionCommandTestSuite(t *testing.T) {
suite.Run(t, new(AttestDecisionCommandTestSuite))
}
1 change: 1 addition & 0 deletions cmd/kosli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ The ^.kosli_ignore^ will be treated as part of the artifact like any other file,
attestationOverrideReasonFlag = "The reason for overriding the attestation."
newComplianceStatusFlag = "The new compliance status to set on the attestation. A boolean flag https://docs.kosli.com/faq/#boolean-flags"
originalAttestationTypeFlag = "The original attestation type being overridden (e.g. generic, snyk, junit, sonar, jira, pull_request, custom)."
attestationDecisionControlFlag = "The control identifier being evaluated (e.g. RCTL-043)."
excludeScalingFlag = "[optional] Exclude scaling events for snapshots. Snapshots with scaling changes will not result in new environment records."
includeScalingFlag = "[optional] Include scaling events for snapshots. Snapshots with scaling changes will result in new environment records."
includedEnvironments = "[optional] Comma separated list of environments to include in logical environment"
Expand Down
19 changes: 19 additions & 0 deletions cmd/kosli/testHelpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"regexp"
Expand All @@ -14,6 +16,7 @@ import (
"testing"

"github.com/kosli-dev/cli/internal/gitview"
"github.com/kosli-dev/cli/internal/requests"
shellwords "github.com/mattn/go-shellwords"
"github.com/pkg/errors"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -518,6 +521,22 @@ func UnSetEnvVars(envVars map[string]string, t *testing.T) {
}
}

// CreateControl creates a control in the org via the API.
func CreateControl(org, identifier, name string, t *testing.T) {
t.Helper()
u, err := url.JoinPath(global.Host, "api/v2/controls", org)
require.NoError(t, err, "control URL should be constructed without error")

reqParams := &requests.RequestParams{
Method: http.MethodPost,
URL: u,
Payload: map[string]string{"identifier": identifier, "name": name},
Token: global.ApiToken,
}
_, err = kosliClient.Do(reqParams)
require.NoError(t, err, "control should be created without error")
}

// CreatePolicy creates a policy on the server
Comment on lines 521 to 540
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: the other helpers like CreateFlow / BeginTrail use command run() methods, which are idempotent on the server side (PUT semantics or "create if not exists"). This helper does a raw POST /api/v2/controls/{org}, which may return an error if a control with this identifier already exists — e.g., if SetupTest() runs multiple times in the same test process.

If the server's control endpoint is idempotent on re-creation, this is fine. Otherwise, consider either checking for existence first, or ignoring "already exists" errors. Worth verifying against the server.

func CreatePolicy(org, policyName string, t *testing.T) {
t.Helper()
Expand Down
Loading