Skip to content

Commit 971be38

Browse files
authored
fix(policy): add referenced script traversal (#2312)
Signed-off-by: Sylwester Piskozub <[email protected]>
1 parent bddf4a1 commit 971be38

File tree

7 files changed

+83
-63
lines changed

7 files changed

+83
-63
lines changed

app/cli/cmd/policy_develop_eval.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,12 @@ evaluates the policy against the provided material or attestation.`,
6565
},
6666
}
6767

68-
cmd.Flags().StringVar(&materialPath, "material", "", "path to material or attestation file")
68+
cmd.Flags().StringVar(&materialPath, "material", "", "Path to material or attestation file")
6969
cobra.CheckErr(cmd.MarkFlagRequired("material"))
70-
cmd.Flags().StringVar(&kind, "kind", "", fmt.Sprintf("kind of the material: %q", schemaapi.ListAvailableMaterialKind()))
71-
cmd.Flags().StringSliceVar(&annotations, "annotation", []string{}, "key-value pairs of material annotations (key=value)")
72-
cmd.Flags().StringVar(&policyPath, "policy", "", "path to custom policy file")
73-
cobra.CheckErr(cmd.MarkFlagRequired("policy"))
74-
cmd.Flags().StringSliceVar(&inputs, "input", []string{}, "key-value pairs of policy inputs (key=value)")
70+
cmd.Flags().StringVar(&kind, "kind", "", fmt.Sprintf("Kind of the material: %q", schemaapi.ListAvailableMaterialKind()))
71+
cmd.Flags().StringSliceVar(&annotations, "annotation", []string{}, "Key-value pairs of material annotations (key=value)")
72+
cmd.Flags().StringVarP(&policyPath, "policy", "p", "policy.yaml", "Path to custom policy file")
73+
cmd.Flags().StringSliceVar(&inputs, "input", []string{}, "Key-value pairs of policy inputs (key=value)")
7574

7675
return cmd
7776
}

app/cli/cmd/policy_develop_lint.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ func newPolicyDevelopLintCmd() *cobra.Command {
5959
},
6060
}
6161

62-
cmd.Flags().StringVarP(&policyPath, "policy", "p", ".", "Path to policy directory")
62+
cmd.Flags().StringVarP(&policyPath, "policy", "p", "policy.yaml", "Path to policy file")
6363
cmd.Flags().BoolVar(&format, "format", false, "Auto-format file with opa fmt")
6464
cmd.Flags().StringVar(&regalConfig, "regal-config", "", "Path to custom regal config (Default: https://github.com/chainloop-dev/chainloop/tree/main/app/cli/internal/policydevel/.regal.yaml)")
6565
return cmd

app/cli/documentation/cli-reference.mdx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2809,12 +2809,12 @@ chainloop policy eval --policy policy.yaml --material sbom.json --kind SBOM_CYCL
28092809
Options
28102810

28112811
```
2812-
--annotation strings key-value pairs of material annotations (key=value)
2812+
--annotation strings Key-value pairs of material annotations (key=value)
28132813
-h, --help help for eval
2814-
--input strings key-value pairs of policy inputs (key=value)
2815-
--kind string kind of the material: ["ARTIFACT" "ATTESTATION" "BLACKDUCK_SCA_JSON" "CHAINLOOP_RUNNER_CONTEXT" "CONTAINER_IMAGE" "CSAF_INFORMATIONAL_ADVISORY" "CSAF_SECURITY_ADVISORY" "CSAF_SECURITY_INCIDENT_RESPONSE" "CSAF_VEX" "EVIDENCE" "GHAS_CODE_SCAN" "GHAS_DEPENDENCY_SCAN" "GHAS_SECRET_SCAN" "GITLAB_SECURITY_REPORT" "HELM_CHART" "JACOCO_XML" "JUNIT_XML" "OPENVEX" "SARIF" "SBOM_CYCLONEDX_JSON" "SBOM_SPDX_JSON" "SLSA_PROVENANCE" "STRING" "TWISTCLI_SCAN_JSON" "ZAP_DAST_ZIP"]
2816-
--material string path to material or attestation file
2817-
--policy string path to custom policy file
2814+
--input strings Key-value pairs of policy inputs (key=value)
2815+
--kind string Kind of the material: ["ARTIFACT" "ATTESTATION" "BLACKDUCK_SCA_JSON" "CHAINLOOP_RUNNER_CONTEXT" "CONTAINER_IMAGE" "CSAF_INFORMATIONAL_ADVISORY" "CSAF_SECURITY_ADVISORY" "CSAF_SECURITY_INCIDENT_RESPONSE" "CSAF_VEX" "EVIDENCE" "GHAS_CODE_SCAN" "GHAS_DEPENDENCY_SCAN" "GHAS_SECRET_SCAN" "GITLAB_SECURITY_REPORT" "HELM_CHART" "JACOCO_XML" "JUNIT_XML" "OPENVEX" "SARIF" "SBOM_CYCLONEDX_JSON" "SBOM_SPDX_JSON" "SLSA_PROVENANCE" "STRING" "TWISTCLI_SCAN_JSON" "ZAP_DAST_ZIP"]
2816+
--material string Path to material or attestation file
2817+
-p, --policy string Path to custom policy file (default "policy.yaml")
28182818
```
28192819

28202820
Options inherited from parent commands
@@ -2938,7 +2938,7 @@ Options
29382938
```
29392939
--format Auto-format file with opa fmt
29402940
-h, --help help for lint
2941-
-p, --policy string Path to policy directory (default ".")
2941+
-p, --policy string Path to policy file (default "policy.yaml")
29422942
--regal-config string Path to custom regal config (Default: https://github.com/chainloop-dev/chainloop/tree/main/app/cli/internal/policydevel/.regal.yaml)
29432943
```
29442944

app/cli/internal/action/policy_develop_lint.go

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ package action
1818
import (
1919
"context"
2020
"fmt"
21-
"path/filepath"
2221

2322
"github.com/chainloop-dev/chainloop/app/cli/internal/policydevel"
2423
)
@@ -45,14 +44,8 @@ func NewPolicyLint(actionOpts *ActionsOpts) (*PolicyLint, error) {
4544
}
4645

4746
func (action *PolicyLint) Run(_ context.Context, opts *PolicyLintOpts) (*PolicyLintResult, error) {
48-
// Resolve absolute path to policy directory
49-
absPath, err := filepath.Abs(opts.PolicyPath)
50-
if err != nil {
51-
return nil, fmt.Errorf("resolving absolute path: %w", err)
52-
}
53-
5447
// Read policies
55-
policy, err := policydevel.Lookup(absPath, opts.RegalConfig, opts.Format)
48+
policy, err := policydevel.Lookup(opts.PolicyPath, opts.RegalConfig, opts.Format)
5649
if err != nil {
5750
return nil, fmt.Errorf("loading policy: %w", err)
5851
}

app/cli/internal/policydevel/init.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ var templateFS embed.FS
3131
const (
3232
policyTemplateRegoPath = "templates/example-policy.rego"
3333
policyTemplatePath = "templates/example-policy.yaml"
34-
defaultPolicyName = "chainloop-policy"
34+
defaultPolicyName = "policy"
3535
defaultPolicyDescription = "Chainloop validation policy"
3636
defaultMaterialKind = "SBOM_CYCLONEDX_JSON"
3737
)

app/cli/internal/policydevel/lint.go

Lines changed: 52 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"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"
31+
"github.com/chainloop-dev/chainloop/pkg/resourceloader"
3132
"github.com/open-policy-agent/opa/v1/format"
3233
"github.com/styrainc/regal/pkg/config"
3334
"github.com/styrainc/regal/pkg/linter"
@@ -79,30 +80,37 @@ func (p *PolicyToLint) AddError(path, message string, line int) {
7980
})
8081
}
8182

82-
// Read policy files from the given directory or file
83+
// Read policy files
8384
func Lookup(absPath, config string, format bool) (*PolicyToLint, error) {
84-
fileInfo, err := os.Stat(absPath)
85+
resolvedPath, err := resourceloader.GetPathForResource(absPath)
86+
if err != nil {
87+
return nil, fmt.Errorf("failed to resolve policy file: %w", err)
88+
}
89+
90+
fileInfo, err := os.Stat(resolvedPath)
8591
if err != nil {
8692
if os.IsNotExist(err) {
87-
return nil, fmt.Errorf("path does not exist: %s", absPath)
93+
return nil, fmt.Errorf("policy file does not exist: %s", resolvedPath)
8894
}
89-
return nil, fmt.Errorf("failed to stat path %q: %w", absPath, err)
95+
return nil, fmt.Errorf("failed to stat file %q: %w", resolvedPath, err)
96+
}
97+
if fileInfo.IsDir() {
98+
return nil, fmt.Errorf("expected a file but got a directory: %s", resolvedPath)
9099
}
91100

92101
policy := &PolicyToLint{
93-
Path: absPath,
102+
Path: resolvedPath,
94103
Format: format,
95104
Config: config,
96105
}
97106

98-
if fileInfo.IsDir() {
99-
if err := scanDirectory(policy, absPath); err != nil {
100-
return nil, err
101-
}
102-
} else {
103-
if err := processFile(policy, absPath); err != nil {
104-
return nil, err
105-
}
107+
if err := policy.processFile(resolvedPath); err != nil {
108+
return nil, err
109+
}
110+
111+
// Load referenced rego files from all YAML files
112+
if err := policy.loadReferencedRegoFiles(filepath.Dir(resolvedPath)); err != nil {
113+
return nil, err
106114
}
107115

108116
// Verify we found at least one valid file
@@ -113,35 +121,42 @@ func Lookup(absPath, config string, format bool) (*PolicyToLint, error) {
113121
return policy, nil
114122
}
115123

116-
// Performs a one-level directory lookup to find .yaml/.yml or .rego files.
117-
func scanDirectory(policy *PolicyToLint, dirPath string) error {
118-
files, err := os.ReadDir(dirPath)
119-
if err != nil {
120-
return fmt.Errorf("reading directory: %w", err)
121-
}
122-
123-
var foundValidFile bool
124-
for _, file := range files {
125-
if file.IsDir() {
124+
// Loads referenced rego files from all YAML files in the policy
125+
// Loads referenced rego files from all YAML files in the policy
126+
func (p *PolicyToLint) loadReferencedRegoFiles(baseDir string) error {
127+
seen := make(map[string]struct{})
128+
for _, yamlFile := range p.YAMLFiles {
129+
var parsed v1.Policy
130+
if err := unmarshal.FromRaw(yamlFile.Content, unmarshal.RawFormatYAML, &parsed, true); err != nil {
131+
// Ignore parse errors here; they'll be caught in validation
126132
continue
127133
}
128-
129-
filePath := filepath.Join(dirPath, file.Name())
130-
if err := processFile(policy, filePath); err != nil {
131-
// Skip unsupported files but continue processing others
132-
continue
134+
for _, spec := range parsed.Spec.Policies {
135+
regoPath := spec.GetPath()
136+
if regoPath != "" {
137+
// If path is relative, make it relative to the YAML file's directory
138+
if !filepath.IsAbs(regoPath) {
139+
regoPath = filepath.Join(baseDir, regoPath)
140+
}
141+
142+
resolvedPath, err := resourceloader.GetPathForResource(regoPath)
143+
if err != nil {
144+
return fmt.Errorf("failed to resolve rego file %q: %w", regoPath, err)
145+
}
146+
if _, ok := seen[resolvedPath]; ok {
147+
continue // avoid duplicates
148+
}
149+
seen[resolvedPath] = struct{}{}
150+
if err := p.processFile(resolvedPath); err != nil {
151+
return fmt.Errorf("failed to load referenced rego file %q: %w", resolvedPath, err)
152+
}
153+
}
133154
}
134-
foundValidFile = true
135155
}
136-
137-
if !foundValidFile {
138-
return fmt.Errorf("no valid .yaml/.yml or .rego files found in directory")
139-
}
140-
141156
return nil
142157
}
143158

144-
func processFile(policy *PolicyToLint, filePath string) error {
159+
func (p *PolicyToLint) processFile(filePath string) error {
145160
content, err := os.ReadFile(filePath)
146161
if err != nil {
147162
return fmt.Errorf("reading %s: %w", filepath.Base(filePath), err)
@@ -150,12 +165,12 @@ func processFile(policy *PolicyToLint, filePath string) error {
150165
ext := strings.ToLower(filepath.Ext(filePath))
151166
switch ext {
152167
case ".yaml", ".yml":
153-
policy.YAMLFiles = append(policy.YAMLFiles, &File{
168+
p.YAMLFiles = append(p.YAMLFiles, &File{
154169
Path: filePath,
155170
Content: content,
156171
})
157172
case ".rego":
158-
policy.RegoFiles = append(policy.RegoFiles, &File{
173+
p.RegoFiles = append(p.RegoFiles, &File{
159174
Path: filePath,
160175
Content: content,
161176
})

docs/examples/policies/quickstart/README.md

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,26 @@ chainloop policy develop eval --policy cdx-fresh.yaml --material cdx-fresh.json
4949

5050
**Old SBOM (should fail):**
5151
```
52-
INF - cdx-fresh: SBOM created at: 2024-06-15T10:30:00Z which is too old (freshness limit set to 30 days)
53-
INF policy evaluation failed
52+
[
53+
{
54+
"violations": [
55+
"SBOM created at: 2024-06-15T10:30:00Z which is too old (freshness limit set to 30 days)"
56+
],
57+
"skip_reasons": [],
58+
"skipped": false
59+
}
60+
]
5461
```
5562

5663
**Fresh SBOM (should pass):**
5764
```
58-
INF policy evaluation passed
65+
[
66+
{
67+
"violations": [],
68+
"skip_reasons": [],
69+
"skipped": false
70+
}
71+
]
5972
```
6073

6174
## Create Your Own Policy
@@ -68,7 +81,7 @@ Create a new policy with the embedded format (single YAML file):
6881
chainloop policy develop init --embedded --name my-policy --description "My custom policy description"
6982
```
7083

71-
**Note**: This creates a file named `my-policy.yaml` (based on the `--name` parameter). Without `--embedded`, it creates separate `chainloop-policy.yaml` and `chainloop-policy.rego` files.
84+
**Note**: This creates a file named `my-policy.yaml` (based on the `--name` parameter). Without `--embedded` and `--name`, it creates separate `policy.yaml` and `policy.rego` files.
7285

7386
### Step 2: Write Your Policy Rules
7487

0 commit comments

Comments
 (0)