diff --git a/cmd/root.go b/cmd/root.go index b5a3430a..a43b562c 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -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" @@ -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) { diff --git a/internal/config/match.go b/internal/config/match.go index e8b95d8c..5196a95c 100644 --- a/internal/config/match.go +++ b/internal/config/match.go @@ -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) } diff --git a/internal/cpe/cpe.go b/internal/cpe/cpe.go new file mode 100644 index 00000000..df9e560b --- /dev/null +++ b/internal/cpe/cpe.go @@ -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] +} diff --git a/internal/cpe/cpe_test.go b/internal/cpe/cpe_test.go new file mode 100644 index 00000000..d94dbf0d --- /dev/null +++ b/internal/cpe/cpe_test.go @@ -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) + } + }) + } +} diff --git a/test/integration/db_mock_test.go b/test/integration/db_mock_test.go index 7608b819..021ce18e 100644 --- a/test/integration/db_mock_test.go +++ b/test/integration/db_mock_test.go @@ -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 } @@ -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") diff --git a/test/integration/match_by_image_test.go b/test/integration/match_by_image_test.go index 80b9d395..e16a213a 100644 --- a/test/integration/match_by_image_test.go +++ b/test/integration/match_by_image_test.go @@ -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 { @@ -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 diff --git a/test/integration/test-fixtures/image-fedora-29/Dockerfile b/test/integration/test-fixtures/image-fedora-29/Dockerfile new file mode 100644 index 00000000..b9d97cc6 --- /dev/null +++ b/test/integration/test-fixtures/image-fedora-29/Dockerfile @@ -0,0 +1 @@ +FROM docker.io/fedora:29@sha256:2c20e5bb324735427f8a659e36f4fe14d6955c74c7baa25067418dddbb71d67a diff --git a/xeol/db/eol_provider.go b/xeol/db/eol_provider.go index 8e171208..c57f8af8 100644 --- a/xeol/db/eol_provider.go +++ b/xeol/db/eol_provider.go @@ -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" @@ -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) diff --git a/xeol/db/eol_provider_mocks_test.go b/xeol/db/eol_provider_mocks_test.go index 511bac58..7800ab24 100644 --- a/xeol/db/eol_provider_mocks_test.go +++ b/xeol/db/eol_provider_mocks_test.go @@ -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 } diff --git a/xeol/db/v1/eol.go b/xeol/db/v1/eol.go index 9ac8be54..90a2cc64 100644 --- a/xeol/db/v1/eol.go +++ b/xeol/db/v1/eol.go @@ -21,6 +21,10 @@ type Purl struct { Purl string `json:"purl"` } +type Cpe struct { + Cpe string `json:"cpe"` +} + type EolStore interface { EolStoreReader EolStoreWriter @@ -28,6 +32,7 @@ type EolStore interface { type EolStoreReader interface { GetCyclesByPurl(purl string) ([]Cycle, error) + GetCyclesByCpe(cpe string) ([]Cycle, error) GetAllProducts() (*[]Product, error) } diff --git a/xeol/db/v1/store/store.go b/xeol/db/v1/store/store.go index 56cb4c05..630aa6a8 100644 --- a/xeol/db/v1/store/store.go +++ b/xeol/db/v1/store/store.go @@ -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"). diff --git a/xeol/eol/provider.go b/xeol/eol/provider.go index c07e8e5e..494ad76e 100644 --- a/xeol/eol/provider.go +++ b/xeol/eol/provider.go @@ -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) } diff --git a/xeol/lib.go b/xeol/lib.go index 9002908c..b646e680 100644 --- a/xeol/lib.go +++ b/xeol/lib.go @@ -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 { diff --git a/xeol/matcher/distro/matcher.go b/xeol/matcher/distro/matcher.go new file mode 100644 index 00000000..f77a7e6d --- /dev/null +++ b/xeol/matcher/distro/matcher.go @@ -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) +} diff --git a/xeol/matcher/distro/matcher_test.go b/xeol/matcher/distro/matcher_test.go new file mode 100644 index 00000000..e3d406a9 --- /dev/null +++ b/xeol/matcher/distro/matcher_test.go @@ -0,0 +1,180 @@ +package distro + +import ( + "testing" + "time" + + "github.com/anchore/syft/syft/linux" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/noqcks/xeol/xeol/db" + xeolDB "github.com/noqcks/xeol/xeol/db/v1" + "github.com/noqcks/xeol/xeol/eol" + "github.com/noqcks/xeol/xeol/match" + "github.com/noqcks/xeol/xeol/pkg" +) + +type mockStore struct { + backend map[string][]xeolDB.Cycle +} + +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 +} + +func TestMatch(t *testing.T) { + cycle := xeolDB.Cycle{ + ProductName: "Fedora", + ReleaseDate: "2019-11-26", + ReleaseCycle: "29", + Eol: "2019-11-26", + LatestReleaseDate: "2019-11-26", + } + + store := mockStore{ + backend: map[string][]xeolDB.Cycle{ + "cpe:/o:fedoraproject:fedora": {cycle}, + }, + } + + provider, err := db.NewEolProvider(&store) + require.NoError(t, err) + + m := Matcher{} + p := pkg.Package{ + ID: "", + Name: "Fedora", + Version: "29", + Type: "os", + } + + cycleFound, err := eol.NewCycle(cycle) + d := &linux.Release{ + Name: "Fedora", + Version: "29", + CPEName: "cpe:/o:fedoraproject:fedora:29", + } + assert.NoError(t, err) + expected := match.Match{ + Cycle: *cycleFound, + Package: p, + } + actual, err := m.Match(provider, d, time.Now()) + assert.NoError(t, err) + assertMatches(t, expected, actual) +} + +func TestMatchCpeMismatch(t *testing.T) { + cycle := xeolDB.Cycle{ + ProductName: "Fedora", + ReleaseDate: "2019-11-26", + ReleaseCycle: "29", + Eol: "2019-11-26", + LatestReleaseDate: "2019-11-26", + } + + store := mockStore{ + backend: map[string][]xeolDB.Cycle{ + "cpe:/o:canonical:ubuntu": {cycle}, + }, + } + m := Matcher{} + provider, err := db.NewEolProvider(&store) + require.NoError(t, err) + + d := &linux.Release{ + Name: "Fedora", + Version: "29", + CPEName: "cpe:/o:fedoraproject:fedora:29", + } + + actual, err := m.Match(provider, d, time.Now()) + assert.NoError(t, err) + assertMatches(t, match.Match{}, actual) +} + +func TestMatchNoMatchingVersion(t *testing.T) { + cycle := xeolDB.Cycle{ + ProductName: "Fedora", + ReleaseDate: "2019-11-26", + ReleaseCycle: "28", // different version + Eol: "2019-11-26", + LatestReleaseDate: "2019-11-26", + } + + store := mockStore{ + backend: map[string][]xeolDB.Cycle{ + "cpe:/o:fedoraproject:fedora": {cycle}, + }, + } + + provider, err := db.NewEolProvider(&store) + require.NoError(t, err) + + // Set up a matcher and a package with the same PURL but a different version + m := Matcher{} + d := &linux.Release{ + Name: "Fedora", + Version: "29", + CPEName: "cpe:/o:fedoraproject:fedora:29", + } + + actual, err := m.Match(provider, d, time.Now()) + assert.NoError(t, err) + assertMatches(t, match.Match{}, actual) +} + +func TestMatchTimeChange(t *testing.T) { + cycle := xeolDB.Cycle{ + ProductName: "Fedora", + ReleaseDate: "2019-11-26", + ReleaseCycle: "29", + Eol: "2019-11-26", + LatestReleaseDate: "2019-11-26", + } + + store := mockStore{ + backend: map[string][]xeolDB.Cycle{ + "cpe:/o:fedoraproject:fedora": {cycle}, + }, + } + + provider, err := db.NewEolProvider(&store) + require.NoError(t, err) + + m := Matcher{} + d := &linux.Release{ + Name: "Fedora", + Version: "29", + CPEName: "cpe:/o:fedoraproject:fedora:29", + } + + eolMatchTime, err := time.Parse("2006-01-02", "2018-01-01") + assert.NoError(t, err) + + actual, err := m.Match(provider, d, eolMatchTime) + assert.NoError(t, err) + assertMatches(t, match.Match{}, actual) +} + +func assertMatches(t *testing.T, expected, actual match.Match) { + t.Helper() + var opts = []cmp.Option{ + cmpopts.IgnoreFields(pkg.Package{}, "Locations"), + } + + if diff := cmp.Diff(expected, actual, opts...); diff != "" { + t.Errorf("mismatch (-want +got):\n%s", diff) + } +} diff --git a/xeol/matcher/matcher.go b/xeol/matcher/matcher.go index b0f65995..24cfb7d9 100644 --- a/xeol/matcher/matcher.go +++ b/xeol/matcher/matcher.go @@ -1,12 +1,9 @@ package matcher import ( - syftPkg "github.com/anchore/syft/syft/pkg" - "github.com/noqcks/xeol/xeol/match" ) type Matcher interface { - PackageTypes() []syftPkg.Type Type() match.MatcherType } diff --git a/xeol/matcher/matchers.go b/xeol/matcher/matchers.go index d0ee53d9..cbff31c4 100644 --- a/xeol/matcher/matchers.go +++ b/xeol/matcher/matchers.go @@ -9,10 +9,10 @@ import ( "github.com/noqcks/xeol/internal/bus" "github.com/noqcks/xeol/internal/log" - "github.com/noqcks/xeol/xeol/distro" "github.com/noqcks/xeol/xeol/eol" "github.com/noqcks/xeol/xeol/event" "github.com/noqcks/xeol/xeol/match" + distroMatcher "github.com/noqcks/xeol/xeol/matcher/distro" pkgMatcher "github.com/noqcks/xeol/xeol/matcher/packages" "github.com/noqcks/xeol/xeol/pkg" ) @@ -25,11 +25,13 @@ type Monitor struct { // Config contains values used by individual matcher structs for advanced configuration type Config struct { Packages pkgMatcher.MatcherConfig + Distro distroMatcher.MatcherConfig } func NewDefaultMatchers(mc Config) []Matcher { return []Matcher{ &pkgMatcher.Matcher{}, + &distroMatcher.Matcher{}, } } @@ -49,17 +51,23 @@ func trackMatcher() (*progress.Manual, *progress.Manual) { func FindMatches(store interface { eol.Provider -}, release *linux.Release, matchers []Matcher, packages []pkg.Package, failOnEolFound bool, eolMatchDate time.Time) match.Matches { - var err error +}, distro *linux.Release, matchers []Matcher, packages []pkg.Package, failOnEolFound bool, eolMatchDate time.Time) match.Matches { + // var err error res := match.NewMatches() - defaultMatcher := &pkgMatcher.Matcher{UsePurls: true} + defaultMatcher := &pkgMatcher.Matcher{ + UsePurls: true, + } + distroMatcher := &distroMatcher.Matcher{ + UseCpes: true, + } - var d *distro.Distro - if release != nil { - d, err = distro.NewFromRelease(*release) - if err != nil { - log.Warnf("unable to determine linux distribution: %+v", err) - } + distroMatch, err := distroMatcher.Match(store, distro, eolMatchDate) + if err != nil { + log.Warnf("matcher failed for distro=%s: %+v", distro, err) + } + if (distroMatch.Cycle != eol.Cycle{}) { + logDistroMatch(distro) + res.Add(distroMatch) } packagesProcessed, eolDiscovered := trackMatcher() @@ -68,12 +76,12 @@ func FindMatches(store interface { packagesProcessed.N++ log.Debugf("searching for eol matches for pkg=%s", p) - pkgMatch, err := defaultMatcher.Match(store, d, p, eolMatchDate) + pkgMatch, err := defaultMatcher.Match(store, p, eolMatchDate) if err != nil { log.Warnf("matcher failed for pkg=%s: %+v", p, err) } if (pkgMatch.Cycle != eol.Cycle{}) { - logMatch(p) + logPkgMatch(p) res.Add(pkgMatch) eolDiscovered.N++ } @@ -85,6 +93,10 @@ func FindMatches(store interface { return res } -func logMatch(p pkg.Package) { - log.Debugf("found eol match for purl=%s \n", p.PURL) +func logDistroMatch(d *linux.Release) { + log.Debugf("found eol match for distro cpe=%s \n", d.CPEName) +} + +func logPkgMatch(p pkg.Package) { + log.Debugf("found eol match for pkg purl=%s \n", p.PURL) } diff --git a/xeol/matcher/packages/matcher.go b/xeol/matcher/packages/matcher.go index a7b970e2..e7d72ad9 100644 --- a/xeol/matcher/packages/matcher.go +++ b/xeol/matcher/packages/matcher.go @@ -5,7 +5,6 @@ import ( syftPkg "github.com/anchore/syft/syft/pkg" - "github.com/noqcks/xeol/xeol/distro" "github.com/noqcks/xeol/xeol/eol" "github.com/noqcks/xeol/xeol/match" "github.com/noqcks/xeol/xeol/pkg" @@ -34,6 +33,6 @@ func (m *Matcher) Type() match.MatcherType { return match.PackageMatcher } -func (m *Matcher) Match(store eol.Provider, d *distro.Distro, p pkg.Package, eolMatchDate time.Time) (match.Match, error) { +func (m *Matcher) Match(store eol.Provider, p pkg.Package, eolMatchDate time.Time) (match.Match, error) { return search.ByPackagePURL(store, p, m.Type(), eolMatchDate) } diff --git a/xeol/matcher/packages/matcher_test.go b/xeol/matcher/packages/matcher_test.go index 5034d84d..578e05e3 100644 --- a/xeol/matcher/packages/matcher_test.go +++ b/xeol/matcher/packages/matcher_test.go @@ -13,7 +13,6 @@ import ( "github.com/noqcks/xeol/xeol/db" xeolDB "github.com/noqcks/xeol/xeol/db/v1" - "github.com/noqcks/xeol/xeol/distro" "github.com/noqcks/xeol/xeol/eol" "github.com/noqcks/xeol/xeol/match" "github.com/noqcks/xeol/xeol/pkg" @@ -27,6 +26,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 } @@ -50,10 +53,6 @@ func TestMatch(t *testing.T) { require.NoError(t, err) m := Matcher{} - d, err := distro.New(distro.Alpine, "3.12.0", "") - if err != nil { - t.Fatalf("failed to create a new distro: %+v", err) - } p := pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "mongodb-org-server", @@ -68,7 +67,7 @@ func TestMatch(t *testing.T) { Cycle: *cycleFound, Package: p, } - actual, err := m.Match(provider, d, p, time.Now()) + actual, err := m.Match(provider, p, time.Now()) assert.NoError(t, err) assertMatches(t, expected, actual) } @@ -90,10 +89,6 @@ func TestMatchPurlMismatch(t *testing.T) { require.NoError(t, err) m := Matcher{} - d, err := distro.New(distro.Alpine, "3.12.0", "") - if err != nil { - t.Fatalf("failed to create a new distro: %+v", err) - } p := pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "mongodb-org-server", @@ -102,7 +97,7 @@ func TestMatchPurlMismatch(t *testing.T) { PURL: "pkg:deb/debian/mongodb-org-server@3.2.21?arch=amd64&upstream=mongodb-org&distro=debian-8", } - actual, err := m.Match(provider, d, p, time.Now()) + actual, err := m.Match(provider, p, time.Now()) assert.NoError(t, err) assertMatches(t, match.Match{}, actual) } @@ -127,10 +122,6 @@ func TestMatchNoMatchingVersion(t *testing.T) { // Set up a matcher and a package with the same PURL but a different version m := Matcher{} - d, err := distro.New(distro.Alpine, "3.12.0", "") - if err != nil { - t.Fatalf("failed to create a new distro: %+v", err) - } p := pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "mongodb-org-server", @@ -139,7 +130,7 @@ func TestMatchNoMatchingVersion(t *testing.T) { PURL: "pkg:deb/debian/mongodb-org-server@3.2.21?arch=amd64&upstream=mongodb-org&distro=debian-8", } - actual, err := m.Match(provider, d, p, time.Now()) + actual, err := m.Match(provider, p, time.Now()) assert.NoError(t, err) assertMatches(t, match.Match{}, actual) } @@ -162,12 +153,7 @@ func TestMatchTimeChange(t *testing.T) { provider, err := db.NewEolProvider(&store) require.NoError(t, err) - // Set up a matcher and a package with the same PURL but a different version m := Matcher{} - d, err := distro.New(distro.Alpine, "3.12.0", "") - if err != nil { - t.Fatalf("failed to create a new distro: %+v", err) - } p := pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "mongodb-org-server", @@ -179,7 +165,7 @@ func TestMatchTimeChange(t *testing.T) { eolMatchTime, err := time.Parse("2006-01-02", "2018-01-01") assert.NoError(t, err) - actual, err := m.Match(provider, d, p, eolMatchTime) + actual, err := m.Match(provider, p, eolMatchTime) assert.NoError(t, err) assertMatches(t, match.Match{}, actual) } diff --git a/xeol/pkg/syft_provider.go b/xeol/pkg/syft_provider.go index f6f357af..992a9437 100644 --- a/xeol/pkg/syft_provider.go +++ b/xeol/pkg/syft_provider.go @@ -22,7 +22,7 @@ func syftProvider(userInput string, config ProviderConfig) ([]Package, Context, } defer cleanup() - catalog, relationships, theDistro, err := syft.CatalogPackages(src, config.CatalogingOptions) + catalog, relationships, distro, err := syft.CatalogPackages(src, config.CatalogingOptions) if err != nil { return nil, Context{}, nil, err } @@ -32,7 +32,7 @@ func syftProvider(userInput string, config ProviderConfig) ([]Package, Context, packages := FromCatalog(catalog, config.SynthesisConfig) context := Context{ Source: &src.Metadata, - Distro: theDistro, + Distro: distro, } sbom := &sbom.SBOM{ diff --git a/xeol/presenter/table/actual.txt b/xeol/presenter/table/actual.txt index e57b8343..caeec412 100644 --- a/xeol/presenter/table/actual.txt +++ b/xeol/presenter/table/actual.txt @@ -1,3 +1,3 @@ -NAME VERSION EOL DAYS EOL METHOD +NAME VERSION EOL DAYS EOL TYPE package-1 1.1.1 2018-07-31 1614 rpm package-2 2.2.2 2016-07-31 2344 deb diff --git a/xeol/presenter/table/presenter.go b/xeol/presenter/table/presenter.go index f354309f..f730e83f 100644 --- a/xeol/presenter/table/presenter.go +++ b/xeol/presenter/table/presenter.go @@ -34,7 +34,7 @@ func NewPresenter(pb models.PresenterConfig) *Presenter { func (pres *Presenter) Present(output io.Writer) error { rows := make([][]string, 0) - columns := []string{"NAME", "VERSION", "EOL", "DAYS EOL", "METHOD"} + columns := []string{"NAME", "VERSION", "EOL", "DAYS EOL", "TYPE"} // Generate rows for matches for m := range pres.results.Enumerate() { if m.Package.Name == "" { diff --git a/xeol/presenter/table/test-fixtures/snapshot/TestTablePresenter.golden b/xeol/presenter/table/test-fixtures/snapshot/TestTablePresenter.golden index fe07fba8..effd0642 100644 --- a/xeol/presenter/table/test-fixtures/snapshot/TestTablePresenter.golden +++ b/xeol/presenter/table/test-fixtures/snapshot/TestTablePresenter.golden @@ -1,3 +1,3 @@ -NAME VERSION EOL DAYS EOL METHOD -package-1 1.1.1 2018-07-31 1614 rpm -package-2 2.2.2 YES - deb +NAME VERSION EOL DAYS EOL TYPE +package-1 1.1.1 2018-07-31 1614 rpm +package-2 2.2.2 YES - deb diff --git a/xeol/search/purl.go b/xeol/search/purl.go index 60a1dafd..b85c8a74 100644 --- a/xeol/search/purl.go +++ b/xeol/search/purl.go @@ -5,21 +5,40 @@ import ( "time" "github.com/Masterminds/semver" + "github.com/anchore/syft/syft/linux" "github.com/noqcks/xeol/internal/log" - "github.com/noqcks/xeol/internal/purl" "github.com/noqcks/xeol/xeol/eol" "github.com/noqcks/xeol/xeol/match" "github.com/noqcks/xeol/xeol/pkg" ) func ByPackagePURL(store eol.Provider, p pkg.Package, upstreamMatcher match.MatcherType, eolMatchDate time.Time) (match.Match, error) { - shortPurl, err := purl.ShortPurl(p) + cycles, err := store.GetByPackagePurl(p) if err != nil { return match.Match{}, err } + if len(cycles) < 1 { + return match.Match{}, nil + } + + cycle, err := cycleMatch(p.Version, cycles, eolMatchDate) + if err != nil { + log.Warnf("failed to match cycle for package %s: %v", p, err) + return match.Match{}, nil + } + + if (cycle != eol.Cycle{}) { + return match.Match{ + Cycle: cycle, + Package: p, + }, nil + } + return match.Match{}, nil +} - cycles, err := store.GetByPurl(p) +func ByDistroCpe(store eol.Provider, distro *linux.Release, eolMatchDate time.Time) (match.Match, error) { + version, cycles, err := store.GetByDistroCpe(distro) if err != nil { return match.Match{}, err } @@ -27,7 +46,26 @@ func ByPackagePURL(store eol.Provider, p pkg.Package, upstreamMatcher match.Matc return match.Match{}, nil } - return packageEOLMatch(shortPurl, p, cycles, eolMatchDate) + log.Debugf("matching distro %s with version %s", distro.Name, version) + cycle, err := cycleMatch(version, cycles, eolMatchDate) + if err != nil { + log.Warnf("failed to match cycle for distro %s: %v", distro.Name, err) + return match.Match{}, nil + } + + if (cycle != eol.Cycle{}) { + return match.Match{ + Cycle: cycle, + Package: pkg.Package{ + Name: distro.Name, + Version: version, + Type: "os", + }, + }, nil + } + + log.Warnf("failed to match cycle for distro %s: %v", distro.Name, err) + return match.Match{}, nil } func returnMatchingCycle(version string, cycles []eol.Cycle) (eol.Cycle, error) { @@ -67,37 +105,31 @@ func returnMatchingCycle(version string, cycles []eol.Cycle) (eol.Cycle, error) return eol.Cycle{}, nil } -func packageEOLMatch(shortPurl string, p pkg.Package, cycles []eol.Cycle, eolMatchDate time.Time) (match.Match, error) { - cycle, err := returnMatchingCycle(p.Version, cycles) +func cycleMatch(version string, cycles []eol.Cycle, eolMatchDate time.Time) (eol.Cycle, error) { + cycle, err := returnMatchingCycle(version, cycles) if err != nil { - log.Debugf("error matching cycle for %s: %s", shortPurl, err) - return match.Match{}, err + log.Debugf("error matching cycle for %s: %s", err) + return eol.Cycle{}, err } if cycle == (eol.Cycle{}) { - return match.Match{}, nil + return cycle, nil } // return the cycle if it is boolean EOL if cycle.EolBool { - return match.Match{ - Cycle: cycle, - Package: p, - }, nil + return cycle, nil } // return the cycle if the EOL date is after the match date cycleEolDate, err := time.Parse("2006-01-02", cycle.Eol) if err != nil { - log.Debugf("error parsing cycle eol date '%s' for %s: %s", cycle.Eol, shortPurl, err) - return match.Match{}, err + log.Debugf("error parsing cycle eol date '%s' for %s: %s", cycle.Eol, err) + return eol.Cycle{}, err } if eolMatchDate.After(cycleEolDate) { - return match.Match{ - Cycle: cycle, - Package: p, - }, nil + return cycle, nil } - return match.Match{}, nil + return eol.Cycle{}, nil }