Skip to content

Commit ab4120d

Browse files
committed
oracle: dynamically check OVAL files available
Instead of just assuming OVAL files will exist for a given year, this patch checks which files are available and consumes all of them within a Now - 10 year window. Signed-off-by: crozzy <joseph.crosland@gmail.com>
1 parent f992bec commit ab4120d

7 files changed

Lines changed: 239 additions & 44 deletions

File tree

oracle/factory.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package oracle
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"log/slog"
8+
"net/http"
9+
"regexp"
10+
"strings"
11+
"time"
12+
13+
"github.com/quay/claircore/libvuln/driver"
14+
)
15+
16+
// FactoryConfig configures the Oracle Factory.
17+
type FactoryConfig struct {
18+
// URL indicates the index root. It should have a trailing slash.
19+
URL string `json:"url" yaml:"url"`
20+
}
21+
22+
// indexURL is the Oracle OVAL index root.
23+
//
24+
//doc:url updater
25+
const indexURL = `https://linux.oracle.com/security/oval/`
26+
27+
// Factory provides a driver.UpdaterSetFactory for Oracle with an injected client.
28+
type Factory struct {
29+
c *http.Client
30+
base string
31+
}
32+
33+
// Configure implements driver.Configurable.
34+
func (f *Factory) Configure(ctx context.Context, cf driver.ConfigUnmarshaler, c *http.Client) error {
35+
f.c = c
36+
var cfg FactoryConfig
37+
if err := cf(&cfg); err != nil {
38+
return err
39+
}
40+
if cfg.URL != "" {
41+
f.base = cfg.URL
42+
} else {
43+
f.base = indexURL
44+
}
45+
return nil
46+
}
47+
48+
// UpdaterSet implements driver.UpdaterSetFactory with inlined discovery logic.
49+
func (f *Factory) UpdaterSet(ctx context.Context) (driver.UpdaterSet, error) {
50+
us := driver.NewUpdaterSet()
51+
52+
cl := f.c
53+
if cl == nil {
54+
slog.InfoContext(ctx, "unconfigured")
55+
return us, nil
56+
}
57+
base := f.base
58+
if base == "" {
59+
base = indexURL
60+
}
61+
if !strings.HasSuffix(base, "/") {
62+
base += "/"
63+
}
64+
65+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, base, nil)
66+
if err != nil {
67+
return us, fmt.Errorf("oracle: unable to construct request: %w", err)
68+
}
69+
res, err := cl.Do(req)
70+
if err != nil {
71+
return us, fmt.Errorf("oracle: error requesting %q: %w", base, err)
72+
}
73+
defer res.Body.Close()
74+
if res.StatusCode != http.StatusOK {
75+
return us, fmt.Errorf("oracle: unexpected status requesting OVAL dir: %v", res.Status)
76+
}
77+
78+
body, err := io.ReadAll(res.Body)
79+
if err != nil {
80+
return us, fmt.Errorf("oracle: unable to read index body: %w", err)
81+
}
82+
re := regexp.MustCompile(`href="(com\.oracle\.elsa-(\d{4})\.xml\.bz2)"`)
83+
matches := re.FindAllStringSubmatch(string(body), -1)
84+
if len(matches) == 0 {
85+
return us, fmt.Errorf("oracle: no OVAL entries discovered at index")
86+
}
87+
seen := map[int]struct{}{}
88+
cutoff := time.Now().Year() - 9
89+
for _, m := range matches {
90+
var y int
91+
fmt.Sscanf(m[2], "%d", &y)
92+
if y < cutoff {
93+
continue
94+
}
95+
if _, ok := seen[y]; ok {
96+
continue
97+
}
98+
seen[y] = struct{}{}
99+
uri := base + m[1]
100+
up, err := NewUpdater(y, WithURL(uri, "bzip2"))
101+
if err != nil {
102+
return us, fmt.Errorf("oracle: unable to create updater for %d: %w", y, err)
103+
}
104+
if err := us.Add(up); err != nil {
105+
return us, err
106+
}
107+
slog.DebugContext(ctx, "oracle: added updater", "name", up.Name())
108+
}
109+
return us, nil
110+
}

