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
57 changes: 57 additions & 0 deletions pkg/vulnsrc/rootio/bucket.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package rootio

import (
"fmt"
"strings"

"github.com/samber/oops"

"github.com/aquasecurity/trivy-db/pkg/ecosystem"
"github.com/aquasecurity/trivy-db/pkg/types"
"github.com/aquasecurity/trivy-db/pkg/vulnsrc/bucket"
)

// rootioBucket wraps a base OS ecosystem bucket with the Root.io naming
// convention: "root.io {os} {version}" (e.g. "root.io alpine 3.18").
type rootioBucket struct {
base bucket.Bucket
dataSource types.DataSource
}

func (r rootioBucket) Name() string { return fmt.Sprintf("root.io %s", r.base.Name()) }
func (r rootioBucket) Ecosystem() ecosystem.Type { return r.base.Ecosystem() }
func (r rootioBucket) DataSource() types.DataSource { return r.dataSource }

// resolveBucket parses the suffix from a "Root:<os>:<version>" ecosystem string
// and returns the matching Root.io OS bucket. The OSV parser lowercases the raw
// ecosystem and splits on the first ":", so this function receives e.g.
// "alpine:3.18", "debian:11", "ubuntu:20.04". Non-OS Root.io ecosystems
// ("npm", "pypi", "maven", "go") have no version suffix and are rejected,
// causing the OSV parser to skip those entries — language-ecosystem support
// is out of scope for this source.
func resolveBucket(suffix string) (bucket.Bucket, error) {
eco, version, ok := strings.Cut(suffix, ":")
if !ok || version == "" {
return nil, oops.With("ecosystem", suffix).Errorf("non-OS Root.io ecosystem")
}

ds := source
ds.Name = fmt.Sprintf("%s (%s)", source.Name, eco)
ds.BaseID = types.SourceID(eco)
return newOSBucket(ecosystem.Type(eco), version, ds)
}

func newOSBucket(baseEco ecosystem.Type, version string, ds types.DataSource) (bucket.Bucket, error) {
var b bucket.Bucket
switch baseEco {
case ecosystem.Alpine:
b = bucket.NewAlpine(version)
case ecosystem.Debian:
b = bucket.NewDebian(version)
case ecosystem.Ubuntu:
b = bucket.NewUbuntu(version)
default:
return nil, oops.With("base", baseEco).Errorf("unsupported base ecosystem for Root.io bucket")
}
return rootioBucket{base: b, dataSource: ds}, nil
}
192 changes: 28 additions & 164 deletions pkg/vulnsrc/rootio/rootio.go
Original file line number Diff line number Diff line change
@@ -1,198 +1,60 @@
package rootio

import (
"encoding/json"
"fmt"
"maps"
"os"
"path/filepath"
"slices"
"sort"
"strings"

"github.com/samber/lo"
"github.com/samber/oops"
bolt "go.etcd.io/bbolt"

"github.com/aquasecurity/trivy-db/pkg/db"
"github.com/aquasecurity/trivy-db/pkg/log"
"github.com/aquasecurity/trivy-db/pkg/ecosystem"
"github.com/aquasecurity/trivy-db/pkg/types"
"github.com/aquasecurity/trivy-db/pkg/vulnsrc/alpine"
"github.com/aquasecurity/trivy-db/pkg/vulnsrc/debian"
"github.com/aquasecurity/trivy-db/pkg/vulnsrc/osv"
"github.com/aquasecurity/trivy-db/pkg/vulnsrc/ubuntu"
"github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability"
)

const (
rootioDir = "rootio"
feedFileName = "cve_feed.json"
platformFormat = "root.io %s %s" // "root.io {baseOS} {version}"
)

var (
vulnsDir = filepath.Join("vuln-list", "rootio")

source = types.DataSource{
ID: vulnerability.RootIO,
Name: "Root.io Security Patches",
URL: "https://api.root.io/external/patch_feed",
}

supportedOSes = []types.SourceID{
vulnerability.Alpine,
vulnerability.Debian,
vulnerability.Ubuntu,
}
)

type config struct {
dbc db.Operation
logger *log.Logger
}

type VulnSrc struct {
supportedOSes []types.SourceID
config
}

func NewVulnSrc() VulnSrc {
return VulnSrc{
supportedOSes: supportedOSes,
config: config{
dbc: db.Config{},
logger: log.WithPrefix("rootio"),
},
}
}

func (vs VulnSrc) Name() types.SourceID {
return source.ID
}

