package grype

import (
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
	"github.com/google/uuid"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/wagoodman/go-partybus"

	v5 "github.com/anchore/grype/grype/db/v5"
	"github.com/anchore/grype/grype/db/v5/matcher"
	"github.com/anchore/grype/grype/db/v5/matcher/ruby"
	"github.com/anchore/grype/grype/db/v5/search"
	"github.com/anchore/grype/grype/distro"
	"github.com/anchore/grype/grype/event"
	"github.com/anchore/grype/grype/event/monitor"
	"github.com/anchore/grype/grype/grypeerr"
	"github.com/anchore/grype/grype/match"
	"github.com/anchore/grype/grype/pkg"
	"github.com/anchore/grype/grype/pkg/qualifier"
	"github.com/anchore/grype/grype/version"
	"github.com/anchore/grype/grype/vex"
	"github.com/anchore/grype/grype/vulnerability"
	"github.com/anchore/grype/internal/bus"
	"github.com/anchore/syft/syft/cpe"
	"github.com/anchore/syft/syft/file"
	"github.com/anchore/syft/syft/linux"
	syftPkg "github.com/anchore/syft/syft/pkg"
	"github.com/anchore/syft/syft/source"
)

type ack interface {
	v5.VulnerabilityStoreReader
	v5.VulnerabilityMetadataStoreReader
	v5.VulnerabilityMatchExclusionStoreReader
}

var _ ack = (*mockStore)(nil)

type mockStore struct {
	vulnerabilities map[string]map[string][]v5.Vulnerability
	metadata        map[string]map[string]*v5.VulnerabilityMetadata
}

func (d *mockStore) GetVulnerabilityMatchExclusion(id string) ([]v5.VulnerabilityMatchExclusion, error) {
	// panic("implement me")
	return nil, nil
}

// A mockStoreStubFn takes a reference to a mockStore and mutates it to contain
// a set of prescribed test data.
type mockStoreStubFn func(*mockStore)

// newMockStore returns a new mock implementation of a Grype database store. If
// the stubFn parameter is not set to nil, the given stub function will be used
// to modify the data in the mock store, such as in preparation for tests.
func newMockStore(stubFn mockStoreStubFn) *mockStore {
	d := &mockStore{
		vulnerabilities: make(map[string]map[string][]v5.Vulnerability),
		metadata:        make(map[string]map[string]*v5.VulnerabilityMetadata),
	}
	if stubFn != nil {
		stubFn(d)
	}
	return d
}

func (d *mockStore) GetVulnerabilityMetadata(id, namespace string) (*v5.VulnerabilityMetadata, error) {
	return d.metadata[id][namespace], nil
}

func (d *mockStore) GetAllVulnerabilityMetadata() (*[]v5.VulnerabilityMetadata, error) {
	panic("implement me")
}

func (d *mockStore) GetVulnerability(namespace, id string) ([]v5.Vulnerability, error) {
	var results []v5.Vulnerability
	for _, vulns := range d.vulnerabilities[namespace] {
		for _, vuln := range vulns {
			if vuln.ID == id {
				results = append(results, vuln)
			}
		}
	}
	return results, nil
}

func (d *mockStore) SearchForVulnerabilities(namespace, name string) ([]v5.Vulnerability, error) {
	return d.vulnerabilities[namespace][name], nil
}

func (d *mockStore) GetAllVulnerabilities() (*[]v5.Vulnerability, error) {
	panic("implement me")
}

func (d *mockStore) GetVulnerabilityNamespaces() ([]string, error) {
	keys := make([]string, 0, len(d.vulnerabilities))
	for k := range d.vulnerabilities {
		keys = append(keys, k)
	}

	return keys, nil
}

