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
3 changes: 2 additions & 1 deletion java/coalescer.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/quay/claircore"
"github.com/quay/claircore/indexer"
rheljava "github.com/quay/claircore/rhel/java"
)

type coalescer struct{}
Expand Down Expand Up @@ -144,7 +145,7 @@ func sbomRepositoryIDs(ctx context.Context, pkg *claircore.Package, repos []*cla
for _, want := range wants {
var matched bool
for _, r := range repos {
if r.Key != RedHatCPERepositoryKey {
if r.Key != rheljava.RepositoryKey {
continue
}
if r.Name != want && r.CPE.String() != want {
Expand Down
9 changes: 5 additions & 4 deletions java/coalescer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/quay/claircore"
"github.com/quay/claircore/indexer"
rheljava "github.com/quay/claircore/rhel/java"
"github.com/quay/claircore/test"
"github.com/quay/claircore/toolkit/types/cpe"
)
Expand All @@ -18,7 +19,7 @@ func TestCoalescerSplitsRepositories(t *testing.T) {
cpeRepo := &claircore.Repository{
ID: "1",
Name: w.String(),
Key: RedHatCPERepositoryKey,
Key: rheljava.RepositoryKey,
CPE: w,
}
maven := Repository
Expand Down Expand Up @@ -64,8 +65,8 @@ func TestCoalescerMultipleProductCPEs(t *testing.T) {
ctx := test.Logging(t)
q := cpe.MustUnbind("cpe:/a:redhat:quarkus:3.33")
c := cpe.MustUnbind("cpe:/a:redhat:apache_camel_quarkus:3.33")
quarkus := &claircore.Repository{ID: "1", Name: q.String(), Key: RedHatCPERepositoryKey, CPE: q}
camel := &claircore.Repository{ID: "2", Name: c.String(), Key: RedHatCPERepositoryKey, CPE: c}
quarkus := &claircore.Repository{ID: "1", Name: q.String(), Key: rheljava.RepositoryKey, CPE: q}
camel := &claircore.Repository{ID: "2", Name: c.String(), Key: rheljava.RepositoryKey, CPE: c}
maven := Repository
maven.ID = "3"
pkg := &claircore.Package{
Expand Down Expand Up @@ -100,7 +101,7 @@ func TestCoalescerDropsCoveredJars(t *testing.T) {
cpeRepo := &claircore.Repository{
ID: "1",
Name: w.String(),
Key: RedHatCPERepositoryKey,
Key: rheljava.RepositoryKey,
CPE: w,
}
maven := Repository
Expand Down
3 changes: 2 additions & 1 deletion java/detector.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/quay/claircore/indexer"
"github.com/quay/claircore/java/bom"
"github.com/quay/claircore/java/jar"
rheljava "github.com/quay/claircore/rhel/java"
"github.com/quay/claircore/rpm"
"github.com/quay/claircore/toolkit/types/cpe"
)
Expand Down Expand Up @@ -54,7 +55,7 @@ func (*Detector) Scan(ctx context.Context, layer *claircore.Layer) ([]*claircore
}
seen[s] = &claircore.Repository{
Name: s,
Key: RedHatCPERepositoryKey,
Key: rheljava.RepositoryKey,
CPE: w,
}
}
Expand Down
5 changes: 0 additions & 5 deletions java/packagescanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,6 @@ var (
}
)

// RedHatCPERepositoryKey is the repository key for a Red Hat product CPE
// attributed to a Java component. It is distinct from the Maven Central
// repository and from the RPM CPE repository.
const RedHatCPERepositoryKey = "redhat-java-cpe-repository"

// DefaultSearchAPI is a maven-like REST API that may be used to do
// reverse lookups based on an archive's sha1 sum.
//
Expand Down
2 changes: 2 additions & 0 deletions matchers/defaults/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/quay/claircore/photon"
"github.com/quay/claircore/python"
"github.com/quay/claircore/rhel"
rheljava "github.com/quay/claircore/rhel/java"
"github.com/quay/claircore/rhel/rhcc"
"github.com/quay/claircore/ruby"
"github.com/quay/claircore/suse"
Expand Down Expand Up @@ -52,6 +53,7 @@ var defaultMatchers = []driver.Matcher{
&oracle.Matcher{},
&photon.Matcher{},
&python.Matcher{},
&rheljava.RedHatMatcher{},
rhcc.Matcher,
&ruby.Matcher{},
&suse.Matcher{},
Expand Down
73 changes: 73 additions & 0 deletions rhel/java/matcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Package java matches Java packages recorded under a Red Hat product CPE.
package java

import (
"context"
"log/slog"

"github.com/quay/claircore"
"github.com/quay/claircore/internal/maven"
"github.com/quay/claircore/libvuln/driver"
"github.com/quay/claircore/rhel"
"github.com/quay/claircore/toolkit/types/cpe"
)

// RedHatMatcher matches Java packages recorded under a Red Hat product CPE.
type RedHatMatcher struct{}

var _ driver.Matcher = (*RedHatMatcher)(nil)

// Name implements [driver.Matcher].
func (*RedHatMatcher) Name() string { return "redhat-java" }

// Filter implements [driver.Matcher].
func (*RedHatMatcher) Filter(record *claircore.IndexRecord) bool {
return record.Repository != nil && record.Repository.Key == RepositoryKey
}

// Query implements [driver.Matcher].
func (*RedHatMatcher) Query() []driver.MatchConstraint {
return []driver.MatchConstraint{driver.RepositoryKey}
}

// Vulnerable implements [driver.Matcher].
//
// The vulnerability repository name is a normalized CPE. It must match
// the indexed CPE, including the Red Hat version-stream rule. An empty
// FixedInVersion matches any installed version. An empty installed version
// is not compared when a fix exists: a Maven null version is older than
// every real fix. Otherwise the installed Maven version is vulnerable when
// it is strictly older.
func (*RedHatMatcher) Vulnerable(ctx context.Context, record *claircore.IndexRecord, vuln *claircore.Vulnerability) (bool, error) {
if vuln.Repo == nil || record.Repository == nil || vuln.Repo.Key != RepositoryKey {
return false, nil
}
var err error
// Vulnerability repositories do not persist CPE. The name is the normalized form.
vuln.Repo.CPE, err = cpe.Unbind(vuln.Repo.Name)
if err != nil {
slog.WarnContext(ctx, "unable to unbind repo CPE", "reason", err, "vulnerability name", vuln.Name)
return false, nil
}
if !rhel.IsCPESubstringMatch(record.Repository.CPE, vuln.Repo.CPE) {
return false, nil
}
if vuln.FixedInVersion == "" {
return true, nil
}
// An empty version parses as a Maven null version, which compares older
// than every real fix. There is no installed version to compare.
if record.Package == nil || record.Package.Version == "" {
slog.WarnContext(ctx, "empty package version", "vulnerability name", vuln.Name)
return false, nil
}
installed, err := maven.ParseVersion(record.Package.Version)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it may be possible to have an empty ("") record.Package.Version

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will check and return not vulnerable if the record.Package.Version is empty

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if err != nil {
return false, err
}
fixed, err := maven.ParseVersion(vuln.FixedInVersion)
if err != nil {
return false, err
}
return installed.Compare(fixed) < 0, nil
}
115 changes: 115 additions & 0 deletions rhel/java/matcher_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package java

import (
"testing"

"github.com/quay/claircore"
"github.com/quay/claircore/libvuln/driver"
"github.com/quay/claircore/test"
"github.com/quay/claircore/toolkit/types/cpe"
)

func TestRedHatMatcherVulnerable(t *testing.T) {
ctx := test.Logging(t)
m := &RedHatMatcher{}
quarkus := cpe.MustUnbind("cpe:/a:redhat:quarkus:3.33")
quarkusEl := cpe.MustUnbind("cpe:/a:redhat:quarkus:3.33::el8")
quarkusMajor := cpe.MustUnbind("cpe:/a:redhat:quarkus:3")
eap := cpe.MustUnbind("cpe:/a:redhat:jboss_enterprise_application_platform:8.1")
eapMajor := cpe.MustUnbind("cpe:/a:redhat:jboss_enterprise_application_platform:8")

record := func(version string, w cpe.WFN) *claircore.IndexRecord {
return &claircore.IndexRecord{
Package: &claircore.Package{Name: "io.netty:netty-codec", Version: version},
Repository: &claircore.Repository{
Name: w.String(),
Key: RepositoryKey,
CPE: w,
},
}
}
vuln := func(fixed string, name string) *claircore.Vulnerability {
return &claircore.Vulnerability{
Name: "CVE-2026-59889",
FixedInVersion: fixed,
Repo: &claircore.Repository{
Name: name,
Key: RepositoryKey,
},
}
}

tests := []struct {
name string
record *claircore.IndexRecord
vuln *claircore.Vulnerability
want bool
}{
{
name: "older than fixed",
record: record("4.1.100.Final-redhat-00001", quarkus),
vuln: vuln("4.1.115.Final-redhat-00002", quarkus.String()),
want: true,
},
{
name: "equal to fixed",
record: record("4.1.115.Final-redhat-00002", quarkus),
vuln: vuln("4.1.115.Final-redhat-00002", quarkus.String()),
},
{
name: "newer than fixed",
record: record("4.1.120.Final-redhat-00001", quarkus),
vuln: vuln("4.1.115.Final-redhat-00002", quarkus.String()),
},
{
name: "unversioned known_affected",
record: record("9.9.9", eap),
vuln: vuln("", eapMajor.String()),
want: true,
},
{
name: "major CPE pattern",
record: record("1.0", quarkus),
vuln: vuln("", quarkusMajor.String()),
want: true,
},
{
name: "edition makes the advisory narrower",
record: record("1.0", quarkus),
vuln: vuln("", quarkusEl.String()),
},
{
name: "different product",
record: record("1.0", quarkus),
vuln: vuln("", eapMajor.String()),
},
{
name: "empty package version",
record: record("", quarkus),
vuln: vuln("4.1.115.Final-redhat-00002", quarkus.String()),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := m.Vulnerable(ctx, tt.record, tt.vuln)
if err != nil {
t.Fatal(err)
}
if got != tt.want {
t.Fatalf("Vulnerable() = %v, want %v", got, tt.want)
}
})
}

if !m.Filter(record("1.0", quarkus)) {
t.Fatal("product CPE record was filtered out")
}
central := &claircore.IndexRecord{Repository: &claircore.Repository{Name: "maven"}}
if m.Filter(central) {
t.Fatal("Maven Central record was accepted")
}
q := m.Query()
if len(q) != 1 || q[0] != driver.RepositoryKey {
t.Fatalf("Query() = %v", q)
}
}
6 changes: 6 additions & 0 deletions rhel/java/repository.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package java

// RepositoryKey is the repository key for a Red Hat product CPE attributed
// to a Java component. It is distinct from the Maven Central repository and
// from the RPM CPE repository.
const RepositoryKey = "redhat-java-cpe-repository"
19 changes: 15 additions & 4 deletions rhel/matcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,24 @@ func (m *Matcher) Query() []driver.MatchConstraint {
// "cpe:/a:redhat:openshift:4.13::el8".
//
// This is defined (citation needed) to be a substring match on the "pattern"
// and "target" CPEs. Since we always normalize CPEs into v2.3 "Formatted
// String" form, we need to trim the added "ANY" attributes from the pattern.
// and "target" CPEs. Attributes other than version use the normal superset
// comparison. The version matches when it is equal or the record continues
// with ".", so version 4 matches 4.13 and version 1 does not match 10. A dot
// in another attribute, such as a product node.js, does not match.
//
// TODO(crozzy) Remove once RH VEX data updates CPEs with standard matching
// expressions.
func IsCPESubstringMatch(recordCPE cpe.WFN, vulnCPE cpe.WFN) bool {
return strings.HasPrefix(recordCPE.String(), strings.TrimRight(vulnCPE.String(), ":*"))
p := vulnCPE.Attr[cpe.Version]
r := recordCPE.Attr[cpe.Version]
// A dotted extension of the pattern version is the same version for this hack.
if p.Kind == cpe.ValueSet && r.Kind == cpe.ValueSet {
rest, ok := strings.CutPrefix(strings.ToLower(r.String()), strings.ToLower(p.String()))
if ok && strings.HasPrefix(rest, ".") {
vulnCPE.Attr[cpe.Version] = r
}
}
return cpe.Compare(vulnCPE, recordCPE).IsSuperset()
}

// Vulnerable implements [driver.Matcher].
Expand All @@ -86,7 +97,7 @@ func (m *Matcher) Vulnerable(ctx context.Context, record *claircore.IndexRecord,
slog.WarnContext(ctx, "unable to unbind repo CPE", "reason", err, "vulnerability name", vuln.Name)
return false, nil
}
if !cpe.Compare(vuln.Repo.CPE, record.Repository.CPE).IsSuperset() && !IsCPESubstringMatch(record.Repository.CPE, vuln.Repo.CPE) {
if !IsCPESubstringMatch(record.Repository.CPE, vuln.Repo.CPE) {
return false, nil
}

Expand Down
Loading
Loading