Skip to content
Open
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
2 changes: 2 additions & 0 deletions Documentation/reference/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@ If the value is nil the default list of Matchers will run:
* alpine-matcher
* aws-matcher
* debian-matcher
* echo-matcher
* gobin
* java-maven
* oracle
Expand Down Expand Up @@ -366,6 +367,7 @@ If the value is nil (or `null` in yaml) the default set of Updaters will run:
* alpine
* aws
* debian
* echo
* oracle
* osv
* photon
Expand Down
1 change: 1 addition & 0 deletions cmd/clair/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"golang.org/x/sync/errgroup"

"github.com/quay/clair/v4/cmd"
_ "github.com/quay/clair/v4/echo"
"github.com/quay/clair/v4/health"
"github.com/quay/clair/v4/httptransport"
"github.com/quay/clair/v4/initialize"
Expand Down
1 change: 1 addition & 0 deletions cmd/clairctl/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
_ "github.com/quay/claircore/updater/defaults"
"github.com/urfave/cli/v2"

_ "github.com/quay/clair/v4/echo"
"github.com/quay/clair/v4/internal/httputil"
)

Expand Down
1 change: 1 addition & 0 deletions cmd/clairctl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/urfave/cli/v2"

"github.com/quay/clair/v4/cmd"
_ "github.com/quay/clair/v4/echo"
"github.com/quay/clair/v4/internal/logging"
)

