package v6

import (
	"errors"
	"fmt"
	"io"
	"strings"
	"time"

	"github.com/hashicorp/go-multierror"
	"github.com/iancoleman/strcase"
	"github.com/scylladb/go-set/strset"

	"github.com/anchore/go-logger"
	"github.com/anchore/grype/grype/db/v6/name"
	"github.com/anchore/grype/grype/distro"
	"github.com/anchore/grype/grype/pkg"
	"github.com/anchore/grype/grype/search"
	"github.com/anchore/grype/grype/version"
	"github.com/anchore/grype/grype/vulnerability"
	"github.com/anchore/grype/internal/log"
	"github.com/anchore/syft/syft/cpe"
)

var (
	_ vulnerability.Provider              = (*vulnerabilityProvider)(nil)
	_ vulnerability.StoreMetadataProvider = (*vulnerabilityProvider)(nil)
	_ vulnerability.EOLChecker            = (*vulnerabilityProvider)(nil)
)

func NewVulnerabilityProvider(rdr Reader) vulnerability.Provider {
	return &vulnerabilityProvider{
		reader: rdr,
	}
}

type vulnerabilityProvider struct {
	reader Reader
}

// Deprecated: vulnerability.Vulnerability objects now have metadata included
func (vp vulnerabilityProvider) VulnerabilityMetadata(ref vulnerability.Reference) (*vulnerability.Metadata, error) {
	vuln, ok := ref.Internal.(*VulnerabilityHandle)
	if !ok {
		var err error
		vuln, err = vp.fetchVulnerability(ref)
		if err != nil {
			return nil, err
		}
	}

	if vuln == nil {
		log.WithFields("id", ref.ID, "namespace", ref.Namespace).Debug("unable to find vulnerability for given reference")
		return &vulnerability.Metadata{
			ID:         ref.ID,
			DataSource: strings.Split(ref.Namespace, ":")[0],
			Namespace:  ref.Namespace,
			Severity:   toSeverityString(vulnerability.UnknownSeverity),
		}, nil
	}

	return vp.getVulnerabilityMetadata(vuln, ref.Namespace)
}

func (vp vulnerabilityProvider) getVulnerabilityMetadata(vuln *VulnerabilityHandle, namespace string) (*vulnerability.Metadata, error) {
	cves := getCVEs(vuln)

	kevs, err := vp.fetchKnownExploited(cves)
	if err != nil {
		log.WithFields("id", vuln.Name, "vulnerability", vuln.String(), "error", err).Debug("unable to fetch known exploited from vulnerability")
	}

	epss, err := vp.fetchEpss(cves)
	if err != nil {
		log.WithFields("id", vuln.Name, "vulnerability", vuln.String(), "error", err).Debug("unable to fetch epss from vulnerability")
	}

	cwes, err := vp.fetchCWE(cves)
	if err != nil {
		log.WithFields("id", vuln.Name, "vulnerability", vuln.String(), "error", err).Debug("unable to fetch cwes from vulnerability")
	}

	return newVulnerabilityMetadata(vuln, namespace, kevs, epss, cwes)
}

func (vp vulnerabilityProvider) fetchCWE(cves []string) ([]vulnerability.CWE, error) {
	var out []vulnerability.CWE
	var errs error
	for _, cve := range cves {
		entries, err := vp.reader.GetCWEs(cve)
		if err != nil {
			errs = multierror.Append(errs, err)
			continue
		}
		for _, entry := range entries {
			out = append(out, vulnerability.CWE{
				CVE:    entry.CVE,
				CWE:    entry.CWE,
				Source: entry.Source,
				Type:   entry.Type,
			})
		}
	}
	return out, errs
}

