package grype

import (
	"errors"
	"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"

	"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/matcher"
	"github.com/anchore/grype/grype/matcher/apk"
	matcherMock "github.com/anchore/grype/grype/matcher/mock"
	"github.com/anchore/grype/grype/matcher/ruby"
	"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/grype/vulnerability/mock"
	"github.com/anchore/grype/internal/bus"
	"github.com/anchore/syft/syft/cpe"
	"github.com/anchore/syft/syft/file"
	syftPkg "github.com/anchore/syft/syft/pkg"
	"github.com/anchore/syft/syft/source"
)

func testVulnerabilities() []vulnerability.Vulnerability {
	return []vulnerability.Vulnerability{
		{
			Reference: vulnerability.Reference{
				ID:        "CVE-2014-fake-1",
				Namespace: "debian:distro:debian:8",
				Internal: vulnerability.Metadata{
					Severity: "medium",
				},
			},
			PackageName: "neutron",
			Constraint:  version.MustGetConstraint("< 2014.1.3-6", version.DebFormat),
		},
		{
			Reference: vulnerability.Reference{
				ID:        "CVE-2013-fake-2",
				Namespace: "debian:distro:debian:8",
			},
			PackageName: "neutron",
			Constraint:  version.MustGetConstraint("< 2013.0.2-1", version.DebFormat),
		},
		{
			Reference: vulnerability.Reference{
				ID:        "GHSA-2014-fake-3",
				Namespace: "github:language:ruby",
				Internal: vulnerability.Metadata{
					Severity: "medium",
				},
			},
			PackageName: "activerecord",
			Constraint:  version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
			RelatedVulnerabilities: []vulnerability.Reference{
				{
					ID:        "CVE-2014-fake-3",
					Namespace: "nvd:cpe",
				},
			},
		},
		{
			Reference: vulnerability.Reference{
				ID:        "CVE-2014-fake-3",
				Namespace: "nvd:cpe",
				Internal: vulnerability.Metadata{
					Severity: "critical",
				},
			},
			PackageName: "activerecord",
			Constraint:  version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
			CPEs: []cpe.CPE{
				cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*", ""),
			},
		},
		{
			Reference: vulnerability.Reference{
				ID:        "CVE-2014-fake-4",
				Namespace: "nvd:cpe",
			},
			PackageName: "activerecord",
			Constraint:  version.MustGetConstraint("< 3.7.4", version.UnknownFormat),
			CPEs: []cpe.CPE{
				cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:something:*:*:ruby:*:*", ""),
			},
		},
		{
			Reference: vulnerability.Reference{
				ID:        "CVE-2014-fake-5",
				Namespace: "nvd:cpe",
			},
			PackageName: "activerecord",
			Constraint:  version.MustGetConstraint("= 4.0.1", version.UnknownFormat),
			CPEs: []cpe.CPE{
				cpe.Must("cpe:2.3:*:couldntgetthisrightcouldyou:activerecord:4.0.1:*:*:*:*:*:*:*", ""),
			},
		},
		{
			Reference: vulnerability.Reference{
				ID:        "CVE-2014-fake-6",
				Namespace: "nvd:cpe",
			},
			PackageName: "activerecord",
			Constraint:  version.MustGetConstraint("< 98SP3", version.UnknownFormat),
			CPEs: []cpe.CPE{
				cpe.Must("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 := mock.VulnerabilityProvider(testVulnerabilities()...)

	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) {
	vp := mock.VulnerabilityProvider(testVulnerabilities()...)

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

	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,
	}

	openvexProcessor, _ := vex.NewProcessor(vex.ProcessorOptions{
		Documents: []string{
			"vex/testdata/vex-docs/openvex-debian.json",
		},
		IgnoreRules: []match.IgnoreRule{
			{
				VexStatus: "fixed",
			},
		},
	})

	type fields struct {
		Matchers       []match.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{
				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{},
			},
		},
		{
			name: "matches by exact-direct match (OS)",
			fields: fields{
				Matchers: matcher.NewDefaultMatchers(matcher.Config{}),
			},
			args: args{
				pkgs: []pkg.Package{
					neutron2013Pkg,
				},
				context: pkg.Context{},
			},
			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: match.DistroParameters{
								Distro:    match.DistroIdentification{Type: "debian", Version: "8"},
								Namespace: "debian:distro:debian:8",
								Package:   match.PackageParameter{Name: "neutron", Version: "2013.1.1-1"},
							},
							Found: match.DistroResult{
								VulnerabilityID:   "CVE-2014-fake-1",
								VersionConstraint: "< 2014.1.3-6 (deb)",
							},
							Matcher:    "dpkg-matcher",
							Confidence: 1,
						},
					},
				},
			),
			wantIgnoredMatches: nil,
			wantErr:            nil,
		},
		{
			name: "fail on severity threshold",
			fields: fields{
				Matchers: matcher.NewDefaultMatchers(matcher.Config{}),
				FailSeverity: func() *vulnerability.Severity {
					x := vulnerability.LowSeverity
					return &x
				}(),
			},
			args: args{
				pkgs: []pkg.Package{
					neutron2013Pkg,
				},
				context: pkg.Context{},
			},
			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: match.DistroParameters{
								Distro:    match.DistroIdentification{Type: "debian", Version: "8"},
								Namespace: "debian:distro:debian:8",
								Package:   match.PackageParameter{Name: "neutron", Version: "2013.1.1-1"},
							},
							Found: match.DistroResult{
								VulnerabilityID:   "CVE-2014-fake-1",
								VersionConstraint: "< 2014.1.3-6 (deb)",
							},
							Matcher:    "dpkg-matcher",
							Confidence: 1,
						},
					},
				},
			),
			wantIgnoredMatches: nil,
			wantErr:            grypeerr.ErrAboveSeverityThreshold,
		},
		{
			name: "pass on severity threshold with VEX",
			fields: fields{
				Matchers: matcher.NewDefaultMatchers(matcher.Config{}),
				FailSeverity: func() *vulnerability.Severity {
					x := vulnerability.LowSeverity
					return &x
				}(),
				VexProcessor: openvexProcessor,
			},
			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",
							},
						},
					},
				},
			},
			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: match.DistroParameters{
									Distro:    match.DistroIdentification{Type: "debian", Version: "8"},
									Namespace: "debian:distro:debian:8",
									Package:   match.PackageParameter{Name: "neutron", Version: "2013.1.1-1"},
								},
								Found: match.DistroResult{
									VulnerabilityID:   "CVE-2014-fake-1",
									VersionConstraint: "< 2014.1.3-6 (deb)",
								},
								Matcher:    "dpkg-matcher",
								Confidence: 1,
							},
						},
					},
				},
			},
			wantErr: nil,
		},
		{
			name: "matches by exact-direct match (language)",
			fields: fields{
				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: match.CPEParameters{
								Namespace: "nvd:cpe",
								CPEs: []string{
									"cpe:2.3:*:activerecord:activerecord:3.7.5:*:*:*:*:rails:*:*",
								},
								Package: match.PackageParameter{
									Name:    "activerecord",
									Version: "3.7.5",
								},
							},
							Found: match.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: match.EcosystemParameters{
								Language:  "ruby",
								Namespace: "github:language:ruby",
								Package:   match.PackageParameter{Name: "activerecord", Version: "3.7.5"},
							},
							Found: match.EcosystemResult{
								VulnerabilityID:   "GHSA-2014-fake-3",
								VersionConstraint: "< 3.7.6 (unknown)",
							},
							Matcher:    "ruby-gem-matcher",
							Confidence: 1,
						},
					},
				},
			),
			wantIgnoredMatches: nil,
			wantErr:            nil,
		},
		{
			name: "normalize by cve",
			fields: fields{
				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: match.EcosystemParameters{
								Language:  "ruby",
								Namespace: "github:language:ruby",
								Package:   match.PackageParameter{Name: "activerecord", Version: "3.7.5"},
							},
							Found: match.EcosystemResult{
								VulnerabilityID:   "GHSA-2014-fake-3",
								VersionConstraint: "< 3.7.6 (unknown)",
							},
							Matcher:    "ruby-gem-matcher",
							Confidence: 1,
						},
						{
							Type: match.CPEMatch,
							SearchedBy: match.CPEParameters{
								Namespace: "nvd:cpe",
								CPEs: []string{
									"cpe:2.3:*:activerecord:activerecord:3.7.5:*:*:*:*:rails:*:*",
								},
								Package: match.PackageParameter{
									Name:    "activerecord",
									Version: "3.7.5",
								},
							},
							Found: match.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{
				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: match.CPEParameters{
								Namespace: "nvd:cpe",
								CPEs: []string{
									"cpe:2.3:*:activerecord:activerecord:3.7.5:*:*:*:*:rails:*:*",
								},
								Package: match.PackageParameter{
									Name:    "activerecord",
									Version: "3.7.5",
								},
							},
							Found: match.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: match.EcosystemParameters{
									Language:  "ruby",
									Namespace: "github:language:ruby",
									Package:   match.PackageParameter{Name: "activerecord", Version: "3.7.5"},
								},
								Found: match.EcosystemResult{
									VulnerabilityID:   "GHSA-2014-fake-3",
									VersionConstraint: "< 3.7.6 (unknown)",
								},
								Matcher:    "ruby-gem-matcher",
								Confidence: 1,
							},
						},
					},
					AppliedIgnoreRules: []match.IgnoreRule{
						{
							Vulnerability: "GHSA-2014-fake-3",
						},
					},
				},
			},
		},
		{
			name: "normalize by cve -- ignore CVE",
			fields: fields{
				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: match.CPEParameters{
									Namespace: "nvd:cpe",
									CPEs: []string{
										"cpe:2.3:*:activerecord:activerecord:3.7.5:*:*:*:*:rails:*:*",
									},
									Package: match.PackageParameter{
										Name:    "activerecord",
										Version: "3.7.5",
									},
								},
								Found: match.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: match.EcosystemParameters{
									Language:  "ruby",
									Namespace: "github:language:ruby",
									Package:   match.PackageParameter{Name: "activerecord", Version: "3.7.5"},
								},
								Found: match.EcosystemResult{
									VulnerabilityID:   "GHSA-2014-fake-3",
									VersionConstraint: "< 3.7.6 (unknown)",
								},
								Matcher:    "ruby-gem-matcher",
								Confidence: 1,
							},
						},
					},
				},
			},
			wantErr: nil,
		},
		{
			name: "ignore CVE (not normalized by CVE)",
			fields: fields{
				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: match.EcosystemParameters{
								Language:  "ruby",
								Namespace: "github:language:ruby",
								Package:   match.PackageParameter{Name: "activerecord", Version: "3.7.5"},
							},
							Found: match.EcosystemResult{
								VulnerabilityID:   "GHSA-2014-fake-3",
								VersionConstraint: "< 3.7.6 (unknown)",
							},
							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: match.CPEParameters{
									Namespace: "nvd:cpe",
									CPEs: []string{
										"cpe:2.3:*:activerecord:activerecord:3.7.5:*:*:*:*:rails:*:*",
									},
									Package: match.PackageParameter{
										Name:    "activerecord",
										Version: "3.7.5",
									},
								},
								Found: match.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{
				VulnerabilityProvider: vp,
				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.EquateEmpty(),
				cmpopts.IgnoreUnexported(match.Match{}),
				cmpopts.IgnoreFields(vulnerability.Vulnerability{}, "Constraint"),
				cmpopts.IgnoreFields(vulnerability.Reference{}, "Internal"),
				cmpopts.IgnoreFields(pkg.Package{}, "Locations", "Distro"),
				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_fatalErrors(t *testing.T) {
	tests := []struct {
		name        string
		matcherFunc matcherMock.MatchFunc
		assertErr   assert.ErrorAssertionFunc
	}{
		{
			name: "no error",
			matcherFunc: func(_ vulnerability.Provider, _ pkg.Package) ([]match.Match, []match.IgnoreFilter, error) {
				return nil, nil, nil
			},
			assertErr: assert.NoError,
		},
		{
			name: "non-fatal error",
			matcherFunc: func(_ vulnerability.Provider, _ pkg.Package) ([]match.Match, []match.IgnoreFilter, error) {
				return nil, nil, errors.New("some error")
			},
			assertErr: assert.NoError,
		},
		{
			name: "fatal error",
			matcherFunc: func(_ vulnerability.Provider, _ pkg.Package) ([]match.Match, []match.IgnoreFilter, error) {
				return nil, nil, match.NewFatalError(match.UnknownMatcherType, errors.New("some error"))
			},
			assertErr: assert.Error,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			m := &VulnerabilityMatcher{
				Matchers: []match.Matcher{matcherMock.New(syftPkg.JavaPkg, tt.matcherFunc)},
			}

			_, _, err := m.FindMatches([]pkg.Package{
				{
					Name:    "foo",
					Version: "1.2.3",
					Type:    syftPkg.JavaPkg,
				},
			},
				pkg.Context{},
			)

			tt.assertErr(t, err)
		})
	}
}

func Test_indexFalsePositivesByLocation(t *testing.T) {
	cases := []struct {
		name           string
		pkgs           []pkg.Package
		vulns          []vulnerability.Vulnerability
		expectedResult map[string][]string
		errAssertion   assert.ErrorAssertionFunc
	}{
		{
			name: "false positive in wolfi package adds index entry",
			pkgs: []pkg.Package{
				{
					Name:   "foo",
					Distro: &distro.Distro{Type: distro.Wolfi},
					Metadata: pkg.ApkMetadata{Files: []pkg.ApkFileRecord{
						{
							Path: "/bin/foo-binary",
						},
					}},
				},
			},
			vulns: []vulnerability.Vulnerability{
				{
					Reference: vulnerability.Reference{
						ID:        "GHSA-2014-fake-3",
						Namespace: "wolfi:distro:wolfi:rolling",
					},
					PackageName: "foo",
					Constraint:  version.MustGetConstraint("< 0", version.ApkFormat),
				},
			},
			expectedResult: map[string][]string{
				"/bin/foo-binary": {"GHSA-2014-fake-3"},
			},
			errAssertion: assert.NoError,
		},
		{
			name: "false positive in wolfi subpackage adds index entry",
			pkgs: []pkg.Package{
				{
					Name:   "subpackage-foo",
					Distro: &distro.Distro{Type: distro.Wolfi},
					Metadata: pkg.ApkMetadata{Files: []pkg.ApkFileRecord{
						{
							Path: "/bin/foo-subpackage-binary",
						},
					}},
					Upstreams: []pkg.UpstreamPackage{
						{
							Name: "origin-foo",
						},
					},
				},
			},
			vulns: []vulnerability.Vulnerability{
				{
					Reference: vulnerability.Reference{
						ID:        "GHSA-2014-fake-3",
						Namespace: "wolfi:distro:wolfi:rolling",
					},
					PackageName: "origin-foo",
					Constraint:  version.MustGetConstraint("< 0", version.ApkFormat),
				},
			},
			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",
			pkgs: []pkg.Package{
				{
					Name:   "foo",
					Distro: &distro.Distro{Type: distro.Wolfi},
					Metadata: pkg.ApkMetadata{Files: []pkg.ApkFileRecord{
						{
							Path: "/bin/foo-binary",
						},
					}},
				},
			},
			vulns: []vulnerability.Vulnerability{
				{
					Reference: vulnerability.Reference{
						ID:        "GHSA-2014-fake-3",
						Namespace: "wolfi:distro:wolfi:rolling",
					},
					PackageName: "foo",
					Constraint:  version.MustGetConstraint("< 1.2.3-r4", version.ApkFormat),
				},
			},
			expectedResult: map[string][]string{},
			errAssertion:   assert.NoError,
		},
		{
			name: "no vuln data for wolfi package",
			pkgs: []pkg.Package{
				{
					Name:   "foo",
					Distro: &distro.Distro{Type: distro.Wolfi},
					Metadata: pkg.ApkMetadata{Files: []pkg.ApkFileRecord{
						{
							Path: "/bin/foo-binary",
						},
					}},
				},
			},
			vulns:          []vulnerability.Vulnerability{},
			expectedResult: map[string][]string{},
			errAssertion:   assert.NoError,
		},
		{
			name: "no files listed for a wolfi package",
			pkgs: []pkg.Package{
				{
					Name:     "foo",
					Distro:   &distro.Distro{Type: distro.Wolfi},
					Metadata: pkg.ApkMetadata{Files: nil},
				},
			},
			vulns: []vulnerability.Vulnerability{
				{
					Reference: vulnerability.Reference{
						ID:        "GHSA-2014-fake-3",
						Namespace: "wolfi:distro:wolfi:rolling",
					},
					PackageName: "foo",
					Constraint:  version.MustGetConstraint("< 0", version.ApkFormat),
				},
			},
			expectedResult: map[string][]string{},
			errAssertion:   assert.NoError,
		},
	}

	for _, tt := range cases {
		t.Run(tt.name, func(t *testing.T) {
			// create mock vulnerability provider
			vp := mock.VulnerabilityProvider(tt.vulns...)
			apkMatcher := &apk.Matcher{}

			var allMatches []match.Match
			var allIgnores []match.IgnoreFilter
			for _, p := range tt.pkgs {
				matches, ignores, err := apkMatcher.Match(vp, p)
				require.NoError(t, err)
				allMatches = append(allMatches, matches...)
				allIgnores = append(allIgnores, ignores...)
			}

			actualResult := map[string][]string{}
			for _, ignore := range allIgnores {
				rule, ok := ignore.(match.IgnoreRule)
				if !ok {
					continue
				}
				if rule.Package.Location == "" {
					continue
				}
				actualResult[rule.Package.Location] = append(actualResult[rule.Package.Location], rule.Vulnerability)
			}
			assert.Equal(t, tt.expectedResult, actualResult)
		})
	}
}

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) {
			var allIgnores []match.IgnoreFilter
			for path, cves := range tt.fpIndex {
				for _, cve := range cves {
					allIgnores = append(allIgnores, match.IgnoreRule{
						Vulnerability:  cve,
						IncludeAliases: true,
						Package: match.IgnoreRulePackage{
							Location: path,
						},
					})
				}
			}

			filter := ignoredMatchFilter(allIgnores)

			actual, _ := match.ApplyIgnoreFilters(tt.inputMatches, filter)

			assert.Equal(t, tt.expected, actual)
		})
	}
}

