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/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..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 @@ -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/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" ) diff --git a/app/cli/internal/policydevel/lint.go b/app/cli/internal/policydevel/lint.go index e59585933..adf630a58 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,30 +80,37 @@ 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 err := scanDirectory(policy, absPath); err != nil { - return nil, err - } - } else { - if err := processFile(policy, absPath); err != nil { - return nil, err - } + if err := policy.processFile(resolvedPath); err != nil { + return nil, err + } + + // Load referenced rego files from all YAML files + if err := policy.loadReferencedRegoFiles(filepath.Dir(resolvedPath)); err != nil { + return nil, err } // Verify we found at least one valid file @@ -113,35 +121,42 @@ 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 +// 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 + 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 + 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) + } + if _, ok := seen[resolvedPath]; ok { + continue // avoid duplicates + } + seen[resolvedPath] = struct{}{} + if err := p.processFile(resolvedPath); err != nil { + return fmt.Errorf("failed to load referenced rego file %q: %w", resolvedPath, err) + } + } } - foundValidFile = true } - - if !foundValidFile { - return fmt.Errorf("no valid .yaml/.yml or .rego files found in directory") - } - 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) @@ -150,12 +165,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, }) 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