mirror of
https://github.com/anchore/grype
synced 2024-11-14 16:27:15 +00:00
83c6ee23a9
* Update grype-db dependency, add some SLES tests Signed-off-by: Dan Palmer <dan.palmer@anchore.com>
420 lines
13 KiB
Go
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)
|
|
}
|
|
}
|
|
|
|
}
|