func newVulnerabilityMetadata(vuln *VulnerabilityHandle, namespace string, kevs []vulnerability.KnownExploited, epss []vulnerability.EPSS, cwes []vulnerability.CWE) (*vulnerability.Metadata, error) {
	if vuln == nil {
		return nil, nil
	}

	sev, cvss, err := extractSeverities(vuln)
	if err != nil {
		log.WithFields("id", vuln.Name, "vulnerability", vuln.String()).Debug("unable to extract severity from vulnerability")
	}

	return &vulnerability.Metadata{
		ID:             vuln.Name,
		DataSource:     firstReferenceURL(vuln),
		Namespace:      namespace,
		Severity:       toSeverityString(sev),
		URLs:           lastReferenceURLs(vuln),
		Description:    vuln.BlobValue.Description,
		Cvss:           cvss,
		KnownExploited: kevs,
		EPSS:           epss,
		CWEs:           cwes,
	}, nil
}

func (vp vulnerabilityProvider) DataProvenance() (map[string]vulnerability.DataProvenance, error) {
	providers, err := vp.reader.AllProviders()
	if err != nil {
		return nil, err
	}
	dps := make(map[string]vulnerability.DataProvenance)

	for _, p := range providers {
		var date time.Time
		if p.DateCaptured != nil {
			date = *p.DateCaptured
		}
		dps[p.ID] = vulnerability.DataProvenance{
			DateCaptured: date,
			InputDigest:  p.InputDigest,
		}
	}
	return dps, nil
}

func (vp vulnerabilityProvider) fetchVulnerability(ref vulnerability.Reference) (*VulnerabilityHandle, error) {
	provider := strings.Split(ref.Namespace, ":")[0]
	vulns, err := vp.reader.GetVulnerabilities(&VulnerabilitySpecifier{Name: ref.ID, Providers: []string{provider}}, &GetVulnerabilityOptions{Preload: true})
	if err != nil {
		return nil, err
	}
	if len(vulns) > 0 {
		return &vulns[0], nil
	}
	return nil, nil
}

func (vp vulnerabilityProvider) fetchKnownExploited(cves []string) ([]vulnerability.KnownExploited, error) {
	var out []vulnerability.KnownExploited
	var errs error
	for _, cve := range cves {
		kevs, err := vp.reader.GetKnownExploitedVulnerabilities(cve)
		if err != nil {
			errs = multierror.Append(errs, err)
			continue
		}
		for _, kev := range kevs {
			out = append(out, vulnerability.KnownExploited{
				CVE:                        kev.Cve,
				VendorProject:              kev.BlobValue.VendorProject,
				Product:                    kev.BlobValue.Product,
				DateAdded:                  kev.BlobValue.DateAdded,
				RequiredAction:             kev.BlobValue.RequiredAction,
				DueDate:                    kev.BlobValue.DueDate,
				KnownRansomwareCampaignUse: kev.BlobValue.KnownRansomwareCampaignUse,
				Notes:                      kev.BlobValue.Notes,
				URLs:                       kev.BlobValue.URLs,
				CWEs:                       kev.BlobValue.CWEs,
			})
		}
	}
	return out, errs
}

func (vp vulnerabilityProvider) fetchEpss(cves []string) ([]vulnerability.EPSS, error) {
	var out []vulnerability.EPSS
	var errs error
	for _, cve := range cves {
		entries, err := vp.reader.GetEpss(cve)
		if err != nil {
			errs = multierror.Append(errs, err)
			continue
		}
		for _, entry := range entries {
			out = append(out, vulnerability.EPSS{
				CVE:        entry.Cve,
				EPSS:       entry.Epss,
				Percentile: entry.Percentile,
				Date:       entry.Date,
			})
		}
	}
	return out, errs
}

func (vp vulnerabilityProvider) PackageSearchNames(p pkg.Package) []string {
	return name.PackageNames(p)
}

func (vp vulnerabilityProvider) Close() error {
	return vp.reader.(io.Closer).Close()
}

