From 2ebe9569d522b0a6dc11afff234254a0d34764c5 Mon Sep 17 00:00:00 2001 From: Juan Antonio Osorio Date: Sat, 20 Jan 2024 13:55:43 +0200 Subject: [PATCH] rego: Add new function to list files using a glob pattern This makes it easy for us to write policies that take `yaml` or `yml` extensions into account without much hassle. --- internal/engine/eval/rego/lib.go | 65 ++++++++++++++++++++++ internal/engine/eval/rego/lib_test.go | 77 +++++++++++++++++++++++++++ 2 files changed, 142 insertions(+) diff --git a/internal/engine/eval/rego/lib.go b/internal/engine/eval/rego/lib.go index 64978e9482..41d7bd501b 100644 --- a/internal/engine/eval/rego/lib.go +++ b/internal/engine/eval/rego/lib.go @@ -19,10 +19,12 @@ import ( "errors" "fmt" "io" + "io/fs" "os" "path/filepath" "github.com/go-git/go-billy/v5" + billyutil "github.com/go-git/go-billy/v5/util" "github.com/open-policy-agent/opa/ast" "github.com/open-policy-agent/opa/rego" "github.com/open-policy-agent/opa/types" @@ -36,6 +38,7 @@ import ( var MinderRegoLib = []func(res *engif.Result) func(*rego.Rego){ FileExists, FileLs, + FileLsGlob, FileRead, ListGithubActions, } @@ -202,6 +205,68 @@ func FileLs(res *engif.Result) func(*rego.Rego) { ) } +// FileLsGlob is a rego function that lists the files matching a glob in a directory +// in the filesystem being evaluated (which comes from the ingester). +// It takes one argument, the path to the pattern to match. It's exposed +// as `file.ls_glob`. +func FileLsGlob(res *engif.Result) func(*rego.Rego) { + return rego.Function1( + ®o.Function{ + Name: "file.ls_glob", + Decl: types.NewFunction(types.Args(types.S), types.A), + }, + func(bctx rego.BuiltinContext, op1 *ast.Term) (*ast.Term, error) { + var path string + if err := ast.As(op1.Value, &path); err != nil { + return nil, err + } + + if res.Fs == nil { + return nil, fmt.Errorf("cannot walk file without a filesystem") + } + + rfs := res.Fs + + // Let's get the base directory and the glob pattern + base := filepath.Dir(path) + + pattern := filepath.Base(path) + files := []*ast.Term{} + + err := billyutil.Walk(rfs, base, func(path string, info fs.FileInfo, err error) error { + // If we got an error, skip the file + if err != nil { + return nil + } + + // If we gave no pattern, we're listing all files + if len(pattern) == 0 { + files = append(files, ast.NewTerm(ast.String(path))) + return nil + } + + // list all files matching the pattern + matched, merr := filepath.Match(pattern, info.Name()) + if merr != nil { + return merr + } + + if matched { + files = append(files, ast.NewTerm(ast.String(path))) + } + + return nil + }) + if err != nil { + return nil, err + } + + return ast.NewTerm( + ast.NewArray(files...)), nil + }, + ) +} + func fileLsHandleError(err error) (*ast.Term, error) { // If the file does not exist return null if errors.Is(err, os.ErrNotExist) { diff --git a/internal/engine/eval/rego/lib_test.go b/internal/engine/eval/rego/lib_test.go index fd0913ff6c..df43a0f6ee 100644 --- a/internal/engine/eval/rego/lib_test.go +++ b/internal/engine/eval/rego/lib_test.go @@ -554,3 +554,80 @@ allow { }) require.NoError(t, err, "could not evaluate") } + +func TestListYamlUsingLSGlob(t *testing.T) { + t.Parallel() + + fs := memfs.New() + + require.NoError(t, fs.MkdirAll(".github", 0755)) + + _, err := fs.Create(".github/dependabot.yaml") + require.NoError(t, err, "could not create dependabot file") + + e, err := rego.NewRegoEvaluator( + &minderv1.RuleType_Definition_Eval_Rego{ + Type: rego.DenyByDefaultEvaluationType.String(), + Def: ` +package minder + +default allow = false + +allow { + files := file.ls_glob(".github/dependabot.y*ml") + count(files) == 1 +}`, + }, + ) + require.NoError(t, err, "could not create evaluator") + + emptyPol := map[string]any{} + + err = e.Eval(context.Background(), emptyPol, &engif.Result{ + Object: nil, + Fs: fs, + }) + require.NoError(t, err, "could not evaluate") +} + +func TestListYamlsUsingLSGlob(t *testing.T) { + t.Parallel() + + fs := memfs.New() + + require.NoError(t, fs.MkdirAll(".github", 0755)) + require.NoError(t, fs.MkdirAll(".github/workflows", 0755)) + + _, err := fs.Create(".github/workflows/security.yaml") + require.NoError(t, err, "could not create sec workflow file") + + _, err = fs.Create(".github/workflows/build.yml") + require.NoError(t, err, "could not create build workflow file") + + _, err = fs.Create(".github/workflows/release.yaml") + require.NoError(t, err, "could not create release workflow file") + + e, err := rego.NewRegoEvaluator( + &minderv1.RuleType_Definition_Eval_Rego{ + Type: rego.DenyByDefaultEvaluationType.String(), + Def: ` +package minder + +default allow = false + +allow { + files := file.ls_glob(".github/workflows/*.y*ml") + count(files) == 3 +}`, + }, + ) + require.NoError(t, err, "could not create evaluator") + + emptyPol := map[string]any{} + + err = e.Eval(context.Background(), emptyPol, &engif.Result{ + Object: nil, + Fs: fs, + }) + require.NoError(t, err, "could not evaluate") +}