Skip to content

Commit

Permalink
rego: Add new function to list files using a glob pattern
Browse files Browse the repository at this point in the history
This makes it easy for us to write policies that take `yaml` or `yml`
extensions into account without much hassle.
  • Loading branch information
JAORMX committed Jan 20, 2024
1 parent 4416c7f commit 2ebe956
Show file tree
Hide file tree
Showing 2 changed files with 142 additions and 0 deletions.
65 changes: 65 additions & 0 deletions internal/engine/eval/rego/lib.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -36,6 +38,7 @@ import (
var MinderRegoLib = []func(res *engif.Result) func(*rego.Rego){
FileExists,
FileLs,
FileLsGlob,
FileRead,
ListGithubActions,
}
Expand Down Expand Up @@ -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(
&rego.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) {
Expand Down
77 changes: 77 additions & 0 deletions internal/engine/eval/rego/lib_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}

0 comments on commit 2ebe956

Please sign in to comment.