func (vp vulnerabilityProvider) FindVulnerabilities(criteria ...vulnerability.Criteria) ([]vulnerability.Vulnerability, error) {
	if err := search.ValidateCriteria(criteria); err != nil {
		return nil, err
	}

	var out []vulnerability.Vulnerability
	for _, criteriaSet := range search.CriteriaIterator(criteria) {
		// parse criteria into a search query object
		query, remainingCriteria, err := newSearchQuery(criteriaSet)
		if err != nil {
			return nil, err
		}

		// fetch and process packages
		pkgVulns, err := vp.fetchAndProcessPackages(query)
		if err != nil {
			return nil, err
		}

		// fetch and process CPEs
		cpeVulns, err := vp.fetchAndProcessCPEs(query)
		if err != nil {
			return nil, err
		}

		// combine vulnerabilities
		var vulns []vulnerability.Vulnerability
		vulns = append(vulns, pkgVulns...)
		vulns = append(vulns, cpeVulns...)

		// apply remaining filters
		vulns, err = vp.filterVulnerabilities(vulns, remainingCriteria...)
		if err != nil {
			return nil, err
		}

		out = append(out, vulns...)
	}

	return out, nil
}

// fetchAndProcessPackages fetches packages and returns vulnerabilities directly
func (vp vulnerabilityProvider) fetchAndProcessPackages(query *searchQuery) ([]vulnerability.Vulnerability, error) {
	// only fetch if we have package specifications or vulnerability specifications
	if query.pkgSpec == nil && len(query.vulnSpecs) == 0 {
		return nil, nil
	}

	if query.unaffectedOnly {
		return vp.fetchAndProcessUnaffectedPackages(query)
	}
	return vp.fetchAndProcessAffectedPackages(query)
}

// fetchAndProcessAffectedPackages fetches affected packages and returns vulnerabilities
func (vp vulnerabilityProvider) fetchAndProcessAffectedPackages(query *searchQuery) ([]vulnerability.Vulnerability, error) {
	var vulns []vulnerability.Vulnerability
	metadataCache := make(map[string]*vulnerability.Metadata)

	affectedPackages, err := vp.reader.GetAffectedPackages(query.pkgSpec, &GetPackageOptions{
		OSs:             query.osSpecs,
		Vulnerabilities: query.vulnSpecs,
		PreloadBlob:     true,
	})
	if err != nil {
		if err = vp.handleOSError(err, query.osSpecs); err != nil {
			return nil, err
		}
		// if handleOSError returned nil, it means ErrOSNotPresent was handled
		return vulns, nil
	}

	affectedPackages = filterAffectedPackageVersions(query.versionMatcher, affectedPackages)

	if err = fillAffectedPackageHandles(vp.reader, ptrs(affectedPackages)); err != nil {
		return nil, err
	}

	vulns, err = vp.processAffectedPackageHandles(vulns, affectedPackages, metadataCache)
	if err != nil {
		return nil, err
	}

	return vulns, nil
}

// fetchAndProcessUnaffectedPackages fetches unaffected packages and returns vulnerabilities
func (vp vulnerabilityProvider) fetchAndProcessUnaffectedPackages(query *searchQuery) ([]vulnerability.Vulnerability, error) {
	var vulns []vulnerability.Vulnerability
	metadataCache := make(map[string]*vulnerability.Metadata)

	unaffectedPackages, err := vp.reader.GetUnaffectedPackages(query.pkgSpec, &GetPackageOptions{
		OSs:             query.osSpecs,
		Vulnerabilities: query.vulnSpecs,
		PreloadBlob:     true,
	})
	if err != nil {
		if err = vp.handleOSError(err, query.osSpecs); err != nil {
			return nil, err
		}
		// if handleOSError returned nil, it means ErrOSNotPresent was handled
		return vulns, nil
	}

	unaffectedPackages = filterUnaffectedPackageVersions(query.versionMatcher, unaffectedPackages)

	if err = fillUnaffectedPackageHandles(vp.reader, ptrs(unaffectedPackages)); err != nil {
		return nil, err
	}

	vulns, err = vp.processUnaffectedPackageHandles(vulns, unaffectedPackages, metadataCache)
	if err != nil {
		return nil, err
	}

	return vulns, nil
}

