Skip to content

Commit

Permalink
ruby: add vuln matching
Browse files Browse the repository at this point in the history
Signed-off-by: RTann <rtannenb@redhat.com>
  • Loading branch information
RTann committed Jun 8, 2023
1 parent 14434af commit 135cf26
Show file tree
Hide file tree
Showing 5 changed files with 427 additions and 7 deletions.
97 changes: 97 additions & 0 deletions ruby/matcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package ruby

import (
"context"
"fmt"
"net/url"

"github.com/Masterminds/semver"
"github.com/quay/zlog"

"github.com/quay/claircore"
"github.com/quay/claircore/libvuln/driver"
)

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

// Matcher attempts to correlate discovered ruby packages with reported
// vulnerabilities.
type Matcher struct{}

// Name implements driver.Matcher.
func (*Matcher) Name() string { return "ruby" }

// Filter implements driver.Matcher.
func (*Matcher) Filter(record *claircore.IndexRecord) bool {
return record.Repository != nil && record.Repository.Name == repository
}

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

// Vulnerable implements driver.Matcher.
func (*Matcher) Vulnerable(ctx context.Context, record *claircore.IndexRecord, vuln *claircore.Vulnerability) (bool, error) {
if vuln.FixedInVersion == "" {
return true, nil
}

decodedVersions, err := url.ParseQuery(vuln.FixedInVersion)
if err != nil {
return false, err
}

// Check for missing upper version
if !decodedVersions.Has("fixed") && !decodedVersions.Has("lastAffected") {
return false, fmt.Errorf("ruby: missing upper version")
}

upperVersion := decodedVersions.Get("fixed")
if upperVersion == "" {
upperVersion = decodedVersions.Get("lastAffected")
}

rv, err := semver.NewVersion(record.Package.Version)
if err != nil {
zlog.Warn(ctx).
Str("package", record.Package.Name).
Str("version", record.Package.Version).
Msg("unable to parse ruby package version")
return false, err
}

uv, err := semver.NewVersion(upperVersion)
if err != nil {
zlog.Warn(ctx).
Str("vulnerability", vuln.Name).
Str("package", vuln.Package.Name).
Str("version", upperVersion).
Msg("unable to parse ruby vulnerability 'fixed version' or 'last affected'")
return false, err
}

switch {
case decodedVersions.Has("fixed") && rv.Compare(uv) >= 0:
return false, nil
case decodedVersions.Has("lastAffected") && rv.Compare(uv) > 0:
return false, nil
case decodedVersions.Has("introduced"):
introduced := decodedVersions.Get("introduced")
iv, err := semver.NewVersion(introduced)
if err != nil {
zlog.Warn(ctx).
Str("vulnerability", vuln.Name).
Str("package", vuln.Package.Name).
Str("version", introduced).
Msg("unable to parse ruby vulnerability 'introduced version'")
return false, err
}

if rv.Compare(iv) < 0 {
return false, nil
}
}

return true, nil
}
94 changes: 94 additions & 0 deletions ruby/matcher_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package ruby

import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"

"github.com/quay/zlog"

"github.com/quay/claircore"
"github.com/quay/claircore/datastore/postgres"
internalMatcher "github.com/quay/claircore/internal/matcher"
"github.com/quay/claircore/libvuln/driver"
"github.com/quay/claircore/libvuln/updates"
"github.com/quay/claircore/pkg/ctxlock"
"github.com/quay/claircore/test/integration"
pgtest "github.com/quay/claircore/test/postgres"
"github.com/quay/claircore/updater/osv"
)

func TestMain(m *testing.M) {
var c int
defer func() { os.Exit(c) }()
defer integration.DBSetup()()
c = m.Run()
}

func TestMatcherIntegration(t *testing.T) {
integration.NeedDB(t)
ctx := zlog.Test(context.Background(), t)
pool := pgtest.TestMatcherDB(ctx, t)
store := postgres.NewMatcherStore(pool)

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusTeapot)
}))
defer srv.Close()

m := &Matcher{}
locks, err := ctxlock.New(ctx, pool)
if err != nil {
t.Fatalf("%v", err)
}
defer locks.Close(ctx)

cfg := map[string]driver.ConfigUnmarshaler{
"osv": func(v interface{}) error {
cfg := v.(*osv.Config)
cfg.URL = osv.DefaultURL
return nil
},
}

facs := map[string]driver.UpdaterSetFactory{
"osv": osv.Factory,
}
mgr, err := updates.NewManager(ctx, store, locks, srv.Client(),
updates.WithFactories(facs), updates.WithConfigs(cfg))
if err != nil {
t.Fatalf("%v", err)
}

// force update
if err := mgr.Run(ctx); err != nil {
t.Fatalf("%v", err)
}

