// Copyright 2017 The OPA Authors. All rights reserved. // Use of this source code is governed by an Apache2 // license that can be found in the LICENSE file. package cmd import ( "errors" "fmt" "io/fs" "maps" "os" "github.com/spf13/cobra" "github.com/open-policy-agent/opa/cmd/formats" "github.com/open-policy-agent/opa/cmd/internal/env" pr "github.com/open-policy-agent/opa/internal/presentation" "github.com/open-policy-agent/opa/v1/ast" "github.com/open-policy-agent/opa/v1/bundle" "github.com/open-policy-agent/opa/v1/loader" "github.com/open-policy-agent/opa/v1/util" ) type checkParams struct { format *util.EnumFlag errLimit int ignore []string bundleMode bool capabilities *capabilitiesFlag schema *schemaFlags strict bool regoV1 bool v0Compatible bool v1Compatible bool } func newCheckParams() checkParams { return checkParams{ format: formats.Flag(formats.Pretty, formats.JSON), capabilities: newCapabilitiesFlag(), schema: &schemaFlags{}, } } func (p *checkParams) regoVersion() ast.RegoVersion { // The '--rego-v1' flag takes precedence over the '--v1-compatible' flag. if p.regoV1 { return ast.RegoV0CompatV1 } // The '--v0-compatible' flag takes precedence over the '--v1-compatible' flag. if p.v0Compatible { return ast.RegoV0 } if p.v1Compatible { return ast.RegoV1 } return ast.DefaultRegoVersion } func checkModules(params checkParams, args []string) error { // ensure custom builtins are properly captured capabilities := params.capabilities.C if capabilities == nil { capabilities = ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(params.regoVersion())) } l := loader.NewFileLoader(). WithRegoVersion(params.regoVersion()). WithProcessAnnotation(true). WithBundleLazyLoadingMode(bundle.HasExtension()). WithCapabilities(capabilities) ss, err := loader.Schemas(params.schema.path) if err != nil { return err } compiler := ast.NewCompiler(). SetErrorLimit(params.errLimit). WithCapabilities(capabilities). WithSchemas(ss). WithEnablePrintStatements(true). WithStrict(params.strict). WithUseTypeCheckAnnotations(true) var modules map[string]*ast.Module if params.bundleMode { bundles := make([]*bundle.Bundle, 0, len(args)) for _, path := range args { b, err := l.WithSkipBundleVerification(true).WithFilter(filterFromPaths(params.ignore)).AsBundle(path) if err != nil { return err } bundles = append(bundles, b) } b, err := bundle.Merge(bundles) if err != nil { return err } modules = maps.Clone(b.ParsedModules("")) if len(b.Data) > 0 { compiler = compiler.WithPathConflictsCheck(mapFinder(b.Data)) } } else { result, err := l.Filtered(args, ignoredOnlyRego(params.ignore).Apply) if err != nil { return err } modules = result.ParsedModules() } if compiler.Compile(modules); compiler.Failed() { return compiler.Errors } return nil } // emulate storage.NonEmpty without having to create storage / transaction // returned function returns false, nil when m or path is empty func mapFinder(m map[string]any) func(path []string) (bool, error) { if len(m) == 0 { return emptyMapFinder } return func(path []string) (bool, error) { if len(path) == 0 { return false, nil } node := m for _, key := range path { if val, ok := node[key]; ok { if subMap, ok := val.(map[string]any); ok { node = subMap } else { return true, nil } } else { return false, nil } } return true, nil } } func emptyMapFinder(path []string) (bool, error) { return false, nil } func filterFromPaths(paths []string) loader.Filter { return func(abspath string, info fs.FileInfo, depth int) bool { return ignored(paths).Apply(abspath, info, depth) } } func outputErrors(format string, err error) { out := os.Stdout if err != nil { out = os.Stderr } switch format { case formats.JSON: if err := pr.JSON(out, pr.Output{Errors: pr.NewOutputErrors(err)}); err != nil { fmt.Fprintln(os.Stderr, err.Error()) } default: fmt.Fprintln(out, err) } } func initCheck(root *cobra.Command, _ string) { checkParams := newCheckParams() checkCommand := &cobra.Command{ Use: "check [path [...]]", Short: "Check Rego source files", Long: `Check Rego source files for parse and compilation errors. If the 'check' command succeeds in parsing and compiling the source file(s), no output is produced. If the parsing or compiling fails, 'check' will output the errors and exit with a non-zero exit code.`, PreRunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { return errors.New("specify at least one file") } return env.CmdFlags.CheckEnvironmentVariables(cmd) }, RunE: func(cmd *cobra.Command, args []string) error { cmd.SilenceErrors = true cmd.SilenceUsage = true if err := checkModules(checkParams, args); err != nil { outputErrors(checkParams.format.String(), err) return err } return nil }, } addMaxErrorsFlag(checkCommand.Flags(), &checkParams.errLimit) addIgnoreFlag(checkCommand.Flags(), &checkParams.ignore) addOutputFormat(checkCommand.Flags(), checkParams.format) addBundleModeFlag(checkCommand.Flags(), &checkParams.bundleMode, false) addCapabilitiesFlag(checkCommand.Flags(), checkParams.capabilities) addSchemaFlags(checkCommand.Flags(), checkParams.schema) addStrictFlag(checkCommand.Flags(), &checkParams.strict, false) addRegoV0V1FlagWithDescription(checkCommand.Flags(), &checkParams.regoV1, false, "check for Rego v0 and v1 compatibility (policies must be compatible with both Rego versions)") addV0CompatibleFlag(checkCommand.Flags(), &checkParams.v0Compatible, false) addV1CompatibleFlag(checkCommand.Flags(), &checkParams.v1Compatible, false) root.AddCommand(checkCommand) }