Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OS Matching #33

Merged
merged 6 commits into from
Feb 23, 2023
Merged
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
4 changes: 3 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/noqcks/xeol/xeol/db"
"github.com/noqcks/xeol/xeol/event"
"github.com/noqcks/xeol/xeol/matcher"
distroMatcher "github.com/noqcks/xeol/xeol/matcher/distro"
pkgMatcher "github.com/noqcks/xeol/xeol/matcher/packages"
"github.com/noqcks/xeol/xeol/pkg"
"github.com/noqcks/xeol/xeol/presenter"
Expand Down Expand Up @@ -248,9 +249,10 @@ func startWorker(userInput string, failOnEolFound bool, eolMatchDate time.Time)
log.Debugf("gathering matches")
matchers := matcher.NewDefaultMatchers(matcher.Config{
Packages: pkgMatcher.MatcherConfig(appConfig.Match.Packages),
Distro: distroMatcher.MatcherConfig(appConfig.Match.Distro),
})

allMatches, err := xeol.FindEolForPackage(*store, pkgContext.Distro, matchers, sbomPackages, failOnEolFound, eolMatchDate)
allMatches, err := xeol.FindEol(*store, pkgContext.Distro, matchers, sbomPackages, failOnEolFound, eolMatchDate)
if err != nil {
errs <- err
if !errors.Is(err, xeolerr.ErrEolFound) {
Expand Down
10 changes: 8 additions & 2 deletions internal/config/match.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,19 @@ import "github.com/spf13/viper"

// matchConfig contains all matching-related configuration options available to the user via the application config.
type matchConfig struct {
Packages matcherConfig `mapstructure:"packages"`
Packages pkgMatcherConfig `mapstructure:"packages"`
Distro distroMatcherConfig `mapstructure:"distro"`
}

type matcherConfig struct {
type pkgMatcherConfig struct {
UsePurls bool `yaml:"using-purls" json:"using-purls" mapstructure:"using-purls"` // if Purls should be used during matching
}

type distroMatcherConfig struct {
UseCpes bool `yaml:"using-cpes" json:"using-cpes" mapstructure:"using-cpes"` // if CPEs should be used during matching
}

func (cfg matchConfig) loadDefaultValues(v *viper.Viper) {
v.SetDefault("match.packages.using-purls", true)
v.SetDefault("match.distro.using-cpes", true)
}
25 changes: 25 additions & 0 deletions internal/cpe/cpe.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package cpe

import (
"strings"

"github.com/noqcks/xeol/internal/log"
)

func Destructure(cpe string) (shortCPE, version string) {
parts := strings.Split(cpe, ":")

if len(parts) < 5 {
log.Debugf("CPE string '%s' is too short", cpe)
return "", ""
}

var splitIndex int
if parts[1] == "2.3" {
splitIndex = 5
} else {
splitIndex = 4
}

return strings.Join(parts[:splitIndex], ":"), parts[splitIndex]
}
58 changes: 58 additions & 0 deletions internal/cpe/cpe_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package cpe

import (
"testing"
)

func TestCpeDestructure(t *testing.T) {
testCases := []struct {
name string
input string
expectedShortCpe string
expectedVersion string
}{
{
name: "Exact CPE 2.2",
input: "cpe:/a:apache:struts:2.5.10",
expectedShortCpe: "cpe:/a:apache:struts",
expectedVersion: "2.5.10",
},
{
name: "Exact CPE 2.3",
input: "cpe:2.3:a:apache:struts:2.5.10",
expectedShortCpe: "cpe:2.3:a:apache:struts",
expectedVersion: "2.5.10",
},
{
name: "CPE 2.2",
input: "cpe:/a:apache:struts:2.5:*:*:*:*:*:*:*",
expectedShortCpe: "cpe:/a:apache:struts",
expectedVersion: "2.5",
},
{
name: "CPE 2.3",
input: "cpe:2.3:a:apache:struts:2.5:*:*:*:*:*:*:*",
expectedShortCpe: "cpe:2.3:a:apache:struts",
expectedVersion: "2.5",
},
{
name: "Empty CPE",
input: "",
expectedShortCpe: "",
expectedVersion: "",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
gotCpe, gotVersion := Destructure(tc.input)

if gotVersion != tc.expectedVersion {
t.Errorf("Expected version '%v', got '%v'", tc.expectedVersion, gotVersion)
}
if gotCpe != tc.expectedShortCpe {
t.Errorf("Expected short CPE '%v', got '%v'", tc.expectedShortCpe, gotCpe)
}
})
}
}
12 changes: 12 additions & 0 deletions test/integration/db_mock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ func (s *mockStore) GetCyclesByPurl(purl string) ([]xeolDB.Cycle, error) {
return s.backend[purl], nil
}

func (s *mockStore) GetCyclesByCpe(cpe string) ([]xeolDB.Cycle, error) {
return s.backend[cpe], nil
}

func (s *mockStore) GetAllProducts() (*[]xeolDB.Product, error) {
return nil, nil
}
Expand Down Expand Up @@ -133,11 +137,19 @@ func cycles(name string) []xeolDB.Cycle {
Eol: "2022-02-10",
},
},
"fedora": {
{
ProductName: "Fedora",
ReleaseCycle: "29",
Eol: "2019-11-26",
},
},
}
return cycleDict[name]
}