func (vs VulnSrc) Update(dir string) error {
feedFilePath := filepath.Join(dir, "vuln-list", rootioDir, feedFileName)
eb := oops.In("rootio").With("file_path", feedFilePath)

feedFile, err := os.Open(feedFilePath)
if err != nil {
return eb.Wrapf(err, "failed to open feed file %s", feedFilePath)
}
defer feedFile.Close()

var rawFeed RawFeed
if err = json.NewDecoder(feedFile).Decode(&rawFeed); err != nil {
return eb.With("file_path", feedFilePath).Wrapf(err, "json decode error")
}
type VulnSrc struct{}

// Take rawDistroData for each base OS
for baseOS, rawDistroData := range rawFeed {
if !slices.Contains(vs.supportedOSes, types.SourceID(baseOS)) {
vs.logger.Warn("Unsupported base OS", "base_os", baseOS)
continue
func NewVulnSrc() VulnSrc { return VulnSrc{} }

}
func (VulnSrc) Name() types.SourceID { return source.ID }

// platform => feeds
feeds := make(map[string][]Feed)
// Convert each distro version to our internal Feed format
for _, distro := range rawDistroData {
platformName := fmt.Sprintf(platformFormat, strings.ToLower(baseOS), distro.DistroVersion)

// Convert packages to patches
for _, pkg := range distro.Packages {
for cveID, cveInfo := range pkg.Pkg.CVEs {
// Parse severity from the feed
severity := types.SeverityUnknown
if cveInfo.Severity != "" {
if sev, err := types.NewSeverity(cveInfo.Severity); err == nil {
severity = sev
} else {
vs.logger.Warn("Invalid severity value", "cve", cveID, "severity", cveInfo.Severity, "error", err)
}
}

feed := Feed{
VulnerabilityID: cveID,
PkgName: pkg.Pkg.Name,
Patch: types.Advisory{
VulnerableVersions: cveInfo.VulnerableRanges,
PatchedVersions: cveInfo.FixedVersions,
Severity: severity,
},
}
feeds[platformName] = append(feeds[platformName], feed)
}
}
}
func (vs VulnSrc) Update(root string) error {
eb := oops.In("rootio").With("root", root)

// Save feeds for the current base OS
if err = vs.save(baseOS, feeds); err != nil {
return eb.Wrapf(err, "save error")
}
o := osv.New(vulnsDir, source.ID, nil,
osv.WithBucketResolver("root", resolveBucket),
)
if err := o.Update(root); err != nil {
return eb.Wrapf(err, "failed to update Root.io vulnerability data")
}

return nil
}

func (vs VulnSrc) save(baseOS string, feeds map[string][]Feed) error {
vs.logger.Info("Saving Root.io DB", "base_os", baseOS)
err := vs.dbc.BatchUpdate(func(tx *bolt.Tx) error {
for platform, platformFeeds := range feeds {
dataSource := types.DataSource{
ID: source.ID,
Name: source.Name + fmt.Sprintf(" (%s)", baseOS),
URL: source.URL,
BaseID: types.SourceID(baseOS),
}
if err := vs.dbc.PutDataSource(tx, platform, dataSource); err != nil {
return oops.Wrapf(err, "failed to put data source")
}
if err := vs.commit(tx, platform, platformFeeds); err != nil {
return err
}
}
return nil
})
if err != nil {
return oops.Wrapf(err, "batch update error")
}
return nil
}

func (vs VulnSrc) commit(tx *bolt.Tx, platform string, feeds []Feed) error {
for _, feed := range feeds {
if err := vs.put(tx, platform, feed); err != nil {
return oops.Wrapf(err, "put error")
}
}
return nil
}

func (vs VulnSrc) put(tx *bolt.Tx, platform string, feed Feed) error {
eb := oops.With("platform", platform).With("package", feed.PkgName).With("cve", feed.VulnerabilityID)

if err := vs.dbc.PutAdvisoryDetail(tx, feed.VulnerabilityID, feed.PkgName, []string{platform}, feed.Patch); err != nil {
return eb.Wrapf(err, "failed to save advisory")
}

// For optimization
if err := vs.dbc.PutVulnerabilityID(tx, feed.VulnerabilityID); err != nil {
return eb.Wrapf(err, "failed to save the vulnerability ID")
}

return nil
}

type VulnSrcGetter struct {
baseOS types.SourceID
config
baseOS ecosystem.Type
dbc db.Operation
}

func NewVulnSrcGetter(baseOS types.SourceID) VulnSrcGetter {
func NewVulnSrcGetter(baseOS ecosystem.Type) VulnSrcGetter {
return VulnSrcGetter{
baseOS: baseOS,
config: config{
dbc: db.Config{},
logger: log.WithPrefix(fmt.Sprintf("rootio-%s", baseOS)),
},
dbc: db.Config{},
}
}

Expand All @@ -210,13 +72,16 @@ func (vs VulnSrcGetter) Get(params db.GetParams) ([]types.Advisory, error) {
if adv.FixedVersion != "" {
adv.VulnerableVersions = []string{"<" + adv.FixedVersion}
adv.PatchedVersions = []string{adv.FixedVersion}
adv.FixedVersion = "" // Clear fixed version to avoid confusion
adv.FixedVersion = ""
}
allAdvs[adv.VulnerabilityID] = adv
}

rootioOSVer := fmt.Sprintf(platformFormat, vs.baseOS, params.Release)
advs, err = vs.dbc.GetAdvisories(rootioOSVer, params.PkgName)
bkt, err := newOSBucket(vs.baseOS, params.Release, source)
if err != nil {
return nil, eb.Wrapf(err, "failed to build bucket")
}
advs, err = vs.dbc.GetAdvisories(bkt.Name(), params.PkgName)
if err != nil {
return nil, eb.Wrapf(err, "failed to get advisories")
}
Expand All @@ -225,8 +90,7 @@ func (vs VulnSrcGetter) Get(params db.GetParams) ([]types.Advisory, error) {
return adv.VulnerabilityID, adv
})

// Merge the advisories from the original distributors with Root.io's advisories.
// If both have the same vulnerability ID - only Root.io recommendation will be kept.
// On vulnerability-ID collision, Root.io's entry wins over the upstream's.
maps.Copy(allAdvs, rootAdvs)

if len(allAdvs) == 0 {
Expand All @@ -243,11 +107,11 @@ func (vs VulnSrcGetter) Get(params db.GetParams) ([]types.Advisory, error) {

func (vs VulnSrcGetter) baseOSGetter() db.Getter {
switch vs.baseOS {
case vulnerability.Debian:
case ecosystem.Debian:
return debian.NewVulnSrc()
case vulnerability.Ubuntu:
case ecosystem.Ubuntu:
return ubuntu.NewVulnSrc()
case vulnerability.Alpine:
case ecosystem.Alpine:
return alpine.NewVulnSrc()
}
return nil
Expand Down
Loading
Loading