Skip to content

Commit eca8fdf

Browse files
authored
Added --output option to assert artifact command (#537)
* Added output option json for assert artifact command * Added test of json response in assert * Added a comment * More information in assert table output * Fixed lint * Added test
1 parent 7730624 commit eca8fdf

3 files changed

Lines changed: 96 additions & 14 deletions

File tree

cmd/kosli/assertArtifact.go

Lines changed: 77 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"net/http"
88
"net/url"
99

10+
"github.com/kosli-dev/cli/internal/output"
1011
"github.com/kosli-dev/cli/internal/requests"
1112
"github.com/spf13/cobra"
1213
)
@@ -45,6 +46,7 @@ type assertArtifactOptions struct {
4546
fingerprint string // This is calculated or provided by the user
4647
flowName string
4748
envName string
49+
output string
4850
}
4951

5052
func newAssertArtifactCmd(out io.Writer) *cobra.Command {
@@ -75,6 +77,8 @@ func newAssertArtifactCmd(out io.Writer) *cobra.Command {
7577
cmd.Flags().StringVarP(&o.fingerprint, "fingerprint", "F", "", fingerprintFlag)
7678
cmd.Flags().StringVarP(&o.flowName, "flow", "f", "", flowNameFlag)
7779
cmd.Flags().StringVar(&o.envName, "environment", "", envNameFlag)
80+
cmd.Flags().StringVarP(&o.output, "output", "o", "table", outputFlag)
81+
7882
addFingerprintFlags(cmd, o.fingerprintOptions)
7983
addDryRunFlag(cmd)
8084

@@ -116,31 +120,92 @@ func (o *assertArtifactOptions) run(out io.Writer, args []string) error {
116120
return err
117121
}
118122

123+
return output.FormattedPrint(response.Body, o.output, out, 0,
124+
map[string]output.FormatOutputFunc{
125+
"table": printAssertAsTable,
126+
"json": output.PrintJson,
127+
})
128+
}
129+
130+
func printAssertAsTable(raw string, out io.Writer, page int) error {
119131
var evaluationResult map[string]interface{}
120-
err = json.Unmarshal([]byte(response.Body), &evaluationResult)
132+
err := json.Unmarshal([]byte(raw), &evaluationResult)
121133
if err != nil {
122134
return err
123135
}
124136

137+
flow, _ := evaluationResult["flow"].(string)
138+
trail, _ := evaluationResult["trail"].(string)
125139
scope := evaluationResult["scope"].(string)
140+
complianceStatus, _ := evaluationResult["compliance_status"].(map[string]interface{})
141+
attestationsStatuses, _ := complianceStatus["attestations_statuses"].([]interface{})
126142

127143
if evaluationResult["compliant"].(bool) {
128144
logger.Info("COMPLIANT")
129-
if scope == "flow" {
130-
logger.Info("See more details at %s", evaluationResult["html_url"].(string))
131-
}
132145
} else {
133-
if scope == "flow" {
134-
return fmt.Errorf("not compliant\nSee more details at %s", evaluationResult["html_url"].(string))
135-
} else {
136-
jsonData, err := json.MarshalIndent(evaluationResult["policy_evaluations"], "", " ")
137-
if err != nil {
138-
return fmt.Errorf("error marshalling evaluation result: %v", err)
146+
logger.Info("Error: NON-COMPLIANT")
147+
}
148+
logger.Info("Flow: %v\nTrail %v", flow, trail)
149+
logger.Info("%-32v %-30v %-15v %-10v", "Attestation-name", "type", "status", "compliant")
150+
151+
for _, item := range attestationsStatuses {
152+
attestation := item.(map[string]interface{})
153+
name := attestation["attestation_name"]
154+
attType := attestation["attestation_type"]
155+
status := attestation["status"]
156+
isCompliant, _ := attestation["is_compliant"].(bool)
157+
unexpected, _ := attestation["unexpected"].(bool)
158+
unexpectedStr := ""
159+
if unexpected {
160+
unexpectedStr = "unexpected"
161+
}
162+
163+
logger.Info(" %-32v %-30v %-15v %-10v %-10v", name, attType, status, isCompliant, unexpectedStr)
164+
}
165+
if scope == "environment" {
166+
logger.Info("%-32v %-30v", "Policy-name", "status")
167+
policyEvaluations := evaluationResult["policy_evaluations"].([]interface{})
168+
for _, item := range policyEvaluations {
169+
policyEvaluation := item.(map[string]interface{})
170+
policyName := policyEvaluation["policy_name"]
171+
policyStatus := policyEvaluation["status"]
172+
logger.Info(" %-32v %-30v", policyName, policyStatus)
173+
if policyStatus != "COMPLIANT" {
174+
ruleEvaluations := policyEvaluation["rule_evaluations"].([]interface{})
175+
var failures []string
176+
for _, item2 := range ruleEvaluations {
177+
ruleEvaluation := item2.(map[string]interface{})
178+
ignored := ruleEvaluation["ignored"].(bool)
179+
satisfied, _ := ruleEvaluation["satisfied"].(bool)
180+
if !ignored && !satisfied {
181+
rule := ruleEvaluation["rule"].(map[string]interface{})
182+
resolutions := ruleEvaluation["resolutions"].([]interface{})
183+
for _, item3 := range resolutions {
184+
resolution := item3.(map[string]interface{})
185+
resolutionType := resolution["type"].(string)
186+
ruleDefinition := rule["definition"].(map[string]interface{})
187+
attestationName := ruleDefinition["name"]
188+
attestationType := ruleDefinition["type"]
189+
switch resolutionType {
190+
case "legacy_flow":
191+
failures = append(failures, "artifact comes from a legacy flow and does not have the new attestations")
192+
case "missing_attestation":
193+
failures = append(failures, fmt.Sprintf("artifact is missing required '%v' (type: %v) attestation in trail", attestationName, attestationType))
194+
case "non_compliant_attestation":
195+
failures = append(failures, fmt.Sprintf("attestation '%v' is non-compliant in trail", attestationName))
196+
case "non_compliant_in_trail":
197+
failures = append(failures, "artifact is not compliant in trail")
198+
}
199+
}
200+
}
201+
}
202+
for _, fail := range failures {
203+
logger.Info(" %v", fail)
204+
}
139205
}
140-
return fmt.Errorf("not compliant for env [%s]: \n %v", o.envName,
141-
string(jsonData))
142206
}
143207
}
208+
logger.Info("\nSee more details at %s", evaluationResult["html_url"].(string))
144209

145210
return nil
146211
}

cmd/kosli/assertArtifact_test.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@ type AssertArtifactCommandTestSuite struct {
1515
suite.Suite
1616
defaultKosliArguments string
1717
flowName string
18+
envName string
1819
artifactName string
1920
artifactPath string
2021
fingerprint string
2122
}
2223

2324
func (suite *AssertArtifactCommandTestSuite) SetupTest() {
2425
suite.flowName = "assert-artifact"
26+
suite.envName = "assert-artifact-environment"
2527
suite.artifactName = "arti"
2628
suite.artifactPath = "testdata/folder1/hello.txt"
2729
global = &GlobalOpts{
@@ -35,6 +37,7 @@ func (suite *AssertArtifactCommandTestSuite) SetupTest() {
3537
fingerprintOptions := &fingerprintOptions{
3638
artifactType: "file",
3739
}
40+
CreateEnv(global.Org, suite.envName, "server", suite.Suite.T())
3841
var err error
3942
suite.fingerprint, err = GetSha256Digest(suite.artifactPath, fingerprintOptions, logger)
4043
require.NoError(suite.Suite.T(), err)
@@ -58,12 +61,25 @@ func (suite *AssertArtifactCommandTestSuite) TestAssertArtifactCmd() {
5861
{
5962
name: "asserting an existing compliant artifact (using --fingerprint) results in OK and zero exit",
6063
cmd: fmt.Sprintf(`assert artifact --fingerprint %s --flow %s %s`, suite.fingerprint, suite.flowName, suite.defaultKosliArguments),
61-
goldenRegex: "COMPLIANT\nSee more details at http://localhost(:8001)?/docs-cmd-test-user/flows/assert-artifact/artifacts/fcf33337634c2577a5d86fd7ecb0a25a7c1bb5d89c14fd236f546a5759252c02(?:\\?artifact_id=[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{8})?\n",
64+
goldenRegex: "(?s)^COMPLIANT\n.*Attestation-name.*See more details at http://localhost(:8001)?/docs-cmd-test-user/flows/assert-artifact/artifacts/fcf33337634c2577a5d86fd7ecb0a25a7c1bb5d89c14fd236f546a5759252c02(?:\\?artifact_id=[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{8})?\n",
65+
},
66+
{
67+
name: "asserting an existing compliant artifact (using --fingerprint) for an environment results in OK and zero exit",
68+
cmd: fmt.Sprintf(`assert artifact --fingerprint %s --flow %s --environment %s %s`, suite.fingerprint, suite.flowName, suite.envName, suite.defaultKosliArguments),
69+
goldenRegex: "(?s)^COMPLIANT\n.*Attestation-name.*Policy-name.*See more details at http://localhost(:8001)?/docs-cmd-test-user/flows/assert-artifact/artifacts/fcf33337634c2577a5d86fd7ecb0a25a7c1bb5d89c14fd236f546a5759252c02(?:\\?artifact_id=[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{8})?\n",
6270
},
6371
{
6472
name: "asserting an existing compliant artifact (using --artifact-type) results in OK and zero exit",
6573
cmd: fmt.Sprintf(`assert artifact %s --artifact-type file --flow %s %s`, suite.artifactPath, suite.flowName, suite.defaultKosliArguments),
66-
goldenRegex: "COMPLIANT\nSee more details at http://localhost(:8001)?/docs-cmd-test-user/flows/assert-artifact/artifacts/fcf33337634c2577a5d86fd7ecb0a25a7c1bb5d89c14fd236f546a5759252c02?(?:\\?artifact_id=[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{8})?\n",
74+
goldenRegex: "(?s)^COMPLIANT\n.*See more details at http://localhost(:8001)?/docs-cmd-test-user/flows/assert-artifact/artifacts/fcf33337634c2577a5d86fd7ecb0a25a7c1bb5d89c14fd236f546a5759252c02?(?:\\?artifact_id=[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{8})?\n",
75+
},
76+
{
77+
name: "json output of asserting an existing compliant artifact (using --artifact-type) results in OK and zero exit",
78+
cmd: fmt.Sprintf(`assert artifact %s --output json --artifact-type file --flow %s %s`, suite.artifactPath, suite.flowName, suite.defaultKosliArguments),
79+
goldenJson: []jsonCheck{
80+
{"compliant", true},
81+
{"scope", "flow"},
82+
},
6783
},
6884
{
6985
wantError: true,

cmd/kosli/testHelpers.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ func goldenJsonContains(t *testing.T, output string, path string, want interface
164164

165165
require.Equal(t, want, current, "unexpected value at path %s", path)
166166
}
167+
167168
func compareTwoFiles(actualFilename, expectedFilename string) error {
168169
actual, err := os.ReadFile(actualFilename)
169170
if err != nil {

0 commit comments

Comments
 (0)