Expand Down
2 changes: 2 additions & 0 deletions config.yaml.sample
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ matcher:
- "alpine"
- "aws"
- "debian"
- "echo"
- "oracle"
- "osv"
- "photon"
Expand All @@ -41,6 +42,7 @@ matchers:
- "alpine-matcher"
- "aws-matcher"
- "debian-matcher"
- "echo-matcher"
- "gobin"
- "java-maven"
- "oracle"
Expand Down
1 change: 1 addition & 0 deletions config/matchers.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type Matchers struct {
// "alpine"
// "aws"
// "debian"
// "echo-matcher"
// "oracle"
// "photon"
// "python"
Expand Down
1 change: 1 addition & 0 deletions config/updaters.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type Updaters struct {
// "aws"
// "clair.cvss"
// "debian"
// "echo"
// "oracle"
// "osv"
// "photon"
Expand Down
40 changes: 40 additions & 0 deletions echo/defaults.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package echo

import (
"context"
"sync"
"time"

"github.com/quay/claircore/libvuln/driver"
"github.com/quay/claircore/matchers/registry"
"github.com/quay/claircore/updater"
)

var (
once sync.Once
regerr error
)

func init() {
ctx, done := context.WithTimeout(context.Background(), 1*time.Minute)
defer done()
once.Do(func() { regerr = register(ctx) })
}

// Error reports if an error was encountered when initializing the Echo
// updater and matcher.
func Error() error {
return regerr
}

func register(ctx context.Context) error {
f, err := NewFactory(ctx)
if err != nil {
return err
}
updater.Register("echo", f)

registry.Register("echo-matcher", driver.MatcherStatic(&Matcher{}))

return nil
}
80 changes: 80 additions & 0 deletions echo/distributionscanner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package echo

import (
"context"
"errors"
"fmt"
"io/fs"
"log/slog"
"runtime/trace"

"github.com/quay/claircore"
"github.com/quay/claircore/indexer"
"github.com/quay/claircore/osrelease"
)

var (
_ indexer.DistributionScanner = (*DistributionScanner)(nil)
_ indexer.VersionedScanner = (*DistributionScanner)(nil)
)

// DistributionScanner attempts to discover if a layer
// displays characteristics of an Echo distribution.
type DistributionScanner struct{}

// Name implements [indexer.VersionedScanner].
func (*DistributionScanner) Name() string { return "echo" }

// Version implements [indexer.VersionedScanner].
func (*DistributionScanner) Version() string { return "1" }

// Kind implements [indexer.VersionedScanner].
func (*DistributionScanner) Kind() string { return "distribution" }

// Scan implements [indexer.DistributionScanner].
func (ds *DistributionScanner) Scan(ctx context.Context, l *claircore.Layer) ([]*claircore.Distribution, error) {
defer trace.StartRegion(ctx, "Scanner.Scan").End()
log := slog.With("version", ds.Version(), "layer", l.Hash.String())
log.DebugContext(ctx, "start")
defer log.DebugContext(ctx, "done")

sys, err := l.FS()
if err != nil {
return nil, fmt.Errorf("echo: unable to open layer: %w", err)
}
d, err := findDist(ctx, log, sys)
if err != nil {
return nil, err
}
if d == nil {
return nil, nil
}
return []*claircore.Distribution{d}, nil
}

func findDist(ctx context.Context, log *slog.Logger, sys fs.FS) (*claircore.Distribution, error) {
f, err := sys.Open(osrelease.Path)
switch {
case errors.Is(err, nil):
case errors.Is(err, fs.ErrNotExist):
log.DebugContext(ctx, "no os-release file")
return nil, nil
default:
return nil, fmt.Errorf("echo: unexpected error: %w", err)
}
kv, err := osrelease.Parse(ctx, f)
if err != nil {
log.InfoContext(ctx, "malformed os-release file", "reason", err)
return nil, nil
}
if kv[`ID`] != `echo` {
return nil, nil
}

ver := kv[`VERSION_ID`]
if ver == "" {
log.InfoContext(ctx, "echo os-release missing VERSION_ID")
return nil, nil
}
return mkDist(ver), nil
}
2 changes: 2 additions & 0 deletions echo/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package echo contains an Indexer, Matcher, and Updater for Echo Linux.
package echo
38 changes: 38 additions & 0 deletions echo/ecosystem.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package echo

import (
"context"

"github.com/quay/claircore/debian"
"github.com/quay/claircore/dpkg"
"github.com/quay/claircore/indexer"
"github.com/quay/claircore/linux"
"github.com/quay/claircore/ubuntu"
)

// NewDpkgEcosystem provides the set of scanners and coalescers for the dpkg
// ecosystem, extended to include the Echo distribution scanner alongside the
// Debian and Ubuntu scanners.
func NewDpkgEcosystem(ctx context.Context) *indexer.Ecosystem {
return &indexer.Ecosystem{
PackageScanners: func(ctx context.Context) ([]indexer.PackageScanner, error) {
return []indexer.PackageScanner{
&dpkg.Scanner{},
&dpkg.DistrolessScanner{},
}, nil
},
DistributionScanners: func(ctx context.Context) ([]indexer.DistributionScanner, error) {
return []indexer.DistributionScanner{
&debian.DistributionScanner{},
&ubuntu.DistributionScanner{},
&DistributionScanner{},
}, nil
},
RepositoryScanners: func(ctx context.Context) ([]indexer.RepositoryScanner, error) {
return []indexer.RepositoryScanner{}, nil
},
Coalescer: func(ctx context.Context) (indexer.Coalescer, error) {
return linux.NewCoalescer(), nil
},
}
}
61 changes: 61 additions & 0 deletions echo/matcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package echo

import (
"context"

version "github.com/knqyf263/go-deb-version"

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

// Matcher is a [driver.Matcher] for Echo distributions.
type Matcher struct{}

var _ driver.Matcher = (*Matcher)(nil)

// Name implements [driver.Matcher].
func (*Matcher) Name() string {
return "echo-matcher"
}

// Filter implements [driver.Matcher].
func (*Matcher) Filter(record *claircore.IndexRecord) bool {
if record.Distribution == nil {
return false
}
return record.Distribution.DID == "echo"
}

// Query implements [driver.Matcher].
func (*Matcher) Query() []driver.MatchConstraint {
return []driver.MatchConstraint{
driver.DistributionDID,
}
}

// Vulnerable implements [driver.Matcher].
func (*Matcher) Vulnerable(ctx context.Context, record *claircore.IndexRecord, vuln *claircore.Vulnerability) (bool, error) {
if vuln.FixedInVersion == "" {
return true, nil
}
// If fixed_version is 0, the package is unaffected.
if vuln.FixedInVersion == "0" {
return false, nil
}

v1, err := version.NewVersion(record.Package.Version)
if err != nil {
return false, err
}
v2, err := version.NewVersion(vuln.FixedInVersion)
if err != nil {
return false, err
}

if v1.LessThan(v2) {
return true, nil
}

return false, nil
}
53 changes: 53 additions & 0 deletions echo/parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package echo

import (
"context"
"encoding/json"
"fmt"
"io"
"log/slog"

"github.com/quay/claircore"
)

// advisoryData maps source package name to its vulnerabilities.
type advisoryData map[string]map[string]cveEntry

// cveEntry holds vulnerability data for a single CVE.
type cveEntry struct {
FixedVersion string `json:"fixed_version"`
}

// Parse implements [driver.Parser].
func (u *echoUpdater) Parse(ctx context.Context, r io.ReadCloser) ([]*claircore.Vulnerability, error) {
slog.InfoContext(ctx, "starting parse")
defer r.Close()

var data advisoryData
if err := json.NewDecoder(r).Decode(&data); err != nil {
return nil, fmt.Errorf("echo: unable to parse advisory JSON: %w", err)
}

dist := getDist()

var vs []*claircore.Vulnerability
for pkg, cves := range data {
for cveID, entry := range cves {
v := &claircore.Vulnerability{
Updater: u.Name(),
Name: cveID,
Links: linkPrefix + cveID,
Dist: dist,
FixedInVersion: entry.FixedVersion,
Package: &claircore.Package{
Name: pkg,
Kind: claircore.SOURCE,
},
}
vs = append(vs, v)
}
}

slog.InfoContext(ctx, "parsed advisory database", "vulnerabilities", len(vs))
return vs, nil
}
37 changes: 37 additions & 0 deletions echo/releases.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package echo

import (
"sync"

"github.com/quay/claircore"
)

var releases sync.Map

func mkDist(ver string) *claircore.Distribution {
v, _ := releases.LoadOrStore(ver, &claircore.Distribution{
PrettyName: "Echo Linux",
Name: "Echo Linux",
VersionID: ver,
DID: "echo",
})
return v.(*claircore.Distribution)
}

func getDist() *claircore.Distribution {
v, ok := releases.Load("generic")
if !ok {
return mkDist("generic")
}
return v.(*claircore.Distribution)
}

const (
// linkPrefix is the URL prefix for Echo advisory links.
linkPrefix = `https://advisory.echohq.com/cve/`

// DefaultAdvisoryURL is the URL for the Echo advisory data.
//
//doc:url updater
DefaultAdvisoryURL = `https://advisory.echohq.com/data.json`
)
Loading