path := filepath.Join("testdata", "indexreport-bullseye-ruby.json")
f, err := os.Open(path)
if err != nil {
t.Fatalf("%v", err)
}
defer f.Close()
var ir claircore.IndexReport
err = json.NewDecoder(f).Decode(&ir)
if err != nil {
t.Fatalf("failed to decode IndexReport: %v", err)
}
vr, err := internalMatcher.Match(ctx, &ir, []driver.Matcher{m}, store)
if err != nil {
t.Fatalf("expected error to be nil but got %v", err)
}

vulns := vr.Vulnerabilities
t.Logf("Number of Vulnerabilities found: %d", len(vulns))

if len(vulns) < 1 {
t.Fatalf("failed to match vulns: %v", err)
}
}
159 changes: 159 additions & 0 deletions ruby/matcher_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package ruby

import (
"context"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/quay/claircore"
)

func TestVulnerable(t *testing.T) {
matcher := &Matcher{}

testcases := []struct {
name string
record *claircore.IndexRecord
vuln *claircore.Vulnerability
want bool
}{
{
name: "bootstrap affected",
record: &claircore.IndexRecord{
Package: &claircore.Package{
Name: "bootstrap",
Version: "3.2.9",
Kind: "binary",
},
},
vuln: &claircore.Vulnerability{
Updater: "osv",
Name: "GHSA-7mvr-5x2g-wfc8",
Description: "Bootstrap Cross-site Scripting vulnerability",
Package: &claircore.Package{
Name: "bootstrap",
RepositoryHint: "RubyGems",
},
FixedInVersion: "fixed=4.1.2",
},
want: true,
},
{
name: "bootstrap unaffected",
record: &claircore.IndexRecord{
Package: &claircore.Package{
Name: "bootstrap",
Version: "4.1.2",
Kind: "binary",
},
},
vuln: &claircore.Vulnerability{
Updater: "osv",
Name: "GHSA-7mvr-5x2g-wfc8",
Description: "Bootstrap Cross-site Scripting vulnerability",
Package: &claircore.Package{
Name: "bootstrap",
RepositoryHint: "rubygems",
},
FixedInVersion: "fixed=4.1.2-alpha",
},
want: false,
},
{
name: "openshift-origin-node unfixed",
record: &claircore.IndexRecord{
Package: &claircore.Package{
Name: "openshift-origin-node",
Version: "1.3.2",
Kind: "binary",
},
},
vuln: &claircore.Vulnerability{
Updater: "osv",
Name: "GHSA-2c25-xfpq-8n9r",
Description: "Ruby gem openshift-origin-node before 2014-02-14 does not contain a cronjob timeout which could result in a denial of service in cron.daily and cron.weekly.",
Package: &claircore.Package{
Name: "openshift-origin-node",
RepositoryHint: "rubygems",
},
FixedInVersion: "lastAffected=1.3.3",
},
want: true,
},
{
name: "openshift-origin-node unfixed again",
record: &claircore.IndexRecord{
Package: &claircore.Package{
Name: "openshift-origin-node",
Version: "1.3.3",
Kind: "binary",
},
},
vuln: &claircore.Vulnerability{
Updater: "osv",
Name: "GHSA-2c25-xfpq-8n9r",
Description: "Ruby gem openshift-origin-node before 2014-02-14 does not contain a cronjob timeout which could result in a denial of service in cron.daily and cron.weekly.",
Package: &claircore.Package{
Name: "openshift-origin-node",
RepositoryHint: "rubygems",
},
FixedInVersion: "lastAffected=1.3.3",
},
want: true,
},
{
name: "dependabot-omnibus affected",
record: &claircore.IndexRecord{
Package: &claircore.Package{
Name: "dependabot-omnibus",
Version: "0.120.0-beta2",
Kind: "binary",
},
},
vuln: &claircore.Vulnerability{
Updater: "osv",
Name: "GHSA-23f7-99jx-m54r",
Description: "Remote code execution in dependabot-core branch names when cloning",
Package: &claircore.Package{
Name: "dependabot-omnibus",
RepositoryHint: "rubygems",
},
FixedInVersion: "fixed=0.125.1&introduced=0.119.0-beta1",
},
want: true,
},
{
name: "dependabot-omnibus unaffected",
record: &claircore.IndexRecord{
Package: &claircore.Package{
Name: "dependabot-omnibus",
Version: "0.119.0-alpha3",
Kind: "binary",
},
},
vuln: &claircore.Vulnerability{
Updater: "osv",
Name: "GHSA-23f7-99jx-m54r",
Description: "Remote code execution in dependabot-core branch names when cloning",
Package: &claircore.Package{
Name: "dependabot-omnibus",
RepositoryHint: "rubygems",
},
FixedInVersion: "fixed=0.125.1&introduced=0.119.0-beta1",
},
want: false,
},
}

for _, testcase := range testcases {
t.Run(testcase.name, func(t *testing.T) {
got, err := matcher.Vulnerable(context.Background(), testcase.record, testcase.vuln)
if err != nil {
t.Fatal(err)
}
if !cmp.Equal(got, testcase.want) {
t.Error(cmp.Diff(got, testcase.want))
}
})
}
}

0 comments on commit 135cf26

Please sign in to comment.