Skip to content
Draft
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
110 changes: 110 additions & 0 deletions oracle/factory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package oracle

import (
"context"
"fmt"
"io"
"log/slog"
"net/http"
"regexp"
"strings"
"time"

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

// FactoryConfig configures the Oracle Factory.
type FactoryConfig struct {
// URL indicates the index root. It should have a trailing slash.
URL string `json:"url" yaml:"url"`
}

// indexURL is the Oracle OVAL index root.
//
//doc:url updater
const indexURL = `https://linux.oracle.com/security/oval/`

// Factory provides a driver.UpdaterSetFactory for Oracle with an injected client.
type Factory struct {
c *http.Client
base string
}

// Configure implements driver.Configurable.
func (f *Factory) Configure(ctx context.Context, cf driver.ConfigUnmarshaler, c *http.Client) error {
f.c = c
var cfg FactoryConfig
if err := cf(&cfg); err != nil {
return err
}
if cfg.URL != "" {
f.base = cfg.URL
} else {
f.base = indexURL
}
return nil
}

// UpdaterSet implements driver.UpdaterSetFactory with inlined discovery logic.
func (f *Factory) UpdaterSet(ctx context.Context) (driver.UpdaterSet, error) {
us := driver.NewUpdaterSet()

cl := f.c
if cl == nil {
slog.InfoContext(ctx, "unconfigured")
return us, nil
}
base := f.base
if base == "" {
base = indexURL
}
if !strings.HasSuffix(base, "/") {
base += "/"
}

req, err := http.NewRequestWithContext(ctx, http.MethodGet, base, nil)
if err != nil {
return us, fmt.Errorf("oracle: unable to construct request: %w", err)
}
res, err := cl.Do(req)
if err != nil {
return us, fmt.Errorf("oracle: error requesting %q: %w", base, err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return us, fmt.Errorf("oracle: unexpected status requesting OVAL dir: %v", res.Status)
}

body, err := io.ReadAll(res.Body)
if err != nil {
return us, fmt.Errorf("oracle: unable to read index body: %w", err)
}
re := regexp.MustCompile(`href="(com\.oracle\.elsa-(\d{4})\.xml\.bz2)"`)
matches := re.FindAllStringSubmatch(string(body), -1)
if len(matches) == 0 {
return us, fmt.Errorf("oracle: no OVAL entries discovered at index")
}
seen := map[int]struct{}{}
cutoff := time.Now().Year() - 9
for _, m := range matches {
var y int
fmt.Sscanf(m[2], "%d", &y)
if y < cutoff {
continue
}
if _, ok := seen[y]; ok {
continue
}
seen[y] = struct{}{}
uri := base + m[1]
up, err := NewUpdater(y, WithURL(uri, "bzip2"))
if err != nil {
return us, fmt.Errorf("oracle: unable to create updater for %d: %w", y, err)
}
if err := us.Add(up); err != nil {
return us, err
}
slog.DebugContext(ctx, "oracle: added updater", "name", up.Name())
}
return us, nil
}
120 changes: 120 additions & 0 deletions oracle/factory_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package oracle

import (
"context"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"time"

"github.com/quay/claircore/pkg/ovalutil"
)

func TestUpdaterSetDynamicDiscovery(t *testing.T) {
t.Parallel()
ctx := context.Background()
now := time.Now().Year()

cases := []struct {
name string
entries []string
wantYears []int
wantErr bool
}{
{
name: "happy-path-two-years-dedupe-and-filter",
entries: []string{
`<a href="com.oracle.elsa-` + strconv.Itoa(now) + `.xml.bz2">com.oracle.elsa-` + strconv.Itoa(now) + `.xml.bz2</a>`,
`<a href="com.oracle.elsa-` + strconv.Itoa(now-5) + `.xml.bz2">com.oracle.elsa-` + strconv.Itoa(now-5) + `.xml.bz2</a>`,
`<a href="com.oracle.elsa-` + strconv.Itoa(now-15) + `.xml.bz2">com.oracle.elsa-` + strconv.Itoa(now-15) + `.xml.bz2</a>`,
`<a href="com.oracle.elsa-` + strconv.Itoa(now) + `.xml.bz2">com.oracle.elsa-` + strconv.Itoa(now) + `.xml.bz2</a>`,
},
wantYears: []int{now, now - 5},
},
{
name: "no-matches",
entries: []string{`<a href="unrelated.txt">unrelated.txt</a>`},
wantYears: nil,
wantErr: true,
},
}

for _, tt := range cases {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
body := `<html><body>` + strings.Join(tt.entries, "\n") + `</body></html>`
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(body))
}))
defer srv.Close()

