diff --git a/photon/distributionscanner.go b/photon/distributionscanner.go index 3d2bb0d27..abc926d78 100644 --- a/photon/distributionscanner.go +++ b/photon/distributionscanner.go @@ -12,9 +12,6 @@ import ( "github.com/quay/claircore/indexer" ) -// Photon provides one security database file per major version. So far, there are 3 versions -// Photon 1.0, Photon 2.0 and Photon 3.0 - const ( scannerName = "photon" scannerVersion = "v0.0.1" @@ -26,28 +23,9 @@ const ( photonReleasePath = `etc/photon-release` ) -type photonRegex struct { - release Release - regexp *regexp.Regexp -} - -var photonRegexes = []photonRegex{ - { - release: Photon1, - // regex for /etc/os-release - regexp: regexp.MustCompile(`^.*"VMware Photon"\sVERSION="1.0"`), - }, - { - release: Photon2, - // regex for /etc/os-release - regexp: regexp.MustCompile(`^.*"VMware Photon OS"\sVERSION="2.0"`), - }, - { - release: Photon3, - // regex for /etc/os-release - regexp: regexp.MustCompile(`^.*"VMware Photon OS"\sVERSION="3.0"`), - }, -} +var ( + photonVersionRe = regexp.MustCompile(`VMware Photon(?: OS)?"\s*VERSION="([0-9]+\.[0-9]+)"`) +) var ( _ indexer.DistributionScanner = (*DistributionScanner)(nil) @@ -99,10 +77,14 @@ func (ds *DistributionScanner) Scan(ctx context.Context, l *claircore.Layer) ([] // // separated into its own method to aid testing. func (ds *DistributionScanner) parse(buff *bytes.Buffer) *claircore.Distribution { - for _, ur := range photonRegexes { - if ur.regexp.Match(buff.Bytes()) { - return releaseToDist(ur.release) - } + b := buff.Bytes() + m := photonVersionRe.FindSubmatch(b) + if len(m) < 2 { + return nil + } + ver := string(m[1]) + if ver == "" { + return nil } - return nil + return mkDist(ver) } diff --git a/photon/distributionscanner_test.go b/photon/distributionscanner_test.go index 6d2fc9d67..27a7b5bee 100644 --- a/photon/distributionscanner_test.go +++ b/photon/distributionscanner_test.go @@ -34,6 +34,24 @@ ANSI_COLOR="1;34" HOME_URL="https://vmware.github.io/photon/" BUG_REPORT_URL="https://github.com/vmware/photon/issues"`) +var photon4OSRelease []byte = []byte(`NAME="VMware Photon OS" +VERSION="4.0" +ID=photon +VERSION_ID="4.0" +PRETTY_NAME="VMware Photon OS/Linux" +ANSI_COLOR="1;34" +HOME_URL="https://vmware.github.io/photon/" +BUG_REPORT_URL="https://github.com/vmware/photon/issues"`) + +var photon5OSRelease []byte = []byte(`NAME="VMware Photon OS" +VERSION="5.0" +ID=photon +VERSION_ID="5.0" +PRETTY_NAME="VMware Photon OS/Linux" +ANSI_COLOR="1;34" +HOME_URL="https://vmware.github.io/photon/" +BUG_REPORT_URL="https://github.com/vmware/photon/issues"`) + func TestDistributionScanner(t *testing.T) { table := []struct { name string @@ -42,26 +60,36 @@ func TestDistributionScanner(t *testing.T) { }{ { name: "photon 1.0", - release: Photon1, + release: Release("1.0"), osRelease: photon1OSRelease, }, { name: "photon 2.0", - release: Photon2, + release: Release("2.0"), osRelease: photon2OSRelease, }, { name: "photon 3.0", - release: Photon3, + release: Release("3.0"), osRelease: photon3OSRelease, }, + { + name: "photon 4.0", + release: Release("4.0"), + osRelease: photon4OSRelease, + }, + { + name: "photon 5.0", + release: Release("5.0"), + osRelease: photon5OSRelease, + }, } for _, tt := range table { t.Run(tt.name, func(t *testing.T) { scanner := DistributionScanner{} dist := scanner.parse(bytes.NewBuffer(tt.osRelease)) - if !cmp.Equal(dist, releaseToDist(tt.release)) { - t.Fatalf("%v", cmp.Diff(dist, releaseToDist(tt.release))) + if !cmp.Equal(dist, mkDist(string(tt.release))) { + t.Fatalf("%v", cmp.Diff(dist, mkDist(string(tt.release)))) } }) } diff --git a/photon/parser.go b/photon/parser.go index 7467b1462..39dc9b3b3 100644 --- a/photon/parser.go +++ b/photon/parser.go @@ -42,7 +42,7 @@ func (u *Updater) Parse(ctx context.Context, r io.ReadCloser) ([]*claircore.Vuln // each updater is configured to parse a photon release // specific xml database. we'll use the updater's release // to map the parsed vulnerabilities - Dist: releaseToDist(u.release), + Dist: mkDist(string(u.release)), }, }, nil } diff --git a/photon/photon.go b/photon/photon.go index ad27585f0..308bbef30 100644 --- a/photon/photon.go +++ b/photon/photon.go @@ -3,6 +3,7 @@ package photon import ( "fmt" "net/url" + "strings" "github.com/quay/claircore/libvuln/driver" "github.com/quay/claircore/pkg/ovalutil" @@ -43,11 +44,21 @@ func NewUpdater(r Release, opts ...Option) (*Updater, error) { } } if u.Fetcher.URL == nil { + // Default to gzip-compressed Photon OVAL filenames: + // com.vmware.phsa-photon.xml.gz + s := string(u.release) + maj := s + if i := strings.IndexByte(s, '.'); i >= 0 { + maj = s[:i] + } + filename := "com.vmware.phsa-photon" + maj + ".xml.gz" var err error - u.Fetcher.URL, err = upstreamBase.Parse("com.vmware.phsa-" + string(u.release) + ".xml") + u.Fetcher.URL, err = upstreamBase.Parse(filename) if err != nil { return nil, err } + // Configure default compression to gzip. + u.Fetcher.Compression = ovalutil.CompressionGzip } return u, nil } diff --git a/photon/releases.go b/photon/releases.go index 8468aa6c5..388fb71e8 100644 --- a/photon/releases.go +++ b/photon/releases.go @@ -1,51 +1,23 @@ package photon -import "github.com/quay/claircore" +import ( + "sync" -// Release indicates the Photon release OVAL database to pull from. -type Release string - -// These are some known Releases. -const ( - Photon1 Release = `photon1` - Photon2 Release = `photon2` - Photon3 Release = `photon3` + "github.com/quay/claircore" ) -var photon1Dist = &claircore.Distribution{ - Name: "VMware Photon OS", - Version: "1.0", - VersionID: "1.0", - PrettyName: "VMware Photon OS/Linux", - DID: "photon", -} - -var photon2Dist = &claircore.Distribution{ - Name: "VMware Photon OS", - Version: "2.0", - VersionID: "2.0", - PrettyName: "VMware Photon OS/Linux", - DID: "photon", -} +// Release indicates the Photon release OVAL database to pull from. +type Release string -var photon3Dist = &claircore.Distribution{ - Name: "VMware Photon OS", - Version: "3.0", - VersionID: "3.0", - PrettyName: "VMware Photon OS/Linux", - DID: "photon", -} +var distCache sync.Map // key: version string (e.g., "1.0"), value: *claircore.Distribution -func releaseToDist(r Release) *claircore.Distribution { - switch r { - case Photon1: - return photon1Dist - case Photon2: - return photon2Dist - case Photon3: - return photon3Dist - default: - // return empty dist - return &claircore.Distribution{} - } +func mkDist(ver string) *claircore.Distribution { + v, _ := distCache.LoadOrStore(ver, &claircore.Distribution{ + Name: "VMware Photon OS", + Version: ver, + VersionID: ver, + PrettyName: "VMware Photon OS/Linux", + DID: "photon", + }) + return v.(*claircore.Distribution) } diff --git a/photon/updaterset.go b/photon/updaterset.go index 14c41a259..3575bfec5 100644 --- a/photon/updaterset.go +++ b/photon/updaterset.go @@ -3,25 +3,112 @@ package photon import ( "context" "fmt" + "io" + "net/http" + "net/url" + "regexp" + "strings" "github.com/quay/claircore/libvuln/driver" ) -var photonReleases = []Release{ - Photon1, - Photon2, - Photon3, +// UpdaterSet dynamically discovers available Photon OVAL databases from the +// upstream index and returns one updater per discovered major release. +// +// Discovery rules: +// - Match files named com.vmware.phsa-photon.xml.gz +// Factory implements a dynamic UpdaterSetFactory for Photon that discovers +// available OVAL feeds and constructs per-release updaters. +type Factory struct { + c *http.Client + base *url.URL } -func UpdaterSet(_ context.Context) (driver.UpdaterSet, error) { +var ( + _ driver.UpdaterSetFactory = (*Factory)(nil) + _ driver.Configurable = (*Factory)(nil) +) + +// FactoryConfig is the configuration accepted by the Factory. +// +// By convention, this is keyed by the string "photon". +type FactoryConfig struct { + // URL indicates the base URL for the OVAL layout. It should have a trailing slash. + URL string `json:"url" yaml:"url"` +} + +// NewFactory returns an unconfigured Factory. +func NewFactory(_ context.Context) (*Factory, error) { + return &Factory{}, nil +} + +// Configure implements driver.Configurable. +func (f *Factory) Configure(_ context.Context, cf driver.ConfigUnmarshaler, c *http.Client) error { + f.c = c + var cfg FactoryConfig + if err := cf(&cfg); err != nil { + return err + } + u := upstreamBase.String() + if cfg.URL != "" { + u = cfg.URL + if !strings.HasSuffix(u, "/") { + u += "/" + } + } + var err error + f.base, err = url.Parse(u) + return err +} + +// UpdaterSet dynamically discovers available Photon OVAL databases from the +// configured index and returns one updater per discovered major release. +// +// This will match files named com.vmware.phsa-photon.xml.gz +func (f *Factory) UpdaterSet(ctx context.Context) (driver.UpdaterSet, error) { us := driver.NewUpdaterSet() - for _, release := range photonReleases { - u, err := NewUpdater(release) + c := f.c + if c == nil { + c = http.DefaultClient + } + base := f.base + if base == nil { + base = upstreamBase + } + + res, err := c.Get(base.String()) + if err != nil { + return us, fmt.Errorf("photon: discovery request failed: %w", err) + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return us, fmt.Errorf("photon: unexpected status from index: %s", res.Status) + } + body, err := io.ReadAll(res.Body) + if err != nil { + return us, fmt.Errorf("photon: reading index body: %w", err) + } + + re := regexp.MustCompile(`href="com\.vmware\.phsa-photon(\d+)\.xml\.gz"`) + matches := re.FindAllStringSubmatch(string(body), -1) + if len(matches) == 0 { + return us, fmt.Errorf("photon: no OVAL entries discovered at index") + } + for _, m := range matches { + if len(m) < 2 { + continue + } + filename := "com.vmware.phsa-photon" + m[1] + ".xml.gz" + u, err := base.Parse(filename) if err != nil { - return us, fmt.Errorf("failed to create updater: %v", err) + return us, fmt.Errorf("photon: building feed url: %w", err) } - err = us.Add(u) + rel := Release(m[1] + ".0") + up, err := NewUpdater(rel, WithURL(u.String(), "gz")) if err != nil { + return us, fmt.Errorf("photon: creating updater for %s: %w", rel, err) + } + if err := us.Add(up); err != nil { return us, err } } diff --git a/test/periodic/updater_test.go b/test/periodic/updater_test.go index fb6b64849..bc675d6f7 100644 --- a/test/periodic/updater_test.go +++ b/test/periodic/updater_test.go @@ -80,7 +80,12 @@ func TestOracle(t *testing.T) { func TestPhoton(t *testing.T) { ctx := zlog.Test(context.Background(), t) - set, err := photon.UpdaterSet(ctx) + fac := new(photon.Factory) + err := fac.Configure(ctx, noopConfigure, pkgClient) + if err != nil { + t.Fatal(err) + } + set, err := fac.UpdaterSet(ctx) if err != nil { t.Fatal(err) } diff --git a/updater/defaults/defaults.go b/updater/defaults/defaults.go index f3abbcbb8..1bc6623f5 100644 --- a/updater/defaults/defaults.go +++ b/updater/defaults/defaults.go @@ -60,7 +60,7 @@ func inner(ctx context.Context) error { updater.Register("rhel-vex", new(vex.Factory)) updater.Register("aws", driver.UpdaterSetFactoryFunc(aws.UpdaterSet)) updater.Register("oracle", driver.UpdaterSetFactoryFunc(oracle.UpdaterSet)) - updater.Register("photon", driver.UpdaterSetFactoryFunc(photon.UpdaterSet)) + updater.Register("photon", new(photon.Factory)) updater.Register("suse", new(suse.Factory)) cvssSet := driver.NewUpdaterSet()