Skip to content

Commit

Permalink
OS Matching (#33)
Browse files Browse the repository at this point in the history
Signed-off-by: Benji Visser <benji@093b.org>
  • Loading branch information
noqcks authored Feb 23, 2023
1 parent 859447f commit 9e2505b
Show file tree
Hide file tree
Showing 24 changed files with 521 additions and 78 deletions.
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

0 comments on commit 9e2505b

Please sign in to comment.