Skip to content

Commit

Permalink
Implement custom rego functions for PURL handling
Browse files Browse the repository at this point in the history
Signed-off-by: Mark Bestavros <mbestavr@redhat.com>
  • Loading branch information
mbestavros committed Nov 22, 2023
1 parent e876d3d commit f9f4690
Show file tree
Hide file tree
Showing 4 changed files with 184 additions and 0 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ require (
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc5 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/package-url/packageurl-go v0.1.2 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/peterh/liner v1.2.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1026,6 +1026,8 @@ github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/openzipkin/zipkin-go v0.3.0 h1:XtuXmOLIXLjiU2XduuWREDT0LOKtSgos/g7i7RYyoZQ=
github.com/openzipkin/zipkin-go v0.3.0/go.mod h1:4c3sLeE8xjNqehmF5RpAFLPLJxXscc0R4l6Zg0P1tTQ=
github.com/package-url/packageurl-go v0.1.2 h1:0H2DQt6DHd/NeRlVwW4EZ4oEI6Bn40XlNPRqegcxuo4=
github.com/package-url/packageurl-go v0.1.2/go.mod h1:uQd4a7Rh3ZsVg5j0lNyAfyxIeGde9yrlhjF78GzeW0c=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
Expand Down
103 changes: 103 additions & 0 deletions internal/evaluator/rego.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,15 @@ import (
"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/rego"
"github.com/open-policy-agent/opa/types"
"github.com/package-url/packageurl-go"
log "github.com/sirupsen/logrus"

"github.com/enterprise-contract/ec-cli/internal/fetchers/oci"
)

const ociBlobName = "ec.oci.blob"
const purlIsValidName = "ec.purl.is_valid"
const purlParseName = "ec.purl.parse"

func registerOCIBlob() {
decl := rego.Function{
Expand All @@ -58,6 +61,62 @@ func registerOCIBlob() {
rego.RegisterBuiltin1(&decl, ociBlob)
}

func registerPURLIsValid() {
decl := rego.Function{
Name: purlIsValidName,
Decl: types.NewFunction(
types.Args(
types.Named("purl", types.S).Description("the PURL"),
),
types.Named("result", types.S).Description("PURL validity"),
),
// As per the documentation, enable memoization to ensure function evaluation is
// deterministic.
Memoize: true,
Nondeterministic: false,
}

rego.RegisterBuiltin1(&decl, purlIsValid)
}

func registerPURLParse() {
decl := rego.Function{
Name: purlParseName,
Decl: types.NewFunction(
types.Args(
types.Named("purl", types.S).Description("the PURL"),
),
types.Named("object", types.NewObject(
[]*types.StaticProperty{
// Specifying the properties like this ensure the compiler catches typos when
// evaluating rego functions.
{Key: "type", Value: types.S},
{Key: "namespace", Value: types.S},
{Key: "name", Value: types.S},
{Key: "version", Value: types.S},
{Key: "qualifiers", Value: types.NewArray(
nil, types.NewObject(
[]*types.StaticProperty{
{Key: "key", Value: types.S},
{Key: "value", Value: types.S},
},
nil,
),
)},
{Key: "subpath", Value: types.S},
},
nil,
)).Description("the parsed PURL object"),
),
// As per the documentation, enable memoization to ensure function evaluation is
// deterministic.
Memoize: true,
Nondeterministic: false,
}

rego.RegisterBuiltin1(&decl, purlParse)
}

const maxBytes = 10 * 1024 * 1024 // 10 MB

func ociBlob(bctx rego.BuiltinContext, a *ast.Term) (*ast.Term, error) {
Expand Down Expand Up @@ -118,6 +177,50 @@ func ociBlob(bctx rego.BuiltinContext, a *ast.Term) (*ast.Term, error) {
return ast.StringTerm(blob.String()), nil
}

func purlIsValid(bctx rego.BuiltinContext, a *ast.Term) (*ast.Term, error) {
uri, ok := a.Value.(ast.String)
if !ok {
return ast.BooleanTerm(false), nil
}
_, err := packageurl.FromString(string(uri))
if err != nil {
log.Errorf("Parsing PURL %s failed: %s", uri, err)
return ast.BooleanTerm(false), nil
}
return ast.BooleanTerm(true), nil
}

func purlParse(bctx rego.BuiltinContext, a *ast.Term) (*ast.Term, error) {
uri, ok := a.Value.(ast.String)
if !ok {
return nil, nil
}
instance, err := packageurl.FromString(string(uri))
if err != nil {
log.Errorf("Parsing PURL %s failed: %s", uri, err)
return nil, nil
}

qualifiers := ast.NewArray()
for _, q := range instance.Qualifiers {
o := ast.NewObject(
ast.Item(ast.StringTerm("key"), ast.StringTerm(q.Key)),
ast.Item(ast.StringTerm("value"), ast.StringTerm(q.Value)),
)
qualifiers = qualifiers.Append(ast.NewTerm(o))
}
return ast.ObjectTerm(
ast.Item(ast.StringTerm("type"), ast.StringTerm(instance.Type)),
ast.Item(ast.StringTerm("namespace"), ast.StringTerm(instance.Namespace)),
ast.Item(ast.StringTerm("name"), ast.StringTerm(instance.Name)),
ast.Item(ast.StringTerm("version"), ast.StringTerm(instance.Version)),
ast.Item(ast.StringTerm("qualifiers"), ast.NewTerm(qualifiers)),
ast.Item(ast.StringTerm("subpath"), ast.StringTerm(instance.Subpath)),
), nil
}

func init() {
registerOCIBlob()
registerPURLIsValid()
registerPURLParse()
}
78 changes: 78 additions & 0 deletions internal/evaluator/rego_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,87 @@ func TestOCIBlob(t *testing.T) {
}
}

func TestPURLIsValid(t *testing.T) {
cases := []struct {
name string
uri *ast.Term
expected bool
}{
{
name: "success",
uri: ast.StringTerm("pkg:rpm/fedora/curl@7.50.3-1.fc25?arch=i386&distro=fedora-25"),
expected: true,
},
{
name: "unexpected uri type",
uri: ast.IntNumberTerm(42),
expected: false,
},
{
name: "malformed PURL string",
uri: ast.StringTerm("pkg::rpm//fedora/curl7.50.3-1.fc25?arch=i386&distro=fedora-"),
expected: false,
},
}

for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
ctx := context.Background()
bctx := rego.BuiltinContext{Context: ctx}

isValid, err := purlIsValid(bctx, c.uri)
require.NoError(t, err)
require.NotNil(t, isValid)
require.Equal(t, isValid, ast.BooleanTerm(c.expected))
})
}
}

func TestPURLParse(t *testing.T) {
cases := []struct {
name string
uri *ast.Term
err bool
}{
{
name: "success",
uri: ast.StringTerm("pkg:rpm/fedora/curl@7.50.3-1.fc25?arch=i386&distro=fedora-25"),
},
{
name: "unexpected uri type",
uri: ast.IntNumberTerm(42),
err: true,
},
{
name: "malformed PURL string",
uri: ast.StringTerm("pkg::rpm//fedora/curl7.50.3-1.fc25?arch=i386&distro=fedora-"),
err: true,
},
}

for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
ctx := context.Background()
bctx := rego.BuiltinContext{Context: ctx}

instance, err := purlParse(bctx, c.uri)
require.NoError(t, err)
if c.err {
require.Nil(t, instance)
} else {
require.NotNil(t, instance)
data := instance.Get(ast.StringTerm("type")).Value
require.Equal(t, ast.String("rpm"), data)
}
})
}
}

func TestFunctionsRegistered(t *testing.T) {
names := []string{
ociBlobName,
purlIsValidName,
purlParseName,
}
for _, name := range names {
t.Run(name, func(t *testing.T) {
Expand Down

0 comments on commit f9f4690

Please sign in to comment.