Skip to content

Commit 6f6a4cb

Browse files
authored
fix(policy): policy devel lint --format removes the comments (#2325)
Signed-off-by: Sylwester Piskozub <[email protected]>
1 parent 1d2d066 commit 6f6a4cb

14 files changed

+431
-356
lines changed

app/cli/internal/policydevel/lint.go

Lines changed: 84 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package policydevel
1717

1818
import (
19+
"bytes"
1920
"context"
2021
"embed"
2122
"fmt"
@@ -25,15 +26,14 @@ import (
2526
"strconv"
2627
"strings"
2728

28-
"github.com/bufbuild/protoyaml-go"
2929
v1 "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1"
3030
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/unmarshal"
3131
"github.com/chainloop-dev/chainloop/pkg/resourceloader"
3232
"github.com/open-policy-agent/opa/v1/format"
3333
"github.com/styrainc/regal/pkg/config"
3434
"github.com/styrainc/regal/pkg/linter"
3535
"github.com/styrainc/regal/pkg/rules"
36-
"gopkg.in/yaml.v2"
36+
"gopkg.in/yaml.v3"
3737
)
3838

3939
//go:embed .regal.yaml
@@ -204,11 +204,30 @@ func (p *PolicyToLint) validateYAMLFile(file *File) {
204204

205205
// Update policy file with formatted content
206206
if p.Format {
207-
outYAML, err := protoyaml.Marshal(&policy)
208-
if err != nil {
209-
p.AddError(file.Path, fmt.Sprintf("failed to marshal updated YAML: %v", err), 0)
210-
} else if err := os.WriteFile(file.Path, outYAML, 0600); err != nil {
211-
p.AddError(file.Path, fmt.Sprintf("failed to save updated file: %v", err), 0)
207+
var root yaml.Node
208+
if err := yaml.Unmarshal(file.Content, &root); err != nil {
209+
p.AddError(file.Path, fmt.Sprintf("failed to parse YAML: %v", err), 0)
210+
return
211+
}
212+
213+
if err := p.updateEmbeddedRegoInYAML(file, &root); err != nil {
214+
p.AddError(file.Path, fmt.Sprintf("failed to update embedded Rego: %v", err), 0)
215+
return
216+
}
217+
218+
var buf bytes.Buffer
219+
enc := yaml.NewEncoder(&buf)
220+
enc.SetIndent(2)
221+
defer enc.Close()
222+
223+
if err := enc.Encode(&root); err != nil {
224+
p.AddError(file.Path, fmt.Sprintf("failed to encode YAML: %v", err), 0)
225+
return
226+
}
227+
228+
outYAML := buf.Bytes()
229+
if err := os.WriteFile(file.Path, outYAML, 0600); err != nil {
230+
p.AddError(file.Path, fmt.Sprintf("failed to write updated file: %v", err), 0)
212231
} else {
213232
file.Content = outYAML
214233
}
@@ -404,3 +423,61 @@ func (p *PolicyToLint) processRegalViolation(rawErr error, path string) {
404423
p.AddError(path, line, 0)
405424
}
406425
}
426+
427+
// Updates the embedded rego policies in a YAML file
428+
// Manual update required due to yaml.marshal limitations
429+
func (p *PolicyToLint) updateEmbeddedRegoInYAML(file *File, rootNode *yaml.Node) error {
430+
if rootNode.Kind != yaml.DocumentNode || len(rootNode.Content) == 0 {
431+
return fmt.Errorf("unexpected YAML root structure")
432+
}
433+
434+
doc := rootNode.Content[0]
435+
if doc.Kind != yaml.MappingNode {
436+
return fmt.Errorf("expected mapping node at document root")
437+
}
438+
439+
// Locate spec policy node
440+
var specNode *yaml.Node
441+
for i := 0; i < len(doc.Content)-1; i += 2 {
442+
if doc.Content[i].Value == "spec" && doc.Content[i+1].Kind == yaml.MappingNode {
443+
specNode = doc.Content[i+1]
444+
break
445+
}
446+
}
447+
if specNode == nil {
448+
return fmt.Errorf("spec node not found")
449+
}
450+
451+
// Locate policies node within spec
452+
var policiesNode *yaml.Node
453+
for i := 0; i < len(specNode.Content)-1; i += 2 {
454+
if specNode.Content[i].Value == "policies" && specNode.Content[i+1].Kind == yaml.SequenceNode {
455+
policiesNode = specNode.Content[i+1]
456+
break
457+
}
458+
}
459+
if policiesNode == nil {
460+
return fmt.Errorf("spec.policies node not found")
461+
}
462+
463+
// Iterate over and update each rego policy
464+
for _, policy := range policiesNode.Content {
465+
if policy.Kind != yaml.MappingNode {
466+
continue
467+
}
468+
469+
for i := 0; i < len(policy.Content)-1; i += 2 {
470+
key := policy.Content[i]
471+
val := policy.Content[i+1]
472+
473+
if key.Value == "embedded" && val.Kind == yaml.ScalarNode {
474+
formatted := p.validateAndFormatRego(val.Value, file.Path)
475+
if formatted != val.Value {
476+
val.Value = formatted
477+
}
478+
}
479+
}
480+
}
481+
482+
return nil
483+
}

docs/examples/policies/chainloop-commit.yaml

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -21,45 +21,44 @@ spec:
2121
- kind: ATTESTATION
2222
embedded: |
2323
package main
24-
24+
2525
import rego.v1
26-
26+
2727
################################
2828
# Common section do NOT change #
2929
################################
30-
30+
3131
result := {
32-
"skipped": skipped,
33-
"violations": violations,
34-
"skip_reason": skip_reason,
32+
"skipped": skipped,
33+
"violations": violations,
34+
"skip_reason": skip_reason,
3535
}
36-
36+
3737
default skip_reason := ""
38-
38+
3939
skip_reason := m if {
40-
not valid_input
41-
m := "the file content is not recognized"
40+
not valid_input
41+
m := "the file content is not recognized"
4242
}
43-
43+
4444
default skipped := true
45-
45+
4646
skipped := false if valid_input
47-
47+
4848
########################################
4949
# EO Common section, custom code below #
5050
########################################
51-
51+
5252
# TODO: update to validate if the file is expected, i.e checking the tool that generates it
5353
valid_input := true
54-
54+
5555
violations contains msg if {
56-
not has_commit
57-
msg := "missing commit in attestation material"
56+
not has_commit
57+
msg := "missing commit in attestation material"
5858
}
59-
59+
6060
has_commit if {
61-
some sub in input.subject
62-
sub.name == "git.head"
63-
sub.digest.sha1
61+
some sub in input.subject
62+
sub.name == "git.head"
63+
sub.digest.sha1
6464
}
65-

docs/examples/policies/chainloop-qa.yaml

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -24,45 +24,45 @@ spec:
2424
- kind: ATTESTATION
2525
embedded: |
2626
package main
27-
27+
2828
import rego.v1
29-
29+
3030
################################
3131
# Common section do NOT change #
3232
################################
33-
33+
3434
result := {
35-
"skipped": skipped,
36-
"violations": violations,
37-
"skip_reason": skip_reason,
35+
"skipped": skipped,
36+
"violations": violations,
37+
"skip_reason": skip_reason,
3838
}
39-
39+
4040
default skip_reason := ""
41-
41+
4242
skip_reason := m if {
43-
not valid_input
44-
m := "the file content is not recognized"
43+
not valid_input
44+
m := "the file content is not recognized"
4545
}
46-
46+
4747
default skipped := true
48-
48+
4949
skipped := false if valid_input
50-
50+
5151
########################################
5252
# EO Common section, custom code below #
5353
########################################
54-
54+
5555
# TODO: update to validate if the file is expected, i.e checking the tool that generates it
5656
valid_input := true
57-
57+
5858
violations contains msg if {
59-
not is_approved
60-
61-
msg:= "Container image is not approved"
59+
not is_approved
60+
61+
msg := "Container image is not approved"
6262
}
63-
63+
6464
is_approved if {
65-
input.predicate.annotations.approval == "true"
66-
some material in input.predicate.materials
67-
material.annotations["chainloop.material.type"] == "CONTAINER_IMAGE"
65+
input.predicate.annotations.approval == "true"
66+
some material in input.predicate.materials
67+
material.annotations["chainloop.material.type"] == "CONTAINER_IMAGE"
6868
}
Lines changed: 53 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,57 @@
11
apiVersion: workflowcontract.chainloop.dev/v1
22
kind: Policy
33
metadata:
4-
name: cdx-fresh
5-
description: Checks that SBOM is maximum of 30 days old
6-
annotations:
7-
category: quickstart
4+
name: cdx-fresh
5+
description: Checks that SBOM is maximum of 30 days old
6+
annotations:
7+
category: quickstart
88
spec:
9-
policies:
10-
- embedded: |
11-
package main
12-
13-
import rego.v1
14-
15-
################################
16-
# Common section do NOT change #
17-
################################
18-
19-
result := {
20-
"skipped": skipped,
21-
"violations": violations,
22-
"skip_reason": skip_reason,
23-
"ignore": ignore,
24-
}
25-
26-
default skip_reason := ""
27-
28-
skip_reason := m if {
29-
not valid_input
30-
m := "invalid input"
31-
}
32-
33-
default skipped := true
34-
35-
skipped := false if valid_input
36-
37-
default ignore := false
38-
39-
########################################
40-
# EO Common section, custom code below #
41-
########################################
42-
# Validates if the input is valid and can be understood by this policy
43-
valid_input := true
44-
45-
limit := 30
46-
nanosecs_per_second := (1000 * 1000) * 1000
47-
nanosecs_per_day := ((24 * 60) * 60) * nanosecs_per_second
48-
maximum_age := limit * nanosecs_per_day
49-
50-
# If the input is valid, check for any policy violation here
51-
violations contains msg if {
52-
sbom_ns = time.parse_rfc3339_ns(input.metadata.timestamp)
53-
exceeding = time.now_ns() - (sbom_ns + maximum_age)
54-
exceeding > 0
55-
msg := sprintf("SBOM created at: %s which is too old (freshness limit set to %d days)", [input.metadata.timestamp, limit])
56-
}
57-
kind: SBOM_CYCLONEDX_JSON
9+
policies:
10+
- embedded: |
11+
package main
12+
13+
import rego.v1
14+
15+
################################
16+
# Common section do NOT change #
17+
################################
18+
19+
result := {
20+
"skipped": skipped,
21+
"violations": violations,
22+
"skip_reason": skip_reason,
23+
"ignore": ignore,
24+
}
25+
26+
default skip_reason := ""
27+
28+
skip_reason := m if {
29+
not valid_input
30+
m := "invalid input"
31+
}
32+
33+
default skipped := true
34+
35+
skipped := false if valid_input
36+
37+
default ignore := false
38+
39+
########################################
40+
# EO Common section, custom code below #
41+
########################################
42+
# Validates if the input is valid and can be understood by this policy
43+
valid_input := true
44+
45+
limit := 30
46+
nanosecs_per_second := (1000 * 1000) * 1000
47+
nanosecs_per_day := ((24 * 60) * 60) * nanosecs_per_second
48+
maximum_age := limit * nanosecs_per_day
49+
50+
# If the input is valid, check for any policy violation here
51+
violations contains msg if {
52+
sbom_ns = time.parse_rfc3339_ns(input.metadata.timestamp)
53+
exceeding = time.now_ns() - (sbom_ns + maximum_age)
54+
exceeding > 0
55+
msg := sprintf("SBOM created at: %s which is too old (freshness limit set to %d days)", [input.metadata.timestamp, limit])
56+
}
57+
kind: SBOM_CYCLONEDX_JSON

0 commit comments

Comments
 (0)