// handleOSError handles the common pattern of checking for ErrOSNotPresent
func (vp vulnerabilityProvider) handleOSError(err error, osSpecs OSSpecifiers) error {
	if err == nil {
		return nil
	}
	if errors.Is(err, ErrOSNotPresent) {
		log.WithFields("os", osSpecs).Debug("no OS found in the DB for the given criteria")
		return nil
	}
	return err
}

// GetOperatingSystemEOL returns the EOL and EOAS dates for the given distro.
// Uses exact matching (no aliasing) since each distro has its own EOL dates
// (e.g., CentOS EOL != RHEL EOL even though CentOS aliases to RHEL for vulns).
func (vp vulnerabilityProvider) GetOperatingSystemEOL(d *distro.Distro) (eolDate, eoasDate *time.Time, err error) {
	if d == nil {
		return nil, nil, nil
	}

	spec := OSSpecifier{
		Name:             d.Name(),
		MajorVersion:     d.MajorVersion(),
		MinorVersion:     d.MinorVersion(),
		RemainingVersion: d.RemainingVersion(),
		LabelVersion:     d.LabelVersion(),
		DisableAliasing:  true, // EOL lookups must use exact distro match
		DisableFallback:  true, // don't fall back to major-only matching (e.g. Alpine 3.24 shouldn't match EOL'd 3.12)
	}

	results, err := vp.reader.GetOperatingSystems(spec)
	if err != nil {
		if errors.Is(err, ErrMissingOSIdentification) {
			return nil, nil, nil
		}
		return nil, nil, err
	}

	if len(results) == 0 {
		return nil, nil, nil
	}

	if len(results) > 1 {
		log.WithFields("distro", d.String(), "matches", len(results)).Debug("multiple OS records matched for EOL lookup, using first match")
	}

	// Return EOL data from the first matching OS record
	os := results[0]
	return os.EOLDate, os.EOASDate, nil
}

// fetchAndProcessCPEs fetches CPEs and returns vulnerabilities directly
func (vp vulnerabilityProvider) fetchAndProcessCPEs(query *searchQuery) ([]vulnerability.Vulnerability, error) {
	var vulns []vulnerability.Vulnerability
	metadataCache := make(map[string]*vulnerability.Metadata)

	// only fetch if we have CPE specifications
	if query.cpeSpec == nil {
		return vulns, nil
	}

	if query.unaffectedOnly {
		unaffectedCPEs, err := vp.reader.GetUnaffectedCPEs(query.cpeSpec, &GetCPEOptions{
			Vulnerabilities: query.vulnSpecs,
			PreloadBlob:     true,
		})
		if err != nil {
			return nil, err
		}

		unaffectedCPEs = filterUnaffectedCPEVersions(query.versionMatcher, unaffectedCPEs, query.cpeSpec)

		if err = fillUnaffectedCPEHandles(vp.reader, ptrs(unaffectedCPEs)); err != nil {
			return nil, err
		}

		vulns, err = vp.processUnaffectedCPEHandles(vulns, unaffectedCPEs, metadataCache)
		if err != nil {
			return nil, err
		}
	} else {
		affectedCPEs, err := vp.reader.GetAffectedCPEs(query.cpeSpec, &GetCPEOptions{
			Vulnerabilities: query.vulnSpecs,
			PreloadBlob:     true,
		})
		if err != nil {
			return nil, err
		}

		affectedCPEs = filterAffectedCPEVersions(query.versionMatcher, affectedCPEs, query.cpeSpec)

		if err = fillAffectedCPEHandles(vp.reader, ptrs(affectedCPEs)); err != nil {
			return nil, err
		}

		vulns, err = vp.processAffectedCPEHandles(vulns, affectedCPEs, metadataCache)
		if err != nil {
			return nil, err
		}
	}

	return vulns, nil
}

