Skip to content

Commit bbbb54a

Browse files
pbeckhamclaude
andauthored
feat: display control identifier in assert artifact output for for_control policy failures (#926)
* feat: display control identifier in assert artifact output for for_control policy failures When a policy rule has for_control set, the resolution context now includes the control identifier (server PR #5814). The CLI now surfaces it in failure messages so CI pipelines can identify which specific control is unsatisfied. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 234e439 commit bbbb54a

6 files changed

Lines changed: 104 additions & 3 deletions

File tree

cmd/kosli/assertArtifact.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,13 +221,23 @@ func printAssertAsTable(raw string, out io.Writer, page int) error {
221221
ruleDefinition := rule["definition"].(map[string]interface{})
222222
attestationName := ruleDefinition["name"]
223223
attestationType := ruleDefinition["type"]
224+
context, _ := resolution["context"].(map[string]interface{})
225+
forControl, _ := context["for_control"].(string)
224226
switch resolutionType {
225227
case "legacy_flow":
226228
failures = append(failures, "artifact comes from a legacy flow and does not have the new attestations")
227229
case "missing_attestation":
228-
failures = append(failures, fmt.Sprintf("artifact is missing required '%v' (type: %v) attestation in trail", attestationName, attestationType))
230+
if forControl != "" {
231+
failures = append(failures, fmt.Sprintf("artifact is missing required %v for control '%v'", attestationType, forControl))
232+
} else {
233+
failures = append(failures, fmt.Sprintf("artifact is missing required '%v' (type: %v) attestation in trail", attestationName, attestationType))
234+
}
229235
case "non_compliant_attestation":
230-
failures = append(failures, fmt.Sprintf("attestation '%v' is non-compliant in trail", attestationName))
236+
if forControl != "" {
237+
failures = append(failures, fmt.Sprintf("decision for control '%v' is non-compliant in trail", forControl))
238+
} else {
239+
failures = append(failures, fmt.Sprintf("attestation '%v' is non-compliant in trail", attestationName))
240+
}
231241
case "non_compliant_in_trail":
232242
failures = append(failures, "artifact is not compliant in trail")
233243
}

cmd/kosli/assertArtifact_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,17 @@ type AssertArtifactCommandTestSuite struct {
3030
artifactName3 string
3131
artifact3Path string
3232
fingerprint3 string
33+
// for_control test fixtures
34+
controlIdentifier string
35+
controlPolicyName string
36+
controlEnvName string
37+
flowNameForControl string
38+
trailNameNoDecision string
39+
artifact4Path string
40+
fingerprint4 string
41+
trailNameBadDecision string
42+
artifact5Path string
43+
fingerprint5 string
3344
}
3445

3546
func (suite *AssertArtifactCommandTestSuite) SetupTest() {
@@ -78,6 +89,34 @@ func (suite *AssertArtifactCommandTestSuite) SetupTest() {
7889
require.NoError(suite.T(), err)
7990
CreateGenericArtifactAttestation(suite.flowName3, suite.trailName, suite.fingerprint3, "failing-attestation", false, suite.T())
8091
require.NoError(suite.T(), err)
92+
93+
// Setup for for_control policy evaluation tests
94+
suite.controlIdentifier = "assert-artifact-ctl"
95+
suite.controlPolicyName = "assert-artifact-control-pol"
96+
suite.controlEnvName = "assert-artifact-control-env"
97+
suite.flowNameForControl = "assert-artifact-control-flow"
98+
suite.trailNameNoDecision = "assert-artifact-no-decision-trail"
99+
suite.trailNameBadDecision = "assert-artifact-bad-decision-trail"
100+
suite.artifact4Path = "testdata/artifacts/AssertArtifactCommandTestSuiteArtifact4.txt"
101+
suite.artifact5Path = "testdata/artifacts/AssertArtifactCommandTestSuiteArtifact5.txt"
102+
103+
CreateControl(global.Org, suite.controlIdentifier, "Test Control For Assert", suite.T())
104+
CreatePolicyWithFile(global.Org, suite.controlPolicyName, "testdata/policy-files/test-policy-for-control.yml", suite.T())
105+
CreateEnv(global.Org, suite.controlEnvName, "server", suite.T())
106+
AttachPolicy([]string{suite.controlEnvName}, suite.controlPolicyName, suite.T())
107+
108+
CreateFlow(suite.flowNameForControl, suite.T())
109+
110+
BeginTrail(suite.trailNameNoDecision, suite.flowNameForControl, "", suite.T())
111+
suite.fingerprint4, err = GetSha256Digest(suite.artifact4Path, fingerprintOptions, logger)
112+
require.NoError(suite.T(), err)
113+
CreateArtifactOnTrail(suite.flowNameForControl, suite.trailNameNoDecision, "cli", suite.fingerprint4, "arti-no-decision", suite.T())
114+
115+
BeginTrail(suite.trailNameBadDecision, suite.flowNameForControl, "", suite.T())
116+
suite.fingerprint5, err = GetSha256Digest(suite.artifact5Path, fingerprintOptions, logger)
117+
require.NoError(suite.T(), err)
118+
CreateArtifactOnTrail(suite.flowNameForControl, suite.trailNameBadDecision, "cli", suite.fingerprint5, "arti-bad-decision", suite.T())
119+
CreateDecisionAttestation(suite.flowNameForControl, suite.trailNameBadDecision, suite.controlIdentifier, "decision", false, suite.T())
81120
}
82121

83122
func (suite *AssertArtifactCommandTestSuite) TestAssertArtifactCmd() {
@@ -199,6 +238,18 @@ func (suite *AssertArtifactCommandTestSuite) TestAssertArtifactCmd() {
199238
cmd: fmt.Sprintf(`assert artifact %s --artifact-type file %s`, suite.artifact3Path, suite.defaultKosliArguments),
200239
goldenRegex: "^Error: NON-COMPLIANT\n",
201240
},
241+
{
242+
wantError: true,
243+
name: "18 asserting artifact against env policy with for_control reports control ID when decision is missing",
244+
cmd: fmt.Sprintf(`assert artifact --fingerprint %s --environment %s %s`, suite.fingerprint4, suite.controlEnvName, suite.defaultKosliArguments),
245+
goldenRegex: fmt.Sprintf("(?s).*artifact is missing required decision for control '%v'.*", suite.controlIdentifier),
246+
},
247+
{
248+
wantError: true,
249+
name: "19 asserting artifact against env policy with for_control reports control ID when decision is non-compliant",
250+
cmd: fmt.Sprintf(`assert artifact --fingerprint %s --environment %s %s`, suite.fingerprint5, suite.controlEnvName, suite.defaultKosliArguments),
251+
goldenRegex: fmt.Sprintf("(?s).*decision for control '%v' is non-compliant in trail.*", suite.controlIdentifier),
252+
},
202253
}
203254

204255
runTestCmd(suite.T(), tests)

cmd/kosli/testHelpers.go

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,12 @@ func CreateControl(org, identifier, name string, t *testing.T) {
538538

539539
// CreatePolicy creates a policy on the server
540540
func CreatePolicy(org, policyName string, t *testing.T) {
541+
t.Helper()
542+
CreatePolicyWithFile(org, policyName, "testdata/policy-files/test-policy.yml", t)
543+
}
544+
545+
// CreatePolicyWithFile creates a policy on the server using the given policy file.
546+
func CreatePolicyWithFile(org, policyName, policyFilePath string, t *testing.T) {
541547
t.Helper()
542548
o := &createPolicyOptions{
543549
payload: PolicyPayload{
@@ -547,10 +553,33 @@ func CreatePolicy(org, policyName string, t *testing.T) {
547553
},
548554
}
549555

550-
err := o.run([]string{policyName, "testdata/policy-files/test-policy.yml"})
556+
err := o.run([]string{policyName, policyFilePath})
551557
require.NoError(t, err, "policy should be created without error")
552558
}
553559

560+
// CreateDecisionAttestation records a decision attestation against a trail.
561+
func CreateDecisionAttestation(flowName, trailName, controlID, attestationName string, compliant bool, t *testing.T) {
562+
t.Helper()
563+
o := &attestDecisionOptions{
564+
CommonAttestationOptions: &CommonAttestationOptions{
565+
flowName: flowName,
566+
trailName: trailName,
567+
fingerprintOptions: &fingerprintOptions{},
568+
attestationNameTemplate: attestationName,
569+
},
570+
payload: DecisionAttestationPayload{
571+
CommonAttestationPayload: &CommonAttestationPayload{},
572+
TypeName: "decision",
573+
Control: controlID,
574+
AttestationData: DecisionAttestationData{
575+
Compliant: compliant,
576+
},
577+
},
578+
}
579+
err := o.run([]string{})
580+
require.NoError(t, err, "decision attestation should be created without error")
581+
}
582+
554583
func AttachPolicy(envNames []string, policyName string, t *testing.T) {
555584
t.Helper()
556585
o := &attachPolicyOptions{
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
artifact for AssertArtifactCommandTestSuite missing-decision test
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
artifact for AssertArtifactCommandTestSuite non-compliant-decision test
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
_schema: https://kosli.com/schemas/policy/environment/v1
2+
3+
artifacts:
4+
provenance:
5+
required: true
6+
attestations:
7+
- name: decision
8+
type: decision
9+
for_control: assert-artifact-ctl

0 commit comments

Comments
 (0)