oracle/factory_test.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package oracle
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"net/http/httptest"
7+
"strconv"
8+
"strings"
9+
"testing"
10+
"time"
11+
12+
"github.com/quay/claircore/pkg/ovalutil"
13+
)
14+
15+
func TestUpdaterSetDynamicDiscovery(t *testing.T) {
16+
t.Parallel()
17+
ctx := context.Background()
18+
now := time.Now().Year()
19+
20+
cases := []struct {
21+
name string
22+
entries []string
23+
wantYears []int
24+
wantErr bool
25+
}{
26+
{
27+
name: "happy-path-two-years-dedupe-and-filter",
28+
entries: []string{
29+
`<a href="com.oracle.elsa-` + strconv.Itoa(now) + `.xml.bz2">com.oracle.elsa-` + strconv.Itoa(now) + `.xml.bz2</a>`,
30+
`<a href="com.oracle.elsa-` + strconv.Itoa(now-5) + `.xml.bz2">com.oracle.elsa-` + strconv.Itoa(now-5) + `.xml.bz2</a>`,
31+
`<a href="com.oracle.elsa-` + strconv.Itoa(now-15) + `.xml.bz2">com.oracle.elsa-` + strconv.Itoa(now-15) + `.xml.bz2</a>`,
32+
`<a href="com.oracle.elsa-` + strconv.Itoa(now) + `.xml.bz2">com.oracle.elsa-` + strconv.Itoa(now) + `.xml.bz2</a>`,
33+
},
34+
wantYears: []int{now, now - 5},
35+
},
36+
{
37+
name: "no-matches",
38+
entries: []string{`<a href="unrelated.txt">unrelated.txt</a>`},
39+
wantYears: nil,
40+
wantErr: true,
41+
},
42+
}
43+
44+
for _, tt := range cases {
45+
tt := tt
46+
t.Run(tt.name, func(t *testing.T) {
47+
t.Parallel()
48+
body := `<html><body>` + strings.Join(tt.entries, "\n") + `</body></html>`
49+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
50+
w.WriteHeader(http.StatusOK)
51+
_, _ = w.Write([]byte(body))
52+
}))
53+
defer srv.Close()
54+
55+
// Configure factory with test URL/client.
56+
f := &Factory{}
57+
err := f.Configure(ctx, func(v any) error {
58+
if cfg, ok := v.(*FactoryConfig); ok {
59+
cfg.URL = strings.TrimSuffix(srv.URL, "/") + "/"
60+
}
61+
return nil
62+
}, srv.Client())
63+
if err != nil {
64+
t.Fatalf("configure: %v", err)
65+
}
66+
67+
us, err := f.UpdaterSet(ctx)
68+
if tt.wantErr {
69+
if err == nil {
70+
t.Fatalf("expected error, got nil")
71+
}
72+
return
73+
}
74+
if err != nil {
75+
t.Fatalf("UpdaterSet: %v", err)
76+
}
77+
ups := us.Updaters()
78+
if len(ups) != len(tt.wantYears) {
79+
t.Fatalf("unexpected updater count: got %d want %d", len(ups), len(tt.wantYears))
80+
}
81+
want := map[string]bool{}
82+
for _, y := range tt.wantYears {
83+
want[strconv.Itoa(y)] = false
84+
}
85+
for _, u := range ups {
86+
up, ok := u.(*Updater)
87+
if !ok {
88+
t.Fatalf("unexpected updater type: %T", u)
89+
}
90+
n := up.Name()
91+
parts := strings.Split(n, "-")
92+
if len(parts) < 3 {
93+
t.Fatalf("unexpected updater name format: %q", n)
94+
}
95+
yr := parts[1]
96+
if _, ok := want[yr]; !ok {
97+
t.Fatalf("unexpected year in updater name: %q", n)
98+
}
99+
want[yr] = true
100+
// URL and compression
101+
base := strings.TrimSuffix(srv.URL, "/") + "/"
102+
if up.Fetcher.URL == nil {
103+
t.Fatalf("nil URL for updater %q", n)
104+
}
105+
if !strings.HasPrefix(up.Fetcher.URL.String(), base+`com.oracle.elsa-`) ||
106+
!strings.HasSuffix(up.Fetcher.URL.String(), `.xml.bz2`) {
107+
t.Fatalf("unexpected URL: %q", up.Fetcher.URL)
108+
}
109+
if up.Fetcher.Compression != ovalutil.CompressionBzip2 {
110+
t.Fatalf("unexpected compression: got %v want %v", up.Fetcher.Compression, ovalutil.CompressionBzip2)
111+
}
112+
}
113+
for yr, ok := range want {
114+
if !ok {
115+
t.Fatalf("missing updater for year %s", yr)
116+
}
117+
}
118+
})
119+
}
120+
}

