Skip to content

Commit

Permalink
Merge pull request #825 from stacklok/git-rego-rule
Browse files Browse the repository at this point in the history
Putting it all together: Enables us to run rego rules on git contents
  • Loading branch information
JAORMX authored Sep 1, 2023
2 parents 5cea6d9 + f84e3bc commit d8e4171
Show file tree
Hide file tree
Showing 11 changed files with 349 additions and 9 deletions.
5 changes: 3 additions & 2 deletions database/query/repositories.sql
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ webhook_id = $8,
webhook_url = $9,
deploy_url = $10,
provider = $11,
clone_url = $12,
-- set clone_url if the value is not an empty string
clone_url = CASE WHEN sqlc.arg(clone_url)::text = '' THEN clone_url ELSE sqlc.arg(clone_url)::text END,
updated_at = NOW()
WHERE id = $1 RETURNING *;

Expand All @@ -75,7 +76,7 @@ webhook_id = $7,
webhook_url = $8,
deploy_url = $9,
provider = $10,
clone_url = $11,
clone_url = CASE WHEN sqlc.arg(clone_url)::text = '' THEN clone_url ELSE sqlc.arg(clone_url)::text END,
updated_at = NOW()
WHERE repo_id = $1 RETURNING *;

Expand Down
2 changes: 2 additions & 0 deletions examples/github/policies/policy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ repository:
def:
default_workflow_permissions: read
can_approve_pull_request_reviews: false
- type: codeql_enabled
def: {}
# Note that this only applies to private repositories.
# So if you want to test this, you'll need to create a private repository.
# - type: repo_workflow_access_level
Expand Down
45 changes: 45 additions & 0 deletions examples/github/rule-types/codeql_enabled.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
version: v1
type: rule-type
name: codeql_enabled
context:
provider: github
group: Root Group
description: Verifies that CodeQL is enabled for the repository
guidance: |
CodeQL is a tool that can be used to analyze code for security vulnerabilities.
It is recommended that repositories have some form of static analysis enabled
to ensure that vulnerabilities are not introduced into the codebase.
To enable CodeQL, add a GitHub workflow to the repository that runs the
CodeQL analysis. For more information, see the [CodeQL documentation](https://docs.github.com/en/code-security/secure-coding/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#configuring-code-scanning-for-a-private-repository).
def:
# Defines the section of the pipeline the rule will appear in.
# This will affect the template that is used to render multiple parts
# of the rule.
in_entity: repository
# Defines the schema for writing a rule with this rule being checked
# In this case there is no settings that need to be configured
rule_schema: {}
# Defines the configuration for ingesting data relevant for the rule
ingest:
type: git
git:
branch: main
# Defines the configuration for evaluating data ingested against the given policy
eval:
type: rego
rego:
type: deny-by-default
def: |
package mediator
default allow := false
allow {
some i
workflowstr := file.read("./.github/workflows/codeql.yml")
workflow := yaml.unmarshal(workflowstr)
steps := workflow.jobs.analyze.steps[i]
contains(steps.uses, "github/codeql-action/analyze@")
}
5 changes: 2 additions & 3 deletions internal/engine/eval/rego/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
package rego

import (
"bytes"
"context"
"fmt"

Expand Down Expand Up @@ -81,9 +80,9 @@ func (e *Evaluator) Eval(ctx context.Context, pol map[string]any, res *engif.Res
// this explicitly.
obj := res.Object

var buf bytes.Buffer
libFuncs := instantiateRegoLib(res)
r := e.newRegoFromOptions(
rego.Dump(&buf),
libFuncs...,
)
pq, err := r.PrepareForEval(ctx)
if err != nil {
Expand Down
123 changes: 123 additions & 0 deletions internal/engine/eval/rego/lib.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Copyright 2023 Stacklok, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.role/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package rule provides the CLI subcommand for managing rules

package rego

import (
"errors"
"fmt"
"io"
"os"
"path/filepath"

"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/rego"
"github.com/open-policy-agent/opa/types"

engif "github.com/stacklok/mediator/internal/engine/interfaces"
)

var mediatorRegoLib = []func(res *engif.Result) func(*rego.Rego){
FileExists,
FileRead,
}

func instantiateRegoLib(res *engif.Result) []func(*rego.Rego) {
var lib []func(*rego.Rego)
for _, f := range mediatorRegoLib {
lib = append(lib, f(res))
}
return lib
}

// FileExists is a rego function that checks if a file exists
// in the filesystem being evaluated (which comes from the ingester).
// It takes one argument, the path to the file to check.
// It's exposed as `file.exists`.
func FileExists(res *engif.Result) func(*rego.Rego) {
return rego.Function1(
&rego.Function{
Name: "file.exists",
Decl: types.NewFunction(types.Args(types.S), types.B),
},
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 check file existence without a filesystem")
}

fs := res.Fs

cpath := filepath.Clean(path)
finfo, err := fs.Stat(cpath)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return ast.BooleanTerm(false), nil
}
return nil, err
}

if finfo.IsDir() {
return ast.BooleanTerm(false), nil
}

return ast.BooleanTerm(true), nil
},
)
}