func defaultStubFn(d *mockStore) {
	// METADATA /////////////////////////////////////////////////////////////////////////////////
	d.metadata["CVE-2014-fake-1"] = map[string]*v5.VulnerabilityMetadata{
		"debian:distro:debian:8": {
			ID:        "CVE-2014-fake-1",
			Namespace: "debian:distro:debian:8",
			Severity:  "medium",
		},
	}

	d.metadata["GHSA-2014-fake-3"] = map[string]*v5.VulnerabilityMetadata{
		"github:language:ruby": {
			ID:        "GHSA-2014-fake-3",
			Namespace: "github:language:ruby",
			Severity:  "medium",
		},
	}

	d.metadata["CVE-2014-fake-3"] = map[string]*v5.VulnerabilityMetadata{
		"nvd:cpe": {
			ID:        "CVE-2014-fake-3",
			Namespace: "nvd:cpe",
			Severity:  "critical",
		},
	}

	// VULNERABILITIES ///////////////////////////////////////////////////////////////////////////
	d.vulnerabilities["debian:distro:debian:8"] = map[string][]v5.Vulnerability{
		"neutron": {
			{
				PackageName:       "neutron",
				Namespace:         "debian:distro:debian:8",
				VersionConstraint: "< 2014.1.3-6",
				ID:                "CVE-2014-fake-1",
				VersionFormat:     "deb",
			},
			{
				PackageName:       "neutron",
				Namespace:         "debian:distro:debian:8",
				VersionConstraint: "< 2013.0.2-1",
				ID:                "CVE-2013-fake-2",
				VersionFormat:     "deb",
			},
		},
	}
	d.vulnerabilities["github:language:ruby"] = map[string][]v5.Vulnerability{
		"activerecord": {
			{
				PackageName:       "activerecord",
				Namespace:         "github:language:ruby",
				VersionConstraint: "< 3.7.6",
				ID:                "GHSA-2014-fake-3",
				VersionFormat:     "unknown",
				RelatedVulnerabilities: []v5.VulnerabilityReference{
					{
						ID:        "CVE-2014-fake-3",
						Namespace: "nvd:cpe",
					},
				},
			},
		},
	}
	d.vulnerabilities["nvd:cpe"] = map[string][]v5.Vulnerability{
		"activerecord": {
			{
				PackageName:       "activerecord",
				Namespace:         "nvd:cpe",
				VersionConstraint: "< 3.7.6",
				ID:                "CVE-2014-fake-3",
				VersionFormat:     "unknown",
				CPEs: []string{
					"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*",
				},
			},
			{
				PackageName:       "activerecord",
				Namespace:         "nvd:cpe",
				VersionConstraint: "< 3.7.4",
				ID:                "CVE-2014-fake-4",
				VersionFormat:     "unknown",
				CPEs: []string{
					"cpe:2.3:*:activerecord:activerecord:*:*:something:*:*:ruby:*:*",
				},
			},
			{
				PackageName:       "activerecord",
				Namespace:         "nvd:cpe",
				VersionConstraint: "= 4.0.1",
				ID:                "CVE-2014-fake-5",
				VersionFormat:     "unknown",
				CPEs: []string{
					"cpe:2.3:*:couldntgetthisrightcouldyou:activerecord:4.0.1:*:*:*:*:*:*:*",
				},
			},
			{
				PackageName:       "activerecord",
				Namespace:         "nvd:cpe",
				VersionConstraint: "< 98SP3",
				ID:                "CVE-2014-fake-6",
				VersionFormat:     "unknown",
				CPEs: []string{
					"cpe:2.3:*:awesome:awesome:*:*:*:*:*:*:*:*",
				},
			},
		},
	}
}

func Test_HasSeverityAtOrAbove(t *testing.T) {
	thePkg := pkg.Package{
		ID:      pkg.ID(uuid.NewString()),
		Name:    "the-package",
		Version: "v0.1",
		Type:    syftPkg.RpmPkg,
	}

	matches := match.NewMatches()
	matches.Add(match.Match{
		Vulnerability: vulnerability.Vulnerability{
			Reference: vulnerability.Reference{
				ID:        "CVE-2014-fake-1",
				Namespace: "debian:distro:debian:8",
			},
		},
		Package: thePkg,
		Details: match.Details{
			{
				Type: match.ExactDirectMatch,
			},
		},
	})

	tests := []struct {
		name           string
		failOnSeverity string
		matches        match.Matches
		expectedResult bool
	}{
		{
			name:           "no-severity-set",
			failOnSeverity: "",
			matches:        matches,
			expectedResult: false,
		},
		{
			name:           "below-threshold",
			failOnSeverity: "high",
			matches:        matches,
			expectedResult: false,
		},
		{
			name:           "at-threshold",
			failOnSeverity: "medium",
			matches:        matches,
			expectedResult: true,
		},
		{
			name:           "above-threshold",
			failOnSeverity: "low",
			matches:        matches,
			expectedResult: true,
		},
	}

	metadataProvider := v5.NewVulnerabilityMetadataProvider(newMockStore(defaultStubFn))

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			var failOnSeverity vulnerability.Severity
			if test.failOnSeverity != "" {
				sev := vulnerability.ParseSeverity(test.failOnSeverity)
				if sev == vulnerability.UnknownSeverity {
					t.Fatalf("could not parse severity")
				}
				failOnSeverity = sev
			}

			actual := HasSeverityAtOrAbove(metadataProvider, failOnSeverity, test.matches)

			if test.expectedResult != actual {
				t.Errorf("expected: %v got : %v", test.expectedResult, actual)
			}
		})
	}
}

