Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions app/cli/cmd/policy_develop_eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion app/cli/cmd/policy_develop_lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we do the same approach in eval?

Also, should we change init so the default created file is policy.yaml?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually eval works like this it seems, it doesn't load a default though.

the init might need update too

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about it, I'll update it in this PR, although some docs changes will be needed as well

cmd.Flags().BoolVar(&format, "format", false, "Auto-format file with opa fmt")
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)")
return cmd
Expand Down
12 changes: 6 additions & 6 deletions app/cli/documentation/cli-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
```

Expand Down
9 changes: 1 addition & 8 deletions app/cli/internal/action/policy_develop_lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package action
import (
"context"
"fmt"
"path/filepath"

"github.com/chainloop-dev/chainloop/app/cli/internal/policydevel"
)
Expand All @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion app/cli/internal/policydevel/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down
89 changes: 52 additions & 37 deletions app/cli/internal/policydevel/lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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{})
Comment thread
migmartri marked this conversation as resolved.
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)
Expand All @@ -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,
})
Expand Down
21 changes: 17 additions & 4 deletions docs/examples/policies/quickstart/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
Loading