func (vp vulnerabilityProvider) filterVulnerabilities(vulns []vulnerability.Vulnerability, criteria ...vulnerability.Criteria) ([]vulnerability.Vulnerability, error) {
	isMatch := func(v vulnerability.Vulnerability) (bool, error) {
		for _, c := range criteria {
			if _, ok := c.(search.VersionConstraintMatcher); ok {
				continue // already run
			}
			matches, reason, err := c.MatchesVulnerability(v)
			if !matches || err != nil {
				fields := logger.Fields{
					"vulnerability": v,
				}
				if err != nil {
					fields["error"] = err
				}

				logDroppedVulnerability(v.ID, reason, fields)
				return false, err
			}
		}
		return true, nil
	}
	for i := 0; i < len(vulns); i++ {
		matches, err := isMatch(vulns[i])
		if err != nil {
			return nil, err
		}
		if !matches {
			vulns = append(vulns[0:i], vulns[i+1:]...)
			i--
		}
	}
	return vulns, nil
}

// processAffectedPackageHandles processes affected package handles and adds them to the vulnerabilities list
func (vp vulnerabilityProvider) processAffectedPackageHandles(out []vulnerability.Vulnerability, handles []AffectedPackageHandle, metadataCache map[string]*vulnerability.Metadata) ([]vulnerability.Vulnerability, error) {
	for _, ph := range handles {
		if ph.BlobValue == nil {
			log.Debugf("unable to find blobValue for %+v", ph)
			continue
		}
		v, err := newVulnerabilityFromAffectedPackageHandle(ph, ph.BlobValue.Ranges)
		if err != nil {
			return nil, err
		}
		if v == nil {
			continue
		}

		meta, err := vp.getCachedMetadata(ph.Vulnerability, v.Namespace, metadataCache)
		if err != nil {
			log.WithFields("error", err, "vulnerability", v.String()).Debug("unable to fetch metadata for vulnerability")
		} else {
			v.Metadata = meta
		}

		out = append(out, *v)
	}
	return out, nil
}

// processAffectedCPEHandles processes affected CPE handles and adds them to the vulnerabilities list
func (vp vulnerabilityProvider) processAffectedCPEHandles(out []vulnerability.Vulnerability, handles []AffectedCPEHandle, metadataCache map[string]*vulnerability.Metadata) ([]vulnerability.Vulnerability, error) {
	for _, ch := range handles {
		if ch.BlobValue == nil {
			log.Debugf("unable to find blobValue for %+v", ch)
			continue
		}
		v, err := newVulnerabilityFromAffectedCPEHandle(ch, ch.BlobValue.Ranges)
		if err != nil {
			return nil, err
		}
		if v == nil {
			continue
		}

		meta, err := vp.getCachedMetadata(ch.Vulnerability, v.Namespace, metadataCache)
		if err != nil {
			log.WithFields("error", err, "vulnerability", v.String()).Debug("unable to fetch metadata for vulnerability")
		} else {
			v.Metadata = meta
		}

		out = append(out, *v)
	}
	return out, nil
}

// processUnaffectedPackageHandles processes unaffected package handles and adds them to the vulnerabilities list
func (vp vulnerabilityProvider) processUnaffectedPackageHandles(out []vulnerability.Vulnerability, handles []UnaffectedPackageHandle, metadataCache map[string]*vulnerability.Metadata) ([]vulnerability.Vulnerability, error) {
	for _, ph := range handles {
		if ph.BlobValue == nil {
			log.Debugf("unable to find blobValue for %+v", ph)
			continue
		}
		v, err := newVulnerabilityFromUnaffectedPackageHandle(ph, ph.BlobValue.Ranges)
		if err != nil {
			return nil, err
		}
		if v == nil {
			continue
		}

		meta, err := vp.getCachedMetadata(ph.Vulnerability, v.Namespace, metadataCache)
		if err != nil {
			log.WithFields("error", err, "vulnerability", v.String()).Debug("unable to fetch metadata for vulnerability")
		} else {
			v.Metadata = meta
		}

		out = append(out, *v)
	}
	return out, nil
}