// Configure factory with test URL/client.
f := &Factory{}
err := f.Configure(ctx, func(v any) error {
if cfg, ok := v.(*FactoryConfig); ok {
cfg.URL = strings.TrimSuffix(srv.URL, "/") + "/"
}
return nil
}, srv.Client())
if err != nil {
t.Fatalf("configure: %v", err)
}

us, err := f.UpdaterSet(ctx)
if tt.wantErr {
if err == nil {
t.Fatalf("expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("UpdaterSet: %v", err)
}
ups := us.Updaters()
if len(ups) != len(tt.wantYears) {
t.Fatalf("unexpected updater count: got %d want %d", len(ups), len(tt.wantYears))
}
want := map[string]bool{}
for _, y := range tt.wantYears {
want[strconv.Itoa(y)] = false
}
for _, u := range ups {
up, ok := u.(*Updater)
if !ok {
t.Fatalf("unexpected updater type: %T", u)
}
n := up.Name()
parts := strings.Split(n, "-")
if len(parts) < 3 {
t.Fatalf("unexpected updater name format: %q", n)
}
yr := parts[1]
if _, ok := want[yr]; !ok {
t.Fatalf("unexpected year in updater name: %q", n)
}
want[yr] = true
// URL and compression
base := strings.TrimSuffix(srv.URL, "/") + "/"
if up.Fetcher.URL == nil {
t.Fatalf("nil URL for updater %q", n)
}
if !strings.HasPrefix(up.Fetcher.URL.String(), base+`com.oracle.elsa-`) ||
!strings.HasSuffix(up.Fetcher.URL.String(), `.xml.bz2`) {
t.Fatalf("unexpected URL: %q", up.Fetcher.URL)
}
if up.Fetcher.Compression != ovalutil.CompressionBzip2 {
t.Fatalf("unexpected compression: got %v want %v", up.Fetcher.Compression, ovalutil.CompressionBzip2)
}
}
for yr, ok := range want {
if !ok {
t.Fatalf("missing updater for year %s", yr)
}
}
})
}
}
18 changes: 1 addition & 17 deletions oracle/updater.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,6 @@ import (
"github.com/quay/claircore/pkg/ovalutil"
)

const (
allDB = `https://linux.oracle.com/security/oval/com.oracle.elsa-all.xml.bz2`
//doc:url updater
baseURL = `https://linux.oracle.com/security/oval/com.oracle.elsa-%d.xml.bz2`
)

// Updater implements driver.Updater for Oracle Linux.
type Updater struct {
year int
Expand All @@ -26,21 +20,11 @@ type Option func(*Updater) error

// NewUpdater returns an updater configured according to the provided Options.
//
// If year is -1, the "all" database will be pulled.
// The URL and compression are expected to be set via WithURL by the UpdaterSet.
func NewUpdater(year int, opts ...Option) (*Updater, error) {
uri := allDB
if year != -1 {
uri = fmt.Sprintf(baseURL, year)
}
u := Updater{
year: year,
}
var err error
u.Fetcher.URL, err = url.Parse(uri)
if err != nil {
return nil, err
}
u.Fetcher.Compression = ovalutil.CompressionBzip2
for _, o := range opts {
if err := o(&u); err != nil {
return nil, err
Expand Down
2 changes: 1 addition & 1 deletion oracle/updater_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ func TestFetch(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "testdata/com.oracle.elsa-2018.xml")
}))
u, err := NewUpdater(-1, WithURL(srv.URL, ""))
u, err := NewUpdater(2018, WithURL(srv.URL, ""))
if err != nil {
t.Fatal(err)
}
Expand Down
24 changes: 0 additions & 24 deletions oracle/updaterset.go

This file was deleted.

7 changes: 6 additions & 1 deletion test/periodic/updater_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,12 @@ func TestDebian(t *testing.T) {

func TestOracle(t *testing.T) {
ctx := test.Logging(t)
set, err := oracle.UpdaterSet(ctx)
fac := new(oracle.Factory)
err := fac.Configure(ctx, noopConfigure, pkgClient)
if err != nil {
t.Fatal(err)
}
set, err := fac.UpdaterSet(ctx)
if err != nil {
t.Fatal(err)
}
Expand Down
2 changes: 1 addition & 1 deletion updater/defaults/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func inner(ctx context.Context) error {
updater.Register("osv", new(osv.Factory))
updater.Register("rhel-vex", new(vex.Factory))
updater.Register("aws", driver.UpdaterSetFactoryFunc(aws.UpdaterSet))
updater.Register("oracle", driver.UpdaterSetFactoryFunc(oracle.UpdaterSet))
updater.Register("oracle", new(oracle.Factory))
updater.Register("photon", driver.UpdaterSetFactoryFunc(photon.UpdaterSet))
updater.Register("suse", new(suse.Factory))

Expand Down