Skip to content

Commit ffce1c0

Browse files
committed
photon: add dynamic distribution discovery
The photon ecosystem still had statically defined distributions which meant that it has gotten stale over time. This change add the ability to dynamically discover those distributions. Signed-off-by: crozzy <joseph.crosland@gmail.com>
1 parent 09093b1 commit ffce1c0

File tree

7 files changed

+170
-100
lines changed

7 files changed

+170
-100
lines changed

photon/distributionscanner.go

Lines changed: 12 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package photon
22

33
import (
4-
"bytes"
54
"context"
65
"regexp"
76
"runtime/trace"
@@ -12,9 +11,6 @@ import (
1211
"github.com/quay/claircore/indexer"
1312
)
1413

15-
// Photon provides one security database file per major version. So far, there are 3 versions
16-
// Photon 1.0, Photon 2.0 and Photon 3.0
17-
1814
const (
1915
scannerName = "photon"
2016
scannerVersion = "v0.0.1"
@@ -26,28 +22,9 @@ const (
2622
photonReleasePath = `etc/photon-release`
2723
)
2824

29-
type photonRegex struct {
30-
release Release
31-
regexp *regexp.Regexp
32-
}
33-
34-
var photonRegexes = []photonRegex{
35-
{
36-
release: Photon1,
37-
// regex for /etc/os-release
38-
regexp: regexp.MustCompile(`^.*"VMware Photon"\sVERSION="1.0"`),
39-
},
40-
{
41-
release: Photon2,
42-
// regex for /etc/os-release
43-
regexp: regexp.MustCompile(`^.*"VMware Photon OS"\sVERSION="2.0"`),
44-
},
45-
{
46-
release: Photon3,
47-
// regex for /etc/os-release
48-
regexp: regexp.MustCompile(`^.*"VMware Photon OS"\sVERSION="3.0"`),
49-
},
50-
}
25+
var (
26+
photonVersionRe = regexp.MustCompile(`VMware Photon(?: OS)?"\s*VERSION="([0-9]+\.[0-9]+)"`)
27+
)
5128

5229
var (
5330
_ indexer.DistributionScanner = (*DistributionScanner)(nil)
@@ -86,23 +63,18 @@ func (ds *DistributionScanner) Scan(ctx context.Context, l *claircore.Layer) ([]
8663
return nil, nil
8764
}
8865
for _, buff := range files {
89-
dist := ds.parse(buff)
66+
m := photonVersionRe.FindSubmatch(buff.Bytes())
67+
if len(m) < 2 {
68+
continue
69+
}
70+
ver := string(m[1])
71+
if ver == "" {
72+
continue
73+
}
74+
dist := mkDist(ver)
9075
if dist != nil {
9176
return []*claircore.Distribution{dist}, nil
9277
}
9378
}
9479
return []*claircore.Distribution{}, nil
9580
}
96-
97-
// parse attempts to match all Photon release regexp and returns the associated
98-
// distribution if it exists.
99-
//
100-
// separated into its own method to aid testing.
101-
func (ds *DistributionScanner) parse(buff *bytes.Buffer) *claircore.Distribution {
102-
for _, ur := range photonRegexes {
103-
if ur.regexp.Match(buff.Bytes()) {
104-
return releaseToDist(ur.release)
105-
}
106-
}
107-
return nil
108-
}

photon/distributionscanner_test.go

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,24 @@ ANSI_COLOR="1;34"
3434
HOME_URL="https://vmware.github.io/photon/"
3535
BUG_REPORT_URL="https://github.com/vmware/photon/issues"`)
3636

37+
var photon4OSRelease []byte = []byte(`NAME="VMware Photon OS"
38+
VERSION="4.0"
39+
ID=photon
40+
VERSION_ID="4.0"
41+
PRETTY_NAME="VMware Photon OS/Linux"
42+
ANSI_COLOR="1;34"
43+
HOME_URL="https://vmware.github.io/photon/"
44+
BUG_REPORT_URL="https://github.com/vmware/photon/issues"`)
45+
46+
var photon5OSRelease []byte = []byte(`NAME="VMware Photon OS"
47+
VERSION="5.0"
48+
ID=photon
49+
VERSION_ID="5.0"
50+
PRETTY_NAME="VMware Photon OS/Linux"
51+
ANSI_COLOR="1;34"
52+
HOME_URL="https://vmware.github.io/photon/"
53+
BUG_REPORT_URL="https://github.com/vmware/photon/issues"`)
54+
3755
func TestDistributionScanner(t *testing.T) {
3856
table := []struct {
3957
name string
@@ -42,26 +60,36 @@ func TestDistributionScanner(t *testing.T) {
4260
}{
4361
{
4462
name: "photon 1.0",
45-
release: Photon1,
63+
release: Release("1.0"),
4664
osRelease: photon1OSRelease,
4765
},
4866
{
4967
name: "photon 2.0",
50-
release: Photon2,
68+
release: Release("2.0"),
5169
osRelease: photon2OSRelease,
5270
},
5371
{
5472
name: "photon 3.0",
55-
release: Photon3,
73+
release: Release("3.0"),
5674
osRelease: photon3OSRelease,
5775
},
76+
{
77+
name: "photon 4.0",
78+
release: Release("4.0"),
79+
osRelease: photon4OSRelease,
80+
},
81+
{
82+
name: "photon 5.0",
83+
release: Release("5.0"),
84+
osRelease: photon5OSRelease,
85+
},
5886
}
5987
for _, tt := range table {
6088
t.Run(tt.name, func(t *testing.T) {
6189
scanner := DistributionScanner{}
6290
dist := scanner.parse(bytes.NewBuffer(tt.osRelease))
63-
if !cmp.Equal(dist, releaseToDist(tt.release)) {
64-
t.Fatalf("%v", cmp.Diff(dist, releaseToDist(tt.release)))
91+
if !cmp.Equal(dist, mkDist(string(tt.release))) {
92+
t.Fatalf("%v", cmp.Diff(dist, mkDist(string(tt.release))))
6593
}
6694
})
6795
}

photon/parser.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ func (u *Updater) Parse(ctx context.Context, r io.ReadCloser) ([]*claircore.Vuln
4242
// each updater is configured to parse a photon release
4343
// specific xml database. we'll use the updater's release
4444
// to map the parsed vulnerabilities
45-
Dist: releaseToDist(u.release),
45+
Dist: mkDist(string(u.release)),
4646
},
4747
}, nil
4848
}

photon/photon.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package photon
33
import (
44
"fmt"
55
"net/url"
6+
"strings"
67

78
"github.com/quay/claircore/libvuln/driver"
89
"github.com/quay/claircore/pkg/ovalutil"
@@ -43,11 +44,21 @@ func NewUpdater(r Release, opts ...Option) (*Updater, error) {
4344
}
4445
}
4546
if u.Fetcher.URL == nil {
47+
// Default to gzip-compressed Photon OVAL filenames:
48+
// com.vmware.phsa-photon<MAJOR>.xml.gz
49+
s := string(u.release)
50+
maj := s
51+
if i := strings.IndexByte(s, '.'); i >= 0 {
52+
maj = s[:i]
53+
}
54+
filename := "com.vmware.phsa-photon" + maj + ".xml.gz"
4655
var err error
47-
u.Fetcher.URL, err = upstreamBase.Parse("com.vmware.phsa-" + string(u.release) + ".xml")
56+
u.Fetcher.URL, err = upstreamBase.Parse(filename)
4857
if err != nil {
4958
return nil, err
5059
}
60+
// Configure default compression to gzip.
61+
u.Fetcher.Compression = ovalutil.CompressionGzip
5162
}
5263
return u, nil
5364
}

photon/releases.go

Lines changed: 15 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,23 @@
11
package photon
22

3-
import "github.com/quay/claircore"
3+
import (
4+
"sync"
45

5-
// Release indicates the Photon release OVAL database to pull from.
6-
type Release string
7-
8-
// These are some known Releases.
9-
const (
10-
Photon1 Release = `photon1`
11-
Photon2 Release = `photon2`
12-
Photon3 Release = `photon3`
6+
"github.com/quay/claircore"
137
)
148

15-
var photon1Dist = &claircore.Distribution{
16-
Name: "VMware Photon OS",
17-
Version: "1.0",
18-
VersionID: "1.0",
19-
PrettyName: "VMware Photon OS/Linux",
20-
DID: "photon",
21-
}
22-
23-
var photon2Dist = &claircore.Distribution{
24-
Name: "VMware Photon OS",
25-
Version: "2.0",
26-
VersionID: "2.0",
27-
PrettyName: "VMware Photon OS/Linux",
28-
DID: "photon",
29-
}
9+
// Release indicates the Photon release OVAL database to pull from.
10+
type Release string
3011

31-
var photon3Dist = &claircore.Distribution{
32-
Name: "VMware Photon OS",
33-
Version: "3.0",
34-
VersionID: "3.0",
35-
PrettyName: "VMware Photon OS/Linux",
36-
DID: "photon",
37-
}
12+
var distCache sync.Map // key: version string (e.g., "1.0"), value: *claircore.Distribution
3813

39-
func releaseToDist(r Release) *claircore.Distribution {
40-
switch r {
41-
case Photon1:
42-
return photon1Dist
43-
case Photon2:
44-
return photon2Dist
45-
case Photon3:
46-
return photon3Dist
47-
default:
48-
// return empty dist
49-
return &claircore.Distribution{}
50-
}
14+
func mkDist(ver string) *claircore.Distribution {
15+
v, _ := distCache.LoadOrStore(ver, &claircore.Distribution{
16+
Name: "VMware Photon OS",
17+
Version: ver,
18+
VersionID: ver,
19+
PrettyName: "VMware Photon OS/Linux",
20+
DID: "photon",
21+
})
22+
return v.(*claircore.Distribution)
5123
}

photon/updaterset.go

Lines changed: 96 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,112 @@ package photon
33
import (
44
"context"
55
"fmt"
6+
"io"
7+
"net/http"
8+
"net/url"
9+
"regexp"
10+
"strings"
611

712
"github.com/quay/claircore/libvuln/driver"
813
)
914

10-
var photonReleases = []Release{
11-
Photon1,
12-
Photon2,
13-
Photon3,
15+
// UpdaterSet dynamically discovers available Photon OVAL databases from the
16+
// upstream index and returns one updater per discovered major release.
17+
//
18+
// Discovery rules:
19+
// - Match files named com.vmware.phsa-photon<MAJOR>.xml.gz
20+
// Factory implements a dynamic UpdaterSetFactory for Photon that discovers
21+
// available OVAL feeds and constructs per-release updaters.
22+
type Factory struct {
23+
c *http.Client
24+
base *url.URL
1425
}
1526

16-
func UpdaterSet(_ context.Context) (driver.UpdaterSet, error) {
27+
var (
28+
_ driver.UpdaterSetFactory = (*Factory)(nil)
29+
_ driver.Configurable = (*Factory)(nil)
30+
)
31+
32+
// FactoryConfig is the configuration accepted by the Factory.
33+
//
34+
// By convention, this is keyed by the string "photon".
35+
type FactoryConfig struct {
36+
// URL indicates the base URL for the OVAL layout. It should have a trailing slash.
37+
URL string `json:"url" yaml:"url"`
38+
}
39+
40+
// NewFactory returns an unconfigured Factory.
41+
func NewFactory(_ context.Context) (*Factory, error) {
42+
return &Factory{}, nil
43+
}
44+
45+
// Configure implements driver.Configurable.
46+
func (f *Factory) Configure(_ context.Context, cf driver.ConfigUnmarshaler, c *http.Client) error {
47+
f.c = c
48+
var cfg FactoryConfig
49+
if err := cf(&cfg); err != nil {
50+
return err
51+
}
52+
u := upstreamBase.String()
53+
if cfg.URL != "" {
54+
u = cfg.URL
55+
if !strings.HasSuffix(u, "/") {
56+
u += "/"
57+
}
58+
}
59+
var err error
60+
f.base, err = url.Parse(u)
61+
return err
62+
}
63+
64+
// UpdaterSet dynamically discovers available Photon OVAL databases from the
65+
// configured index and returns one updater per discovered major release.
66+
//
67+
// This will match files named com.vmware.phsa-photon<MAJOR>.xml.gz
68+
func (f *Factory) UpdaterSet(ctx context.Context) (driver.UpdaterSet, error) {
1769
us := driver.NewUpdaterSet()
18-
for _, release := range photonReleases {
19-
u, err := NewUpdater(release)
70+
c := f.c
71+
if c == nil {
72+
c = http.DefaultClient
73+
}
74+
base := f.base
75+
if base == nil {
76+
base = upstreamBase
77+
}
78+
79+
res, err := c.Get(base.String())
80+
if err != nil {
81+
return us, fmt.Errorf("photon: discovery request failed: %w", err)
82+
}
83+
defer res.Body.Close()
84+
if res.StatusCode != http.StatusOK {
85+
return us, fmt.Errorf("photon: unexpected status from index: %s", res.Status)
86+
}
87+
body, err := io.ReadAll(res.Body)
88+
if err != nil {
89+
return us, fmt.Errorf("photon: reading index body: %w", err)
90+
}
91+
92+
re := regexp.MustCompile(`href="com\.vmware\.phsa-photon(\d+)\.xml\.gz"`)
93+
matches := re.FindAllStringSubmatch(string(body), -1)
94+
if len(matches) == 0 {
95+
return us, fmt.Errorf("photon: no OVAL entries discovered at index")
96+
}
97+
for _, m := range matches {
98+
if len(m) < 2 {
99+
continue
100+
}
101+
filename := "com.vmware.phsa-photon" + m[1] + ".xml.gz"
102+
u, err := base.Parse(filename)
20103
if err != nil {
21-
return us, fmt.Errorf("failed to create updater: %v", err)
104+
return us, fmt.Errorf("photon: building feed url: %w", err)
22105
}
23-
err = us.Add(u)
106+
rel := Release(m[1] + ".0")
107+
up, err := NewUpdater(rel, WithURL(u.String(), "gz"))
24108
if err != nil {
109+
return us, fmt.Errorf("photon: creating updater for %s: %w", rel, err)
110+
}
111+
if err := us.Add(up); err != nil {
25112
return us, err
26113
}
27114
}

updater/defaults/defaults.go

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

6666
cvssSet := driver.NewUpdaterSet()

0 commit comments

Comments
 (0)