diff --git a/app/cli/cmd/policy_develop_lint.go b/app/cli/cmd/policy_develop_lint.go index 996506ead..439becd7d 100644 --- a/app/cli/cmd/policy_develop_lint.go +++ b/app/cli/cmd/policy_develop_lint.go @@ -24,8 +24,9 @@ import ( func newPolicyDevelopLintCmd() *cobra.Command { var ( - policyPath string - format bool + policyPath string + format bool + regalConfig string ) cmd := &cobra.Command{ @@ -41,8 +42,9 @@ func newPolicyDevelopLintCmd() *cobra.Command { } result, err := a.Run(cmd.Context(), &action.PolicyLintOpts{ - PolicyPath: policyPath, - Format: format, + PolicyPath: policyPath, + Format: format, + RegalConfig: regalConfig, }) if err != nil { return fmt.Errorf("linting failed: %w", err) @@ -63,5 +65,6 @@ func newPolicyDevelopLintCmd() *cobra.Command { cmd.Flags().StringVarP(&policyPath, "policy", "p", ".", "Path to policy directory") 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 113371865..13ce257e6 100755 --- a/app/cli/documentation/cli-reference.mdx +++ b/app/cli/documentation/cli-reference.mdx @@ -2887,9 +2887,10 @@ chainloop policy develop lint [flags] Options ``` ---format Auto-format file with opa fmt --h, --help help for lint --p, --policy string Path to policy directory (default ".") +--format Auto-format file with opa fmt +-h, --help help for lint +-p, --policy string Path to policy directory (default ".") +--regal-config string Path to custom regal config (Default: https://github.com/chainloop-dev/chainloop/tree/main/app/cli/internal/policydevel/.regal.yaml) ``` Options inherited from parent commands diff --git a/app/cli/internal/action/policy_develop_lint.go b/app/cli/internal/action/policy_develop_lint.go index abc5475f8..13feefe93 100644 --- a/app/cli/internal/action/policy_develop_lint.go +++ b/app/cli/internal/action/policy_develop_lint.go @@ -24,8 +24,9 @@ import ( ) type PolicyLintOpts struct { - PolicyPath string - Format bool + PolicyPath string + Format bool + RegalConfig string } type PolicyLintResult struct { @@ -51,7 +52,7 @@ func (action *PolicyLint) Run(_ context.Context, opts *PolicyLintOpts) (*PolicyL } // Read policies - policy, err := policydevel.Lookup(absPath, opts.Format) + policy, err := policydevel.Lookup(absPath, opts.RegalConfig, opts.Format) if err != nil { return nil, fmt.Errorf("loading policy: %w", err) } diff --git a/app/cli/internal/policydevel/.regal.yaml b/app/cli/internal/policydevel/.regal.yaml new file mode 100644 index 000000000..8aa928533 --- /dev/null +++ b/app/cli/internal/policydevel/.regal.yaml @@ -0,0 +1,15 @@ +# based from https://github.com/spacelift-io/spacelift-policies-example-library/blob/main/.regal/config.yaml +# Default config +rules: + idiomatic: + directory-package-mismatch: + level: ignore + no-defined-entrypoint: + level: warning + style: + todo-comment: + level: ignore + line-length: + level: ignore + rule-length: + level: ignore \ No newline at end of file diff --git a/app/cli/internal/policydevel/lint.go b/app/cli/internal/policydevel/lint.go index 8faaee3d9..e59585933 100644 --- a/app/cli/internal/policydevel/lint.go +++ b/app/cli/internal/policydevel/lint.go @@ -17,6 +17,7 @@ package policydevel import ( "context" + "embed" "fmt" "os" "path/filepath" @@ -28,15 +29,21 @@ import ( v1 "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/unmarshal" "github.com/open-policy-agent/opa/v1/format" + "github.com/styrainc/regal/pkg/config" "github.com/styrainc/regal/pkg/linter" "github.com/styrainc/regal/pkg/rules" + "gopkg.in/yaml.v2" ) +//go:embed .regal.yaml +var regalConfigFS embed.FS + type PolicyToLint struct { Path string YAMLFiles []*File RegoFiles []*File Format bool + Config string Errors []ValidationError } @@ -73,7 +80,7 @@ func (p *PolicyToLint) AddError(path, message string, line int) { } // Read policy files from the given directory or file -func Lookup(absPath string, format bool) (*PolicyToLint, error) { +func Lookup(absPath, config string, format bool) (*PolicyToLint, error) { fileInfo, err := os.Stat(absPath) if err != nil { if os.IsNotExist(err) { @@ -85,6 +92,7 @@ func Lookup(absPath string, format bool) (*PolicyToLint, error) { policy := &PolicyToLint{ Path: absPath, Format: format, + Config: config, } if fileInfo.IsDir() { @@ -277,7 +285,18 @@ func (p *PolicyToLint) runRegalLinter(filePath, content string) { return } + // Initialize linter with input modules lntr := linter.NewLinter().WithInputModules(&inputModules) + + // Load and apply configuration + cfg, err := p.loadConfig() + if err != nil { + p.AddError(filePath, fmt.Sprintf("%s", err), 0) + } + if cfg != nil { + lntr = lntr.WithUserConfig(*cfg) + } + report, err := lntr.Lint(context.Background()) if err != nil { p.AddError(filePath, fmt.Sprintf("linting failed: %v", err), 0) @@ -290,6 +309,51 @@ func (p *PolicyToLint) runRegalLinter(filePath, content string) { } } +// Attempts to load configuration in this order: +// 1. User-specified config +// 2. Default config +// Returns nil config if no config found at all +func (p *PolicyToLint) loadConfig() (*config.Config, error) { + // 1. Try user-specified config first + if p.Config != "" { + userCfg, err := config.FromPath(p.Config) + if err == nil { + return &userCfg, nil + } + // If user config fails, we'll fall through to default config + userErr := fmt.Errorf("failed to load user config from %q: %w (using default config)", p.Config, err) + + // Continue to try default config + defaultCfg, defaultErr := p.loadDefaultConfig() + if defaultErr == nil { + return defaultCfg, userErr + } + return nil, fmt.Errorf("%w; also %w", userErr, defaultErr) + } + + // 2. No user config specified - try default config + return p.loadDefaultConfig() +} + +func (p *PolicyToLint) loadDefaultConfig() (*config.Config, error) { + cfgData, err := regalConfigFS.ReadFile(".regal.yaml") + if err != nil { + return nil, fmt.Errorf("failed to read default config: %w", err) + } + + var configMap map[string]interface{} + if err := yaml.Unmarshal(cfgData, &configMap); err != nil { + return nil, fmt.Errorf("failed to parse default config: %w", err) + } + + cfg, err := config.FromMap(configMap) + if err != nil { + return nil, fmt.Errorf("failed to convert default config: %w", err) + } + + return &cfg, nil +} + // Splits grouped errors into individual errors func (p *PolicyToLint) processRegalViolation(rawErr error, path string) { if rawErr == nil {