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
42 changes: 12 additions & 30 deletions photon/distributionscanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)
Expand Down Expand Up @@ -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)
}
38 changes: 33 additions & 5 deletions photon/distributionscanner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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))))
}
})
}
Expand Down
2 changes: 1 addition & 1 deletion photon/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
13 changes: 12 additions & 1 deletion photon/photon.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package photon
import (
"fmt"
"net/url"
"strings"

"github.com/quay/claircore/libvuln/driver"
"github.com/quay/claircore/pkg/ovalutil"
Expand Down Expand Up @@ -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<MAJOR>.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
}
Expand Down
58 changes: 15 additions & 43 deletions photon/releases.go
Original file line number Diff line number Diff line change
@@ -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)
}
105 changes: 96 additions & 9 deletions photon/updaterset.go
Original file line number Diff line number Diff line change
Expand Up @@ -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<MAJOR>.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<MAJOR>.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
}
}
Expand Down
7 changes: 6 additions & 1 deletion test/periodic/updater_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion updater/defaults/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down