oracle/updater.go

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,6 @@ import (
99
"github.com/quay/claircore/pkg/ovalutil"
1010
)
1111

12-
const (
13-
allDB = `https://linux.oracle.com/security/oval/com.oracle.elsa-all.xml.bz2`
14-
//doc:url updater
15-
baseURL = `https://linux.oracle.com/security/oval/com.oracle.elsa-%d.xml.bz2`
16-
)
17-
1812
// Updater implements driver.Updater for Oracle Linux.
1913
type Updater struct {
2014
year int
@@ -26,21 +20,11 @@ type Option func(*Updater) error
2620

2721
// NewUpdater returns an updater configured according to the provided Options.
2822
//
29-
// If year is -1, the "all" database will be pulled.
23+
// The URL and compression are expected to be set via WithURL by the UpdaterSet.
3024
func NewUpdater(year int, opts ...Option) (*Updater, error) {
31-
uri := allDB
32-
if year != -1 {
33-
uri = fmt.Sprintf(baseURL, year)
34-
}
3525
u := Updater{
3626
year: year,
3727
}
38-
var err error
39-
u.Fetcher.URL, err = url.Parse(uri)
40-
if err != nil {
41-
return nil, err
42-
}
43-
u.Fetcher.Compression = ovalutil.CompressionBzip2
4428
for _, o := range opts {
4529
if err := o(&u); err != nil {
4630
return nil, err

oracle/updater_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ func TestFetch(t *testing.T) {
1414
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1515
http.ServeFile(w, r, "testdata/com.oracle.elsa-2018.xml")
1616
}))
17-
u, err := NewUpdater(-1, WithURL(srv.URL, ""))
17+
u, err := NewUpdater(2018, WithURL(srv.URL, ""))
1818
if err != nil {
1919
t.Fatal(err)
2020
}

oracle/updaterset.go

Lines changed: 0 additions & 24 deletions
This file was deleted.

test/periodic/updater_test.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,12 @@ func TestDebian(t *testing.T) {
7070

7171
func TestOracle(t *testing.T) {
7272
ctx := test.Logging(t)
73-
set, err := oracle.UpdaterSet(ctx)
73+
fac := new(oracle.Factory)
74+
err := fac.Configure(ctx, noopConfigure, pkgClient)
75+
if err != nil {
76+
t.Fatal(err)
77+
}
78+
set, err := fac.UpdaterSet(ctx)
7479
if err != nil {
7580
t.Fatal(err)
7681
}

updater/defaults/defaults.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ func inner(ctx context.Context) error {
5959
updater.Register("osv", new(osv.Factory))
6060
updater.Register("rhel-vex", new(vex.Factory))
6161
updater.Register("aws", driver.UpdaterSetFactoryFunc(aws.UpdaterSet))
62-
updater.Register("oracle", driver.UpdaterSetFactoryFunc(oracle.UpdaterSet))
62+
updater.Register("oracle", new(oracle.Factory))
6363
updater.Register("photon", driver.UpdaterSetFactoryFunc(photon.UpdaterSet))
6464
updater.Register("suse", new(suse.Factory))
6565

0 commit comments

Comments
 (0)