Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ go 1.25.0

require (
github.com/Masterminds/semver v1.5.0
github.com/cespare/xxhash/v2 v2.3.0
github.com/doug-martin/goqu/v8 v8.6.0
github.com/google/cel-go v0.30.0
github.com/google/go-cmp v0.7.0
github.com/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.10.0
Expand Down Expand Up @@ -35,9 +37,10 @@ require (
)

require (
cel.dev/expr v0.25.1 // indirect
github.com/anchore/go-struct-converter v0.1.0 // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
Expand All @@ -56,7 +59,11 @@ require (
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 // indirect
golang.org/x/mod v0.38.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 // indirect
google.golang.org/protobuf v1.36.11 // indirect
modernc.org/libc v1.74.1 // indirect
modernc.org/mathutil v1.7.1 // indirect
Expand Down
22 changes: 22 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08=
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/anchore/go-struct-converter v0.1.0 h1:2rDRssAl6mgKBSLNiVCMADgZRhoqtw9dedlWa0OhD30=
github.com/anchore/go-struct-converter v0.1.0/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA=
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
Expand All @@ -26,6 +30,8 @@ github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/cel-go v0.30.0 h1:ll54AkzKunWkBn9wSoiUXbFZXYZTkdJGNXTBXUoolGo=
github.com/google/cel-go v0.30.0/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
Expand All @@ -49,6 +55,10 @@ github.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f h1:GvCU5GX
github.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f/go.mod h1:q59u9px8b7UTj0nIjEjvmTWekazka6xIt6Uogz5Dm+8=
github.com/knqyf263/go-deb-version v0.0.0-20190517075300-09fca494f03d h1:X4cedH4Kn3JPupAwwWuo4AzYp16P0OyLO9d7OnMZc/c=
github.com/knqyf263/go-deb-version v0.0.0-20190517075300-09fca494f03d/go.mod h1:o8sgWoz3JADecfc/cTYD92/Et1yMqMy0utV1z+VaZao=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
Expand Down Expand Up @@ -85,6 +95,8 @@ github.com/regclient/regclient v0.11.5 h1:OHRsXO0F3qHGfa4HEUv+EkMH9NXNcCTBKjNzyC
github.com/regclient/regclient v0.11.5/go.mod h1:DZUOfIT14WFTK2Pj4vjd93avy9O4Fdpjrf9ir23TbRE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/spdx/tools-golang v0.5.7 h1:+sWcKGnhwp3vLdMqPcLdA6QK679vd86cK9hQWH3AwCg=
Expand Down Expand Up @@ -114,10 +126,14 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA=
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
Expand Down Expand Up @@ -149,9 +165,15 @@ golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 h1:YcyjlL1PRr2Q17/I0dPk2JmYS5CDXfcdb2Z3YRioEbw=
google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 h1:2035KHhUv+EpyB+hWgJnaWKJOdX1E95w2S8Rr4uWKTs=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down
93 changes: 93 additions & 0 deletions rhel/vex/fixed_in_cel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package vex

import (
"fmt"

"github.com/cespare/xxhash/v2"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/ext"
"github.com/package-url/packageurl-go"
)

// CompileFixedInVersionCEL compiles a CEL expression that evaluates to a string.
//
// The expression may use these variables:
//
// - type (string): PURL type (oci, rpm, etc.)
// - namespace (string): PURL namespace
// - name (string): PURL name
// - version (string): PURL version field
// - qualifiers (map[string]string): PURL qualifiers
// - fixed_in (string): default FixedInVersion from stock extraction
//
// The environment includes the CEL strings extension (startsWith,
// substring, split etc.) and bindings to be able to set variables.
//
// An empty expression returns a nil program.
// Production expressions are supplied by callers if desired.
func CompileFixedInVersionCEL(expr string) (cel.Program, error) {
if expr == "" {
return nil, nil
}
env, err := fixedInCELEnv()
if err != nil {
return nil, err
}
ast, iss := env.Compile(expr)
if iss.Err() != nil {
return nil, fmt.Errorf("fixed_in_version_cel: compile: %w", iss.Err())
}
if !ast.OutputType().IsExactType(cel.StringType) {
return nil, fmt.Errorf("fixed_in_version_cel: expression must evaluate to string, got %v", ast.OutputType())
}
prog, err := env.Program(ast)
if err != nil {
return nil, fmt.Errorf("fixed_in_version_cel: program: %w", err)
}
return prog, nil
}

// EvalFixedInVersionCEL evaluates a compiled FixedInVersion CEL program.
func EvalFixedInVersionCEL(prog cel.Program, p *packageurl.PackageURL, defaultVersion string) (string, error) {
if prog == nil {
return defaultVersion, nil
}
out, _, err := prog.Eval(map[string]any{
"type": p.Type,
"namespace": p.Namespace,
"name": p.Name,
"version": p.Version,
"qualifiers": p.Qualifiers.Map(),
"fixed_in": defaultVersion,
})
if err != nil {
return "", fmt.Errorf("fixed_in_version_cel: eval: %w", err)
}
s, ok := out.Value().(string)
if !ok {
return "", fmt.Errorf("fixed_in_version_cel: result type %T, want string", out.Value())
}
return s, nil
}

func fixedInCELEnv() (*cel.Env, error) {
return cel.NewEnv(
cel.Variable("type", cel.StringType),
cel.Variable("namespace", cel.StringType),
cel.Variable("name", cel.StringType),
cel.Variable("version", cel.StringType),
cel.Variable("qualifiers", cel.MapType(cel.StringType, cel.StringType)),
cel.Variable("fixed_in", cel.StringType),
ext.Strings(),
ext.Bindings(),
)
}

// CelExprFingerprintDigest returns a hex-encoded xxhash of expr for fingerprinting.
// Empty expr returns an empty string.
func celExprFingerprintDigest(expr string) string {
if expr == "" {
return ""
}
return fmt.Sprintf("%016x", xxhash.Sum64String(expr))
}
176 changes: 176 additions & 0 deletions rhel/vex/fixed_in_cel_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package vex

import (
"testing"

"github.com/package-url/packageurl-go"
)

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

testcases := []struct {
name string
expr string
wantNil bool
wantErr bool
}{
{
name: "empty",
expr: "",
wantNil: true,
},
{
name: "invalid",
expr: "this is not valid CEL",
wantErr: true,
},
{
name: "non-string",
expr: "true",
wantErr: true,
},
{
name: "valid",
expr: `fixed_in`,
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
prog, err := CompileFixedInVersionCEL(tc.expr)
if tc.wantErr {
if err == nil {
t.Fatal("expected compile error")
}
return
}
if err != nil {
t.Fatalf("compile: %v", err)
}
if tc.wantNil {
if prog != nil {
t.Fatal("expected nil program")
}
return
}
if prog == nil {
t.Fatal("expected non-nil program")
}
})
}
}

// TestEvalFixedInVersionCEL exercises rewriting FixedInVersion via a CEL
// expression that uses PURL fields such as type and qualifiers.
func TestEvalFixedInVersionCEL(t *testing.T) {
t.Parallel()

testcases := []struct {
name string
expr string
purl packageurl.PackageURL
stock string
want string
}{
{
name: "oci with tag2",
// Prefer an alternate qualifier when present; otherwise keep stock fixed_in.
expr: `type == "oci" && has(qualifiers.tag2) ? qualifiers.tag2 : fixed_in`,
purl: packageurl.PackageURL{
Type: packageurl.TypeOCI,
Namespace: "redhat",
Name: "example",
Qualifiers: packageurl.QualifiersFromMap(map[string]string{
"tag": "1.0.0",
"tag2": "2.0.0-from-tag2",
"repository_url": "registry.example/example",
}),
},
stock: "1.0.0",
want: "2.0.0-from-tag2",
},
{
name: "oci without tag2",
expr: `type == "oci" && has(qualifiers.tag2) ? qualifiers.tag2 : fixed_in`,
purl: packageurl.PackageURL{
Type: packageurl.TypeOCI,
Name: "example",
Qualifiers: packageurl.QualifiersFromMap(map[string]string{
"tag": "1.0.0",
}),
},
stock: "1.0.0",
want: "1.0.0",
},
{
name: "rpm unchanged",
expr: `type == "oci" && has(qualifiers.tag2) ? qualifiers.tag2 : fixed_in`,
purl: packageurl.PackageURL{
Type: packageurl.TypeRPM,
Namespace: "redhat",
Name: "bash",
Version: "5.1.8-6.el9",
Qualifiers: packageurl.QualifiersFromMap(map[string]string{
"epoch": "0",
}),
},
stock: "0:5.1.8-6.el9",
want: "0:5.1.8-6.el9",
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
prog, err := CompileFixedInVersionCEL(tc.expr)
if err != nil {
t.Fatalf("compile: %v", err)
}
opt, err := WithFixedInVersionCEL(tc.expr)
if err != nil {
t.Fatalf("WithFixedInVersionCEL: %v", err)
}
p := NewParser(opt)

stock, err := extractFixedInVersion(&tc.purl)
if err != nil {
t.Fatalf("stock extract: %v", err)
}
if stock != tc.stock {
t.Fatalf("stock = %q, want %q", stock, tc.stock)
}
got, err := EvalFixedInVersionCEL(prog, &tc.purl, stock)
if err != nil {
t.Fatalf("cel eval: %v", err)
}
if got != tc.want {
t.Fatalf("cel rewrite = %q, want %q", got, tc.want)
}
gotCreator, err := (&creator{fixedInCEL: p.fixedInCEL}).FixedInVersion(&tc.purl)
if err != nil {
t.Fatalf("creator.FixedInVersion: %v", err)
}
if gotCreator != tc.want {
t.Fatalf("creator rewrite = %q, want %q", gotCreator, tc.want)
}
})
}
}

func TestFingerprintVersionCEL(t *testing.T) {
t.Parallel()
base := &Updater{}
withCEL := &Updater{fixedInCELDigest: celExprFingerprintDigest(`fixed_in`)}
otherCEL := &Updater{fixedInCELDigest: celExprFingerprintDigest(`type == "oci" ? fixed_in : fixed_in`)}

if base.fingerprintVersion() != updaterVersion {
t.Fatalf("base = %q, want %q", base.fingerprintVersion(), updaterVersion)
}
if withCEL.fingerprintVersion() == base.fingerprintVersion() {
t.Fatal("CEL fingerprint should differ from base")
}
if withCEL.fingerprintVersion() == otherCEL.fingerprintVersion() {
t.Fatal("different CEL expressions should produce different fingerprints")
}
if d := celExprFingerprintDigest(""); d != "" {
t.Fatalf("empty digest = %q", d)
}
}
Loading
Loading