grype/test/integration/match_by_image_test.go
Dan Palmer 83c6ee23a9
Update grype-db dependency, add some SLES tests (#413)
* Update grype-db dependency, add some SLES tests

Signed-off-by: Dan Palmer <dan.palmer@anchore.com>
2021-09-14 15:08:32 -04:00

420 lines
13 KiB
Go

package integration
import (
"testing"
"github.com/anchore/grype/grype"
"github.com/anchore/grype/grype/match"
"github.com/anchore/grype/grype/pkg"
"github.com/anchore/grype/grype/vulnerability"
"github.com/anchore/grype/internal"
"github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/syft"
syftPkg "github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
"github.com/sergi/go-diff/diffmatchpatch"
)
func addAlpineMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Catalog, theStore *mockStore, theResult *match.Matches) {
packages := catalog.PackagesByPath("/lib/apk/db/installed")
if len(packages) != 1 {
t.Logf("Alpine Packages: %+v", packages)
t.Fatalf("problem with upstream syft cataloger (alpine)")
}
thePkg := pkg.New(packages[0])
theVuln := theStore.backend["alpine:3.12"][thePkg.Name][0]
vulnObj, err := vulnerability.NewVulnerability(theVuln)
if err != nil {
t.Fatalf("failed to create vuln obj: %+v", err)
}
theResult.Add(thePkg, match.Match{
// note: we are matching on the secdb record, not NVD primarily
Type: match.ExactDirectMatch,
Vulnerability: *vulnObj,
Package: thePkg,
MatchDetails: []match.Details{
{
Confidence: 1.0,
SearchedBy: map[string]interface{}{
"cpe": "cpe:2.3:*:*:libvncserver:0.9.9:*:*:*:*:*:*:*",
},
Found: map[string]interface{}{
"cpes": []string{"cpe:2.3:*:*:libvncserver:0.9.9:*:*:*:*:*:*:*"},
"constraint": "< 0.9.10 (unknown)",
},
Matcher: match.ApkMatcher,
},
},
})
}
func addJavascriptMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Catalog, theStore *mockStore, theResult *match.Matches) {
packages := catalog.PackagesByPath("/javascript/pkg-json/package.json")
if len(packages) != 1 {
t.Logf("Javascript Packages: %+v", packages)
t.Fatalf("problem with upstream syft cataloger (javascript)")
}
thePkg := pkg.New(packages[0])
theVuln := theStore.backend["github:npm"][thePkg.Name][0]
vulnObj, err := vulnerability.NewVulnerability(theVuln)
if err != nil {
t.Fatalf("failed to create vuln obj: %+v", err)
}
theResult.Add(thePkg, match.Match{
Type: match.ExactDirectMatch,
Vulnerability: *vulnObj,
Package: thePkg,
MatchDetails: []match.Details{
{
Confidence: 1.0,
SearchedBy: map[string]interface{}{
"language": "javascript",
},
Found: map[string]interface{}{
"constraint": "< 3.2.1 (unknown)",
},
Matcher: match.JavascriptMatcher,
},
},
})
}
func addPythonMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Catalog, theStore *mockStore, theResult *match.Matches) {
packages := catalog.PackagesByPath("/python/dist-info/METADATA")
if len(packages) != 1 {
for _, p := range packages {
t.Logf("Python Package: %s %+v", p.ID, p)
}
t.Fatalf("problem with upstream syft cataloger (python)")
}
thePkg := pkg.New(packages[0])
theVuln := theStore.backend["github:python"][thePkg.Name][0]
vulnObj, err := vulnerability.NewVulnerability(theVuln)
if err != nil {
t.Fatalf("failed to create vuln obj: %+v", err)
}
theResult.Add(thePkg, match.Match{
Type: match.ExactDirectMatch,
Vulnerability: *vulnObj,
Package: thePkg,
MatchDetails: []match.Details{
{
Confidence: 1.0,
SearchedBy: map[string]interface{}{
"language": "python",
},
Found: map[string]interface{}{
"constraint": "< 2.6.2 (python)",
},
Matcher: match.PythonMatcher,
},
},
})
}
func addRubyMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Catalog, theStore *mockStore, theResult *match.Matches) {
packages := catalog.PackagesByPath("/ruby/specifications/bundler.gemspec")
if len(packages) != 1 {
t.Logf("Ruby Packages: %+v", packages)
t.Fatalf("problem with upstream syft cataloger (ruby)")
}
thePkg := pkg.New(packages[0])
theVuln := theStore.backend["github:gem"][thePkg.Name][0]
vulnObj, err := vulnerability.NewVulnerability(theVuln)
if err != nil {
t.Fatalf("failed to create vuln obj: %+v", err)
}
theResult.Add(thePkg, match.Match{
Type: match.ExactDirectMatch,
Vulnerability: *vulnObj,
Package: thePkg,
MatchDetails: []match.Details{
{
Confidence: 1.0,
SearchedBy: map[string]interface{}{
"language": "ruby",
},
Found: map[string]interface{}{
"constraint": "> 4.0.0, <= 4.1.1 (semver)",
},
Matcher: match.RubyGemMatcher,
},
},
})
}
func addJavaMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Catalog, theStore *mockStore, theResult *match.Matches) {
packages := make([]*syftPkg.Package, 0)
for p := range catalog.Enumerate(syftPkg.JavaPkg) {
packages = append(packages, p)
}
if len(packages) != 2 { // 2, because there's a nested JAR inside the test fixture JAR
t.Logf("Java Packages: %+v", packages)
t.Fatalf("problem with upstream syft cataloger (java)")
}
theSyftPkg := packages[0]
groupId := theSyftPkg.Metadata.(syftPkg.JavaMetadata).PomProperties.GroupID
lookup := groupId + ":" + theSyftPkg.Name
thePkg := pkg.New(theSyftPkg)
theVuln := theStore.backend["github:java"][lookup][0]
vulnObj, err := vulnerability.NewVulnerability(theVuln)
if err != nil {
t.Fatalf("failed to create vuln obj: %+v", err)
}
theResult.Add(thePkg, match.Match{
Type: match.ExactDirectMatch,
Vulnerability: *vulnObj,
Package: thePkg,
MatchDetails: []match.Details{
{
Confidence: 1.0,
SearchedBy: map[string]interface{}{
"language": "java",
},
Found: map[string]interface{}{
"constraint": ">= 0.0.1, < 1.2.0 (unknown)",
},
Matcher: match.JavaMatcher,
},
},
})
}
func addDpkgMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Catalog, theStore *mockStore, theResult *match.Matches) {
packages := catalog.PackagesByPath("/var/lib/dpkg/status")
if len(packages) != 1 {
t.Logf("Dpkg Packages: %+v", packages)
t.Fatalf("problem with upstream syft cataloger (dpkg)")
}
thePkg := pkg.New(packages[0])
// NOTE: this is an indirect match, in typical debian style
theVuln := theStore.backend["debian:8"][thePkg.Name+"-dev"][0]
vulnObj, err := vulnerability.NewVulnerability(theVuln)
if err != nil {
t.Fatalf("failed to create vuln obj: %+v", err)
}
theResult.Add(thePkg, match.Match{
Type: match.ExactIndirectMatch,
Vulnerability: *vulnObj,
Package: thePkg,
MatchDetails: []match.Details{
{
Confidence: 1.0,
SearchedBy: map[string]interface{}{
"distro": map[string]string{
"type": "debian",
"version": "8",
},
},
Found: map[string]interface{}{
"constraint": "<= 1.8.2 (deb)",
},
Matcher: match.DpkgMatcher,
},
},
})
}
func addRhelMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Catalog, theStore *mockStore, theResult *match.Matches) {
packages := catalog.PackagesByPath("/var/lib/rpm/Packages")
if len(packages) != 1 {
t.Logf("RPMDB Packages: %+v", packages)
t.Fatalf("problem with upstream syft cataloger (RPMDB)")
}
thePkg := pkg.New(packages[0])
theVuln := theStore.backend["rhel:8"][thePkg.Name][0]
vulnObj, err := vulnerability.NewVulnerability(theVuln)
if err != nil {
t.Fatalf("failed to create vuln obj: %+v", err)
}
theResult.Add(thePkg, match.Match{
Type: match.ExactDirectMatch,
Vulnerability: *vulnObj,
Package: thePkg,
MatchDetails: []match.Details{
{
Confidence: 1.0,
SearchedBy: map[string]interface{}{
"distro": map[string]string{
"type": "centos",
"version": "8",
},
},
Found: map[string]interface{}{
"constraint": "<= 1.0.42 (rpm)",
},
Matcher: match.RpmDBMatcher,
},
},
})
}
func addSlesMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Catalog, theStore *mockStore, theResult *match.Matches) {
packages := catalog.PackagesByPath("/var/lib/rpm/Packages")
if len(packages) != 1 {
t.Logf("Sles Packages: %+v", packages)
t.Fatalf("problem with upstream syft cataloger (RPMDB)")
}
thePkg := pkg.New(packages[0])
theVuln := theStore.backend["rhel:8"][thePkg.Name][0]
vulnObj, err := vulnerability.NewVulnerability(theVuln)
if err != nil {
t.Fatalf("failed to create vuln obj: %+v", err)
}
theResult.Add(thePkg, match.Match{
Type: match.ExactDirectMatch,
Vulnerability: *vulnObj,
Package: thePkg,
MatchDetails: []match.Details{
{
Confidence: 1.0,
SearchedBy: map[string]interface{}{
"distro": map[string]string{
"type": "sles",
"version": "12.5",
},
},
Found: map[string]interface{}{
"constraint": "<= 1.0.42 (rpm)",
},
Matcher: match.RpmDBMatcher,
},
},
})
}
func TestMatchByImage(t *testing.T) {
observedMatchers := internal.NewStringSet()
definedMatchers := internal.NewStringSet()
for _, l := range match.AllMatcherTypes {
definedMatchers.Add(l.String())
}
tests := []struct {
fixtureImage string
expectedFn func(source.Source, *syftPkg.Catalog, *mockStore) match.Matches
}{
{
fixtureImage: "image-debian-match-coverage",
expectedFn: func(theSource source.Source, catalog *syftPkg.Catalog, theStore *mockStore) match.Matches {
expectedMatches := match.NewMatches()
addPythonMatches(t, theSource, catalog, theStore, &expectedMatches)
addRubyMatches(t, theSource, catalog, theStore, &expectedMatches)
addJavaMatches(t, theSource, catalog, theStore, &expectedMatches)
addDpkgMatches(t, theSource, catalog, theStore, &expectedMatches)
addJavascriptMatches(t, theSource, catalog, theStore, &expectedMatches)
return expectedMatches
},
},
{
fixtureImage: "image-centos-match-coverage",
expectedFn: func(theSource source.Source, catalog *syftPkg.Catalog, theStore *mockStore) match.Matches {
expectedMatches := match.NewMatches()
addRhelMatches(t, theSource, catalog, theStore, &expectedMatches)
return expectedMatches
},
},
{
fixtureImage: "image-alpine-match-coverage",
expectedFn: func(theSource source.Source, catalog *syftPkg.Catalog, theStore *mockStore) match.Matches {
expectedMatches := match.NewMatches()
addAlpineMatches(t, theSource, catalog, theStore, &expectedMatches)
return expectedMatches
},
},
{
fixtureImage: "image-sles-match-coverage",
expectedFn: func(theSource source.Source, catalog *syftPkg.Catalog, theStore *mockStore) match.Matches {
expectedMatches := match.NewMatches()
addSlesMatches(t, theSource, catalog, theStore, &expectedMatches)
return expectedMatches
},
},
}
for _, test := range tests {
t.Run(test.fixtureImage, func(t *testing.T) {
theStore := newMockDbStore()
imagetest.GetFixtureImage(t, "docker-archive", test.fixtureImage)
tarPath := imagetest.GetFixtureImageTarPath(t, test.fixtureImage)
userImage := "docker-archive:" + tarPath
// this is purely done to help setup mocks
theSource, cleanup, err := source.New(userImage, nil)
if err != nil {
t.Fatalf("failed to determine image source: %+v", err)
}
defer cleanup()
theCatalog, theDistro, err := syft.CatalogPackages(theSource, source.SquashedScope)
if err != nil {
t.Fatalf("could not get the source obj: %+v", err)
}
actualResults := grype.FindVulnerabilitiesForPackage(
vulnerability.NewProviderFromStore(theStore),
theDistro,
pkg.FromCatalog(theCatalog)...,
)
// build expected matches from what's discovered from the catalog
expectedMatches := test.expectedFn(*theSource, theCatalog, theStore)
// build expected match set...
expectedMatchSet := map[string]string{}
for eMatch := range expectedMatches.Enumerate() {
// NOTE: this does not include all fields...
expectedMatchSet[eMatch.Package.Name] = eMatch.String()
}
expectedCount := len(expectedMatchSet)
// ensure that all matches are covered
actualCount := 0
for aMatch := range actualResults.Enumerate() {
actualCount++
for _, details := range aMatch.MatchDetails {
observedMatchers.Add(details.Matcher.String())
}
value, ok := expectedMatchSet[aMatch.Package.Name]
if !ok {
t.Errorf("Package: %s was expected but not found", aMatch.Package.Name)
}
if value != aMatch.String() {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(value, aMatch.String(), true)
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
}
}
if expectedCount != actualCount {
t.Errorf("expected %d matches but got %d matches", expectedCount, actualCount)
}
})
}
// ensure that integration test cases stay in sync with the implemented matchers
observedMatchers.Remove(match.UnknownMatcherType.String())
definedMatchers.Remove(match.UnknownMatcherType.String())
definedMatchers.Remove(match.MsrcMatcher.String())
if len(observedMatchers) != len(definedMatchers) {
t.Errorf("matcher coverage incomplete (matchers=%d, coverage=%d)", len(definedMatchers), len(observedMatchers))
for _, m := range definedMatchers.ToSlice() {
t.Logf(" defined: %+v\n", m)
}
for _, m := range observedMatchers.ToSlice() {
t.Logf(" found: %+v\n", m)
}
}
}