func TestVulnerabilityMatcher_FindMatches(t *testing.T) {
	mkStr := newMockStore(defaultStubFn)
	vp, err := v5.NewVulnerabilityProvider(mkStr)
	require.NoError(t, err)
	str := v5.ProviderStore{
		VulnerabilityProvider:         vp,
		VulnerabilityMetadataProvider: v5.NewVulnerabilityMetadataProvider(mkStr),
		ExclusionProvider:             v5.NewMatchExclusionProvider(mkStr),
	}

	neutron2013Pkg := pkg.Package{
		ID:      pkg.ID(uuid.NewString()),
		Name:    "neutron",
		Version: "2013.1.1-1",
		Type:    syftPkg.DebPkg,
	}

	mustCPE := func(c string) cpe.CPE {
		cp, err := cpe.New(c, "")
		if err != nil {
			t.Fatal(err)
		}
		return cp
	}

	activerecordPkg := pkg.Package{
		ID:      pkg.ID(uuid.NewString()),
		Name:    "activerecord",
		Version: "3.7.5",
		CPEs: []cpe.CPE{
			mustCPE("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"),
		},
		Type:     syftPkg.GemPkg,
		Language: syftPkg.Ruby,
	}

	type fields struct {
		Store          v5.ProviderStore
		Matchers       []matcher.Matcher
		IgnoreRules    []match.IgnoreRule
		FailSeverity   *vulnerability.Severity
		NormalizeByCVE bool
		VexProcessor   *vex.Processor
	}
	type args struct {
		pkgs    []pkg.Package
		context pkg.Context
	}

	tests := []struct {
		name               string
		fields             fields
		args               args
		wantMatches        match.Matches
		wantIgnoredMatches []match.IgnoredMatch
		wantErr            error
	}{
		{
			name: "no matches",
			fields: fields{
				Store:    str,
				Matchers: matcher.NewDefaultMatchers(matcher.Config{}),
			},
			args: args{
				pkgs: []pkg.Package{
					{
						ID:      pkg.ID(uuid.NewString()),
						Name:    "neutrino",
						Version: "2099.1.1-1",
						Type:    syftPkg.DebPkg,
					},
				},
				context: pkg.Context{
					Distro: &linux.Release{
						ID:        "debian",
						VersionID: "8",
					},
				},
			},
		},
		{
			name: "matches by exact-direct match (OS)",
			fields: fields{
				Store:    str,
				Matchers: matcher.NewDefaultMatchers(matcher.Config{}),
			},
			args: args{
				pkgs: []pkg.Package{
					neutron2013Pkg,
				},
				context: pkg.Context{
					Distro: &linux.Release{
						ID:        "debian",
						VersionID: "8",
					},
				},
			},
			wantMatches: match.NewMatches(
				match.Match{
					Vulnerability: vulnerability.Vulnerability{
						PackageName: "neutron",
						Constraint:  version.MustGetConstraint("< 2014.1.3-6", version.DebFormat),
						Reference: vulnerability.Reference{
							ID:        "CVE-2014-fake-1",
							Namespace: "debian:distro:debian:8",
						},
						PackageQualifiers: []qualifier.Qualifier{},
						CPEs:              []cpe.CPE{},
						Advisories:        []vulnerability.Advisory{},
					},
					Package: neutron2013Pkg,
					Details: match.Details{
						{
							Type: match.ExactDirectMatch,
							SearchedBy: map[string]any{
								"distro":    map[string]string{"type": "debian", "version": "8"},
								"namespace": "debian:distro:debian:8",
								"package":   map[string]string{"name": "neutron", "version": "2013.1.1-1"},
							},
							Found: map[string]any{
								"versionConstraint": "< 2014.1.3-6 (deb)",
								"vulnerabilityID":   "CVE-2014-fake-1",
							},
							Matcher:    "dpkg-matcher",
							Confidence: 1,
						},
					},
				},
			),
			wantIgnoredMatches: nil,
			wantErr:            nil,
		},
		{
			name: "fail on severity threshold",
			fields: fields{
				Store:    str,
				Matchers: matcher.NewDefaultMatchers(matcher.Config{}),
				FailSeverity: func() *vulnerability.Severity {
					x := vulnerability.LowSeverity
					return &x
				}(),
			},
			args: args{
				pkgs: []pkg.Package{
					neutron2013Pkg,
				},
				context: pkg.Context{
					Distro: &linux.Release{
						ID:        "debian",
						VersionID: "8",
					},
				},
			},
			wantMatches: match.NewMatches(
				match.Match{
					Vulnerability: vulnerability.Vulnerability{
						PackageName: "neutron",
						Constraint:  version.MustGetConstraint("< 2014.1.3-6", version.DebFormat),
						Reference: vulnerability.Reference{
							ID:        "CVE-2014-fake-1",
							Namespace: "debian:distro:debian:8",
						},
						PackageQualifiers: []qualifier.Qualifier{},
						CPEs:              []cpe.CPE{},
						Advisories:        []vulnerability.Advisory{},
					},
					Package: neutron2013Pkg,
					Details: match.Details{
						{
							Type: match.ExactDirectMatch,
							SearchedBy: map[string]any{
								"distro":    map[string]string{"type": "debian", "version": "8"},
								"namespace": "debian:distro:debian:8",
								"package":   map[string]string{"name": "neutron", "version": "2013.1.1-1"},
							},
							Found: map[string]any{
								"versionConstraint": "< 2014.1.3-6 (deb)",
								"vulnerabilityID":   "CVE-2014-fake-1",
							},
							Matcher:    "dpkg-matcher",
							Confidence: 1,
						},
					},
				},
			),
			wantIgnoredMatches: nil,
			wantErr:            grypeerr.ErrAboveSeverityThreshold,
		},
		{
			name: "pass on severity threshold with VEX",
			fields: fields{
				Store:    str,
				Matchers: matcher.NewDefaultMatchers(matcher.Config{}),
				FailSeverity: func() *vulnerability.Severity {
					x := vulnerability.LowSeverity
					return &x
				}(),
				VexProcessor: vex.NewProcessor(vex.ProcessorOptions{
					Documents: []string{
						"vex/testdata/vex-docs/openvex-debian.json",
					},
					IgnoreRules: []match.IgnoreRule{
						{
							VexStatus: "fixed",
						},
					},
				}),
			},
			args: args{
				pkgs: []pkg.Package{
					neutron2013Pkg,
				},
				context: pkg.Context{
					Source: &source.Description{
						Name:    "debian",
						Version: "2013.1.1-1",
						Metadata: source.ImageMetadata{
							RepoDigests: []string{
								"debian@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126",
							},
						},
					},
					Distro: &linux.Release{
						ID:        "debian",
						VersionID: "8",
					},
				},
			},
			wantMatches: match.NewMatches(),
			wantIgnoredMatches: []match.IgnoredMatch{
				{
					AppliedIgnoreRules: []match.IgnoreRule{
						{
							Namespace: "vex",
							VexStatus: "fixed",
						},
					},
					Match: match.Match{
						Vulnerability: vulnerability.Vulnerability{
							PackageName: "neutron",
							Constraint:  version.MustGetConstraint("< 2014.1.3-6", version.DebFormat),
							Reference: vulnerability.Reference{
								ID:        "CVE-2014-fake-1",
								Namespace: "debian:distro:debian:8",
							},
							PackageQualifiers: []qualifier.Qualifier{},
							CPEs:              []cpe.CPE{},
							Advisories:        []vulnerability.Advisory{},
						},
						Package: neutron2013Pkg,
						Details: match.Details{
							{
								Type: match.ExactDirectMatch,
								SearchedBy: map[string]any{
									"distro":    map[string]string{"type": "debian", "version": "8"},
									"namespace": "debian:distro:debian:8",
									"package":   map[string]string{"name": "neutron", "version": "2013.1.1-1"},
								},
								Found: map[string]any{
									"versionConstraint": "< 2014.1.3-6 (deb)",
									"vulnerabilityID":   "CVE-2014-fake-1",
								},
								Matcher:    "dpkg-matcher",
								Confidence: 1,
							},
						},
					},
				},
			},
			wantErr: nil,
		},
		{
			name: "matches by exact-direct match (language)",
			fields: fields{
				Store: str,
				Matchers: matcher.NewDefaultMatchers(matcher.Config{
					Ruby: ruby.MatcherConfig{
						UseCPEs: true,
					},
				}),
			},
			args: args{
				pkgs: []pkg.Package{
					activerecordPkg,
				},
				context: pkg.Context{},
			},
			wantMatches: match.NewMatches(
				match.Match{
					Vulnerability: vulnerability.Vulnerability{
						PackageName: "activerecord",
						Constraint:  version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
						Reference: vulnerability.Reference{
							ID:        "CVE-2014-fake-3",
							Namespace: "nvd:cpe",
						},
						CPEs: []cpe.CPE{
							mustCPE("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"),
						},
						PackageQualifiers: []qualifier.Qualifier{},
						Advisories:        []vulnerability.Advisory{},
					},
					Package: activerecordPkg,
					Details: match.Details{
						{
							Type: match.CPEMatch,
							SearchedBy: search.CPEParameters{
								Namespace: "nvd:cpe",
								CPEs: []string{
									"cpe:2.3:*:activerecord:activerecord:3.7.5:*:*:*:*:rails:*:*",
								},
								Package: search.CPEPackageParameter{
									Name:    "activerecord",
									Version: "3.7.5",
								},
							},
							Found: search.CPEResult{
								VulnerabilityID:   "CVE-2014-fake-3",
								VersionConstraint: "< 3.7.6 (unknown)",
								CPEs: []string{
									"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*",
								},
							},
							Matcher:    "ruby-gem-matcher",
							Confidence: 0.9,
						},
					},
				},
				match.Match{
					Vulnerability: vulnerability.Vulnerability{
						PackageName: "activerecord",
						Constraint:  version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
						Reference: vulnerability.Reference{
							ID:        "GHSA-2014-fake-3",
							Namespace: "github:language:ruby",
						},
						RelatedVulnerabilities: []vulnerability.Reference{
							{
								ID:        "CVE-2014-fake-3",
								Namespace: "nvd:cpe",
							},
						},
						PackageQualifiers: []qualifier.Qualifier{},
						Advisories:        []vulnerability.Advisory{},
						CPEs:              []cpe.CPE{},
					},
					Package: activerecordPkg,
					Details: match.Details{
						{
							Type: match.ExactDirectMatch,
							SearchedBy: map[string]any{
								"language":  "ruby",
								"namespace": "github:language:ruby",
								"package":   map[string]string{"name": "activerecord", "version": "3.7.5"},
							},
							Found: map[string]any{
								"versionConstraint": "< 3.7.6 (unknown)",
								"vulnerabilityID":   "GHSA-2014-fake-3",
							},
							Matcher:    "ruby-gem-matcher",
							Confidence: 1,
						},
					},
				},
			),
			wantIgnoredMatches: nil,
			wantErr:            nil,
		},
		{
			name: "normalize by cve",
			fields: fields{
				Store: str,
				Matchers: matcher.NewDefaultMatchers(
					matcher.Config{
						Ruby: ruby.MatcherConfig{
							UseCPEs: true,
						},
					},
				),
				NormalizeByCVE: true, // IMPORTANT!
			},
			args: args{
				pkgs: []pkg.Package{
					activerecordPkg,
				},
				context: pkg.Context{},
			},
			wantMatches: match.NewMatches(
				match.Match{
					Vulnerability: vulnerability.Vulnerability{
						PackageName: "activerecord",
						Constraint:  version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
						Reference: vulnerability.Reference{
							ID:        "CVE-2014-fake-3",
							Namespace: "nvd:cpe",
						},
						CPEs: []cpe.CPE{
							mustCPE("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"),
						},
						PackageQualifiers: []qualifier.Qualifier{},
						Advisories:        []vulnerability.Advisory{},
						RelatedVulnerabilities: []vulnerability.Reference{
							{
								ID:        "GHSA-2014-fake-3",
								Namespace: "github:language:ruby",
							},
						},
					},
					Package: activerecordPkg,
					Details: match.Details{
						{
							Type: match.ExactDirectMatch,
							SearchedBy: map[string]any{
								"language":  "ruby",
								"namespace": "github:language:ruby",
								"package":   map[string]string{"name": "activerecord", "version": "3.7.5"},
							},
							Found: map[string]any{
								"versionConstraint": "< 3.7.6 (unknown)",
								"vulnerabilityID":   "GHSA-2014-fake-3",
							},
							Matcher:    "ruby-gem-matcher",
							Confidence: 1,
						},
						{
							Type: match.CPEMatch,
							SearchedBy: search.CPEParameters{
								Namespace: "nvd:cpe",
								CPEs: []string{
									"cpe:2.3:*:activerecord:activerecord:3.7.5:*:*:*:*:rails:*:*",
								},
								Package: search.CPEPackageParameter{
									Name:    "activerecord",
									Version: "3.7.5",
								},
							},
							Found: search.CPEResult{
								VulnerabilityID:   "CVE-2014-fake-3",
								VersionConstraint: "< 3.7.6 (unknown)",
								CPEs: []string{
									"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*",
								},
							},
							Matcher:    "ruby-gem-matcher",
							Confidence: 0.9,
						},
					},
				},
			),
			wantIgnoredMatches: nil,
			wantErr:            nil,
		},
		{
			name: "normalize by cve -- ignore GHSA",
			fields: fields{
				Store: str,
				Matchers: matcher.NewDefaultMatchers(
					matcher.Config{
						Ruby: ruby.MatcherConfig{
							UseCPEs: true,
						},
					},
				),
				IgnoreRules: []match.IgnoreRule{
					{
						Vulnerability: "GHSA-2014-fake-3",
					},
				},
				NormalizeByCVE: true, // IMPORTANT!
			},
			args: args{
				pkgs: []pkg.Package{
					activerecordPkg,
				},
				context: pkg.Context{},
			},
			wantMatches: match.NewMatches(
				match.Match{
					Vulnerability: vulnerability.Vulnerability{
						PackageName: "activerecord",
						Constraint:  version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
						Reference: vulnerability.Reference{
							ID:        "CVE-2014-fake-3",
							Namespace: "nvd:cpe",
						},
						CPEs: []cpe.CPE{
							mustCPE("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"),
						},
						PackageQualifiers: []qualifier.Qualifier{},
						Advisories:        []vulnerability.Advisory{},
					},
					Package: activerecordPkg,
					Details: match.Details{
						{
							Type: match.CPEMatch,
							SearchedBy: search.CPEParameters{
								Namespace: "nvd:cpe",
								CPEs: []string{
									"cpe:2.3:*:activerecord:activerecord:3.7.5:*:*:*:*:rails:*:*",
								},
								Package: search.CPEPackageParameter{
									Name:    "activerecord",
									Version: "3.7.5",
								},
							},
							Found: search.CPEResult{
								VulnerabilityID:   "CVE-2014-fake-3",
								VersionConstraint: "< 3.7.6 (unknown)",
								CPEs: []string{
									"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*",
								},
							},
							Matcher:    "ruby-gem-matcher",
							Confidence: 0.9,
						},
					},
				},
			),
			wantErr: nil,
			wantIgnoredMatches: []match.IgnoredMatch{
				{
					Match: match.Match{
						Vulnerability: vulnerability.Vulnerability{
							PackageName: "activerecord",
							Constraint:  version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
							Reference: vulnerability.Reference{
								ID:        "CVE-2014-fake-3",
								Namespace: "nvd:cpe",
							},
							CPEs:              []cpe.CPE{},
							PackageQualifiers: []qualifier.Qualifier{},
							Advisories:        []vulnerability.Advisory{},
							RelatedVulnerabilities: []vulnerability.Reference{
								{
									ID:        "GHSA-2014-fake-3",
									Namespace: "github:language:ruby",
								},
							},
						},
						Package: activerecordPkg,
						Details: match.Details{
							{
								Type: match.ExactDirectMatch,
								SearchedBy: map[string]any{
									"language":  "ruby",
									"namespace": "github:language:ruby",
									"package":   map[string]string{"name": "activerecord", "version": "3.7.5"},
								},
								Found: map[string]any{
									"versionConstraint": "< 3.7.6 (unknown)",
									"vulnerabilityID":   "GHSA-2014-fake-3",
								},
								Matcher:    "ruby-gem-matcher",
								Confidence: 1,
							},
						},
					},
					AppliedIgnoreRules: []match.IgnoreRule{
						{
							Vulnerability: "GHSA-2014-fake-3",
						},
					},
				},
			},
		},
		{
			name: "normalize by cve -- ignore CVE",
			fields: fields{
				Store: str,
				Matchers: matcher.NewDefaultMatchers(
					matcher.Config{
						Ruby: ruby.MatcherConfig{
							UseCPEs: true,
						},
					},
				),
				IgnoreRules: []match.IgnoreRule{
					{
						Vulnerability: "CVE-2014-fake-3",
					},
				},
				NormalizeByCVE: true, // IMPORTANT!
			},
			args: args{
				pkgs: []pkg.Package{
					activerecordPkg,
				},
				context: pkg.Context{},
			},
			wantMatches: match.NewMatches(),
			wantIgnoredMatches: []match.IgnoredMatch{
				{
					Match: match.Match{
						Vulnerability: vulnerability.Vulnerability{
							PackageName: "activerecord",
							Constraint:  version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
							Reference: vulnerability.Reference{
								ID:        "CVE-2014-fake-3",
								Namespace: "nvd:cpe",
							},
							CPEs: []cpe.CPE{
								mustCPE("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"),
							},
							PackageQualifiers:      []qualifier.Qualifier{},
							Advisories:             []vulnerability.Advisory{},
							RelatedVulnerabilities: nil,
						},
						Package: activerecordPkg,
						Details: match.Details{
							{
								Type: match.CPEMatch,
								SearchedBy: search.CPEParameters{
									Namespace: "nvd:cpe",
									CPEs: []string{
										"cpe:2.3:*:activerecord:activerecord:3.7.5:*:*:*:*:rails:*:*",
									},
									Package: search.CPEPackageParameter{
										Name:    "activerecord",
										Version: "3.7.5",
									},
								},
								Found: search.CPEResult{
									VulnerabilityID:   "CVE-2014-fake-3",
									VersionConstraint: "< 3.7.6 (unknown)",
									CPEs: []string{
										"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*",
									},
								},
								Matcher:    "ruby-gem-matcher",
								Confidence: 0.9,
							},
						},
					},
					AppliedIgnoreRules: []match.IgnoreRule{
						{
							Vulnerability: "CVE-2014-fake-3",
						},
					},
				},
				{
					AppliedIgnoreRules: []match.IgnoreRule{
						{
							Vulnerability: "CVE-2014-fake-3",
						},
					},
					Match: match.Match{
						Vulnerability: vulnerability.Vulnerability{
							PackageName: "activerecord",
							Constraint:  version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
							Reference: vulnerability.Reference{
								ID:        "CVE-2014-fake-3",
								Namespace: "nvd:cpe",
							},
							CPEs:              []cpe.CPE{},
							PackageQualifiers: []qualifier.Qualifier{},
							Advisories:        []vulnerability.Advisory{},
							RelatedVulnerabilities: []vulnerability.Reference{
								{
									ID:        "GHSA-2014-fake-3",
									Namespace: "github:language:ruby",
								},
							},
						},
						Package: activerecordPkg,
						Details: match.Details{
							{
								Type: match.ExactDirectMatch,
								SearchedBy: map[string]any{
									"language":  "ruby",
									"namespace": "github:language:ruby",
									"package":   map[string]string{"name": "activerecord", "version": "3.7.5"},
								},
								Found: map[string]any{
									"versionConstraint": "< 3.7.6 (unknown)",
									"vulnerabilityID":   "GHSA-2014-fake-3",
								},
								Matcher:    "ruby-gem-matcher",
								Confidence: 1,
							},
						},
					},
				},
			},
			wantErr: nil,
		},
		{
			name: "ignore CVE (not normalized by CVE)",
			fields: fields{
				Store: str,
				Matchers: matcher.NewDefaultMatchers(matcher.Config{
					Ruby: ruby.MatcherConfig{
						UseCPEs: true,
					},
				}),
				IgnoreRules: []match.IgnoreRule{
					{
						Vulnerability: "CVE-2014-fake-3",
					},
				},
			},
			args: args{
				pkgs: []pkg.Package{
					activerecordPkg,
				},
			},
			wantMatches: match.NewMatches(
				match.Match{
					Vulnerability: vulnerability.Vulnerability{
						PackageName: "activerecord",
						Constraint:  version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
						Reference: vulnerability.Reference{
							ID:        "GHSA-2014-fake-3",
							Namespace: "github:language:ruby",
						},
						RelatedVulnerabilities: []vulnerability.Reference{
							{
								ID:        "CVE-2014-fake-3",
								Namespace: "nvd:cpe",
							},
						},
						PackageQualifiers: []qualifier.Qualifier{},
						Advisories:        []vulnerability.Advisory{},
						CPEs:              []cpe.CPE{},
					},
					Package: activerecordPkg,
					Details: match.Details{
						{
							Type: match.ExactDirectMatch,
							SearchedBy: map[string]any{
								"language":  "ruby",
								"namespace": "github:language:ruby",
								"package":   map[string]string{"name": "activerecord", "version": "3.7.5"},
							},
							Found: map[string]any{
								"versionConstraint": "< 3.7.6 (unknown)",
								"vulnerabilityID":   "GHSA-2014-fake-3",
							},
							Matcher:    "ruby-gem-matcher",
							Confidence: 1,
						},
					},
				},
			),
			wantIgnoredMatches: []match.IgnoredMatch{
				{
					AppliedIgnoreRules: []match.IgnoreRule{
						{
							Vulnerability: "CVE-2014-fake-3",
						},
					},
					Match: match.Match{
						Vulnerability: vulnerability.Vulnerability{
							PackageName: "activerecord",
							Constraint:  version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
							Reference: vulnerability.Reference{
								ID:        "CVE-2014-fake-3",
								Namespace: "nvd:cpe",
							},
							CPEs: []cpe.CPE{
								mustCPE("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"),
							},
							PackageQualifiers: []qualifier.Qualifier{},
							Advisories:        []vulnerability.Advisory{},
						},
						Package: activerecordPkg,
						Details: match.Details{
							{
								Type: match.CPEMatch,
								SearchedBy: search.CPEParameters{
									Namespace: "nvd:cpe",
									CPEs: []string{
										"cpe:2.3:*:activerecord:activerecord:3.7.5:*:*:*:*:rails:*:*",
									},
									Package: search.CPEPackageParameter{
										Name:    "activerecord",
										Version: "3.7.5",
									},
								},
								Found: search.CPEResult{
									VulnerabilityID:   "CVE-2014-fake-3",
									VersionConstraint: "< 3.7.6 (unknown)",
									CPEs: []string{
										"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*",
									},
								},
								Matcher:    "ruby-gem-matcher",
								Confidence: 0.9,
							},
						},
					},
				},
			},
			wantErr: nil,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			m := &VulnerabilityMatcher{
				Store:          tt.fields.Store,
				Matchers:       tt.fields.Matchers,
				IgnoreRules:    tt.fields.IgnoreRules,
				FailSeverity:   tt.fields.FailSeverity,
				NormalizeByCVE: tt.fields.NormalizeByCVE,
				VexProcessor:   tt.fields.VexProcessor,
			}

			listener := &busListener{}
			bus.Set(listener)
			defer bus.Set(nil)

			actualMatches, actualIgnoreMatches, err := m.FindMatches(tt.args.pkgs, tt.args.context)
			if tt.wantErr != nil {
				require.ErrorIs(t, err, tt.wantErr)
				return
			} else if err != nil {
				t.Errorf("FindMatches() error = %v, wantErr %v", err, tt.wantErr)
				return
			}

			var opts = []cmp.Option{
				cmpopts.IgnoreUnexported(match.Match{}),
				cmpopts.IgnoreFields(vulnerability.Vulnerability{}, "Constraint"),
				cmpopts.IgnoreFields(pkg.Package{}, "Locations"),
				cmpopts.IgnoreUnexported(match.IgnoredMatch{}),
			}

			if d := cmp.Diff(tt.wantMatches.Sorted(), actualMatches.Sorted(), opts...); d != "" {
				t.Errorf("FindMatches() matches mismatch [ha!] (-want +got):\n%s", d)
			}

			if d := cmp.Diff(tt.wantIgnoredMatches, actualIgnoreMatches, opts...); d != "" {
				t.Errorf("FindMatches() ignored matches mismatch [ha!] (-want +got):\n%s", d)
			}

			// validate the bus-reported ignored counts are accurate
			require.Equal(t, int64(len(tt.wantIgnoredMatches)), listener.matching.Ignored.Current())
		})
	}
}

