Skip to content

Commit

Permalink
Introduce rego function to parse TOML files (#5294)
Browse files Browse the repository at this point in the history
This introduces `parse_toml` to our rego library. It allows us to parse
TOML files in order to more easily write policies for things like
`pyproject.toml`.

Signed-off-by: Juan Antonio Osorio <[email protected]>
  • Loading branch information
JAORMX authored Jan 14, 2025
1 parent cfd632b commit cc38ccd
Show file tree
Hide file tree
Showing 2 changed files with 114 additions and 0 deletions.
39 changes: 39 additions & 0 deletions internal/engine/eval/rego/lib.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/open-policy-agent/opa/v1/ast"
"github.com/open-policy-agent/opa/v1/rego"
"github.com/open-policy-agent/opa/v1/types"
"github.com/pelletier/go-toml/v2"
"github.com/stacklok/frizbee/pkg/replacer"
"github.com/stacklok/frizbee/pkg/utils/config"
"gopkg.in/yaml.v3"
Expand All @@ -42,6 +43,7 @@ var MinderRegoLib = []func(res *interfaces.Result) func(*rego.Rego){
FileWalk,
ListGithubActions,
ParseYaml,
ParseToml,
JQIsTrue,
}

Expand Down Expand Up @@ -724,3 +726,40 @@ func ParseYaml(_ *interfaces.Result) func(*rego.Rego) {
},
)
}

// ParseToml is a rego function that parses a TOML configuration string into a structured data format.
// It takes one argument: the TOML content as a string.
// It returns the parsed TOML data as an AST term.
// It's exposed as `parse_toml`.
func ParseToml(_ *interfaces.Result) func(*rego.Rego) {
return rego.Function1(
&rego.Function{
Name: "parse_toml",
// Takes one string argument (the YAML content) and returns any type
Decl: types.NewFunction(types.Args(types.S), types.A),
},
func(_ rego.BuiltinContext, content *ast.Term) (*ast.Term, error) {
var tomlStr string

// Convert the YAML input from the term into a string
if err := ast.As(content.Value, &tomlStr); err != nil {
return nil, err
}

// Convert the YAML string into a Go map
var jsonObj any
err := toml.Unmarshal([]byte(tomlStr), &jsonObj)
if err != nil {
return nil, fmt.Errorf("error converting YAML to JSON: %w", err)
}

// Convert the Go value to an ast.Value
value, err := ast.InterfaceToValue(jsonObj)
if err != nil {
return nil, fmt.Errorf("error converting to AST value: %w", err)
}

return ast.NewTerm(value), nil
},
)
}
75 changes: 75 additions & 0 deletions internal/engine/eval/rego/lib_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1122,3 +1122,78 @@ allow {
})
}
}

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

scenario := []struct {
name string
toml string
want string
wantErr bool
}{
{
name: "simple key-value",
toml: "foo = \"bar\"",
want: `{"foo": "bar"}`,
},
{
name: "nested structure",
toml: `
[foo.bar]
baz = "qux"`,
want: `{"foo": {"bar": {"baz": "qux"}}}`,
},
{
name: "array values",
toml: `
items = ["foo", "bar", "baz"]`,
want: `{"items": ["foo", "bar", "baz"]}`,
},
{
name: "parse array of tables",
toml: `
[[items]]
name = "foo"
[[items]]
name = "bar"`,
want: `{"items": [{"name": "foo"}, {"name": "bar"}]}`,
},
}

for _, s := range scenario {
t.Run(s.name, func(t *testing.T) {
t.Parallel()

regoCode := fmt.Sprintf(`
package minder
default allow = false
allow {
parsed := parse_toml(%q)
print(parsed)
expected := json.unmarshal(%q)
parsed == expected
}`, s.toml, s.want)

e, err := rego.NewRegoEvaluator(
&minderv1.RuleType_Definition_Eval_Rego{
Type: rego.DenyByDefaultEvaluationType.String(),
Def: regoCode,
},
nil,
)

require.NoError(t, err, "could not create evaluator")

_, err = e.Eval(context.Background(), map[string]any{}, nil, &interfaces.Result{})

if s.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}

0 comments on commit cc38ccd

Please sign in to comment.