-
Notifications
You must be signed in to change notification settings - Fork 43
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #825 from stacklok/git-rego-rule
Putting it all together: Enables us to run rego rules on git contents
- Loading branch information
Showing
11 changed files
with
349 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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@") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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( | ||
®o.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( | ||
®o.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 | ||
}, | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} |
Oops, something went wrong.