func Test_ignoredMatchFilter(t *testing.T) {
	matches := []match.Match{
		{
			Vulnerability: vulnerability.Vulnerability{
				Reference: vulnerability.Reference{
					ID: "CVE-123",
				},
			},
			Package: pkg.Package{
				Locations: file.NewLocationSet(file.NewLocation("/usr/bin/thing")),
			},
		},
		{
			Vulnerability: vulnerability.Vulnerability{
				Reference: vulnerability.Reference{
					ID: "CVE-456",
				},
			},
		},
		{
			Vulnerability: vulnerability.Vulnerability{
				Reference: vulnerability.Reference{
					ID: "CVE-789",
				},
				Status: "filter-me",
			},
		},
	}

	ignores := []match.IgnoreFilter{
		match.IgnoreRule{
			Reason: "test-location-ignore-rule",
			Package: match.IgnoreRulePackage{
				Location: "/usr/bin/thing",
			},
		},
		testIgnoreFilter{
			f: func(m match.Match) bool {
				return m.Vulnerability.Status == "filter-me"
			},
		},
	}

	f := ignoredMatchFilter(ignores)

	var ignoredReasons []string
	for _, m := range matches {
		got := f.IgnoreMatch(m)
		for _, r := range got {
			ignoredReasons = append(ignoredReasons, r.Reason)
		}
	}

	require.ElementsMatch(t, []string{"test-location-ignore-rule", "test-filtered"}, ignoredReasons)
}

type testIgnoreFilter struct {
	f func(match.Match) bool
}

func (t testIgnoreFilter) IgnoreMatch(m match.Match) []match.IgnoreRule {
	if t.f(m) {
		return []match.IgnoreRule{
			{
				Reason: "test-filtered",
			},
		}
	}
	return nil
}

type panicyMatcher struct {
	matcherType match.MatcherType
}

func (m *panicyMatcher) PackageTypes() []syftPkg.Type {
	return nil
}

func (m *panicyMatcher) Type() match.MatcherType {
	return m.matcherType
}

func (m *panicyMatcher) Match(_ vulnerability.Provider, _ pkg.Package) ([]match.Match, []match.IgnoreFilter, error) {
	panic("test panic message")
}

func TestCallMatcherSafely_RecoverFromPanic(t *testing.T) {
	matcher := &panicyMatcher{
		matcherType: "test-matcher",
	}
	_, _, err := callMatcherSafely(matcher, nil, pkg.Package{})

	require.Error(t, err)
	assert.True(t, match.IsFatalError(err))
	require.Contains(t, err.Error(), "test panic message", "missing message")
	require.Contains(t, err.Error(), "test-matcher", "missing matcher name")
}

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)