func Test_indexFalsePositivesByLocation(t *testing.T) {
	cases := []struct {
		name           string
		d              distro.Distro
		pkgs           []pkg.Package
		stubFunc       mockStoreStubFn
		expectedResult map[string][]string
		errAssertion   assert.ErrorAssertionFunc
	}{
		{
			name: "false positive in wolfi package adds index entry",
			d:    distro.Distro{Type: distro.Wolfi},
			pkgs: []pkg.Package{
				{
					Name: "foo",
					Metadata: pkg.ApkMetadata{Files: []pkg.ApkFileRecord{
						{
							Path: "/bin/foo-binary",
						},
					}},
				},
			},
			stubFunc: func(d *mockStore) {
				d.vulnerabilities["wolfi:distro:wolfi:rolling"] = map[string][]v5.Vulnerability{
					"foo": {
						{
							ID:                "GHSA-2014-fake-3",
							PackageName:       "foo",
							Namespace:         "wolfi:distro:wolfi:rolling",
							VersionConstraint: "< 0",
							VersionFormat:     "apk",
						},
					},
				}
			},
			expectedResult: map[string][]string{
				"/bin/foo-binary": {"GHSA-2014-fake-3"},
			},
			errAssertion: assert.NoError,
		},
		{
			name: "false positive in wolfi subpackage adds index entry",
			d:    distro.Distro{Type: distro.Wolfi},
			pkgs: []pkg.Package{
				{
					Name: "subpackage-foo",
					Metadata: pkg.ApkMetadata{Files: []pkg.ApkFileRecord{
						{
							Path: "/bin/foo-subpackage-binary",
						},
					}},
					Upstreams: []pkg.UpstreamPackage{
						{
							Name: "origin-foo",
						},
					},
				},
			},
			stubFunc: func(d *mockStore) {
				d.vulnerabilities["wolfi:distro:wolfi:rolling"] = map[string][]v5.Vulnerability{
					"origin-foo": {
						{
							ID:                "GHSA-2014-fake-3",
							PackageName:       "foo",
							Namespace:         "wolfi:distro:wolfi:rolling",
							VersionConstraint: "< 0",
							VersionFormat:     "apk",
						},
					},
				}
			},
			expectedResult: map[string][]string{
				"/bin/foo-subpackage-binary": {"GHSA-2014-fake-3"},
			},
			errAssertion: assert.NoError,
		},
		{
			name: "fixed vuln (not a false positive) in wolfi package",
			d:    distro.Distro{Type: distro.Wolfi},
			pkgs: []pkg.Package{
				{
					Name: "foo",
					Metadata: pkg.ApkMetadata{Files: []pkg.ApkFileRecord{
						{
							Path: "/bin/foo-binary",
						},
					}},
				},
			},
			stubFunc: func(d *mockStore) {
				d.vulnerabilities["wolfi:distro:wolfi:rolling"] = map[string][]v5.Vulnerability{
					"foo": {
						{
							ID:                "GHSA-2014-fake-3",
							PackageName:       "foo",
							Namespace:         "wolfi:distro:wolfi:rolling",
							VersionConstraint: "< 1.2.3-r4",
							VersionFormat:     "apk",
						},
					},
				}
			},
			expectedResult: map[string][]string{},
			errAssertion:   assert.NoError,
		},
		{
			name: "no vuln data for wolfi package",
			d:    distro.Distro{Type: distro.Wolfi},
			pkgs: []pkg.Package{
				{
					Name: "foo",
					Metadata: pkg.ApkMetadata{Files: []pkg.ApkFileRecord{
						{
							Path: "/bin/foo-binary",
						},
					}},
				},
			},
			stubFunc: func(d *mockStore) {
				d.vulnerabilities["wolfi:distro:wolfi:rolling"] = map[string][]v5.Vulnerability{}
			},
			expectedResult: map[string][]string{},
			errAssertion:   assert.NoError,
		},
		{
			name: "no files listed for a wolfi package",
			d:    distro.Distro{Type: distro.Wolfi},
			pkgs: []pkg.Package{
				{
					Name:     "foo",
					Metadata: pkg.ApkMetadata{Files: nil},
				},
			},
			stubFunc: func(d *mockStore) {
				d.vulnerabilities["wolfi:distro:wolfi:rolling"] = map[string][]v5.Vulnerability{
					"foo": {
						{
							ID:                "GHSA-2014-fake-3",
							PackageName:       "foo",
							Namespace:         "wolfi:distro:wolfi:rolling",
							VersionConstraint: "< 0",
							VersionFormat:     "apk",
						},
					},
				}
			},
			expectedResult: map[string][]string{},
			errAssertion:   assert.NoError,
		},
	}

	for _, tt := range cases {
		t.Run(tt.name, func(t *testing.T) {
			s := createMockStore(t, tt.stubFunc)
			actualResult, err := indexFalsePositivesByLocation(&tt.d, tt.pkgs, s)
			tt.errAssertion(t, err)
			assert.Equal(t, tt.expectedResult, actualResult)
		})
	}
}

