From 79baa776bd1c66c222449f301a5dc0e92439e035 Mon Sep 17 00:00:00 2001 From: Sylwester Piskozub Date: Wed, 6 Aug 2025 10:39:42 +0200 Subject: [PATCH 1/8] fix(policy): add referenced script traversal Signed-off-by: Sylwester Piskozub --- app/cli/internal/policydevel/lint.go | 54 ++++++++++++++++------------ 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/app/cli/internal/policydevel/lint.go b/app/cli/internal/policydevel/lint.go index e59585933..7e2235050 100644 --- a/app/cli/internal/policydevel/lint.go +++ b/app/cli/internal/policydevel/lint.go @@ -96,8 +96,13 @@ func Lookup(absPath, config string, format bool) (*PolicyToLint, error) { } if fileInfo.IsDir() { - if err := scanDirectory(policy, absPath); err != nil { - return nil, err + // If it's a directory, look for policy.yaml + policyYamlPath := filepath.Join(absPath, "policy.yaml") + if err := processFile(policy, policyYamlPath); err != nil { + if os.IsNotExist(err) { + return nil, fmt.Errorf("policy.yaml not found in directory: %s", absPath) + } + return nil, fmt.Errorf("failed to read policy.yaml: %w", err) } } else { if err := processFile(policy, absPath); err != nil { @@ -105,6 +110,11 @@ func Lookup(absPath, config string, format bool) (*PolicyToLint, error) { } } + // Load referenced rego files from all YAML files + if err := loadReferencedRegoFiles(policy); err != nil { + return nil, err + } + // Verify we found at least one valid file if len(policy.YAMLFiles) == 0 && len(policy.RegoFiles) == 0 { return nil, fmt.Errorf("no valid .yaml/.yml or .rego files found") @@ -113,31 +123,29 @@ func Lookup(absPath, config string, format bool) (*PolicyToLint, error) { return policy, nil } -// Performs a one-level directory lookup to find .yaml/.yml or .rego files. -func scanDirectory(policy *PolicyToLint, dirPath string) error { - files, err := os.ReadDir(dirPath) - if err != nil { - return fmt.Errorf("reading directory: %w", err) - } - - var foundValidFile bool - for _, file := range files { - if file.IsDir() { +// Loads referenced rego files from all YAML files in the policy +func loadReferencedRegoFiles(policy *PolicyToLint) error { + seen := make(map[string]struct{}) + for _, yamlFile := range policy.YAMLFiles { + var parsed v1.Policy + if err := unmarshal.FromRaw(yamlFile.Content, unmarshal.RawFormatYAML, &parsed, true); err != nil { + // Ignore parse errors here; they'll be caught in validation continue } - - filePath := filepath.Join(dirPath, file.Name()) - if err := processFile(policy, filePath); err != nil { - // Skip unsupported files but continue processing others - continue + dir := filepath.Dir(yamlFile.Path) + for _, spec := range parsed.Spec.Policies { + if spec.GetPath() != "" { + absRegoPath := filepath.Join(dir, spec.GetPath()) + if _, ok := seen[absRegoPath]; ok { + continue // avoid duplicates + } + seen[absRegoPath] = struct{}{} + if err := processFile(policy, absRegoPath); err != nil { + return fmt.Errorf("failed to load referenced rego file %q: %w", absRegoPath, err) + } + } } - foundValidFile = true } - - if !foundValidFile { - return fmt.Errorf("no valid .yaml/.yml or .rego files found in directory") - } - return nil } From b7b7adf89d73f1cf527326fdcb06dcc7b4a403e5 Mon Sep 17 00:00:00 2001 From: Sylwester Piskozub Date: Wed, 6 Aug 2025 10:55:20 +0200 Subject: [PATCH 2/8] support abs paths Signed-off-by: Sylwester Piskozub --- app/cli/internal/policydevel/lint.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/cli/internal/policydevel/lint.go b/app/cli/internal/policydevel/lint.go index 7e2235050..c62ed2aa9 100644 --- a/app/cli/internal/policydevel/lint.go +++ b/app/cli/internal/policydevel/lint.go @@ -134,8 +134,14 @@ func loadReferencedRegoFiles(policy *PolicyToLint) error { } dir := filepath.Dir(yamlFile.Path) for _, spec := range parsed.Spec.Policies { - if spec.GetPath() != "" { - absRegoPath := filepath.Join(dir, spec.GetPath()) + regoPath := spec.GetPath() + if regoPath != "" { + var absRegoPath string + if filepath.IsAbs(regoPath) { + absRegoPath = regoPath + } else { + absRegoPath = filepath.Join(dir, regoPath) + } if _, ok := seen[absRegoPath]; ok { continue // avoid duplicates } From c1567fc53c708d230cdfc35175df3640d8d83f45 Mon Sep 17 00:00:00 2001 From: Sylwester Piskozub Date: Wed, 6 Aug 2025 11:55:32 +0200 Subject: [PATCH 3/8] remove directory handler Signed-off-by: Sylwester Piskozub --- app/cli/cmd/policy_develop_lint.go | 2 +- app/cli/documentation/cli-reference.mdx | 2 +- .../internal/action/policy_develop_lint.go | 9 +--- app/cli/internal/policydevel/lint.go | 51 +++++++++---------- 4 files changed, 26 insertions(+), 38 deletions(-) diff --git a/app/cli/cmd/policy_develop_lint.go b/app/cli/cmd/policy_develop_lint.go index 954ece92e..1c597608f 100644 --- a/app/cli/cmd/policy_develop_lint.go +++ b/app/cli/cmd/policy_develop_lint.go @@ -59,7 +59,7 @@ func newPolicyDevelopLintCmd() *cobra.Command { }, } - cmd.Flags().StringVarP(&policyPath, "policy", "p", ".", "Path to policy directory") + cmd.Flags().StringVarP(&policyPath, "policy", "p", "policy.yaml", "Path to policy file") cmd.Flags().BoolVar(&format, "format", false, "Auto-format file with opa fmt") cmd.Flags().StringVar(®alConfig, "regal-config", "", "Path to custom regal config (Default: https://github.com/chainloop-dev/chainloop/tree/main/app/cli/internal/policydevel/.regal.yaml)") return cmd diff --git a/app/cli/documentation/cli-reference.mdx b/app/cli/documentation/cli-reference.mdx index 0f177fdb7..f1dd90412 100755 --- a/app/cli/documentation/cli-reference.mdx +++ b/app/cli/documentation/cli-reference.mdx @@ -2938,7 +2938,7 @@ Options ``` --format Auto-format file with opa fmt -h, --help help for lint --p, --policy string Path to policy directory (default ".") +-p, --policy string Path to policy file (default "policy.yaml") --regal-config string Path to custom regal config (Default: https://github.com/chainloop-dev/chainloop/tree/main/app/cli/internal/policydevel/.regal.yaml) ``` diff --git a/app/cli/internal/action/policy_develop_lint.go b/app/cli/internal/action/policy_develop_lint.go index 13feefe93..eabb0e231 100644 --- a/app/cli/internal/action/policy_develop_lint.go +++ b/app/cli/internal/action/policy_develop_lint.go @@ -18,7 +18,6 @@ package action import ( "context" "fmt" - "path/filepath" "github.com/chainloop-dev/chainloop/app/cli/internal/policydevel" ) @@ -45,14 +44,8 @@ func NewPolicyLint(actionOpts *ActionsOpts) (*PolicyLint, error) { } func (action *PolicyLint) Run(_ context.Context, opts *PolicyLintOpts) (*PolicyLintResult, error) { - // Resolve absolute path to policy directory - absPath, err := filepath.Abs(opts.PolicyPath) - if err != nil { - return nil, fmt.Errorf("resolving absolute path: %w", err) - } - // Read policies - policy, err := policydevel.Lookup(absPath, opts.RegalConfig, opts.Format) + policy, err := policydevel.Lookup(opts.PolicyPath, opts.RegalConfig, opts.Format) if err != nil { return nil, fmt.Errorf("loading policy: %w", err) } diff --git a/app/cli/internal/policydevel/lint.go b/app/cli/internal/policydevel/lint.go index c62ed2aa9..f1e25b814 100644 --- a/app/cli/internal/policydevel/lint.go +++ b/app/cli/internal/policydevel/lint.go @@ -28,6 +28,7 @@ import ( "github.com/bufbuild/protoyaml-go" v1 "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/unmarshal" + "github.com/chainloop-dev/chainloop/pkg/resourceloader" "github.com/open-policy-agent/opa/v1/format" "github.com/styrainc/regal/pkg/config" "github.com/styrainc/regal/pkg/linter" @@ -79,35 +80,32 @@ func (p *PolicyToLint) AddError(path, message string, line int) { }) } -// Read policy files from the given directory or file +// Read policy files func Lookup(absPath, config string, format bool) (*PolicyToLint, error) { - fileInfo, err := os.Stat(absPath) + resolvedPath, err := resourceloader.GetPathForResource(absPath) + if err != nil { + return nil, fmt.Errorf("failed to resolve policy file: %w", err) + } + + fileInfo, err := os.Stat(resolvedPath) if err != nil { if os.IsNotExist(err) { - return nil, fmt.Errorf("path does not exist: %s", absPath) + return nil, fmt.Errorf("policy file does not exist: %s", resolvedPath) } - return nil, fmt.Errorf("failed to stat path %q: %w", absPath, err) + return nil, fmt.Errorf("failed to stat file %q: %w", resolvedPath, err) + } + if fileInfo.IsDir() { + return nil, fmt.Errorf("expected a file but got a directory: %s", resolvedPath) } policy := &PolicyToLint{ - Path: absPath, + Path: resolvedPath, Format: format, Config: config, } - if fileInfo.IsDir() { - // If it's a directory, look for policy.yaml - policyYamlPath := filepath.Join(absPath, "policy.yaml") - if err := processFile(policy, policyYamlPath); err != nil { - if os.IsNotExist(err) { - return nil, fmt.Errorf("policy.yaml not found in directory: %s", absPath) - } - return nil, fmt.Errorf("failed to read policy.yaml: %w", err) - } - } else { - if err := processFile(policy, absPath); err != nil { - return nil, err - } + if err := processFile(policy, resolvedPath); err != nil { + return nil, err } // Load referenced rego files from all YAML files @@ -132,22 +130,19 @@ func loadReferencedRegoFiles(policy *PolicyToLint) error { // Ignore parse errors here; they'll be caught in validation continue } - dir := filepath.Dir(yamlFile.Path) for _, spec := range parsed.Spec.Policies { regoPath := spec.GetPath() if regoPath != "" { - var absRegoPath string - if filepath.IsAbs(regoPath) { - absRegoPath = regoPath - } else { - absRegoPath = filepath.Join(dir, regoPath) + resolvedPath, err := resourceloader.GetPathForResource(regoPath) + if err != nil { + return fmt.Errorf("failed to resolve rego file %q: %w", regoPath, err) } - if _, ok := seen[absRegoPath]; ok { + if _, ok := seen[resolvedPath]; ok { continue // avoid duplicates } - seen[absRegoPath] = struct{}{} - if err := processFile(policy, absRegoPath); err != nil { - return fmt.Errorf("failed to load referenced rego file %q: %w", absRegoPath, err) + seen[resolvedPath] = struct{}{} + if err := processFile(policy, resolvedPath); err != nil { + return fmt.Errorf("failed to load referenced rego file %q: %w", resolvedPath, err) } } } From 6887386e0033851cc4c320aa1f5ca489f0175135 Mon Sep 17 00:00:00 2001 From: Sylwester Piskozub Date: Wed, 6 Aug 2025 14:06:54 +0200 Subject: [PATCH 4/8] update func signatures Signed-off-by: Sylwester Piskozub --- app/cli/internal/policydevel/lint.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/cli/internal/policydevel/lint.go b/app/cli/internal/policydevel/lint.go index f1e25b814..c6acc772f 100644 --- a/app/cli/internal/policydevel/lint.go +++ b/app/cli/internal/policydevel/lint.go @@ -104,12 +104,12 @@ func Lookup(absPath, config string, format bool) (*PolicyToLint, error) { Config: config, } - if err := processFile(policy, resolvedPath); err != nil { + if err := policy.processFile(resolvedPath); err != nil { return nil, err } // Load referenced rego files from all YAML files - if err := loadReferencedRegoFiles(policy); err != nil { + if err := policy.loadReferencedRegoFiles(); err != nil { return nil, err } @@ -122,9 +122,9 @@ func Lookup(absPath, config string, format bool) (*PolicyToLint, error) { } // Loads referenced rego files from all YAML files in the policy -func loadReferencedRegoFiles(policy *PolicyToLint) error { +func (p *PolicyToLint) loadReferencedRegoFiles() error { seen := make(map[string]struct{}) - for _, yamlFile := range policy.YAMLFiles { + for _, yamlFile := range p.YAMLFiles { var parsed v1.Policy if err := unmarshal.FromRaw(yamlFile.Content, unmarshal.RawFormatYAML, &parsed, true); err != nil { // Ignore parse errors here; they'll be caught in validation @@ -141,7 +141,7 @@ func loadReferencedRegoFiles(policy *PolicyToLint) error { continue // avoid duplicates } seen[resolvedPath] = struct{}{} - if err := processFile(policy, resolvedPath); err != nil { + if err := p.processFile(resolvedPath); err != nil { return fmt.Errorf("failed to load referenced rego file %q: %w", resolvedPath, err) } } @@ -150,7 +150,7 @@ func loadReferencedRegoFiles(policy *PolicyToLint) error { return nil } -func processFile(policy *PolicyToLint, filePath string) error { +func (p *PolicyToLint) processFile(filePath string) error { content, err := os.ReadFile(filePath) if err != nil { return fmt.Errorf("reading %s: %w", filepath.Base(filePath), err) @@ -159,12 +159,12 @@ func processFile(policy *PolicyToLint, filePath string) error { ext := strings.ToLower(filepath.Ext(filePath)) switch ext { case ".yaml", ".yml": - policy.YAMLFiles = append(policy.YAMLFiles, &File{ + p.YAMLFiles = append(p.YAMLFiles, &File{ Path: filePath, Content: content, }) case ".rego": - policy.RegoFiles = append(policy.RegoFiles, &File{ + p.RegoFiles = append(p.RegoFiles, &File{ Path: filePath, Content: content, }) From 21831749d99da485ca364f91e4bdf5c15827dbe6 Mon Sep 17 00:00:00 2001 From: Sylwester Piskozub Date: Thu, 7 Aug 2025 07:39:17 +0200 Subject: [PATCH 5/8] update init file name; add defauly policy name to eval Signed-off-by: Sylwester Piskozub --- app/cli/cmd/policy_develop_eval.go | 11 +++++------ app/cli/internal/policydevel/init.go | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/app/cli/cmd/policy_develop_eval.go b/app/cli/cmd/policy_develop_eval.go index b5e80967b..fcaf9891d 100644 --- a/app/cli/cmd/policy_develop_eval.go +++ b/app/cli/cmd/policy_develop_eval.go @@ -65,13 +65,12 @@ evaluates the policy against the provided material or attestation.`, }, } - cmd.Flags().StringVar(&materialPath, "material", "", "path to material or attestation file") + cmd.Flags().StringVar(&materialPath, "material", "", "Path to material or attestation file") cobra.CheckErr(cmd.MarkFlagRequired("material")) - cmd.Flags().StringVar(&kind, "kind", "", fmt.Sprintf("kind of the material: %q", schemaapi.ListAvailableMaterialKind())) - cmd.Flags().StringSliceVar(&annotations, "annotation", []string{}, "key-value pairs of material annotations (key=value)") - cmd.Flags().StringVar(&policyPath, "policy", "", "path to custom policy file") - cobra.CheckErr(cmd.MarkFlagRequired("policy")) - cmd.Flags().StringSliceVar(&inputs, "input", []string{}, "key-value pairs of policy inputs (key=value)") + cmd.Flags().StringVar(&kind, "kind", "", fmt.Sprintf("Kind of the material: %q", schemaapi.ListAvailableMaterialKind())) + cmd.Flags().StringSliceVar(&annotations, "annotation", []string{}, "Key-value pairs of material annotations (key=value)") + cmd.Flags().StringVarP(&policyPath, "policy", "p", "policy.yaml", "Path to custom policy file") + cmd.Flags().StringSliceVar(&inputs, "input", []string{}, "Key-value pairs of policy inputs (key=value)") return cmd } diff --git a/app/cli/internal/policydevel/init.go b/app/cli/internal/policydevel/init.go index c8a50cf91..afa85dab5 100644 --- a/app/cli/internal/policydevel/init.go +++ b/app/cli/internal/policydevel/init.go @@ -31,7 +31,7 @@ var templateFS embed.FS const ( policyTemplateRegoPath = "templates/example-policy.rego" policyTemplatePath = "templates/example-policy.yaml" - defaultPolicyName = "chainloop-policy" + defaultPolicyName = "policy" defaultPolicyDescription = "Chainloop validation policy" defaultMaterialKind = "SBOM_CYCLONEDX_JSON" ) From 752d38ae428ec93c4de38feb6696833cfebac9c1 Mon Sep 17 00:00:00 2001 From: Sylwester Piskozub Date: Thu, 7 Aug 2025 07:46:20 +0200 Subject: [PATCH 6/8] update policies quickstart Signed-off-by: Sylwester Piskozub --- docs/examples/policies/quickstart/README.md | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/docs/examples/policies/quickstart/README.md b/docs/examples/policies/quickstart/README.md index 303060712..ed20e198f 100644 --- a/docs/examples/policies/quickstart/README.md +++ b/docs/examples/policies/quickstart/README.md @@ -49,13 +49,26 @@ chainloop policy develop eval --policy cdx-fresh.yaml --material cdx-fresh.json **Old SBOM (should fail):** ``` -INF - cdx-fresh: SBOM created at: 2024-06-15T10:30:00Z which is too old (freshness limit set to 30 days) -INF policy evaluation failed +[ + { + "violations": [ + "SBOM created at: 2024-06-15T10:30:00Z which is too old (freshness limit set to 30 days)" + ], + "skip_reasons": [], + "skipped": false + } +] ``` **Fresh SBOM (should pass):** ``` -INF policy evaluation passed +[ + { + "violations": [], + "skip_reasons": [], + "skipped": false + } +] ``` ## Create Your Own Policy @@ -68,7 +81,7 @@ Create a new policy with the embedded format (single YAML file): chainloop policy develop init --embedded --name my-policy --description "My custom policy description" ``` -**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. +**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. ### Step 2: Write Your Policy Rules From a0623dbf67c5a393a605504881f8ebe0dd31f59c Mon Sep 17 00:00:00 2001 From: Sylwester Piskozub Date: Thu, 7 Aug 2025 09:35:36 +0200 Subject: [PATCH 7/8] add missing generated code Signed-off-by: Sylwester Piskozub --- app/cli/documentation/cli-reference.mdx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/cli/documentation/cli-reference.mdx b/app/cli/documentation/cli-reference.mdx index f1dd90412..aedf8b371 100755 --- a/app/cli/documentation/cli-reference.mdx +++ b/app/cli/documentation/cli-reference.mdx @@ -2809,12 +2809,12 @@ chainloop policy eval --policy policy.yaml --material sbom.json --kind SBOM_CYCL Options ``` ---annotation strings key-value pairs of material annotations (key=value) +--annotation strings Key-value pairs of material annotations (key=value) -h, --help help for eval ---input strings key-value pairs of policy inputs (key=value) ---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"] ---material string path to material or attestation file ---policy string path to custom policy file +--input strings Key-value pairs of policy inputs (key=value) +--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"] +--material string Path to material or attestation file +-p, --policy string Path to custom policy file (default "policy.yaml") ``` Options inherited from parent commands From 4fbbab388f43a505e14d5bc9c7268ab3ea0c0162 Mon Sep 17 00:00:00 2001 From: Sylwester Piskozub Date: Fri, 8 Aug 2025 10:51:18 +0200 Subject: [PATCH 8/8] fix for relative rego paths when policy outside of cwd is provided Signed-off-by: Sylwester Piskozub --- app/cli/internal/policydevel/lint.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/cli/internal/policydevel/lint.go b/app/cli/internal/policydevel/lint.go index c6acc772f..adf630a58 100644 --- a/app/cli/internal/policydevel/lint.go +++ b/app/cli/internal/policydevel/lint.go @@ -109,7 +109,7 @@ func Lookup(absPath, config string, format bool) (*PolicyToLint, error) { } // Load referenced rego files from all YAML files - if err := policy.loadReferencedRegoFiles(); err != nil { + if err := policy.loadReferencedRegoFiles(filepath.Dir(resolvedPath)); err != nil { return nil, err } @@ -122,7 +122,8 @@ func Lookup(absPath, config string, format bool) (*PolicyToLint, error) { } // Loads referenced rego files from all YAML files in the policy -func (p *PolicyToLint) loadReferencedRegoFiles() error { +// Loads referenced rego files from all YAML files in the policy +func (p *PolicyToLint) loadReferencedRegoFiles(baseDir string) error { seen := make(map[string]struct{}) for _, yamlFile := range p.YAMLFiles { var parsed v1.Policy @@ -133,6 +134,11 @@ func (p *PolicyToLint) loadReferencedRegoFiles() error { for _, spec := range parsed.Spec.Policies { regoPath := spec.GetPath() if regoPath != "" { + // If path is relative, make it relative to the YAML file's directory + if !filepath.IsAbs(regoPath) { + regoPath = filepath.Join(baseDir, regoPath) + } + resolvedPath, err := resourceloader.GetPathForResource(regoPath) if err != nil { return fmt.Errorf("failed to resolve rego file %q: %w", regoPath, err)