func (d *mockStore) stub() {
d.backend["cpe:/o:fedoraproject:fedora"] = cycles("fedora")
d.backend["pkg:generic/redis"] = cycles("redis")
d.backend["pkg:generic/node"] = cycles("node")
d.backend["pkg:generic/go"] = cycles("golang")
Expand Down
25 changes: 24 additions & 1 deletion test/integration/match_by_image_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,11 +207,34 @@ func addRedis5Matches(t *testing.T, theResult *match.Matches) {
})
}

func addFedora29Matches(t *testing.T, theResult *match.Matches) {
theResult.Add(match.Match{
Package: pkg.Package{
Name: "Fedora",
Version: "29",
Type: "os",
},
Cycle: eol.Cycle{
ProductName: "Fedora",
ReleaseCycle: "29",
Eol: "2019-11-26",
},
})
}

func TestMatchByImage(t *testing.T) {
tests := []struct {
fixtureImage string
expectedFn func() match.Matches
}{
{
fixtureImage: "image-fedora-29",
expectedFn: func() match.Matches {
expectedMatches := match.NewMatches()
addFedora29Matches(t, &expectedMatches)
return expectedMatches
},
},
{
fixtureImage: "image-nodejs-6.13.1",
expectedFn: func() match.Matches {
Expand Down Expand Up @@ -305,7 +328,7 @@ func TestMatchByImage(t *testing.T) {
Provider: ep,
}

actualResults, err := xeol.FindEolForPackage(str, theDistro, matchers, pkg.FromCatalog(theCatalog, pkg.SynthesisConfig{}), false, time.Now())
actualResults, err := xeol.FindEol(str, theDistro, matchers, pkg.FromCatalog(theCatalog, pkg.SynthesisConfig{}), false, time.Now())
require.NoError(t, err)

// build expected matches from what's discovered from the catalog
Expand Down
1 change: 1 addition & 0 deletions test/integration/test-fixtures/image-fedora-29/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FROM docker.io/fedora:29@sha256:2c20e5bb324735427f8a659e36f4fe14d6955c74c7baa25067418dddbb71d67a
35 changes: 34 additions & 1 deletion xeol/db/eol_provider.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package db

import (
"errors"

"github.com/anchore/syft/syft/linux"

"github.com/noqcks/xeol/internal/cpe"
"github.com/noqcks/xeol/internal/purl"
xeolDB "github.com/noqcks/xeol/xeol/db/v1"
"github.com/noqcks/xeol/xeol/eol"
Expand All @@ -19,7 +24,35 @@ func NewEolProvider(reader xeolDB.EolStoreReader) (*EolProvider, error) {
}, nil
}

func (pr *EolProvider) GetByPurl(p pkg.Package) ([]eol.Cycle, error) {
func (pr *EolProvider) GetByDistroCpe(d *linux.Release) (string, []eol.Cycle, error) {
cycles := make([]eol.Cycle, 0)

if d == nil || d.CPEName == "" {
return "", []eol.Cycle{}, errors.New("empty distro CPEName")
}

shortCPE, version := cpe.Destructure(d.CPEName)
if version == "" || shortCPE == "" {
return "", []eol.Cycle{}, errors.New("invalid distro CPEName")
}

allCycles, err := pr.reader.GetCyclesByCpe(shortCPE)
if err != nil {
return "", []eol.Cycle{}, err
}

for _, cycle := range allCycles {
cycleObj, err := eol.NewCycle(cycle)
if err != nil {
return "", []eol.Cycle{}, err
}
cycles = append(cycles, *cycleObj)
}

return version, cycles, nil
}

func (pr *EolProvider) GetByPackagePurl(p pkg.Package) ([]eol.Cycle, error) {
cycles := make([]eol.Cycle, 0)

shortPurl, err := purl.ShortPurl(p)
Expand Down
9 changes: 9 additions & 0 deletions xeol/db/eol_provider_mocks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,21 @@ func (d *mockStore) stub() {
ProductName: "debian:distro:debian:8",
},
}
d.data["cpe:/o:fedoraproject:fedora"] = []xeolDB.Cycle{
{
ProductName: "fedora:distro:fedora:28",
},
}
}

func (s *mockStore) GetCyclesByPurl(purl string) ([]xeolDB.Cycle, error) {
return s.data[purl], nil
}

func (s *mockStore) GetCyclesByCpe(cpe string) ([]xeolDB.Cycle, error) {
return s.data[cpe], nil
}

func (s *mockStore) GetAllProducts() (*[]xeolDB.Product, error) {
return nil, nil
}
5 changes: 5 additions & 0 deletions xeol/db/v1/eol.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,18 @@ type Purl struct {
Purl string `json:"purl"`
}

type Cpe struct {
Cpe string `json:"cpe"`
}

type EolStore interface {
EolStoreReader
EolStoreWriter
}

type EolStoreReader interface {
GetCyclesByPurl(purl string) ([]Cycle, error)
GetCyclesByCpe(cpe string) ([]Cycle, error)
GetAllProducts() (*[]Product, error)
}

Expand Down
21 changes: 21 additions & 0 deletions xeol/db/v1/store/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,27 @@ func (s *store) GetAllProducts() (*[]v1.Product, error) {
return &products, nil
}

func (s *store) GetCyclesByCpe(cpe string) ([]v1.Cycle, error) {
var models []model.CycleModel
if result := s.db.Table("cycles").
Select("cycles.*, products.name as product_name").
Joins("JOIN products ON cycles.product_id = products.id").
Joins("JOIN cpes ON products.id = cpes.product_id").
Where("cpes.cpe = ?", cpe).Find(&models); result.Error != nil {
return nil, result.Error
}
cycles := make([]v1.Cycle, len(models))

for i, m := range models {
c, err := m.Inflate()
if err != nil {
return nil, err
}
cycles[i] = c
}
return cycles, nil
}

func (s *store) GetCyclesByPurl(purl string) ([]v1.Cycle, error) {
var models []model.CycleModel
if result := s.db.Table("cycles").
Expand Down
17 changes: 13 additions & 4 deletions xeol/eol/provider.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
package eol

import "github.com/noqcks/xeol/xeol/pkg"
import (
"github.com/anchore/syft/syft/linux"

"github.com/noqcks/xeol/xeol/pkg"
)

type Provider interface {
ProviderByPurl
ProviderByPackagePurl
ProviderByDistroCpe
}

type ProviderByPackagePurl interface {
GetByPackagePurl(p pkg.Package) ([]Cycle, error)
}

type ProviderByPurl interface {
GetByPurl(p pkg.Package) ([]Cycle, error)
type ProviderByDistroCpe interface {
GetByDistroCpe(distro *linux.Release) (string, []Cycle, error)
}
2 changes: 1 addition & 1 deletion xeol/lib.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func SetLogger(logger logger.Logger) {
log.Log = logger
}

func FindEolForPackage(store store.Store, d *linux.Release, matchers []matcher.Matcher, packages []pkg.Package, failOnEolFound bool, eolMatchDate time.Time) (match.Matches, error) {
func FindEol(store store.Store, d *linux.Release, matchers []matcher.Matcher, packages []pkg.Package, failOnEolFound bool, eolMatchDate time.Time) (match.Matches, error) {
matches := matcher.FindMatches(store, d, matchers, packages, failOnEolFound, eolMatchDate)
var err error
if failOnEolFound && matches.Count() > 0 {
Expand Down
33 changes: 33 additions & 0 deletions xeol/matcher/distro/matcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package distro

import (
"time"

"github.com/anchore/syft/syft/linux"

"github.com/noqcks/xeol/xeol/eol"
"github.com/noqcks/xeol/xeol/match"
"github.com/noqcks/xeol/xeol/search"
)

type Matcher struct {
UseCpes bool
}

type MatcherConfig struct {
UseCpes bool
}

func NewPackageMatcher(cfg MatcherConfig) *Matcher {
return &Matcher{
UseCpes: cfg.UseCpes,
}
}

func (m *Matcher) Type() match.MatcherType {
return match.PackageMatcher
}

func (m *Matcher) Match(store eol.Provider, d *linux.Release, eolMatchDate time.Time) (match.Match, error) {
return search.ByDistroCpe(store, d, eolMatchDate)
}
Loading