// processUnaffectedCPEHandles processes unaffected CPE handles and adds them to the vulnerabilities list
func (vp vulnerabilityProvider) processUnaffectedCPEHandles(out []vulnerability.Vulnerability, handles []UnaffectedCPEHandle, metadataCache map[string]*vulnerability.Metadata) ([]vulnerability.Vulnerability, error) {
	for _, ch := range handles {
		if ch.BlobValue == nil {
			log.Debugf("unable to find blobValue for %+v", ch)
			continue
		}
		v, err := newVulnerabilityFromUnaffectedCPEHandle(ch, ch.BlobValue.Ranges)
		if err != nil {
			return nil, err
		}
		if v == nil {
			continue
		}

		meta, err := vp.getCachedMetadata(ch.Vulnerability, v.Namespace, metadataCache)
		if err != nil {
			log.WithFields("error", err, "vulnerability", v.String()).Debug("unable to fetch metadata for vulnerability")
		} else {
			v.Metadata = meta
		}

		out = append(out, *v)
	}
	return out, nil
}

// getCachedMetadata retrieves metadata from cache or fetches it if not cached
func (vp vulnerabilityProvider) getCachedMetadata(vuln *VulnerabilityHandle, namespace string, metadataCache map[string]*vulnerability.Metadata) (*vulnerability.Metadata, error) {
	if vuln == nil {
		return nil, nil
	}

	if metadata, ok := metadataCache[vuln.Name]; ok {
		return metadata, nil
	}

	metadata, err := vp.getVulnerabilityMetadata(vuln, namespace)
	if err != nil {
		return nil, err
	}

	metadataCache[vuln.Name] = metadata
	return metadata, nil
}

func filterAffectedPackageVersions(constraintMatcher search.VersionConstraintMatcher, packages []AffectedPackageHandle) []AffectedPackageHandle {
	// no constraint matcher, just return all packages
	if constraintMatcher == nil {
		return packages
	}
	var out []AffectedPackageHandle
	for packageIdx := range packages {
		handle := packages[packageIdx]
		vuln := handle.vulnerability()
		allDropped, unmatchedConstraints := filterAffectedPackageRanges(constraintMatcher, handle.BlobValue)
		if !allDropped {
			out = append(out, handle)
			continue // keep this handle
		}

		reason := fmt.Sprintf("not within vulnerability version constraints: %q", strings.Join(unmatchedConstraints, ", "))
		f := make(logger.Fields)
		if handle.Package != nil {
			f["package"] = handle.Package.String()
		} else {
			f["affectedPackage"] = handle
		}

		logDroppedVulnerability(vuln, reason, f)
	}
	return out
}

func filterUnaffectedPackageVersions(constraintMatcher search.VersionConstraintMatcher, packages []UnaffectedPackageHandle) []UnaffectedPackageHandle {
	// no constraint matcher, just return all packages
	if constraintMatcher == nil {
		return packages
	}
	var out []UnaffectedPackageHandle
	for packageIdx := range packages {
		handle := packages[packageIdx]
		vuln := handle.vulnerability()
		pkgHandle := handle.getPackageHandle()
		allDropped, unmatchedConstraints := filterAffectedPackageRanges(constraintMatcher, pkgHandle.BlobValue)
		if !allDropped {
			out = append(out, handle)
			continue // keep this handle
		}

		reason := fmt.Sprintf("not within vulnerability version constraints: %q", strings.Join(unmatchedConstraints, ", "))
		f := make(logger.Fields)
		if pkgHandle.Package != nil {
			f["package"] = pkgHandle.Package.String()
		} else {
			f["unaffectedPackage"] = handle
		}

		logDroppedVulnerability(vuln, reason, f)
	}
	return out
}