// FileRead is a rego function that reads a file from the filesystem
// being evaluated (which comes from the ingester). It takes one argument,
// the path to the file to read. It's exposed as `file.read`.
func FileRead(res *engif.Result) func(*rego.Rego) {
return rego.Function1(
&rego.Function{
Name: "file.read",
Decl: types.NewFunction(types.Args(types.S), types.S),
},
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 read file without a filesystem")
}

fs := res.Fs

cpath := filepath.Clean(path)
f, err := fs.Open(cpath)
if err != nil {
return nil, err
}

defer f.Close()

all, rerr := io.ReadAll(f)
if rerr != nil {
return nil, rerr
}

allstr := ast.String(all)
return ast.NewTerm(allstr), nil
},
)
}
166 changes: 166 additions & 0 deletions internal/engine/eval/rego/lib_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// Copyright 2023 Stacklok, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.role/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package rule provides the CLI subcommand for managing rules

package rego_test

import (
"context"
"testing"

memfs "github.com/go-git/go-billy/v5/memfs"
"github.com/stretchr/testify/require"

engerrors "github.com/stacklok/mediator/internal/engine/errors"
"github.com/stacklok/mediator/internal/engine/eval/rego"
engif "github.com/stacklok/mediator/internal/engine/interfaces"
pb "github.com/stacklok/mediator/pkg/generated/protobuf/go/mediator/v1"
)

func TestFileExistsWithExistingFile(t *testing.T) {
t.Parallel()

fs := memfs.New()

// Create a file
_, err := fs.Create("foo")
require.NoError(t, err, "could not create file")

e, err := rego.NewRegoEvaluator(
&pb.RuleType_Definition_Eval_Rego{
Type: rego.DenyByDefaultEvaluationType.String(),
Def: `
package mediator
default allow = false
allow {
file.exists("foo")
}`,
},
)
require.NoError(t, err, "could not create evaluator")

emptyPol := map[string]any{}

// Matches
err = e.Eval(context.Background(), emptyPol, &engif.Result{
Object: nil,
Fs: fs,
})
require.NoError(t, err, "could not evaluate")
}

func TestFileExistsWithNonExistentFile(t *testing.T) {
t.Parallel()

fs := memfs.New()

e, err := rego.NewRegoEvaluator(
&pb.RuleType_Definition_Eval_Rego{
Type: rego.DenyByDefaultEvaluationType.String(),
Def: `
package mediator
default allow = false
allow {
file.exists("unexistent")
}`,
},
)
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.ErrorIs(t, err, engerrors.ErrEvaluationFailed, "could not evaluate")
}

func TestFileReadWithContentsMatching(t *testing.T) {
t.Parallel()

fs := memfs.New()

// Create a file
f, err := fs.Create("foo")
require.NoError(t, err, "could not create file")

_, err = f.Write([]byte("bar"))
require.NoError(t, err, "could not write to file")

e, err := rego.NewRegoEvaluator(
&pb.RuleType_Definition_Eval_Rego{
Type: rego.DenyByDefaultEvaluationType.String(),
Def: `
package mediator
default allow = false
allow {
contents := file.read("foo")
contents == "bar"
}`,
},
)
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 TestFileReadWithContentsNotMatching(t *testing.T) {
t.Parallel()

fs := memfs.New()

// Create a file
f, err := fs.Create("foo")
require.NoError(t, err, "could not create file")

_, err = f.Write([]byte("baz"))
require.NoError(t, err, "could not write to file")

e, err := rego.NewRegoEvaluator(
&pb.RuleType_Definition_Eval_Rego{
Type: rego.DenyByDefaultEvaluationType.String(),
Def: `
package mediator
default allow = false
allow {
contents := file.read("foo")
contents == "bar"
}`,
},
)
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.ErrorIs(t, err, engerrors.ErrEvaluationFailed, "could not evaluate")
}
Loading

0 comments on commit d8e4171

Please sign in to comment.