func createMockStore(t *testing.T, fn mockStoreStubFn) v5.ProviderStore {
	t.Helper()

	mkStr := newMockStore(fn)
	vp, err := v5.NewVulnerabilityProvider(mkStr)
	require.NoError(t, err)

	return v5.ProviderStore{
		VulnerabilityProvider:         vp,
		VulnerabilityMetadataProvider: v5.NewVulnerabilityMetadataProvider(mkStr),
		ExclusionProvider:             v5.NewMatchExclusionProvider(mkStr),
	}
}

func Test_filterMatchesUsingDistroFalsePositives(t *testing.T) {
	cases := []struct {
		name         string
		inputMatches []match.Match
		fpIndex      map[string][]string
		expected     []match.Match
	}{
		{
			name:         "no input matches",
			inputMatches: nil,
			fpIndex: map[string][]string{
				"/usr/bin/crane": {"CVE-2014-fake-3"},
			},
			expected: nil,
		},
		{
			name: "happy path filtering",
			inputMatches: []match.Match{
				{
					Package: pkg.Package{
						Name:      "crane",
						Locations: file.NewLocationSet(file.NewLocation("/usr/bin/crane")),
					},
					Vulnerability: vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: "CVE-2014-fake-3"}},
				},
			},
			fpIndex: map[string][]string{
				"/usr/bin/crane": {"CVE-2014-fake-3"},
			},
			expected: nil,
		},
		{
			name: "location match but no vulns in FP index",
			inputMatches: []match.Match{
				{
					Package: pkg.Package{
						Name:      "crane",
						Locations: file.NewLocationSet(file.NewLocation("/usr/bin/crane")),
					},
					Vulnerability: vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: "CVE-2014-fake-3"}},
				},
			},
			fpIndex: map[string][]string{
				"/usr/bin/crane": {},
			},
			expected: []match.Match{
				{
					Package: pkg.Package{
						Name:      "crane",
						Locations: file.NewLocationSet(file.NewLocation("/usr/bin/crane")),
					},
					Vulnerability: vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: "CVE-2014-fake-3"}},
				},
			},
		},
		{
			name: "location match but matched vuln not in FP index",
			inputMatches: []match.Match{
				{
					Package: pkg.Package{
						Name:      "crane",
						Locations: file.NewLocationSet(file.NewLocation("/usr/bin/crane")),
					},
					Vulnerability: vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: "CVE-2014-fake-3"}},
				},
			},
			fpIndex: map[string][]string{
				"/usr/bin/crane": {"CVE-2016-fake-3"},
			},
			expected: []match.Match{
				{
					Package: pkg.Package{
						Name:      "crane",
						Locations: file.NewLocationSet(file.NewLocation("/usr/bin/crane")),
					},
					Vulnerability: vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: "CVE-2014-fake-3"}},
				},
			},
		},
		{
			name: "empty FP index",
			inputMatches: []match.Match{
				{
					Package: pkg.Package{
						Name:      "crane",
						Locations: file.NewLocationSet(file.NewLocation("/usr/bin/crane")),
					},
					Vulnerability: vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: "CVE-2014-fake-3"}},
				},
			},
			fpIndex: map[string][]string{},
			expected: []match.Match{
				{
					Package: pkg.Package{
						Name:      "crane",
						Locations: file.NewLocationSet(file.NewLocation("/usr/bin/crane")),
					},
					Vulnerability: vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: "CVE-2014-fake-3"}},
				},
			},
		},
	}

	for _, tt := range cases {
		t.Run(tt.name, func(t *testing.T) {
			actual := filterMatchesUsingDistroFalsePositives(tt.inputMatches, tt.fpIndex)
			assert.Equal(t, tt.expected, actual)
		})
	}
}

type busListener struct {
	matching monitor.Matching
}

func (b *busListener) Publish(e partybus.Event) {
	if e.Type == event.VulnerabilityScanningStarted {
		if m, ok := e.Value.(monitor.Matching); ok {
			b.matching = m
		}
	}
}

var _ partybus.Publisher = (*busListener)(nil)