func filterAffectedCPEVersions(constraintMatcher search.VersionConstraintMatcher, handles []AffectedCPEHandle, cpeSpec *cpe.Attributes) []AffectedCPEHandle {
	// no constraint matcher, just return all packages
	if constraintMatcher == nil {
		return handles
	}
	var out []AffectedCPEHandle
	for i := range handles {
		handle := handles[i]
		vuln := handle.vulnerability()
		allDropped, unmatchedConstraints := filterAffectedPackageRanges(constraintMatcher, handle.BlobValue)
		if !allDropped {
			out = append(out, handle)
			continue // keep this handle
		}

		reason := fmt.Sprintf("not within vulnerability version constraints: %q", strings.Join(unmatchedConstraints, ", "))
		logDroppedVulnerability(vuln, reason, logger.Fields{
			"cpe": cpeSpec.String(),
		})
	}
	return out
}

func filterUnaffectedCPEVersions(constraintMatcher search.VersionConstraintMatcher, handles []UnaffectedCPEHandle, cpeSpec *cpe.Attributes) []UnaffectedCPEHandle {
	// no constraint matcher, just return all packages
	if constraintMatcher == nil {
		return handles
	}
	var out []UnaffectedCPEHandle
	for i := range handles {
		handle := handles[i]
		vuln := handle.vulnerability()
		allDropped, unmatchedConstraints := filterAffectedPackageRanges(constraintMatcher, handle.BlobValue)
		if !allDropped {
			out = append(out, handle)
			continue // keep this handle
		}

		reason := fmt.Sprintf("not within vulnerability version constraints: %q", strings.Join(unmatchedConstraints, ", "))
		logDroppedVulnerability(vuln, reason, logger.Fields{
			"cpe": cpeSpec.String(),
		})
	}
	return out
}

// filterAffectedPackageRanges returns true if all ranges removed
func filterAffectedPackageRanges(matcher search.VersionConstraintMatcher, b *PackageBlob) (bool, []string) {
	if len(b.Ranges) == 0 {
		// no ranges means that we're implicitly vulnerable to all versions
		return false, nil
	}
	var unmatchedConstraints []string
	for _, r := range b.Ranges {
		v := r.Version
		format := version.ParseFormat(v.Type)
		constraint, err := version.GetConstraint(v.Constraint, format)
		if err != nil || constraint == nil {
			log.WithFields("error", err, "constraint", v.Constraint, "format", v.Type).Debug("unable to parse constraint")
			continue
		}
		matches, err := matcher.MatchesConstraint(constraint)
		if err != nil {
			log.WithFields("error", err, "constraint", v.Constraint, "format", v.Type).Debug("match constraint error")
		}
		if matches {
			continue
		}
		unmatchedConstraints = append(unmatchedConstraints, v.Constraint)
	}
	return len(b.Ranges) == len(unmatchedConstraints), unmatchedConstraints
}

func toSeverityString(sev vulnerability.Severity) string {
	return strcase.ToCamel(sev.String())
}

// returns the first reference url to populate the DataSource
func firstReferenceURL(vuln *VulnerabilityHandle) string {
	for _, v := range vuln.BlobValue.References {
		return v.URL
	}
	return ""
}

// skip the first reference URL and return the remainder to populate the URLs
func lastReferenceURLs(vuln *VulnerabilityHandle) []string {
	var out []string
	for i, v := range vuln.BlobValue.References {
		if i == 0 {
			continue
		}
		out = append(out, v.URL)
	}
	return out
}

func getCVEs(vuln *VulnerabilityHandle) []string {
	var cves []string
	set := strset.New()

	addCVE := func(id string) {
		lower := strings.ToLower(id)
		if strings.HasPrefix(lower, "cve-") {
			if !set.Has(lower) {
				cves = append(cves, id)
				set.Add(lower)
			}
		}
	}

	if vuln == nil {
		return cves
	}

	addCVE(vuln.Name)

	if vuln.BlobValue == nil {
		return cves
	}

	addCVE(vuln.BlobValue.ID)

	for _, alias := range vuln.BlobValue.Aliases {
		addCVE(alias)
	}

	return cves
}
