grype/test/integration/match_by_image_test.go
2024-02-27 20:47:51 -05:00

863 lines
26 KiB
Go

package integration
import (
"context"
"sort"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/stretchr/testify/require"
"github.com/anchore/grype/grype"
"github.com/anchore/grype/grype/db"
"github.com/anchore/grype/grype/match"
"github.com/anchore/grype/grype/matcher"
"github.com/anchore/grype/grype/pkg"
"github.com/anchore/grype/grype/store"
"github.com/anchore/grype/grype/vex"
"github.com/anchore/grype/grype/vulnerability"
"github.com/anchore/grype/internal/stringutil"
"github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/cataloging/pkgcataloging"
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/linux"
syftPkg "github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
func addAlpineMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore, theResult *match.Matches) {
packages := catalog.PackagesByPath("/lib/apk/db/installed")
if len(packages) != 3 {
t.Logf("Alpine Packages: %+v", packages)
t.Fatalf("problem with upstream syft cataloger (alpine)")
}
thePkg := pkg.New(packages[0])
theVuln := theStore.backend["alpine:distro:alpine:3.12"][thePkg.Name][0]
vulnObj, err := vulnerability.NewVulnerability(theVuln)
require.NoError(t, err)
theResult.Add(match.Match{
// note: we are matching on the secdb record, not NVD primarily
Vulnerability: *vulnObj,
Package: thePkg,
Details: []match.Detail{
{
// note: the input pURL has an upstream reference (redundant)
Type: "exact-indirect-match",
SearchedBy: map[string]any{
"distro": map[string]string{
"type": "alpine",
"version": "3.12.0",
},
"namespace": "alpine:distro:alpine:3.12",
"package": map[string]string{
"name": "libvncserver",
"version": "0.9.9",
},
},
Found: map[string]any{
"versionConstraint": "< 0.9.10 (unknown)",
"vulnerabilityID": "CVE-alpine-libvncserver",
},
Matcher: "apk-matcher",
Confidence: 1,
},
{
Type: match.ExactDirectMatch,
Confidence: 1.0,
SearchedBy: map[string]interface{}{
"distro": map[string]string{
"type": "alpine",
"version": "3.12.0",
},
"namespace": "alpine:distro:alpine:3.12",
"package": map[string]string{
"name": "libvncserver",
"version": "0.9.9",
},
},
Found: map[string]interface{}{
"versionConstraint": "< 0.9.10 (unknown)",
"vulnerabilityID": vulnObj.ID,
},
Matcher: match.ApkMatcher,
},
},
})
}
func addJavascriptMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, 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:language:javascript"][thePkg.Name][0]
vulnObj, err := vulnerability.NewVulnerability(theVuln)
require.NoError(t, err)
theResult.Add(match.Match{
Vulnerability: *vulnObj,
Package: thePkg,
Details: []match.Detail{
{
Type: match.ExactDirectMatch,
Confidence: 1.0,
SearchedBy: map[string]interface{}{
"language": "javascript",
"namespace": "github:language:javascript",
"package": map[string]string{
"name": thePkg.Name,
"version": thePkg.Version,
},
},
Found: map[string]interface{}{
"versionConstraint": "> 5, < 7.2.1 (unknown)",
"vulnerabilityID": vulnObj.ID,
},
Matcher: match.JavascriptMatcher,
},
},
})
}
func addPythonMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, 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])
normalizedName := theStore.normalizedPackageNames["github:language:python"][thePkg.Name]
theVuln := theStore.backend["github:language:python"][normalizedName][0]
vulnObj, err := vulnerability.NewVulnerability(theVuln)
require.NoError(t, err)
theResult.Add(match.Match{
Vulnerability: *vulnObj,
Package: thePkg,
Details: []match.Detail{
{
Type: match.ExactDirectMatch,
Confidence: 1.0,
SearchedBy: map[string]interface{}{
"language": "python",
"namespace": "github:language:python",
"package": map[string]string{
"name": thePkg.Name,
"version": thePkg.Version,
},
},
Found: map[string]interface{}{
"versionConstraint": "< 2.6.2 (python)",
"vulnerabilityID": vulnObj.ID,
},
Matcher: match.PythonMatcher,
},
},
})
}
func addDotnetMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore, theResult *match.Matches) {
packages := catalog.PackagesByPath("/dotnet/TestLibrary.deps.json")
if len(packages) != 2 { // TestLibrary + AWSSDK.Core
for _, p := range packages {
t.Logf("Dotnet Package: %s %+v", p.ID(), p)
}
t.Fatalf("problem with upstream syft cataloger (dotnet)")
}
thePkg := pkg.New(packages[1])
normalizedName := theStore.normalizedPackageNames["github:language:dotnet"][thePkg.Name]
theVuln := theStore.backend["github:language:dotnet"][normalizedName][0]
vulnObj, err := vulnerability.NewVulnerability(theVuln)
require.NoError(t, err)
theResult.Add(match.Match{
Vulnerability: *vulnObj,
Package: thePkg,
Details: []match.Detail{
{
Type: match.ExactDirectMatch,
Confidence: 1.0,
SearchedBy: map[string]interface{}{
"language": "dotnet",
"namespace": "github:language:dotnet",
"package": map[string]string{
"name": thePkg.Name,
"version": thePkg.Version,
},
},
Found: map[string]interface{}{
"versionConstraint": ">= 3.7.0.0, < 3.7.12.0 (unknown)",
"vulnerabilityID": vulnObj.ID,
},
Matcher: match.DotnetMatcher,
},
},
})
}
func addRubyMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, 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:language:ruby"][thePkg.Name][0]
vulnObj, err := vulnerability.NewVulnerability(theVuln)
require.NoError(t, err)
theResult.Add(match.Match{
Vulnerability: *vulnObj,
Package: thePkg,
Details: []match.Detail{
{
Type: match.ExactDirectMatch,
Confidence: 1.0,
SearchedBy: map[string]interface{}{
"language": "ruby",
"namespace": "github:language:ruby",
"package": map[string]string{
"name": thePkg.Name,
"version": thePkg.Version,
},
},
Found: map[string]interface{}{
"versionConstraint": "> 2.0.0, <= 2.1.4 (unknown)",
"vulnerabilityID": vulnObj.ID,
},
Matcher: match.RubyGemMatcher,
},
},
})
}
func addGolangMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore, theResult *match.Matches) {
modPackages := catalog.PackagesByPath("/golang/go.mod")
if len(modPackages) != 1 {
t.Logf("Golang Mod Packages: %+v", modPackages)
t.Fatalf("problem with upstream syft cataloger (golang)")
}
binPackages := catalog.PackagesByPath("/go-app")
// contains 2 package + a single stdlib package
if len(binPackages) != 3 {
t.Logf("Golang Bin Packages: %+v", binPackages)
t.Fatalf("problem with upstream syft cataloger (golang)")
}
var packages []syftPkg.Package
packages = append(packages, modPackages...)
packages = append(packages, binPackages...)
for _, p := range packages {
// no vuln match supported for main module
if p.Name == "github.com/anchore/coverage" {
continue
}
if p.Name == "stdlib" {
continue
}
thePkg := pkg.New(p)
theVuln := theStore.backend["github:language:go"][thePkg.Name][0]
vulnObj, err := vulnerability.NewVulnerability(theVuln)
require.NoError(t, err)
theResult.Add(match.Match{
Vulnerability: *vulnObj,
Package: thePkg,
Details: []match.Detail{
{
Type: match.ExactDirectMatch,
Confidence: 1.0,
SearchedBy: map[string]interface{}{
"language": "go",
"namespace": "github:language:go",
"package": map[string]string{
"name": thePkg.Name,
"version": thePkg.Version,
},
},
Found: map[string]interface{}{
"versionConstraint": "< 1.4.0 (unknown)",
"vulnerabilityID": vulnObj.ID,
},
Matcher: match.GoModuleMatcher,
},
},
})
}
}
func addJavaMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, 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.JavaArchive).PomProperties.GroupID
lookup := groupId + ":" + theSyftPkg.Name
thePkg := pkg.New(theSyftPkg)
theVuln := theStore.backend["github:language:java"][lookup][0]
vulnObj, err := vulnerability.NewVulnerability(theVuln)
require.NoError(t, err)
theResult.Add(match.Match{
Vulnerability: *vulnObj,
Package: thePkg,
Details: []match.Detail{
{
Type: match.ExactDirectMatch,
Confidence: 1.0,
SearchedBy: map[string]interface{}{
"language": "java",
"namespace": "github:language:java",
"package": map[string]string{
"name": thePkg.Name,
"version": thePkg.Version,
},
},
Found: map[string]interface{}{
"versionConstraint": ">= 0.0.1, < 1.2.0 (unknown)",
"vulnerabilityID": vulnObj.ID,
},
Matcher: match.JavaMatcher,
},
},
})
}
func addDpkgMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, 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:distro:debian:8"][thePkg.Name+"-dev"][0]
vulnObj, err := vulnerability.NewVulnerability(theVuln)
require.NoError(t, err)
theResult.Add(match.Match{
Vulnerability: *vulnObj,
Package: thePkg,
Details: []match.Detail{
{
Type: match.ExactIndirectMatch,
Confidence: 1.0,
SearchedBy: map[string]interface{}{
"distro": map[string]string{
"type": "debian",
"version": "8",
},
"namespace": "debian:distro:debian:8",
"package": map[string]string{
"name": "apt-dev",
"version": "1.8.2",
},
},
Found: map[string]interface{}{
"versionConstraint": "<= 1.8.2 (deb)",
"vulnerabilityID": vulnObj.ID,
},
Matcher: match.DpkgMatcher,
},
},
})
}
func addPortageMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore, theResult *match.Matches) {
packages := catalog.PackagesByPath("/var/db/pkg/app-containers/skopeo-1.5.1/CONTENTS")
if len(packages) != 1 {
t.Logf("Portage Packages: %+v", packages)
t.Fatalf("problem with upstream syft cataloger (portage)")
}
thePkg := pkg.New(packages[0])
theVuln := theStore.backend["gentoo:distro:gentoo:2.8"][thePkg.Name][0]
vulnObj, err := vulnerability.NewVulnerability(theVuln)
require.NoError(t, err)
theResult.Add(match.Match{
Vulnerability: *vulnObj,
Package: thePkg,
Details: []match.Detail{
{
Type: match.ExactDirectMatch,
Confidence: 1.0,
SearchedBy: map[string]interface{}{
"distro": map[string]string{
"type": "gentoo",
"version": "2.8",
},
"namespace": "gentoo:distro:gentoo:2.8",
"package": map[string]string{
"name": "app-containers/skopeo",
"version": "1.5.1",
},
},
Found: map[string]interface{}{
"versionConstraint": "< 1.6.0 (unknown)",
"vulnerabilityID": vulnObj.ID,
},
Matcher: match.PortageMatcher,
},
},
})
}
func addRhelMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, 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["redhat:distro:redhat:8"][thePkg.Name][0]
vulnObj, err := vulnerability.NewVulnerability(theVuln)
require.NoError(t, err)
theResult.Add(match.Match{
Vulnerability: *vulnObj,
Package: thePkg,
Details: []match.Detail{
{
Type: match.ExactDirectMatch,
Confidence: 1.0,
SearchedBy: map[string]interface{}{
"distro": map[string]string{
"type": "centos",
"version": "8",
},
"namespace": "redhat:distro:redhat:8",
"package": map[string]string{
"name": "dive",
"version": "0:0.9.2-1",
},
},
Found: map[string]interface{}{
"versionConstraint": "<= 1.0.42 (rpm)",
"vulnerabilityID": vulnObj.ID,
},
Matcher: match.RpmMatcher,
},
},
})
}
func addSlesMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, 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["redhat:distro:redhat:8"][thePkg.Name][0]
vulnObj, err := vulnerability.NewVulnerability(theVuln)
require.NoError(t, err)
vulnObj.Namespace = "sles:distro:sles:12.5"
theResult.Add(match.Match{
Vulnerability: *vulnObj,
Package: thePkg,
Details: []match.Detail{
{
Type: match.ExactDirectMatch,
Confidence: 1.0,
SearchedBy: map[string]interface{}{
"distro": map[string]string{
"type": "sles",
"version": "12.5",
},
"namespace": "sles:distro:sles:12.5",
"package": map[string]string{
"name": "dive",
"version": "0:0.9.2-1",
},
},
Found: map[string]interface{}{
"versionConstraint": "<= 1.0.42 (rpm)",
"vulnerabilityID": vulnObj.ID,
},
Matcher: match.RpmMatcher,
},
},
})
}
func addHaskellMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore, theResult *match.Matches) {
packages := catalog.PackagesByPath("/haskell/stack.yaml")
if len(packages) < 1 {
t.Logf("Haskel Packages: %+v", packages)
t.Fatalf("problem with upstream syft cataloger (haskell)")
}
thePkg := pkg.New(packages[0])
theVuln := theStore.backend["github:language:haskell"][strings.ToLower(thePkg.Name)][0]
vulnObj, err := vulnerability.NewVulnerability(theVuln)
require.NoError(t, err)
theResult.Add(match.Match{
Vulnerability: *vulnObj,
Package: thePkg,
Details: []match.Detail{
{
Type: match.ExactDirectMatch,
Confidence: 1.0,
SearchedBy: map[string]any{
"language": "haskell",
"namespace": "github:language:haskell",
"package": map[string]string{
"name": thePkg.Name,
"version": thePkg.Version,
},
},
Found: map[string]any{
"versionConstraint": "< 0.9.0 (unknown)",
"vulnerabilityID": "CVE-haskell-sample",
},
Matcher: match.StockMatcher,
},
},
})
}
func addRustMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore, theResult *match.Matches) {
packages := catalog.PackagesByPath("/hello-auditable")
if len(packages) < 1 {
t.Logf("Rust Packages: %+v", packages)
t.Fatalf("problem with upstream syft cataloger (cargo-auditable-binary-cataloger)")
}
for _, p := range packages {
thePkg := pkg.New(p)
theVuln := theStore.backend["github:language:rust"][strings.ToLower(thePkg.Name)][0]
vulnObj, err := vulnerability.NewVulnerability(theVuln)
require.NoError(t, err)
theResult.Add(match.Match{
Vulnerability: *vulnObj,
Package: thePkg,
Details: []match.Detail{
{
Type: match.ExactDirectMatch,
Confidence: 1.0,
SearchedBy: map[string]any{
"language": "rust",
"namespace": "github:language:rust",
"package": map[string]string{
"name": thePkg.Name,
"version": thePkg.Version,
},
},
Found: map[string]any{
"versionConstraint": vulnObj.Constraint.String(),
"vulnerabilityID": vulnObj.ID,
},
Matcher: match.RustMatcher,
},
},
})
}
}
func TestMatchByImage(t *testing.T) {
observedMatchers := stringutil.NewStringSet()
definedMatchers := stringutil.NewStringSet()
for _, l := range match.AllMatcherTypes {
definedMatchers.Add(string(l))
}
tests := []struct {
fixtureImage string
expectedFn func(source.Source, *syftPkg.Collection, *mockStore) match.Matches
}{
{
fixtureImage: "image-debian-match-coverage",
expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, 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)
addDotnetMatches(t, theSource, catalog, theStore, &expectedMatches)
addGolangMatches(t, theSource, catalog, theStore, &expectedMatches)
addHaskellMatches(t, theSource, catalog, theStore, &expectedMatches)
return expectedMatches
},
},
{
fixtureImage: "image-centos-match-coverage",
expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, 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.Collection, 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.Collection, theStore *mockStore) match.Matches {
expectedMatches := match.NewMatches()
addSlesMatches(t, theSource, catalog, theStore, &expectedMatches)
return expectedMatches
},
},
{
fixtureImage: "image-portage-match-coverage",
expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore) match.Matches {
expectedMatches := match.NewMatches()
addPortageMatches(t, theSource, catalog, theStore, &expectedMatches)
return expectedMatches
},
},
{
fixtureImage: "image-rust-auditable-match-coverage",
expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, theStore *mockStore) match.Matches {
expectedMatches := match.NewMatches()
addRustMatches(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)
// this is purely done to help setup mocks
theSource, err := syft.GetSource(context.Background(), tarPath, syft.DefaultGetSourceConfig().WithSources("docker-archive"))
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, theSource.Close())
})
// TODO: relationships are not verified at this time
// enable all catalogers to cover non default cases
config := syft.DefaultCreateSBOMConfig().WithCatalogerSelection(pkgcataloging.NewSelectionRequest().WithDefaults("all"))
config.Search.Scope = source.SquashedScope
s, err := syft.CreateSBOM(context.Background(), theSource, config)
require.NoError(t, err)
require.NotNil(t, s)
matchers := matcher.NewDefaultMatchers(matcher.Config{})
vp, err := db.NewVulnerabilityProvider(theStore)
require.NoError(t, err)
mp := db.NewVulnerabilityMetadataProvider(theStore)
ep := db.NewMatchExclusionProvider(theStore)
str := store.Store{
Provider: vp,
MetadataProvider: mp,
ExclusionProvider: ep,
}
actualResults := grype.FindVulnerabilitiesForPackage(str, s.Artifacts.LinuxDistribution, matchers, pkg.FromCollection(s.Artifacts.Packages, pkg.SynthesisConfig{}))
for _, m := range actualResults.Sorted() {
for _, d := range m.Details {
observedMatchers.Add(string(d.Matcher))
}
}
// build expected matches from what's discovered from the catalog
expectedMatches := test.expectedFn(theSource, s.Artifacts.Packages, theStore)
assertMatches(t, expectedMatches.Sorted(), actualResults.Sorted())
})
}
// Test that VEX matchers produce matches when fed documents with "affected"
// statuses.
for n, tc := range map[string]struct {
vexStatus vex.Status
vexDocuments []string
}{
"openvex-affected": {vex.StatusAffected, []string{"test-fixtures/vex/openvex/affected.openvex.json"}},
"openvex-under_investigation": {vex.StatusUnderInvestigation, []string{"test-fixtures/vex/openvex/under_investigation.openvex.json"}},
} {
t.Run(n, func(t *testing.T) {
ignoredMatches := testIgnoredMatches()
vexedResults := vexMatches(t, ignoredMatches, tc.vexStatus, tc.vexDocuments)
if len(vexedResults.Sorted()) != 1 {
t.Errorf("expected one vexed result, got none")
}
expectedMatches := match.NewMatches()
// The single match in the actual results is the same in ignoredMatched
// but must the details of the VEX matcher appended
result := vexedResults.Sorted()[0]
if len(result.Details) != len(ignoredMatches[0].Match.Details)+1 {
t.Errorf(
"Details in VEXed results don't match (expected %d, got %d)",
len(ignoredMatches[0].Match.Details)+1, len(result.Details),
)
}
result.Details = result.Details[:len(result.Details)-1]
actualResults := match.NewMatches()
actualResults.Add(result)
expectedMatches.Add(ignoredMatches[0].Match)
assertMatches(t, expectedMatches.Sorted(), actualResults.Sorted())
for _, m := range vexedResults.Sorted() {
for _, d := range m.Details {
observedMatchers.Add(string(d.Matcher))
}
}
})
}
// ensure that integration test cases stay in sync with the implemented matchers
observedMatchers.Remove(string(match.StockMatcher))
definedMatchers.Remove(string(match.StockMatcher))
definedMatchers.Remove(string(match.MsrcMatcher))
if len(observedMatchers) != len(definedMatchers) {
t.Errorf("matcher coverage incomplete (matchers=%d, coverage=%d)", len(definedMatchers), len(observedMatchers))
defs := definedMatchers.ToSlice()
sort.Strings(defs)
obs := observedMatchers.ToSlice()
sort.Strings(obs)
t.Log(cmp.Diff(defs, obs))
}
}
// testIgnoredMatches returns an list of ignored matches to test the vex
// matchers
func testIgnoredMatches() []match.IgnoredMatch {
return []match.IgnoredMatch{
{
Match: match.Match{
Vulnerability: vulnerability.Vulnerability{
ID: "CVE-alpine-libvncserver",
Namespace: "alpine:distro:alpine:3.12",
},
Package: pkg.Package{
ID: "44fa3691ae360cac",
Name: "libvncserver",
Version: "0.9.9",
Licenses: []string{"GPL-2.0-or-later"},
Type: "apk",
CPEs: []cpe.CPE{
{
Attributes: cpe.Attributes{
Part: "a",
Vendor: "libvncserver",
Product: "libvncserver",
Version: "0.9.9",
},
},
},
PURL: "pkg:apk/alpine/libvncserver@0.9.9?arch=x86_64&distro=alpine-3.12.0",
Upstreams: []pkg.UpstreamPackage{{Name: "libvncserver"}},
},
Details: []match.Detail{
{
Type: "exact-indirect-match",
SearchedBy: map[string]any{
"distro": map[string]string{
"type": "alpine",
"version": "3.12.0",
},
"namespace": "alpine:distro:alpine:3.12",
"package": map[string]string{
"name": "libvncserver",
"version": "0.9.9",
},
},
Found: map[string]any{
"versionConstraint": "< 0.9.10 (unknown)",
"vulnerabilityID": "CVE-alpine-libvncserver",
},
Matcher: "apk-matcher",
Confidence: 1,
},
},
},
AppliedIgnoreRules: []match.IgnoreRule{},
},
}
}
// vexMatches moves the first match of a matches list to an ignore list and
// applies a VEX "affected" document to it to move it to the matches list.
func vexMatches(t *testing.T, ignoredMatches []match.IgnoredMatch, vexStatus vex.Status, vexDocuments []string) match.Matches {
matches := match.NewMatches()
vexMatcher := vex.NewProcessor(vex.ProcessorOptions{
Documents: vexDocuments,
IgnoreRules: []match.IgnoreRule{
{VexStatus: string(vexStatus)},
},
})
pctx := &pkg.Context{
Source: &source.Description{
Metadata: source.ImageMetadata{
RepoDigests: []string{
"alpine@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
},
},
},
Distro: &linux.Release{},
}
vexedMatches, ignoredMatches, err := vexMatcher.ApplyVEX(pctx, &matches, ignoredMatches)
if err != nil {
t.Errorf("applying VEX data: %s", err)
}
if len(ignoredMatches) != 0 {
t.Errorf("VEX text fixture %s must affect all ignored matches (%d left)", vexDocuments, len(ignoredMatches))
}
return *vexedMatches
}
func assertMatches(t *testing.T, expected, actual []match.Match) {
t.Helper()
opts := []cmp.Option{
cmpopts.IgnoreFields(vulnerability.Vulnerability{}, "Constraint"),
cmpopts.IgnoreFields(pkg.Package{}, "Locations"),
cmpopts.SortSlices(func(a, b match.Match) bool {
return a.Package.ID < b.Package.ID
}),
}
if diff := cmp.Diff(expected, actual, opts...); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
}