Repository: anchore/grype
Pull request #2782 (2025-07-05 10:43:51)

chore: add channel to os / distro
This PR makes the following changes:
- Adds `distro.FixChannel` to be able to specify a channel name that can be applied to one or more distro release IDs within a given version constraint. This will later be used to be able to specify when specific RedHat distros should be using `EUS` (extended support) data when matching packages with vulnerabilities.
- Migrates processing distro override via the `--distro` flag from the `cmd` package to the `pkg` package, within the package provider (as a `pkg.Enhancer` like we do with other distro concerns within `pkg`). This further centralizes distro concerns to be that of the package processing in general (thus in the long term truly belong to `pkg.Package`, and de-leverages using the distro from `pkg.Context`).
- Migrates the `version.NewVersionFromPkg` helper function to `pkg.VersionFormat` (isolating only getting the version format) --this is to prevent a package cycle when using version object within the `distro` package. This also makes a little more sense: a distro has a version, and a package has a version, but a version does not have a distro or package, so the version is a more fundamental package (thus should not depend on either of the other two).
- Changes `version.NewVersion(...)` to `version.New(...)`
- Splits up the distro type IDs from aliases, so that the existing `IDMapping` is purely for IDs (and a new mapping is only for aliases). This makes adding `ID()` to the distro object easier to implement now (leaning toward release IDs over bespoke types as is done now).

This is work broken off from #2540 and partially addresses #2446.

## Work deferred for another PR / for the future
- Introducing an application configuration for distribution fix channels; this will be added in the last PR to complete #2446
- Fully removing the distro object from `pkg.Context` (probably will be part of the grype 1.0 work)

## TODO
- [x] CI will fail until anchore/syft#4023 is merged and incorporated here


Patch:
diff --git a/.gitignore b/.gitignore
index f1e1f81b..b59d0796 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,11 @@
 go.work
 go.work.sum
 .tool-versions
+.jj/
+
+# AI and LLM related files
+CLAUDE.md
+.claude
 
 # app configuration
 /.grype.yaml
diff --git a/cmd/grype/cli/commands/root.go b/cmd/grype/cli/commands/root.go
index 9d3fdaae..953a37b3 100644
--- a/cmd/grype/cli/commands/root.go
+++ b/cmd/grype/cli/commands/root.go
@@ -199,12 +199,13 @@ func runGrype(app clio.Application, opts *options.Grype, userInput string) (errs
 
 	defer log.CloseAndLogError(vp, status.Path)
 
+	warnWhenDistroHintNeeded(packages, &pkgContext)
+
 	if err = applyVexRules(opts); err != nil {
 		return fmt.Errorf("applying vex rules: %w", err)
 	}
 
 	startTime := time.Now()
-	applyDistroHint(packages, &pkgContext, opts)
 
 	vulnMatcher := grype.VulnerabilityMatcher{
 		VulnerabilityProvider: vp,
@@ -248,6 +249,24 @@ func runGrype(app clio.Application, opts *options.Grype, userInput string) (errs
 	return errs
 }
 
+func warnWhenDistroHintNeeded(pkgs []pkg.Package, context *pkg.Context) {
+	hasOSPackageWithoutDistro := false
+	for _, p := range pkgs {
+		switch p.Type {
+		case syftPkg.AlpmPkg, syftPkg.DebPkg, syftPkg.RpmPkg, syftPkg.KbPkg:
+			if p.Distro == nil {
+				hasOSPackageWithoutDistro = true
+				break
+			}
+		}
+	}
+
+	if context.Distro == nil && hasOSPackageWithoutDistro {
+		log.Warnf("Unable to determine the OS distribution of some packages. This may result in missing vulnerabilities. " +
+			"You may specify a distro using: --distro <distro>:<version>")
+	}
+}
+
 func dbInfo(status *vulnerability.ProviderStatus, vp vulnerability.Provider) any {
 	var providers map[string]vulnerability.DataProvenance
 
@@ -271,35 +290,6 @@ func dbInfo(status *vulnerability.ProviderStatus, vp vulnerability.Provider) any
 	}
 }
 
-func applyDistroHint(pkgs []pkg.Package, context *pkg.Context, opts *options.Grype) {
-	if opts.Distro != "" {
-		log.Infof("using distro: %s", opts.Distro)
-
-		split := strings.Split(opts.Distro, ":")
-		d := split[0]
-		v := ""
-		if len(split) > 1 {
-			v = split[1]
-		}
-		context.Distro = distro.NewFromNameVersion(d, v)
-	}
-
-	hasOSPackageWithoutDistro := false
-	for _, p := range pkgs {
-		switch p.Type {
-		case syftPkg.AlpmPkg, syftPkg.DebPkg, syftPkg.RpmPkg, syftPkg.KbPkg:
-			if p.Distro == nil {
-				hasOSPackageWithoutDistro = true
-			}
-		}
-	}
-
-	if context.Distro == nil && hasOSPackageWithoutDistro {
-		log.Warnf("Unable to determine the OS distribution of some packages. This may result in missing vulnerabilities. " +
-			"You may specify a distro using: --distro <distro>:<version>")
-	}
-}
-
 func checkForAppUpdate(id clio.Identification, opts *options.Grype) {
 	if !opts.CheckForAppUpdate {
 		return
@@ -366,10 +356,23 @@ func getProviderConfig(opts *options.Grype) pkg.ProviderConfig {
 		},
 		SynthesisConfig: pkg.SynthesisConfig{
 			GenerateMissingCPEs: opts.GenerateMissingCPEs,
+			Distro: pkg.DistroConfig{
+				Override: applyDistroHint(opts.Distro),
+				// TODO: wire up fix channels when there is EUS data in the DB
+				FixChannels: distro.DefaultFixChannels(),
+			},
 		},
 	}
 }
 
+func applyDistroHint(hint string) *distro.Distro {
+	if hint == "" {
+		return nil
+	}
+
+	return distro.NewFromNameVersion(stringutil.SplitOnFirstString(hint, ":", "@"))
+}
+
 func validateDBLoad(loadErr error, status *vulnerability.ProviderStatus) error {
 	if loadErr != nil {
 		// notify the user about grype db delete to fix checksum errors
diff --git a/go.mod b/go.mod
index 20476dfd..2a19995e 100644
--- a/go.mod
+++ b/go.mod
@@ -44,7 +44,6 @@ require (
 	github.com/hashicorp/go-cleanhttp v0.5.2
 	github.com/hashicorp/go-getter v1.7.8
 	github.com/hashicorp/go-multierror v1.1.1
-	github.com/hashicorp/go-version v1.7.0
 	github.com/iancoleman/strcase v0.3.0
 	github.com/invopop/jsonschema v0.13.0
 	github.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f
@@ -181,6 +180,7 @@ require (
 	github.com/googleapis/gax-go/v2 v2.14.1 // indirect
 	github.com/hashicorp/errwrap v1.1.0 // indirect
 	github.com/hashicorp/go-safetemp v1.0.0 // indirect
+	github.com/hashicorp/go-version v1.7.0 // indirect
 	github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
 	github.com/hashicorp/hcl/v2 v2.24.0 // indirect
 	github.com/huandu/xstrings v1.5.0 // indirect
diff --git a/grype/db/v6/data.go b/grype/db/v6/data.go
index ee80d758..f0d5f1cd 100644
--- a/grype/db/v6/data.go
+++ b/grype/db/v6/data.go
@@ -12,15 +12,31 @@ func KnownOperatingSystemSpecifierOverrides() []OperatingSystemSpecifierOverride
 		return &s
 	}
 	return []OperatingSystemSpecifierOverride{
+		// redhat clones or otherwise shared vulnerability data
 		{Alias: "centos", ReplacementName: strRef("rhel")},
 		{Alias: "rocky", ReplacementName: strRef("rhel")},
 		{Alias: "rockylinux", ReplacementName: strRef("rhel")}, // non-standard, but common (dockerhub uses "rockylinux")
 		{Alias: "alma", ReplacementName: strRef("rhel")},
 		{Alias: "almalinux", ReplacementName: strRef("rhel")}, // non-standard, but common (dockerhub uses "almalinux")
 		{Alias: "gentoo", ReplacementName: strRef("rhel")},
-		{Alias: "alpine", VersionPattern: ".*_alpha.*", ReplacementLabelVersion: strRef("edge"), Rolling: true},
+
+		// to remain backwards compatible, we need to keep old clients from ignoring EUS data.
+		// we do this by diverting any requests for a specific major.minor version of rhel to only
+		// use the major version. But, this only applies to clients before v6.0.3 DB schema version.
+		// Why 6.0.3? This is when OS channel was introduced, which grype-db will leverage, and add additional
+		// rhel rows to the DB, all which have major.minor versions. This means that any old client (which wont
+		// see the new channel column) will assume during OS resolution that there is major.minor vuln data
+		// that should be used (which is incorrect).
+		{Alias: "rhel", VersionPattern: `^\d+\.\d+`, ReplacementMinorVersion: strRef(""), ApplicableClientDBSchemas: "< 6.0.3"},
+		// we pass in the distro.Type into the search specifier, not a raw release-id
+		{Alias: "redhat", VersionPattern: `^\d+\.\d+`, ReplacementMinorVersion: strRef(""), ReplacementName: strRef("rhel"), ApplicableClientDBSchemas: "< 6.0.3"},
+
+		// alpine family
+		{Alias: "alpine", VersionPattern: `.*_alpha.*`, ReplacementLabelVersion: strRef("edge"), Rolling: true},
 		{Alias: "wolfi", Rolling: true},
 		{Alias: "chainguard", Rolling: true},
+
+		// others
 		{Alias: "arch", Rolling: true},
 		{Alias: "minimos", Rolling: true},
 		{Alias: "archlinux", ReplacementName: strRef("arch"), Rolling: true}, // non-standard, but common (dockerhub uses "archlinux")
diff --git a/grype/db/v6/db.go b/grype/db/v6/db.go
index e852f494..cb4b1dfa 100644
--- a/grype/db/v6/db.go
+++ b/grype/db/v6/db.go
@@ -24,12 +24,13 @@ const (
 	Revision = 0
 
 	// Addition indicates how many changes have been introduced that are compatible with all historical data
-	Addition = 2
+	Addition = 3
 
 	// v6 model changelog:
 	// 6.0.0: Initial version 🎉
 	// 6.0.1: Add CISA KEV to VulnerabilityDecorator store
 	// 6.0.2: Add EPSS to VulnerabilityDecorator store
+	// 6.0.3: Add channel column to OperatingSystem model
 )
 
 const (
diff --git a/grype/db/v6/log_dropped.go b/grype/db/v6/log_dropped.go
index d843ba67..10221669 100644
--- a/grype/db/v6/log_dropped.go
+++ b/grype/db/v6/log_dropped.go
@@ -7,6 +7,8 @@ import (
 
 // logDroppedVulnerability is a hook called when vulnerabilities are dropped from consideration in a vulnerability Provider,
 // this offers a convenient location to set a breakpoint
+//
+//go:noinline
 func logDroppedVulnerability(vuln string, reason any, fields logger.Fields) {
 	fields["reason"] = reason
 	fields["vulnerability"] = vuln
diff --git a/grype/db/v6/models.go b/grype/db/v6/models.go
index 8c36731b..104366ba 100644
--- a/grype/db/v6/models.go
+++ b/grype/db/v6/models.go
@@ -470,6 +470,10 @@ type OperatingSystem struct {
 
 	// Codename is the codename of a specific release (e.g. "buster" for debian 10)
 	Codename string `gorm:"column:codename;index,collate:NOCASE"`
+
+	// Channel is a string used to distinguish between fix and vulnerability data for the same OS release.
+	// such as RHEL-9.4+EUS vs RHEL-9
+	Channel string `gorm:"column:channel;index:os_idx,unique;index,collate:NOCASE"`
 }
 
 func (o *OperatingSystem) VersionNumber() string {
@@ -491,11 +495,16 @@ func (o *OperatingSystem) Version() string {
 		return o.LabelVersion
 	}
 
+	var suffix string
+	if o.Channel != "" {
+		suffix = fmt.Sprintf("+%s", o.Channel)
+	}
+
 	if o.MajorVersion != "" {
 		if o.MinorVersion != "" {
-			return fmt.Sprintf("%s.%s", o.MajorVersion, o.MinorVersion)
+			return fmt.Sprintf("%s.%s%s", o.MajorVersion, o.MinorVersion, suffix)
 		}
-		return o.MajorVersion
+		return o.MajorVersion + suffix
 	}
 
 	return o.Codename
@@ -558,7 +567,10 @@ type OperatingSystemSpecifierOverride struct {
 	VersionPattern string `gorm:"column:version_pattern;primaryKey"`
 
 	// Codename is the matching codename as found in the VERSION_CODENAME field if the /etc/os-release file
-	Codename string `gorm:"column:codename"`
+	Codename string `gorm:"column:codename;collate:NOCASE"`
+
+	// Channel is a string used to distinguish between fix and vulnerability data for the same OS release (e.g. RHEL mainline vs EUS).
+	Channel string `gorm:"column:channel;collate:NOCASE"`
 
 	// below are the fields that should be used as replacement for fields in the OperatingSystem table
 
@@ -566,7 +578,11 @@ type OperatingSystemSpecifierOverride struct {
 	ReplacementMajorVersion *string `gorm:"column:replacement_major_version;primaryKey"`
 	ReplacementMinorVersion *string `gorm:"column:replacement_minor_version;primaryKey"`
 	ReplacementLabelVersion *string `gorm:"column:replacement_label_version;primaryKey"`
+	ReplacementChannel      *string `gorm:"column:replacement_channel;primaryKey"`
 	Rolling                 bool    `gorm:"column:rolling;primaryKey"`
+
+	// ApplicableClientDBSchemas is a constraint on the database version that this override can be applied to (relative to the client library being used to access the DB).
+	ApplicableClientDBSchemas string `gorm:"column:applicable_client_db_schemas"`
 }
 
 func (os *OperatingSystemSpecifierOverride) BeforeCreate(_ *gorm.DB) (err error) {
diff --git a/grype/db/v6/operating_system_store.go b/grype/db/v6/operating_system_store.go
index fd187757..af549cbe 100644
--- a/grype/db/v6/operating_system_store.go
+++ b/grype/db/v6/operating_system_store.go
@@ -8,6 +8,7 @@ import (
 
 	"gorm.io/gorm"
 
+	"github.com/anchore/grype/grype/version"
 	"github.com/anchore/grype/internal/log"
 )
 
@@ -29,6 +30,9 @@ type OSSpecifier struct {
 
 	// LabelVersion is a string that represents a floating version (e.g. "edge" or "unstable") or is the CODENAME field in /etc/os-release (e.g. "wheezy" for debian 7)
 	LabelVersion string
+
+	// Channel is a string that represents a different feed for fix and vulnerability data (e.g. "eus" for RHEL)
+	Channel string
 }
 
 func (d *OSSpecifier) clean() {
@@ -121,14 +125,16 @@ type OperatingSystemStoreReader interface {
 }
 
 type operatingSystemStore struct {
-	db        *gorm.DB
-	blobStore *blobStore
+	db            *gorm.DB
+	blobStore     *blobStore
+	clientVersion *version.Version
 }
 
 func newOperatingSystemStore(db *gorm.DB, bs *blobStore) *operatingSystemStore {
 	return &operatingSystemStore{
-		db:        db,
-		blobStore: bs,
+		db:            db,
+		blobStore:     bs,
+		clientVersion: version.New(fmt.Sprintf("%d.%d.%d", ModelVersion, Revision, Addition), version.SemanticFormat),
 	}
 }
 
@@ -222,51 +228,93 @@ func (s *operatingSystemStore) applyOSAlias(d *OSSpecifier) error {
 		return nil
 	}
 
-	var alias *OperatingSystemSpecifierOverride
+	return applyOSSpecifierOverrides(d, aliases, s.clientVersion)
+}
+
+func applyOSSpecifierOverrides(d *OSSpecifier, overrides []OperatingSystemSpecifierOverride, clientVersion *version.Version) error {
+	for _, o := range overrides {
+		canUse, err := canUseOverride(o, clientVersion)
+		if err != nil {
+			log.Tracef("failed to check if override %q is applicable for client version %s: %v", o.Alias, clientVersion, err)
+			// we cannot check if we can use this override, so we assume it is applicable
+		} else if !canUse {
+			// this override is not applicable for the current client version
+			continue
+		}
 
-	for _, a := range aliases {
-		if a.Codename != "" && a.Codename != d.LabelVersion {
+		if o.Codename != "" && o.Codename != d.LabelVersion {
 			continue
 		}
 
-		if a.Version != "" && a.Version != d.version() {
+		if o.Version != "" && o.Version != d.version() {
 			continue
 		}
 
-		if a.VersionPattern != "" && !d.matchesVersionPattern(a.VersionPattern) {
+		if o.VersionPattern != "" && !d.matchesVersionPattern(o.VersionPattern) {
 			continue
 		}
 
-		alias = &a
+		// first match wins, we do not apply any further overrides
+		applyOverride(d, o)
 		break
 	}
 
-	if alias == nil {
-		return nil
-	}
+	return nil
+}
 
-	if alias.ReplacementName != nil {
-		d.Name = *alias.ReplacementName
+func applyOverride(d *OSSpecifier, override OperatingSystemSpecifierOverride) bool {
+	var applied bool
+	if override.ReplacementName != nil {
+		d.Name = *override.ReplacementName
+		applied = true
 	}
 
-	if alias.Rolling {
+	if override.Rolling {
 		d.MajorVersion = ""
 		d.MinorVersion = ""
+		applied = true
 	}
 
-	if alias.ReplacementMajorVersion != nil {
-		d.MajorVersion = *alias.ReplacementMajorVersion
+	if override.ReplacementMajorVersion != nil {
+		d.MajorVersion = *override.ReplacementMajorVersion
+		applied = true
 	}
 
-	if alias.ReplacementMinorVersion != nil {
-		d.MinorVersion = *alias.ReplacementMinorVersion
+	if override.ReplacementMinorVersion != nil {
+		d.MinorVersion = *override.ReplacementMinorVersion
+		applied = true
 	}
 
-	if alias.ReplacementLabelVersion != nil {
-		d.LabelVersion = *alias.ReplacementLabelVersion
+	if override.ReplacementLabelVersion != nil {
+		d.LabelVersion = *override.ReplacementLabelVersion
+		applied = true
 	}
 
-	return nil
+	if override.ReplacementChannel != nil {
+		d.Channel = *override.ReplacementChannel
+		applied = true
+	}
+	return applied
+}
+
+func canUseOverride(override OperatingSystemSpecifierOverride, clientVersion *version.Version) (bool, error) {
+	if override.ApplicableClientDBSchemas == "" || clientVersion == nil {
+		return true, nil
+	}
+	c, err := version.GetConstraint(override.ApplicableClientDBSchemas, version.SemanticFormat)
+	if err != nil {
+		return true, fmt.Errorf("unable to parse version constraint: %w", err)
+	}
+	ok, err := c.Satisfied(clientVersion)
+	if err != nil {
+		return true, fmt.Errorf("unable to check if client constraint: %w", err)
+	}
+	if !ok {
+		// explicitly told that this override does not apply to this client version
+		return false, nil
+	}
+
+	return true, nil
 }
 
 func (s *operatingSystemStore) prepareQuery(d OSSpecifier) *gorm.DB {
@@ -280,6 +328,12 @@ func (s *operatingSystemStore) prepareQuery(d OSSpecifier) *gorm.DB {
 		query = query.Where("codename = ? collate nocase OR label_version = ? collate nocase", d.LabelVersion, d.LabelVersion)
 	}
 
+	if d.Channel != "" {
+		query = query.Where("channel = ? collate nocase", d.Channel)
+	} else {
+		// we specifically want to match vanilla...
+		query = query.Where("channel IS NULL OR channel = ''")
+	}
 	return query
 }
 
diff --git a/grype/db/v6/vulnerability_provider.go b/grype/db/v6/vulnerability_provider.go
index 3d9eac6c..0a56167a 100644
--- a/grype/db/v6/vulnerability_provider.go
+++ b/grype/db/v6/vulnerability_provider.go
@@ -258,13 +258,31 @@ func (vp vulnerabilityProvider) FindVulnerabilities(criteria ...vulnerability.Cr
 				applied = true
 			case *search.DistroCriteria:
 				for _, d := range c.Distros {
-					osSpecs = append(osSpecs, &OSSpecifier{
-						Name:             d.Name(),
-						MajorVersion:     d.MajorVersion(),
-						MinorVersion:     d.MinorVersion(),
-						RemainingVersion: d.RemainingVersion(),
-						LabelVersion:     d.Codename,
-					})
+					var foundChannels int
+					for _, channel := range d.Channels {
+						if channel == "" {
+							// if the channel is empty, we should not add it to the OS specifier
+							continue
+						}
+						foundChannels++
+						osSpecs = append(osSpecs, &OSSpecifier{
+							Name:             d.Name(),
+							MajorVersion:     d.MajorVersion(),
+							MinorVersion:     d.MinorVersion(),
+							RemainingVersion: d.RemainingVersion(),
+							LabelVersion:     d.Codename,
+							Channel:          channel,
+						})
+					}
+					if foundChannels == 0 {
+						osSpecs = append(osSpecs, &OSSpecifier{
+							Name:             d.Name(),
+							MajorVersion:     d.MajorVersion(),
+							MinorVersion:     d.MinorVersion(),
+							RemainingVersion: d.RemainingVersion(),
+							LabelVersion:     d.Codename,
+						})
+					}
 				}
 				applied = true
 			}
diff --git a/grype/deprecated.go b/grype/deprecated.go
index 050974fe..a239c1e9 100644
--- a/grype/deprecated.go
+++ b/grype/deprecated.go
@@ -1,7 +1,6 @@
 package grype
 
 import (
-	"github.com/anchore/grype/grype/distro"
 	"github.com/anchore/grype/grype/match"
 	"github.com/anchore/grype/grype/matcher"
 	"github.com/anchore/grype/grype/pkg"
@@ -29,11 +28,11 @@ func FindVulnerabilities(store vulnerability.Provider, userImageStr string, scop
 
 	matchers := matcher.NewDefaultMatchers(matcher.Config{})
 
-	return FindVulnerabilitiesForPackage(store, context.Distro, matchers, packages), context, packages, nil
+	return FindVulnerabilitiesForPackage(store, matchers, packages), context, packages, nil
 }
 
 // TODO: deprecated, will remove before v1.0.0
-func FindVulnerabilitiesForPackage(store vulnerability.Provider, d *distro.Distro, matchers []match.Matcher, packages []pkg.Package) match.Matches {
+func FindVulnerabilitiesForPackage(store vulnerability.Provider, matchers []match.Matcher, packages []pkg.Package) match.Matches {
 	exclusionProvider, _ := store.(match.ExclusionProvider) // TODO v5 is an exclusion provider, but v6 is not
 	runner := VulnerabilityMatcher{
 		VulnerabilityProvider: store,
@@ -42,9 +41,7 @@ func FindVulnerabilitiesForPackage(store vulnerability.Provider, d *distro.Distr
 		NormalizeByCVE:        false,
 	}
 
-	actualResults, _, err := runner.FindMatches(packages, pkg.Context{
-		Distro: d,
-	})
+	actualResults, _, err := runner.FindMatches(packages, pkg.Context{})
 	if err != nil || actualResults == nil {
 		log.WithFields("error", err).Error("unable to find vulnerabilities")
 		return match.NewMatches()
diff --git a/grype/distro/distro.go b/grype/distro/distro.go
index cd5f4aa1..ffa1a98b 100644
--- a/grype/distro/distro.go
+++ b/grype/distro/distro.go
@@ -4,17 +4,18 @@ import (
 	"fmt"
 	"strings"
 
-	hashiVer "github.com/hashicorp/go-version"
-
+	"github.com/anchore/grype/grype/version"
 	"github.com/anchore/grype/internal/log"
+	"github.com/anchore/grype/internal/stringutil"
 	"github.com/anchore/syft/syft/linux"
 )
 
 // Distro represents a Linux Distribution.
 type Distro struct {
 	Type     Type
-	Version  string
-	Codename string
+	Version  string   // major.minor.patch
+	Codename string   // in lieu of a version e.g. "fossa" instead of "20.04"
+	Channels []string // distinguish between different feeds for fix and vulnerability data, e.g. "eus" for RHEL
 	IDLike   []string
 
 	// fields populated in the constructor
@@ -26,23 +27,7 @@ type Distro struct {
 
 // New creates a new Distro object populated with the given values.
 func New(t Type, version, label string, idLikes ...string) *Distro {
-	var major, minor, remaining string
-	if version != "" {
-		// if starts with a digit, then assume it's a version and extract the major, minor, and remaining versions
-		if version[0] >= '0' && version[0] <= '9' {
-			// extract the major, minor, and remaining versions
-			parts := strings.Split(version, ".")
-			if len(parts) > 0 {
-				major = parts[0]
-				if len(parts) > 1 {
-					minor = parts[1]
-				}
-				if len(parts) > 2 {
-					remaining = strings.Join(parts[2:], ".")
-				}
-			}
-		}
-	}
+	major, minor, remaining, versionWithoutSuffix, channels := parseVersion(version)
 
 	for i := range idLikes {
 		typ, ok := IDMapping[strings.TrimSpace(idLikes[i])]
@@ -52,16 +37,50 @@ func New(t Type, version, label string, idLikes ...string) *Distro {
 	}
 
 	return &Distro{
-		Type:      t,
+		Type:     t,
+		Version:  versionWithoutSuffix,
+		Codename: label,
+		IDLike:   idLikes,
+		Channels: channels,
+
 		major:     major,
 		minor:     minor,
 		remaining: remaining,
-		Version:   version,
-		Codename:  label,
-		IDLike:    idLikes,
 	}
 }
 
+func parseVersion(version string) (major, minor, remaining, versionWithoutSuffix string, channels []string) {
+	if version == "" {
+		return "", "", "", "", nil
+	}
+
+	versionWithoutSuffix = version
+	var channelStr string
+	if strings.Contains(version, "+") {
+		vParts := strings.SplitN(version, "+", 2)
+		version = vParts[0]
+		versionWithoutSuffix = version
+		channelStr = vParts[1]
+	}
+
+	// if starts with a digit, then assume it's a version and extract the major, minor, and remaining versions
+	if version[0] >= '0' && version[0] <= '9' {
+		// extract the major, minor, and remaining versions
+		parts := strings.Split(version, ".")
+		if len(parts) > 0 {
+			major = parts[0]
+			if len(parts) > 1 {
+				minor = parts[1]
+			}
+			if len(parts) > 2 {
+				remaining = strings.Join(parts[2:], ".")
+			}
+		}
+	}
+
+	return major, minor, remaining, versionWithoutSuffix, stringutil.SplitOnAny(strings.TrimSpace(channelStr), ",", "+")
+}
+
 // NewFromNameVersion creates a new Distro object derived from the provided name and version
 func NewFromNameVersion(name, version string) *Distro {
 	var codename string
@@ -76,15 +95,16 @@ func NewFromNameVersion(name, version string) *Distro {
 	if typ == "" {
 		typ = Type(name)
 	}
+
 	return New(typ, version, codename, string(typ))
 }
 
 // FromRelease attempts to get a distro from the linux release, only logging any errors
-func FromRelease(linuxRelease *linux.Release) *Distro {
+func FromRelease(linuxRelease *linux.Release, channels []FixChannel) *Distro {
 	if linuxRelease == nil {
 		return nil
 	}
-	d, err := NewFromRelease(*linuxRelease)
+	d, err := NewFromRelease(*linuxRelease, channels)
 	if err != nil {
 		log.WithFields("error", err).Warn("unable to create distro from linux distribution")
 	}
@@ -92,22 +112,26 @@ func FromRelease(linuxRelease *linux.Release) *Distro {
 }
 
 // NewFromRelease creates a new Distro object derived from a syft linux.Release object.
-func NewFromRelease(release linux.Release) (*Distro, error) {
+func NewFromRelease(release linux.Release, channels []FixChannel) (*Distro, error) {
 	t := TypeFromRelease(release)
 	if t == "" {
 		return nil, fmt.Errorf("unable to determine distro type")
 	}
 
-	var selectedVersion string
+	var (
+		selectedVersion    string
+		selectedVersionObj *version.Version
+	)
 
-	for _, version := range []string{release.VersionID, release.Version} {
-		if version == "" {
+	for _, ver := range []string{release.VersionID, release.Version} {
+		if ver == "" {
 			continue
 		}
 
-		_, err := hashiVer.NewVersion(version)
-		if err == nil {
-			selectedVersion = version
+		selectedVersionObj = version.New(ver, version.SemanticFormat)
+
+		if selectedVersionObj.Validate() == nil {
+			selectedVersion = ver
 			break
 		}
 	}
@@ -116,7 +140,10 @@ func NewFromRelease(release linux.Release) (*Distro, error) {
 		selectedVersion = release.VersionID
 	}
 
-	return New(t, selectedVersion, release.VersionCodename, release.IDLike...), nil
+	d := New(t, selectedVersion, release.VersionCodename, release.IDLike...)
+	d.Channels = applyChannels(release, selectedVersionObj, d.Channels, channels)
+
+	return d, nil
 }
 
 func (d Distro) Name() string {
@@ -139,16 +166,31 @@ func (d Distro) RemainingVersion() string {
 
 // String returns a human-friendly representation of the Linux distribution.
 func (d Distro) String() string {
-	versionStr := "(version unknown)"
+	return fmt.Sprintf("%s %s", d.ID(), d.VersionString())
+}
+
+func (d Distro) ID() string {
+	return typeToIDMapping[d.Type]
+}
+
+func (d Distro) VersionString() string {
+	versionStr := ""
 	if d.Version != "" {
 		versionStr = d.Version
 	} else if d.Codename != "" {
 		versionStr = d.Codename
 	}
-	return fmt.Sprintf("%s %s", d.Type, versionStr)
+
+	channels := nonEmptyStrings(d.Channels...)
+
+	if len(channels) > 0 {
+		versionStr += "+" + strings.Join(channels, ",")
+	}
+
+	return versionStr
 }
 
-// Unsupported Linux distributions
+// Disabled is a way to convey if a Linux distribution is not supported by Grype.
 func (d Distro) Disabled() bool {
 	switch d.Type {
 	case ArchLinux:
@@ -157,3 +199,12 @@ func (d Distro) Disabled() bool {
 		return false
 	}
 }
+
+func nonEmptyStrings(ss ...string) (res []string) {
+	for _, s := range ss {
+		if s != "" {
+			res = append(res, s)
+		}
+	}
+	return res
+}
diff --git a/grype/distro/fix_channel.go b/grype/distro/fix_channel.go
new file mode 100644
index 00000000..bfede8de
--- /dev/null
+++ b/grype/distro/fix_channel.go
@@ -0,0 +1,123 @@
+package distro
+
+import (
+	"github.com/scylladb/go-set/strset"
+
+	"github.com/anchore/grype/grype/version"
+	"github.com/anchore/grype/internal/log"
+	"github.com/anchore/syft/syft/linux"
+)
+
+type FixChannelEnabled string
+
+const (
+	// ChannelNeverEnabled means that the channel should never be applied to the distro
+	ChannelNeverEnabled FixChannelEnabled = "never"
+
+	// ChannelAlwaysEnabled means that the channel should always be applied to the distro
+	ChannelAlwaysEnabled FixChannelEnabled = "always"
+
+	// ChannelConditionallyEnabled means that the channel should conditionally be applied to the distro if there is SBOM material that indicates the channel was configured at build time
+	ChannelConditionallyEnabled FixChannelEnabled = "auto"
+)
+
+// FixChannel represents a subscription or repository where package fixes and updates are provided for a Linux distribution
+type FixChannel struct {
+	// Name is the name of the channel, e.g. "eus" for RHEL
+	Name string
+
+	// IDs is a list of distro release IDs that this channel applies to, e.g. "rhel" for RHEL (this is relative to the /etc/os-release ID field)
+	IDs []string
+
+	// Apply indicates how the channel should be applied to the distro
+	Apply FixChannelEnabled
+
+	// Versions is a version constraint that indicates which versions of the distro this channel applies to (e.g. ">= 8.0" for RHEL 8 and above)
+	Versions version.Constraint
+}
+
+type FixChannels []FixChannel
+
+func DefaultFixChannels() FixChannels {
+	return []FixChannel{
+		{
+			Name:     "eus",
+			IDs:      []string{"rhel"},
+			Apply:    ChannelNeverEnabled, // this will change to ChannelConditionallyEnabled once there is data in the DB to support it
+			Versions: version.MustGetConstraint(">= 8.0", version.SemanticFormat),
+		},
+	}
+}
+
+func (f FixChannels) Apply(enable FixChannelEnabled) FixChannels {
+	for i := range f {
+		f[i].Apply = enable
+	}
+	return f
+}
+
+func applyChannels(release linux.Release, ver *version.Version, existingChannels []string, channels []FixChannel) []string {
+	existingChannelSet := strset.New(existingChannels...)
+	var result []string
+
+	addResult := func(channel string, extendedSupport bool, pref FixChannelEnabled) {
+		res := applyChannel(channel, extendedSupport, pref)
+		if res != "" {
+			result = append(result, res)
+		}
+	}
+
+	for _, channel := range channels {
+		var found bool
+		for _, channelID := range channel.IDs {
+			if release.ID == channelID {
+				found = true
+				break
+			}
+		}
+		if !found {
+			continue
+		}
+
+		// we will either get a direct indication as a flag, or as a result of the channel being applied to the distro already
+		extendedSupport := release.ExtendedSupport || existingChannelSet.Has(channel.Name)
+
+		if ver == nil && release.VersionCodename != "" {
+			// TODO: there is not a good way to do this without a DB call, so for now we will assume the channel applies
+			log.Debugf("using channel %q for distro %q with codename %q", channel.Name, release.ID, release.VersionCodename)
+
+			addResult(channel.Name, extendedSupport, channel.Apply)
+			continue
+		}
+
+		if channel.Versions != nil && ver != nil {
+			isApplicable, err := channel.Versions.Satisfied(ver)
+			if err != nil {
+				log.WithFields("error", err, "constraint", channel.Versions).Debugf("unable to determine if channel %q is applicable for distro %q with version %q", channel.Name, release.ID, ver)
+				continue
+			}
+			if isApplicable {
+				log.Debugf("using channel %q for distro %q with version %q", channel.Name, release.ID, ver)
+				addResult(channel.Name, extendedSupport, channel.Apply)
+				continue
+			}
+		}
+		log.Debugf("using channel %q for distro %q", channel.Name, release.ID)
+		addResult(channel.Name, extendedSupport, channel.Apply)
+	}
+	return result
+}
+
+func applyChannel(channel string, hintsExtendedSupport bool, pref FixChannelEnabled) string {
+	switch pref {
+	case ChannelNeverEnabled:
+		return ""
+	case ChannelAlwaysEnabled:
+		return channel
+	case ChannelConditionallyEnabled:
+		if hintsExtendedSupport {
+			return channel
+		}
+	}
+	return ""
+}
diff --git a/grype/distro/type.go b/grype/distro/type.go
index 4d990a62..5c464e5c 100644
--- a/grype/distro/type.go
+++ b/grype/distro/type.go
@@ -62,7 +62,7 @@ var All = []Type{
 	MinimOS,
 }
 
-// IDMapping connects a distro ID like "ubuntu" to a Distro type
+// IDMapping maps a distro ID from the /etc/os-release (e.g. like "ubuntu") to a Distro type.
 var IDMapping = map[string]Type{
 	"debian":        Debian,
 	"ubuntu":        Ubuntu,
@@ -70,7 +70,6 @@ var IDMapping = map[string]Type{
 	"centos":        CentOS,
 	"fedora":        Fedora,
 	"alpine":        Alpine,
-	"Alpine Linux":  Alpine,
 	"busybox":       Busybox,
 	"amzn":          AmazonLinux,
 	"ol":            OracleLinux,
@@ -79,7 +78,6 @@ var IDMapping = map[string]Type{
 	"sles":          SLES,
 	"photon":        Photon,
 	"echo":          Echo,
-	"windows":       Windows,
 	"mariner":       Mariner,
 	"azurelinux":    Azure,
 	"rocky":         RockyLinux,
@@ -90,10 +88,30 @@ var IDMapping = map[string]Type{
 	"minimos":       MinimOS,
 }
 
+// aliasTypes maps common aliases to their corresponding Type.
+var aliasTypes = map[string]Type{
+	"Alpine Linux": Alpine, // needed for CPE matching (see #2039)
+	"windows":      Windows,
+}
+
+var typeToIDMapping = map[Type]string{}
+
+func init() {
+	for id, t := range IDMapping {
+		if _, ok := typeToIDMapping[t]; ok {
+			panic("duplicate Type found for ID: " + id + " with Type: " + string(t))
+		}
+		typeToIDMapping[t] = id
+	}
+}
+
 func TypeFromRelease(release linux.Release) Type {
 	// first try the release ID
-	t, ok := IDMapping[release.ID]
-	if ok {
+	if t, ok := IDMapping[release.ID]; ok {
+		return t
+	}
+
+	if t, ok := aliasTypes[release.ID]; ok {
 		return t
 	}
 
@@ -102,11 +120,17 @@ func TypeFromRelease(release linux.Release) Type {
 		if t, ok := IDMapping[l]; ok {
 			return t
 		}
+		if t, ok := aliasTypes[l]; ok {
+			return t
+		}
+	}
+
+	// then try the release name as a fallback
+	if t, ok := IDMapping[release.Name]; ok {
+		return t
 	}
 
-	// first try the release name as a fallback
-	t, ok = IDMapping[release.Name]
-	if ok {
+	if t, ok := aliasTypes[release.Name]; ok {
 		return t
 	}
 
diff --git a/grype/matcher/apk/matcher.go b/grype/matcher/apk/matcher.go
index dc7de907..27155f79 100644
--- a/grype/matcher/apk/matcher.go
+++ b/grype/matcher/apk/matcher.go
@@ -91,7 +91,7 @@ func (m *Matcher) cpeMatchesWithoutSecDBFixes(provider vulnerability.Provider, p
 
 	secDBVulnerabilitiesByID := vulnerabilitiesByID(secDBVulnerabilities)
 
-	verObj := version.NewVersionFromPkg(p)
+	verObj := version.New(p.Version, pkg.VersionFormat(p))
 
 	var finalCpeMatches []match.Match
 
diff --git a/grype/matcher/internal/cpe.go b/grype/matcher/internal/cpe.go
index fe501102..a08cf933 100644
--- a/grype/matcher/internal/cpe.go
+++ b/grype/matcher/internal/cpe.go
@@ -68,7 +68,7 @@ func MatchPackageByCPEs(provider vulnerability.Provider, p pkg.Package, upstream
 		// may alter the version based on above processing)
 		c.Attributes.Version = searchVersion
 
-		format := version.FormatFromPkg(p)
+		format := pkg.VersionFormat(p)
 
 		if format == version.JVMFormat {
 			searchVersion = transformJvmVersion(searchVersion, c.Attributes.Update)
@@ -77,7 +77,7 @@ func MatchPackageByCPEs(provider vulnerability.Provider, p pkg.Package, upstream
 		var verObj *version.Version
 		var err error
 		if searchVersion != "" {
-			verObj = version.NewVersion(searchVersion, format)
+			verObj = version.New(searchVersion, format)
 		}
 
 		// find all vulnerability records in the DB for the given CPE (not including version comparisons)
diff --git a/grype/matcher/internal/distro.go b/grype/matcher/internal/distro.go
index e757f249..68a8a0a4 100644
--- a/grype/matcher/internal/distro.go
+++ b/grype/matcher/internal/distro.go
@@ -27,7 +27,7 @@ func MatchPackageByDistro(provider vulnerability.Provider, searchPkg pkg.Package
 		search.ByPackageName(searchPkg.Name),
 		search.ByDistro(*searchPkg.Distro),
 		onlyQualifiedPackages(searchPkg),
-		onlyVulnerableVersions(version.NewVersionFromPkg(searchPkg)),
+		onlyVulnerableVersions(version.New(searchPkg.Version, pkg.VersionFormat(searchPkg))),
 	)
 	if err != nil {
 		return nil, nil, fmt.Errorf("matcher failed to fetch distro=%q pkg=%q: %w", searchPkg.Distro, searchPkg.Name, err)
diff --git a/grype/matcher/internal/language.go b/grype/matcher/internal/language.go
index 2580ba59..853324c0 100644
--- a/grype/matcher/internal/language.go
+++ b/grype/matcher/internal/language.go
@@ -38,7 +38,7 @@ func MatchPackageByEcosystemPackageName(provider vulnerability.Provider, p pkg.P
 		search.ByEcosystem(p.Language, p.Type),
 		search.ByPackageName(packageName),
 		onlyQualifiedPackages(p),
-		onlyVulnerableVersions(version.NewVersionFromPkg(p)),
+		onlyVulnerableVersions(version.New(p.Version, pkg.VersionFormat(p))),
 		onlyNonWithdrawnVulnerabilities(),
 	)
 	if err != nil {
diff --git a/grype/pkg/java_metadata.go b/grype/pkg/java_metadata.go
index 328c53bd..ca18ba68 100644
--- a/grype/pkg/java_metadata.go
+++ b/grype/pkg/java_metadata.go
@@ -30,7 +30,7 @@ type JavaVMReleaseMetadata struct {
 	SemanticVersion    string `json:"semanticVersion,omitempty"`
 }
 
-func IsJvmPackage(p Package) bool {
+func isJvmPackage(p Package) bool {
 	if _, ok := p.Metadata.(JavaVMInstallationMetadata); ok {
 		return true
 	}
diff --git a/grype/pkg/package.go b/grype/pkg/package.go
index ee4d8ea1..68308776 100644
--- a/grype/pkg/package.go
+++ b/grype/pkg/package.go
@@ -3,7 +3,6 @@ package pkg
 import (
 	"fmt"
 	"regexp"
-	"slices"
 	"strings"
 
 	"github.com/anchore/grype/grype/distro"
@@ -45,6 +44,8 @@ type Package struct {
 	Metadata  interface{} // This is NOT 1-for-1 the syft metadata! Only the select data needed for vulnerability matching
 }
 
+type Enhancer func(out *Package, purl packageurl.PackageURL, pkg syftPkg.Package)
+
 func New(p syftPkg.Package, enhancers ...Enhancer) Package {
 	metadata, upstreams := dataFromPkg(p)
 
@@ -89,26 +90,38 @@ func FromCollection(catalog *syftPkg.Collection, config SynthesisConfig, enhance
 	return FromPackages(catalog.Sorted(), config, enhancers...)
 }
 
-func FromPackages(syftpkgs []syftPkg.Package, config SynthesisConfig, enhancers ...Enhancer) []Package {
+func FromPackages(syftPkgs []syftPkg.Package, config SynthesisConfig, enhancers ...Enhancer) []Package {
 	var pkgs []Package
-	for _, p := range syftpkgs {
+
+	// if the user provided a distro explicitly, then use that over any distro that may be inferred from a package url
+	enhancers = append([]Enhancer{applyDistroOverride(config.Distro.Override)}, enhancers...)
+
+	for _, p := range syftPkgs {
 		if len(p.CPEs) == 0 {
-			// For SPDX (or any format, really) we may have no CPEs
+			// for SPDX (or any format, really) we may have no CPEs
 			if config.GenerateMissingCPEs {
 				p.CPEs = cpes.Generate(p)
 			} else {
 				log.Debugf("no CPEs for package: %s", p)
 			}
 		}
+
 		pkgs = append(pkgs, New(p, enhancers...))
 	}
 
 	return pkgs
 }
 
-// Stringer to represent a package.
 func (p Package) String() string {
-	return fmt.Sprintf("Pkg(type=%s, name=%s, version=%s, upstreams=%d)", p.Type, p.Name, p.Version, len(p.Upstreams))
+	var d string
+	if p.Distro != nil {
+		d = fmt.Sprintf(", distro=%s", p.Distro.String())
+	}
+	var u string
+	if len(p.Upstreams) > 0 {
+		u = fmt.Sprintf(", upstreams=%d", len(p.Upstreams))
+	}
+	return fmt.Sprintf("Pkg(type=%s, name=%s, version=%s%s%s)", p.Type, p.Name, p.Version, u, d)
 }
 
 func removePackagesByOverlap(catalog *syftPkg.Collection, relationships []artifact.Relationship, distro *distro.Distro) *syftPkg.Collection {
@@ -493,54 +506,13 @@ func handleDefaultUpstream(pkgName string, value string) []UpstreamPackage {
 	return nil
 }
 
-func setUpstreamsFromPURL(out *Package, purl packageurl.PackageURL, syftPkg syftPkg.Package) {
-	if len(out.Upstreams) == 0 {
-		out.Upstreams = upstreamsFromPURL(purl, syftPkg.Type)
-	}
-}
-
-// upstreamsFromPURL reads any additional data Grype can use, which is ignored by Syft's PURL conversion
-func upstreamsFromPURL(purl packageurl.PackageURL, pkgType syftPkg.Type) (upstreams []UpstreamPackage) {
-	for _, qualifier := range purl.Qualifiers {
-		if qualifier.Key == syftPkg.PURLQualifierUpstream {
-			for _, newUpstream := range parseUpstream(purl.Name, qualifier.Value, pkgType) {
-				if slices.Contains(upstreams, newUpstream) {
-					continue
-				}
-				upstreams = append(upstreams, newUpstream)
-			}
-		}
-	}
-	return upstreams
-}
-
-func setDistroFromPURL(out *Package, purl packageurl.PackageURL, _ syftPkg.Package) {
-	if out.Distro == nil {
-		out.Distro = distroFromPURL(purl)
-	}
-}
-
-// distroFromPURL reads distro data for Grype can use, which is ignored by Syft's PURL conversion
-func distroFromPURL(purl packageurl.PackageURL) (d *distro.Distro) {
-	var distroName, distroVersion string
-
-	for _, qualifier := range purl.Qualifiers {
-		if qualifier.Key == syftPkg.PURLQualifierDistro {
-			fields := strings.SplitN(qualifier.Value, "-", 2)
-			distroName = fields[0]
-			if len(fields) > 1 {
-				distroVersion = fields[1]
-			}
+func applyDistroOverride(override *distro.Distro) Enhancer {
+	// the override here comes from either the --distro flag, which already has a channel indication applied
+	return func(out *Package, _ packageurl.PackageURL, _ syftPkg.Package) {
+		if override == nil {
+			return
 		}
+		// allow downstream matchers to always consider the given user distro
+		out.Distro = override
 	}
-
-	if distroName != "" {
-		d = distro.NewFromNameVersion(distroName, distroVersion)
-	}
-
-	return d
 }
-
-type Enhancer func(out *Package, purl packageurl.PackageURL, pkg syftPkg.Package)
-
-var purlEnhancers = []Enhancer{setUpstreamsFromPURL, setDistroFromPURL}
diff --git a/grype/pkg/provider.go b/grype/pkg/provider.go
index e06af83b..0db7e06c 100644
--- a/grype/pkg/provider.go
+++ b/grype/pkg/provider.go
@@ -3,8 +3,10 @@ package pkg
 import (
 	"errors"
 	"fmt"
+	"strings"
 
 	"github.com/bmatcuk/doublestar/v2"
+	"github.com/scylladb/go-set/strset"
 
 	"github.com/anchore/grype/grype/distro"
 	"github.com/anchore/grype/internal/log"
@@ -16,17 +18,96 @@ var errDoesNotProvide = fmt.Errorf("cannot provide packages from the given sourc
 
 // Provide a set of packages and context metadata describing where they were sourced from.
 func Provide(userInput string, config ProviderConfig) ([]Package, Context, *sbom.SBOM, error) {
-	packages, ctx, s, err := provide(userInput, config)
+	applyChannel := getDistroChannelApplier(config.Distro.FixChannels)
+	if config.Distro.Override != nil {
+		applyChannel(config.Distro.Override)
+		log.Infof("using distro: %s", config.Distro.Override.String())
+	}
+
+	packages, ctx, s, err := provide(userInput, config, applyChannel)
 	if err != nil {
 		return nil, Context{}, nil, err
 	}
 	setContextDistro(packages, &ctx)
+
+	// set the distro on each package if there is not already one set
+	if ctx.Distro != nil {
+		for i := range packages {
+			if packages[i].Distro == nil {
+				packages[i].Distro = ctx.Distro
+			}
+		}
+
+		if config.Distro.Override == nil {
+			log.Infof("using distro: %s", ctx.Distro.String())
+		}
+	}
+
 	return packages, ctx, s, nil
 }
 
+func getDistroChannelApplier(channels []distro.FixChannel) func(d *distro.Distro) bool { // nolint:gocognit
+	// build a map of channel names to channels
+	idx := make(map[string][]distro.FixChannel, len(channels))
+	for _, c := range channels {
+		if c.Name == "" {
+			continue
+		}
+		for _, id := range c.IDs {
+			if id == "" {
+				continue
+			}
+			id = strings.ToLower(id)
+			idx[id] = append(idx[id], c)
+		}
+	}
+
+	return func(d *distro.Distro) bool {
+		if d == nil {
+			return false
+		}
+
+		var result []string
+
+		id := strings.ToLower(d.ID())
+		channels, ok := idx[id]
+		if !ok {
+			return false
+		}
+
+		existing := strset.New(d.Channels...)
+
+		var modified bool
+		for _, channel := range channels {
+			if channel.Name == "" {
+				continue
+			}
+			switch channel.Apply {
+			case distro.ChannelNeverEnabled:
+				if existing.Has(channel.Name) {
+					modified = true
+				}
+			case distro.ChannelAlwaysEnabled:
+				result = append(result, channel.Name)
+				if !existing.Has(channel.Name) {
+					modified = true
+				}
+			case distro.ChannelConditionallyEnabled:
+				if existing.Has(channel.Name) {
+					result = append(result, channel.Name)
+				}
+			}
+		}
+
+		d.Channels = result
+
+		return modified
+	}
+}
+
 // Provide a set of packages and context metadata describing where they were sourced from.
-func provide(userInput string, config ProviderConfig) ([]Package, Context, *sbom.SBOM, error) {
-	packages, ctx, s, err := purlProvider(userInput, config)
+func provide(userInput string, config ProviderConfig, applyChannel func(d *distro.Distro) bool) ([]Package, Context, *sbom.SBOM, error) {
+	packages, ctx, s, err := purlProvider(userInput, config, applyChannel)
 	if !errors.Is(err, errDoesNotProvide) {
 		log.WithFields("input", userInput).Trace("interpreting input as one or more PURLs")
 		return packages, ctx, s, err
@@ -38,7 +119,7 @@ func provide(userInput string, config ProviderConfig) ([]Package, Context, *sbom
 		return packages, ctx, s, err
 	}
 
-	packages, ctx, s, err = syftSBOMProvider(userInput, config)
+	packages, ctx, s, err = syftSBOMProvider(userInput, config, applyChannel)
 	if !errors.Is(err, errDoesNotProvide) {
 		if len(config.Exclusions) > 0 {
 			var exclusionsErr error
@@ -52,7 +133,7 @@ func provide(userInput string, config ProviderConfig) ([]Package, Context, *sbom
 	}
 
 	log.WithFields("input", userInput).Trace("passing input to syft for interpretation")
-	return syftProvider(userInput, config)
+	return syftProvider(userInput, config, applyChannel)
 }
 
 // This will filter the provided packages list based on a set of exclusion expressions. Globs
@@ -117,9 +198,11 @@ func setContextDistro(packages []Package, ctx *Context) {
 			singleDistro = p.Distro
 			continue
 		}
+		// if we have a distro already, ensure that the new one matches...
 		if singleDistro.Type != p.Distro.Type ||
 			singleDistro.Version != p.Distro.Version ||
 			singleDistro.Codename != p.Distro.Codename {
+			// ...if not then we bail, not setting a singular distro in the context
 			return
 		}
 	}
diff --git a/grype/pkg/provider_config.go b/grype/pkg/provider_config.go
index 978f5da3..d18a5a19 100644
--- a/grype/pkg/provider_config.go
+++ b/grype/pkg/provider_config.go
@@ -1,6 +1,7 @@
 package pkg
 
 import (
+	"github.com/anchore/grype/grype/distro"
 	"github.com/anchore/stereoscope/pkg/image"
 	"github.com/anchore/syft/syft"
 )
@@ -21,4 +22,10 @@ type SyftProviderConfig struct {
 
 type SynthesisConfig struct {
 	GenerateMissingCPEs bool
+	Distro              DistroConfig
+}
+
+type DistroConfig struct {
+	Override    *distro.Distro
+	FixChannels []distro.FixChannel
 }
diff --git a/grype/pkg/purl_provider.go b/grype/pkg/purl_provider.go
index 740f3496..d6f901e9 100644
--- a/grype/pkg/purl_provider.go
+++ b/grype/pkg/purl_provider.go
@@ -3,9 +3,13 @@ package pkg
 import (
 	"fmt"
 	"io"
+	"slices"
 	"strings"
 
+	"github.com/anchore/grype/grype/distro"
+	"github.com/anchore/packageurl-go"
 	"github.com/anchore/syft/syft/format"
+	syftPkg "github.com/anchore/syft/syft/pkg"
 	"github.com/anchore/syft/syft/sbom"
 	"github.com/anchore/syft/syft/source"
 )
@@ -19,7 +23,11 @@ type PURLLiteralMetadata struct {
 	PURL string
 }
 
-func purlProvider(userInput string, config ProviderConfig) ([]Package, Context, *sbom.SBOM, error) {
+func purlEnhancers(applyChannel func(*distro.Distro) bool) []Enhancer {
+	return []Enhancer{setUpstreamsFromPURL, setDistroFromPURL(applyChannel)}
+}
+
+func purlProvider(userInput string, config ProviderConfig, applyChannel func(*distro.Distro) bool) ([]Package, Context, *sbom.SBOM, error) {
 	reader, ctx, err := getPurlReader(userInput)
 	if err != nil {
 		return nil, Context{}, nil, err
@@ -30,7 +38,7 @@ func purlProvider(userInput string, config ProviderConfig) ([]Package, Context,
 		return nil, Context{}, nil, fmt.Errorf("unable to decode purl: %w", err)
 	}
 
-	return FromCollection(s.Artifacts.Packages, config.SynthesisConfig, purlEnhancers...), ctx, s, nil
+	return FromCollection(s.Artifacts.Packages, config.SynthesisConfig, purlEnhancers(applyChannel)...), ctx, s, nil
 }
 
 func getPurlReader(userInput string) (r io.Reader, ctx Context, err error) {
@@ -44,3 +52,54 @@ func getPurlReader(userInput string) (r io.Reader, ctx Context, err error) {
 	}
 	return nil, ctx, errDoesNotProvide
 }
+
+func setUpstreamsFromPURL(out *Package, purl packageurl.PackageURL, syftPkg syftPkg.Package) {
+	if len(out.Upstreams) == 0 || out.PURL == "" {
+		out.Upstreams = upstreamsFromPURL(purl, syftPkg.Type)
+	}
+}
+
+// upstreamsFromPURL reads any additional data Grype can use, which is ignored by Syft's PURL conversion
+func upstreamsFromPURL(purl packageurl.PackageURL, pkgType syftPkg.Type) (upstreams []UpstreamPackage) {
+	for _, qualifier := range purl.Qualifiers {
+		if qualifier.Key == syftPkg.PURLQualifierUpstream {
+			for _, newUpstream := range parseUpstream(purl.Name, qualifier.Value, pkgType) {
+				if slices.Contains(upstreams, newUpstream) {
+					continue
+				}
+				upstreams = append(upstreams, newUpstream)
+			}
+		}
+	}
+	return upstreams
+}
+
+func setDistroFromPURL(applyChannel func(*distro.Distro) bool) func(out *Package, purl packageurl.PackageURL, _ syftPkg.Package) {
+	return func(out *Package, purl packageurl.PackageURL, _ syftPkg.Package) {
+		if out.Distro == nil {
+			out.Distro = distroFromPURL(purl)
+			applyChannel(out.Distro)
+		}
+	}
+}
+
+// distroFromPURL reads distro data for Grype can use, which is ignored by Syft's PURL conversion
+func distroFromPURL(purl packageurl.PackageURL) (d *distro.Distro) {
+	var distroName, distroVersion string
+
+	for _, qualifier := range purl.Qualifiers {
+		if qualifier.Key == syftPkg.PURLQualifierDistro {
+			fields := strings.SplitN(qualifier.Value, "-", 2)
+			distroName = fields[0]
+			if len(fields) > 1 {
+				distroVersion = fields[1]
+			}
+		}
+	}
+
+	if distroName != "" {
+		d = distro.NewFromNameVersion(distroName, distroVersion)
+	}
+
+	return d
+}
diff --git a/grype/pkg/syft_provider.go b/grype/pkg/syft_provider.go
index 9bd8a609..7f43aba1 100644
--- a/grype/pkg/syft_provider.go
+++ b/grype/pkg/syft_provider.go
@@ -15,7 +15,7 @@ import (
 	"github.com/anchore/syft/syft/source/sourceproviders"
 )
 
-func syftProvider(userInput string, config ProviderConfig) ([]Package, Context, *sbom.SBOM, error) {
+func syftProvider(userInput string, config ProviderConfig, applyChannel func(*distro.Distro) bool) ([]Package, Context, *sbom.SBOM, error) {
 	src, err := getSource(userInput, config)
 	if err != nil {
 		return nil, Context{}, nil, err
@@ -33,7 +33,7 @@ func syftProvider(userInput string, config ProviderConfig) ([]Package, Context,
 
 	srcDescription := src.Describe()
 
-	d := distro.FromRelease(s.Artifacts.LinuxDistribution)
+	d := distroFromSBOM(s, config, applyChannel)
 
 	pkgCatalog := removePackagesByOverlap(s.Artifacts.Packages, s.Relationships, d)
 
@@ -46,6 +46,16 @@ func syftProvider(userInput string, config ProviderConfig) ([]Package, Context,
 	return packages, pkgCtx, s, nil
 }
 
+func distroFromSBOM(s *sbom.SBOM, config ProviderConfig, applyChannel func(*distro.Distro) bool) (d *distro.Distro) {
+	if config.Distro.Override != nil {
+		d = config.Distro.Override
+	} else {
+		d = distro.FromRelease(s.Artifacts.LinuxDistribution, config.Distro.FixChannels)
+		applyChannel(d)
+	}
+	return d
+}
+
 func getSource(userInput string, config ProviderConfig) (source.Source, error) {
 	if config.SBOMOptions.Search.Scope == "" {
 		return nil, errDoesNotProvide
diff --git a/grype/pkg/syft_sbom_provider.go b/grype/pkg/syft_sbom_provider.go
index f2ebf9bb..b66eec5d 100644
--- a/grype/pkg/syft_sbom_provider.go
+++ b/grype/pkg/syft_sbom_provider.go
@@ -23,7 +23,7 @@ type SBOMFileMetadata struct {
 	Path string
 }
 
-func syftSBOMProvider(userInput string, config ProviderConfig) ([]Package, Context, *sbom.SBOM, error) {
+func syftSBOMProvider(userInput string, config ProviderConfig, applyChannel func(*distro.Distro) bool) ([]Package, Context, *sbom.SBOM, error) {
 	s, fmtID, path, err := getSBOM(userInput)
 	if err != nil {
 		return nil, Context{}, nil, err
@@ -36,13 +36,13 @@ func syftSBOMProvider(userInput string, config ProviderConfig) ([]Package, Conte
 		}
 	}
 
-	d := distro.FromRelease(s.Artifacts.LinuxDistribution)
+	d := distroFromSBOM(s, config, applyChannel)
 
 	catalog := removePackagesByOverlap(s.Artifacts.Packages, s.Relationships, d)
 
 	var enhancers []Enhancer
 	if fmtID != syftjson.ID {
-		enhancers = purlEnhancers
+		enhancers = purlEnhancers(applyChannel)
 	}
 
 	return FromCollection(catalog, config.SynthesisConfig, enhancers...), Context{
diff --git a/grype/pkg/version_format.go b/grype/pkg/version_format.go
new file mode 100644
index 00000000..26a88866
--- /dev/null
+++ b/grype/pkg/version_format.go
@@ -0,0 +1,37 @@
+package pkg
+
+import (
+	"github.com/anchore/grype/grype/version"
+	syftPkg "github.com/anchore/syft/syft/pkg"
+)
+
+func VersionFormat(p Package) version.Format {
+	switch p.Type {
+	case syftPkg.ApkPkg:
+		return version.ApkFormat
+	case syftPkg.BitnamiPkg:
+		return version.BitnamiFormat
+	case syftPkg.DebPkg:
+		return version.DebFormat
+	case syftPkg.JavaPkg:
+		return version.MavenFormat
+	case syftPkg.RpmPkg:
+		return version.RpmFormat
+	case syftPkg.GemPkg:
+		return version.GemFormat
+	case syftPkg.PythonPkg:
+		return version.PythonFormat
+	case syftPkg.KbPkg:
+		return version.KBFormat
+	case syftPkg.PortagePkg:
+		return version.PortageFormat
+	case syftPkg.GoModulePkg:
+		return version.GolangFormat
+	}
+
+	if isJvmPackage(p) {
+		return version.JVMFormat
+	}
+
+	return version.UnknownFormat
+}
diff --git a/grype/presenter/models/distribution.go b/grype/presenter/models/distribution.go
index a8b9d792..219423c9 100644
--- a/grype/presenter/models/distribution.go
+++ b/grype/presenter/models/distribution.go
@@ -2,25 +2,32 @@ package models
 
 import (
 	"github.com/anchore/grype/grype/distro"
+	"github.com/anchore/grype/grype/pkg"
 )
 
 // distribution provides information about a detected Linux distribution.
 type distribution struct {
-	Name    string   `json:"name"`    // Name of the Linux distribution
-	Version string   `json:"version"` // Version of the Linux distribution (major or major.minor version)
-	IDLike  []string `json:"idLike"`  // the ID_LIKE field found within the /etc/os-release file
+	Name     string   `json:"name"`               // Name of the Linux distribution
+	Version  string   `json:"version"`            // Version of the Linux distribution (major or major.minor version)
+	IDLike   []string `json:"idLike"`             // the ID_LIKE field found within the /etc/os-release file
+	Channels []string `json:"channels,omitempty"` // channels for the distribution, if available
 }
 
 // newDistribution creates a struct with the Linux distribution to be represented in JSON.
-func newDistribution(d *distro.Distro) distribution {
+func newDistribution(ctx pkg.Context, d *distro.Distro) distribution {
+	if ctx.Distro != nil {
+		// if the distro is provided in the context, use it
+		d = ctx.Distro
+	}
 	if d == nil {
 		return distribution{}
 	}
 
 	return distribution{
-		Name:    d.Name(),
-		Version: d.Version,
-		IDLike:  cleanIDLike(d.IDLike),
+		Name:     d.Name(),
+		Version:  d.Version,
+		IDLike:   cleanIDLike(d.IDLike),
+		Channels: d.Channels,
 	}
 }
 
diff --git a/grype/presenter/models/document.go b/grype/presenter/models/document.go
index 068486d1..51e3592d 100644
--- a/grype/presenter/models/document.go
+++ b/grype/presenter/models/document.go
@@ -5,6 +5,7 @@ import (
 	"time"
 
 	"github.com/anchore/clio"
+	"github.com/anchore/grype/grype/distro"
 	"github.com/anchore/grype/grype/match"
 	"github.com/anchore/grype/grype/pkg"
 	"github.com/anchore/grype/grype/vulnerability"
@@ -76,7 +77,7 @@ func NewDocument(id clio.Identification, packages []pkg.Package, context pkg.Con
 		Matches:        findings,
 		IgnoredMatches: ignoredMatchModels,
 		Source:         src,
-		Distro:         newDistribution(context.Distro),
+		Distro:         newDistribution(context, selectMostCommonDistro(packages)),
 		Descriptor: descriptor{
 			Name:          id.Name,
 			Version:       id.Version,
@@ -86,3 +87,30 @@ func NewDocument(id clio.Identification, packages []pkg.Package, context pkg.Con
 		},
 	}, nil
 }
+
+// selectMostCommonDistro selects the most common distro from the provided packages.
+func selectMostCommonDistro(pkgs []pkg.Package) *distro.Distro {
+	distros := make(map[string]*distro.Distro)
+	count := make(map[string]int)
+
+	var maxDistro *distro.Distro
+	maxCount := 0
+
+	for _, p := range pkgs {
+		if p.Distro != nil {
+			s := p.Distro.String()
+			count[s]++
+
+			if _, ok := distros[s]; !ok {
+				distros[s] = p.Distro
+			}
+
+			if count[s] > maxCount {
+				maxCount = count[s]
+				maxDistro = p.Distro
+			}
+		}
+	}
+
+	return maxDistro
+}
diff --git a/grype/presenter/models/match.go b/grype/presenter/models/match.go
index b7c4f9b3..2f35adee 100644
--- a/grype/presenter/models/match.go
+++ b/grype/presenter/models/match.go
@@ -56,7 +56,7 @@ func newMatch(m match.Match, p pkg.Package, metadataProvider vulnerability.Metad
 		}
 	}
 
-	format := version.FormatFromPkg(p)
+	format := pkg.VersionFormat(p)
 
 	details := make([]MatchDetails, len(m.Details))
 	for idx, d := range m.Details {
@@ -113,8 +113,8 @@ func calculateSuggestedFixedVersion(p pkg.Package, fixedVersions []string, forma
 	}
 
 	sort.SliceStable(fixedVersions, func(i, j int) bool {
-		v1 := version.NewVersion(fixedVersions[i], format)
-		v2 := version.NewVersion(fixedVersions[j], format)
+		v1 := version.New(fixedVersions[i], format)
+		v2 := version.New(fixedVersions[j], format)
 		err1 := v1.Validate()
 		err2 := v2.Validate()
 		if err1 != nil || err2 != nil {
diff --git a/grype/presenter/models/vulnerability.go b/grype/presenter/models/vulnerability.go
index 836888e4..5b680e62 100644
--- a/grype/presenter/models/vulnerability.go
+++ b/grype/presenter/models/vulnerability.go
@@ -66,7 +66,7 @@ func sortVersions(fixedVersions []string, format version.Format) []string {
 	versionObjs := make([]*version.Version, 0, len(fixedVersions))
 	var invalidVersions []string
 	for _, vStr := range fixedVersions {
-		v := version.NewVersion(vStr, format)
+		v := version.New(vStr, format)
 		err := v.Validate()
 		if err != nil {
 			log.WithFields("version", vStr, "error", err).Trace("error parsing version, skipping")
diff --git a/grype/version/deprecated.go b/grype/version/deprecated.go
new file mode 100644
index 00000000..9fef76b1
--- /dev/null
+++ b/grype/version/deprecated.go
@@ -0,0 +1,10 @@
+package version
+
+// NewVersion creates a new Version instance with the provided raw version string and format.
+// Deprecated: NewVersion is deprecated, use New instead.
+func NewVersion(raw string, format Format) *Version {
+	return &Version{
+		Raw:    raw,
+		Format: format,
+	}
+}
diff --git a/grype/version/format.go b/grype/version/format.go
index 0ca022f2..66d79213 100644
--- a/grype/version/format.go
+++ b/grype/version/format.go
@@ -1,11 +1,6 @@
 package version
 
-import (
-	"strings"
-
-	"github.com/anchore/grype/grype/pkg"
-	syftPkg "github.com/anchore/syft/syft/pkg"
-)
+import "strings"
 
 const (
 	UnknownFormat Format = iota
@@ -86,37 +81,6 @@ func ParseFormat(userStr string) Format {
 	return UnknownFormat
 }
 
-func FormatFromPkg(p pkg.Package) Format {
-	switch p.Type {
-	case syftPkg.ApkPkg:
-		return ApkFormat
-	case syftPkg.BitnamiPkg:
-		return BitnamiFormat
-	case syftPkg.DebPkg:
-		return DebFormat
-	case syftPkg.JavaPkg:
-		return MavenFormat
-	case syftPkg.RpmPkg:
-		return RpmFormat
-	case syftPkg.GemPkg:
-		return GemFormat
-	case syftPkg.PythonPkg:
-		return PythonFormat
-	case syftPkg.KbPkg:
-		return KBFormat
-	case syftPkg.PortagePkg:
-		return PortageFormat
-	case syftPkg.GoModulePkg:
-		return GolangFormat
-	}
-
-	if pkg.IsJvmPackage(p) {
-		return JVMFormat
-	}
-
-	return UnknownFormat
-}
-
 func (f Format) String() string {
 	if int(f) >= len(formatStr) || f < 0 {
 		return formatStr[0]
diff --git a/grype/version/version.go b/grype/version/version.go
index fe01c351..b0a7015d 100644
--- a/grype/version/version.go
+++ b/grype/version/version.go
@@ -3,8 +3,6 @@ package version
 import (
 	"errors"
 	"fmt"
-
-	"github.com/anchore/grype/grype/pkg"
 )
 
 var _ Comparator = (*Version)(nil)
@@ -15,20 +13,13 @@ type Version struct {
 	comparators map[Format]Comparator
 }
 
-func NewVersion(raw string, format Format) *Version {
+func New(raw string, format Format) *Version {
 	return &Version{
 		Raw:    raw,
 		Format: format,
 	}
 }
 
-func NewVersionFromPkg(p pkg.Package) *Version {
-	if p.Version == "" {
-		return nil
-	}
-	return NewVersion(p.Version, FormatFromPkg(p))
-}
-
 func (v *Version) Validate() error {
 	_, err := v.getComparator(v.Format)
 	return err
diff --git a/grype/vulnerability_matcher.go b/grype/vulnerability_matcher.go
index b991e03b..83065745 100644
--- a/grype/vulnerability_matcher.go
+++ b/grype/vulnerability_matcher.go
@@ -10,7 +10,6 @@ import (
 	"github.com/wagoodman/go-partybus"
 	"github.com/wagoodman/go-progress"
 
-	"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"
@@ -65,7 +64,7 @@ func (m *VulnerabilityMatcher) FindMatches(pkgs []pkg.Package, context pkg.Conte
 		}
 	}()
 
-	remainingMatches, ignoredMatches, err = m.findDBMatches(pkgs, context, progressMonitor)
+	remainingMatches, ignoredMatches, err = m.findDBMatches(pkgs, progressMonitor)
 	if err != nil {
 		err = fmt.Errorf("unable to find matches against vulnerability database: %w", err)
 		return remainingMatches, ignoredMatches, err
@@ -89,11 +88,11 @@ func (m *VulnerabilityMatcher) FindMatches(pkgs []pkg.Package, context pkg.Conte
 	return remainingMatches, ignoredMatches, nil
 }
 
-func (m *VulnerabilityMatcher) findDBMatches(pkgs []pkg.Package, context pkg.Context, progressMonitor *monitorWriter) (*match.Matches, []match.IgnoredMatch, error) {
+func (m *VulnerabilityMatcher) findDBMatches(pkgs []pkg.Package, progressMonitor *monitorWriter) (*match.Matches, []match.IgnoredMatch, error) {
 	var ignoredMatches []match.IgnoredMatch
 
 	log.Trace("finding matches against DB")
-	matches, err := m.searchDBForMatches(context.Distro, pkgs, progressMonitor)
+	matches, err := m.searchDBForMatches(pkgs, progressMonitor)
 	if err != nil {
 		if match.IsFatalError(err) {
 			return nil, nil, err
@@ -139,7 +138,6 @@ func (m *VulnerabilityMatcher) mergeIgnoredMatches(allIgnoredMatches ...[]match.
 
 //nolint:funlen
 func (m *VulnerabilityMatcher) searchDBForMatches(
-	d *distro.Distro,
 	packages []pkg.Package,
 	progressMonitor *monitorWriter,
 ) (match.Matches, error) {
@@ -156,12 +154,6 @@ func (m *VulnerabilityMatcher) searchDBForMatches(
 		progressMonitor.PackagesProcessed.Increment()
 		log.WithFields("package", displayPackage(p)).Trace("searching for vulnerability matches")
 
-		// if there is no distro set, default to global distro
-		orig := p.Distro
-		if orig == nil {
-			p.Distro = d
-		}
-
 		matchAgainst, ok := matcherIndex[p.Type]
 		if !ok {
 			matchAgainst = []match.Matcher{defaultMatcher}
@@ -194,8 +186,6 @@ func (m *VulnerabilityMatcher) searchDBForMatches(
 			// dropped: matches that are filtered out due to hard-coded rules
 			updateVulnerabilityList(progressMonitor, additionalMatches, nil, dropped, m.VulnerabilityProvider)
 		}
-
-		p.Distro = orig
 	}
 
 	// apply ignores based on matchers returning ignore rules
@@ -477,7 +467,7 @@ func logPackageMatches(p pkg.Package, matches []match.Match) {
 	log.WithFields("package", displayPackage(p)).Debugf("found %d vulnerabilities", len(matches))
 	for idx, m := range matches {
 		arm := selectArm(idx, len(matches))
-		log.WithFields("vuln", m.Vulnerability.ID, "namespace", m.Vulnerability.Namespace).Debugf("  %s", arm)
+		log.WithFields("vuln", m.Vulnerability.ID, "namespace", m.Vulnerability.Namespace).Tracef("  %s", arm)
 	}
 }
 
@@ -497,7 +487,7 @@ func logExplicitDroppedPackageMatches(p pkg.Package, ignored []match.IgnoredMatc
 	for idx, i := range ignored {
 		arm := selectArm(idx, len(ignored))
 
-		log.WithFields("vuln", i.Match.Vulnerability.ID, "rules", len(i.AppliedIgnoreRules)).Debugf("  %s", arm)
+		log.WithFields("vuln", i.Match.Vulnerability.ID, "rules", len(i.AppliedIgnoreRules)).Tracef("  %s", arm)
 	}
 }
 
diff --git a/internal/stringutil/string_helpers.go b/internal/stringutil/string_helpers.go
index 25d21f02..4e3cff8d 100644
--- a/internal/stringutil/string_helpers.go
+++ b/internal/stringutil/string_helpers.go
@@ -1,6 +1,9 @@
 package stringutil
 
-import "strings"
+import (
+	"sort"
+	"strings"
+)
 
 // HasAnyOfSuffixes returns an indication if the given string has any of the given suffixes.
 func HasAnyOfSuffixes(input string, suffixes ...string) bool {
@@ -34,3 +37,45 @@ func SplitCommaSeparatedString(input string) []string {
 	}
 	return output
 }
+
+// SplitOnFirstString splits the input string on the first occurrence of any of the provided separators.
+func SplitOnFirstString(s string, separators ...string) (before, after string) {
+	minIdx := len(s)
+	foundSep := ""
+
+	for _, sep := range separators {
+		if idx := strings.Index(s, sep); idx != -1 && idx < minIdx {
+			minIdx = idx
+			foundSep = sep
+		}
+	}
+
+	if foundSep == "" {
+		return s, ""
+	}
+
+	return s[:minIdx], s[minIdx+len(foundSep):]
+}
+
+func SplitOnAny(s string, separators ...string) []string {
+	if s == "" {
+		return nil
+	}
+	parts := []string{s}
+
+	// sort separators by length in descending order to ensure longer separators are processed first.
+	// This isn't foolproof, but it helps with common cases where longer separators should take precedence.
+	separators = append([]string{}, separators...)
+	sort.Slice(separators, func(i, j int) bool {
+		return len(separators[i]) > len(separators[j])
+	})
+
+	for _, sep := range separators {
+		var newParts []string
+		for _, part := range parts {
+			newParts = append(newParts, strings.Split(part, sep)...)
+		}
+		parts = newParts
+	}
+	return parts
+}


Test patch:
diff --git a/cmd/grype/cli/commands/root_test.go b/cmd/grype/cli/commands/root_test.go
index edfa10dc..1dfa8669 100644
--- a/cmd/grype/cli/commands/root_test.go
+++ b/cmd/grype/cli/commands/root_test.go
@@ -10,8 +10,10 @@ import (
 
 	"github.com/anchore/clio"
 	"github.com/anchore/grype/cmd/grype/cli/options"
+	"github.com/anchore/grype/grype/distro"
 	"github.com/anchore/grype/grype/match"
 	"github.com/anchore/grype/grype/pkg"
+	"github.com/anchore/grype/grype/version"
 	"github.com/anchore/grype/grype/vex"
 	"github.com/anchore/stereoscope/pkg/image"
 	"github.com/anchore/syft/syft"
@@ -19,38 +21,6 @@ import (
 	"github.com/anchore/syft/syft/pkg/cataloger/binary"
 )
 
-func Test_applyDistroHint(t *testing.T) {
-	ctx := pkg.Context{}
-	cfg := options.Grype{}
-
-	applyDistroHint([]pkg.Package{}, &ctx, &cfg)
-	assert.Nil(t, ctx.Distro)
-
-	// works when distro is nil
-	cfg.Distro = "alpine:3.10"
-	applyDistroHint([]pkg.Package{}, &ctx, &cfg)
-	assert.NotNil(t, ctx.Distro)
-
-	assert.Equal(t, "alpine", ctx.Distro.Name())
-	assert.Equal(t, "3.10", ctx.Distro.Version)
-
-	// does override an existing distro
-	cfg.Distro = "ubuntu:24.04"
-	applyDistroHint([]pkg.Package{}, &ctx, &cfg)
-	assert.NotNil(t, ctx.Distro)
-
-	assert.Equal(t, "ubuntu", ctx.Distro.Name())
-	assert.Equal(t, "24.04", ctx.Distro.Version)
-
-	// doesn't remove an existing distro when empty
-	cfg.Distro = ""
-	applyDistroHint([]pkg.Package{}, &ctx, &cfg)
-	assert.NotNil(t, ctx.Distro)
-
-	assert.Equal(t, "ubuntu", ctx.Distro.Name())
-	assert.Equal(t, "24.04", ctx.Distro.Version)
-}
-
 func Test_getProviderConfig(t *testing.T) {
 	tests := []struct {
 		name string
@@ -74,6 +44,20 @@ func Test_getProviderConfig(t *testing.T) {
 						Credentials: []image.RegistryCredentials{},
 					},
 				},
+				SynthesisConfig: pkg.SynthesisConfig{
+					GenerateMissingCPEs: false,
+					Distro: pkg.DistroConfig{
+						Override: nil,
+						FixChannels: []distro.FixChannel{
+							{
+								Name:     "eus",
+								IDs:      []string{"rhel"},
+								Apply:    "never", // TODO: this will change to "auto" once there is data in the DB to support it
+								Versions: version.MustGetConstraint(">= 8.0", version.SemanticFormat),
+							},
+						},
+					},
+				},
 			},
 		},
 	}
diff --git a/grype/db/v6/models_test.go b/grype/db/v6/models_test.go
index dd6e9fe4..de6ef56f 100644
--- a/grype/db/v6/models_test.go
+++ b/grype/db/v6/models_test.go
@@ -119,6 +119,11 @@ func TestOperatingSystem_Version(t *testing.T) {
 			os:             &OperatingSystem{Codename: "buster"},
 			expectedResult: "buster",
 		},
+		{
+			name:           "with channel",
+			os:             &OperatingSystem{MajorVersion: "10", MinorVersion: "1", Channel: "stable"},
+			expectedResult: "10.1+stable",
+		},
 	}
 
 	for _, tt := range tests {
diff --git a/grype/db/v6/operating_system_store_test.go b/grype/db/v6/operating_system_store_test.go
index 28dfe66e..38d99d97 100644
--- a/grype/db/v6/operating_system_store_test.go
+++ b/grype/db/v6/operating_system_store_test.go
@@ -7,6 +7,8 @@ import (
 	"github.com/google/go-cmp/cmp/cmpopts"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
+
+	"github.com/anchore/grype/grype/version"
 )
 
 func TestOperatingSystemStore_ResolveOperatingSystem(t *testing.T) {
@@ -522,3 +524,378 @@ func TestOSSpecifier_clean(t *testing.T) {
 		})
 	}
 }
+
+func TestApplyOverride(t *testing.T) {
+	tests := []struct {
+		name        string
+		osSpecifier OSSpecifier
+		override    OperatingSystemSpecifierOverride
+		expected    OSSpecifier
+		wantApplied bool
+	}{
+		{
+			name: "replace name",
+			osSpecifier: OSSpecifier{
+				Name:         "centos",
+				MajorVersion: "8",
+				MinorVersion: "1",
+			},
+			override: OperatingSystemSpecifierOverride{
+				ReplacementName: strPtr("rhel"),
+			},
+			expected: OSSpecifier{
+				Name:         "rhel",
+				MajorVersion: "8",
+				MinorVersion: "1",
+			},
+			wantApplied: true,
+		},
+		{
+			name: "replace major version",
+			osSpecifier: OSSpecifier{
+				Name:         "rhel",
+				MajorVersion: "8",
+				MinorVersion: "1",
+			},
+			override: OperatingSystemSpecifierOverride{
+				ReplacementMajorVersion: strPtr("9"),
+			},
+			expected: OSSpecifier{
+				Name:         "rhel",
+				MajorVersion: "9",
+				MinorVersion: "1",
+			},
+			wantApplied: true,
+		},
+		{
+			name: "replace minor version",
+			osSpecifier: OSSpecifier{
+				Name:         "ubuntu",
+				MajorVersion: "20",
+				MinorVersion: "04",
+			},
+			override: OperatingSystemSpecifierOverride{
+				ReplacementMinorVersion: strPtr("10"),
+			},
+			expected: OSSpecifier{
+				Name:         "ubuntu",
+				MajorVersion: "20",
+				MinorVersion: "10",
+			},
+			wantApplied: true,
+		},
+		{
+			name: "replace label version",
+			osSpecifier: OSSpecifier{
+				Name:         "ubuntu",
+				MajorVersion: "20",
+				MinorVersion: "04",
+				LabelVersion: "focal",
+			},
+			override: OperatingSystemSpecifierOverride{
+				ReplacementLabelVersion: strPtr("jammy"),
+			},
+			expected: OSSpecifier{
+				Name:         "ubuntu",
+				MajorVersion: "20",
+				MinorVersion: "04",
+				LabelVersion: "jammy",
+			},
+			wantApplied: true,
+		},
+		{
+			name: "replace channel",
+			osSpecifier: OSSpecifier{
+				Name:         "rhel",
+				MajorVersion: "9",
+				MinorVersion: "1",
+				Channel:      "eeus",
+			},
+			override: OperatingSystemSpecifierOverride{
+				ReplacementChannel: strPtr("eus"),
+			},
+			expected: OSSpecifier{
+				Name:         "rhel",
+				MajorVersion: "9",
+				MinorVersion: "1",
+				Channel:      "eus",
+			},
+			wantApplied: true,
+		},
+		{
+			name: "rolling flag clears versions",
+			osSpecifier: OSSpecifier{
+				Name:         "arch",
+				MajorVersion: "2024",
+				MinorVersion: "01",
+				LabelVersion: "rolling",
+			},
+			override: OperatingSystemSpecifierOverride{
+				Rolling: true,
+			},
+			expected: OSSpecifier{
+				Name:         "arch",
+				MajorVersion: "",
+				MinorVersion: "",
+				LabelVersion: "rolling",
+			},
+			wantApplied: true,
+		},
+		{
+			name: "comprehensive override - all fields",
+			osSpecifier: OSSpecifier{
+				Name:         "centos",
+				MajorVersion: "7",
+				MinorVersion: "5",
+				LabelVersion: "core",
+			},
+			override: OperatingSystemSpecifierOverride{
+				ReplacementName:         strPtr("rhel"),
+				ReplacementMajorVersion: strPtr("7"),
+				ReplacementMinorVersion: strPtr("9"),
+				ReplacementLabelVersion: strPtr("server"),
+			},
+			expected: OSSpecifier{
+				Name:         "rhel",
+				MajorVersion: "7",
+				MinorVersion: "9",
+				LabelVersion: "server",
+			},
+			wantApplied: true,
+		},
+		{
+			name: "no replacement fields - no changes",
+			osSpecifier: OSSpecifier{
+				Name:         "ubuntu",
+				MajorVersion: "20",
+				MinorVersion: "04",
+				LabelVersion: "focal",
+			},
+			override: OperatingSystemSpecifierOverride{
+				Alias: "ubuntu",
+			},
+			expected: OSSpecifier{
+				Name:         "ubuntu",
+				MajorVersion: "20",
+				MinorVersion: "04",
+				LabelVersion: "focal",
+			},
+			wantApplied: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			// make a copy to avoid modifying the original
+			d := tt.osSpecifier
+			applied := applyOverride(&d, tt.override)
+
+			require.Equal(t, tt.wantApplied, applied)
+
+			if diff := cmp.Diff(tt.expected, d); diff != "" {
+				t.Errorf("unexpected result (-want +got):\n%s", diff)
+			}
+		})
+	}
+}
+
+func TestApplyOSSpecifierOverrides(t *testing.T) {
+	tests := []struct {
+		name          string
+		osSpecifier   OSSpecifier
+		aliases       []OperatingSystemSpecifierOverride
+		clientVersion *version.Version
+		wantErr       require.ErrorAssertionFunc
+		expected      OSSpecifier
+	}{
+		{
+			name: "no aliases - no change",
+			osSpecifier: OSSpecifier{
+				Name:         "ubuntu",
+				MajorVersion: "20",
+				MinorVersion: "04",
+				LabelVersion: "focal",
+			},
+			aliases: []OperatingSystemSpecifierOverride{},
+			expected: OSSpecifier{
+				Name:         "ubuntu",
+				MajorVersion: "20",
+				MinorVersion: "04",
+				LabelVersion: "focal",
+			},
+		},
+		{
+			name: "multiple overrides - first match wins",
+			osSpecifier: OSSpecifier{
+				Name:         "centos",
+				MajorVersion: "8",
+				MinorVersion: "1",
+			},
+			aliases: []OperatingSystemSpecifierOverride{
+				{
+					Alias:           "centos",
+					ReplacementName: strPtr("rhel"),
+				},
+				{
+					Alias:           "centos",
+					ReplacementName: strPtr("fedora"),
+				},
+			},
+			expected: OSSpecifier{
+				Name:         "rhel", // overridden
+				MajorVersion: "8",
+				MinorVersion: "1",
+			},
+		},
+		{
+			name: "codename mismatch - no override",
+			osSpecifier: OSSpecifier{
+				Name:         "ubuntu",
+				MajorVersion: "20",
+				MinorVersion: "04",
+				LabelVersion: "focal",
+			},
+			aliases: []OperatingSystemSpecifierOverride{
+				{
+					Alias:           "ubuntu",
+					Codename:        "jammy",
+					ReplacementName: strPtr("ubuntu-lts"),
+				},
+			},
+			expected: OSSpecifier{
+				Name:         "ubuntu",
+				MajorVersion: "20",
+				MinorVersion: "04",
+				LabelVersion: "focal", // not overridden
+			},
+		},
+		{
+			name: "version mismatch - no override",
+			osSpecifier: OSSpecifier{
+				Name:         "debian",
+				MajorVersion: "10",
+				MinorVersion: "5",
+			},
+			aliases: []OperatingSystemSpecifierOverride{
+				{
+					Alias:           "debian",
+					Version:         "11.0",
+					ReplacementName: strPtr("debian-bullseye"),
+				},
+			},
+			expected: OSSpecifier{
+				Name:         "debian",
+				MajorVersion: "10", // not overridden
+				MinorVersion: "5",  // not overridden
+			},
+		},
+		{
+			name: "version pattern mismatch - no override",
+			osSpecifier: OSSpecifier{
+				Name:         "alpine",
+				MajorVersion: "2",
+				MinorVersion: "18",
+			},
+			aliases: []OperatingSystemSpecifierOverride{
+				{
+					Alias:           "alpine",
+					VersionPattern:  "^3\\.[0-9]+$",
+					ReplacementName: strPtr("alpine-stable"),
+				},
+			},
+			expected: OSSpecifier{
+				Name:         "alpine", // not overridden
+				MajorVersion: "2",
+				MinorVersion: "18",
+			},
+		},
+		{
+			name: "client version constraint satisfied",
+			osSpecifier: OSSpecifier{
+				Name:         "ubuntu",
+				MajorVersion: "20",
+				MinorVersion: "04",
+			},
+			aliases: []OperatingSystemSpecifierOverride{
+				{
+					Alias:                     "ubuntu",
+					ApplicableClientDBSchemas: ">=1.0.0",
+					ReplacementName:           strPtr("ubuntu-new"),
+				},
+			},
+			clientVersion: version.New("1.2.0", version.SemanticFormat), // matches the constraint, thus allowed to override
+			expected: OSSpecifier{
+				Name:         "ubuntu-new", // overridden
+				MajorVersion: "20",
+				MinorVersion: "04",
+			},
+		},
+		{
+			name: "client version constraint not satisfied",
+			osSpecifier: OSSpecifier{
+				Name:         "ubuntu",
+				MajorVersion: "20",
+				MinorVersion: "04",
+			},
+			aliases: []OperatingSystemSpecifierOverride{
+				{
+					Alias:                     "ubuntu",
+					ApplicableClientDBSchemas: ">=2.0.0", // does not match the client version, thus no override
+					ReplacementName:           strPtr("ubuntu-new"),
+				},
+			},
+			clientVersion: version.New("1.2.0", version.SemanticFormat),
+			expected: OSSpecifier{
+				Name:         "ubuntu", // not overridden
+				MajorVersion: "20",
+				MinorVersion: "04",
+			},
+		},
+		{
+			name: "invalid client version constraint - honor the override",
+			osSpecifier: OSSpecifier{
+				Name:         "ubuntu",
+				MajorVersion: "20",
+				MinorVersion: "04",
+			},
+			aliases: []OperatingSystemSpecifierOverride{
+				{
+					Alias:                     "ubuntu",
+					ApplicableClientDBSchemas: "invalid-constraint", // oops!
+					ReplacementName:           strPtr("ubuntu-new"),
+				},
+			},
+			clientVersion: version.New("1.2.0", version.SemanticFormat),
+			expected: OSSpecifier{
+				Name:         "ubuntu-new", // overridden
+				MajorVersion: "20",
+				MinorVersion: "04",
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if tt.wantErr == nil {
+				tt.wantErr = require.NoError
+			}
+
+			// make a copy to avoid modifying the original
+			d := tt.osSpecifier
+			err := applyOSSpecifierOverrides(&d, tt.aliases, tt.clientVersion)
+			tt.wantErr(t, err)
+
+			if err != nil {
+				return
+			}
+
+			if diff := cmp.Diff(tt.expected, d); diff != "" {
+				t.Errorf("unexpected result (-want +got):\n%s", diff)
+			}
+		})
+	}
+}
+
+func strPtr(s string) *string {
+	return &s
+}
diff --git a/grype/db/v6/vulnerability_provider_test.go b/grype/db/v6/vulnerability_provider_test.go
index b4c04418..21c9d9f2 100644
--- a/grype/db/v6/vulnerability_provider_test.go
+++ b/grype/db/v6/vulnerability_provider_test.go
@@ -32,7 +32,7 @@ func Test_FindVulnerabilitiesByDistro(t *testing.T) {
 		Type:    syftPkg.DebPkg,
 	}
 
-	actual, err := provider.FindVulnerabilities(search.ByDistro(*d), search.ByPackageName(p.Name), search.ByVersion(*version.NewVersionFromPkg(p)))
+	actual, err := provider.FindVulnerabilities(search.ByDistro(*d), search.ByPackageName(p.Name), search.ByVersion(*version.New(p.Version, pkg.VersionFormat(p))))
 	require.NoError(t, err)
 
 	expected := []vulnerability.Vulnerability{
diff --git a/grype/distro/distro_test.go b/grype/distro/distro_test.go
index 6cad6917..7b705317 100644
--- a/grype/distro/distro_test.go
+++ b/grype/distro/distro_test.go
@@ -14,10 +14,23 @@ import (
 	"github.com/anchore/syft/syft/source/directorysource"
 )
 
+func testFixChannels() []FixChannel {
+	// TODO: change this to DefaultFixChannels() when EUS is configured "auto" by default
+	//return DefaultFixChannels()
+	return []FixChannel{
+		{
+			Name:  "eus",
+			IDs:   []string{"rhel"},
+			Apply: ChannelConditionallyEnabled,
+		},
+	}
+}
+
 func Test_NewDistroFromRelease(t *testing.T) {
 	tests := []struct {
 		name      string
 		release   linux.Release
+		channels  []FixChannel
 		expected  *Distro
 		minor     string
 		major     string
@@ -31,6 +44,7 @@ func Test_NewDistroFromRelease(t *testing.T) {
 				Version:   "7",
 				IDLike:    []string{"rhel"},
 			},
+			channels: testFixChannels(),
 			expected: &Distro{
 				Type:    CentOS,
 				Version: "8",
@@ -45,6 +59,7 @@ func Test_NewDistroFromRelease(t *testing.T) {
 				Name:      "windows",
 				VersionID: "8",
 			},
+			channels: testFixChannels(),
 			expected: &Distro{
 				Type:    Windows,
 				Version: "8",
@@ -58,6 +73,7 @@ func Test_NewDistroFromRelease(t *testing.T) {
 				ID:      "centos",
 				Version: "8",
 			},
+			channels: testFixChannels(),
 			expected: &Distro{
 				Type:    CentOS,
 				Version: "8",
@@ -71,6 +87,7 @@ func Test_NewDistroFromRelease(t *testing.T) {
 			release: linux.Release{
 				ID: "centos",
 			},
+			channels: testFixChannels(),
 			expected: &Distro{
 				Type: CentOS,
 			},
@@ -81,6 +98,7 @@ func Test_NewDistroFromRelease(t *testing.T) {
 				ID:        "bogosity",
 				VersionID: "8",
 			},
+			channels:  testFixChannels(),
 			expectErr: require.Error,
 		},
 		{
@@ -94,6 +112,7 @@ func Test_NewDistroFromRelease(t *testing.T) {
 				VersionCodename: "trixie",
 				Name:            "Debian GNU/Linux",
 			},
+			channels: testFixChannels(),
 			expected: &Distro{
 				Type:     Debian,
 				Codename: "trixie",
@@ -108,6 +127,7 @@ func Test_NewDistroFromRelease(t *testing.T) {
 				Version:   "3.0.20240417",
 				VersionID: "3.0",
 			},
+			channels: testFixChannels(),
 			expected: &Distro{
 				Type:    Azure,
 				Version: "3.0",
@@ -115,6 +135,149 @@ func Test_NewDistroFromRelease(t *testing.T) {
 			major: "3",
 			minor: "0",
 		},
+		{
+			name: "eus hint ignored when configured to never apply",
+			release: linux.Release{
+				ID:              "rhel",
+				Version:         "9.4",
+				ExtendedSupport: true,
+			},
+			channels: testFixChannels(),
+			expected: &Distro{
+				Type:     RedHat,
+				Version:  "9.4",
+				Channels: names("eus"),
+			},
+			major: "9",
+			minor: "4",
+		},
+		{
+			name: "eus hinted at as attribute",
+			release: linux.Release{
+				ID:              "rhel",
+				Version:         "9.4",
+				ExtendedSupport: true,
+			},
+			channels: []FixChannel{
+				{
+					Name:  "eus",
+					IDs:   []string{"rhel"},
+					Apply: ChannelConditionallyEnabled,
+				},
+			},
+			expected: &Distro{
+				Type:     RedHat,
+				Version:  "9.4",
+				Channels: names("eus"),
+			},
+			major: "9",
+			minor: "4",
+		},
+		{
+			name: "eus embedded in the version",
+			release: linux.Release{
+				ID:      "rhel",
+				Version: "9.4+eus",
+			},
+			channels: []FixChannel{
+				{
+					Name:  "eus",
+					IDs:   []string{"rhel"},
+					Apply: ChannelConditionallyEnabled,
+				},
+			},
+			expected: &Distro{
+				Type:     RedHat,
+				Version:  "9.4",
+				Channels: names("eus"),
+			},
+			major: "9",
+			minor: "4",
+		},
+		{
+			name: "eus hinted at as attribute (always apply)",
+			release: linux.Release{
+				ID:              "rhel",
+				Version:         "9.4",
+				ExtendedSupport: true,
+			},
+			channels: []FixChannel{
+				{
+					Name:  "eus",
+					IDs:   []string{"rhel"},
+					Apply: ChannelAlwaysEnabled, // important!
+				},
+			},
+			expected: &Distro{
+				Type:     RedHat,
+				Version:  "9.4",
+				Channels: names("eus"),
+			},
+			major: "9",
+			minor: "4",
+		},
+		{
+			name: "eus embedded in the version (always apply)",
+			release: linux.Release{
+				ID:      "rhel",
+				Version: "9.4+eus",
+			},
+			channels: []FixChannel{
+				{
+					Name:  "eus",
+					IDs:   []string{"rhel"},
+					Apply: ChannelAlwaysEnabled, // important!
+				},
+			},
+			expected: &Distro{
+				Type:     RedHat,
+				Version:  "9.4",
+				Channels: names("eus"),
+			},
+			major: "9",
+			minor: "4",
+		},
+		{
+			name: "eus hinted at as attribute (never apply)",
+			release: linux.Release{
+				ID:              "rhel",
+				Version:         "9.4",
+				ExtendedSupport: true,
+			},
+			channels: []FixChannel{
+				{
+					Name:  "eus",
+					IDs:   []string{"rhel"},
+					Apply: ChannelNeverEnabled, // important!
+				},
+			},
+			expected: &Distro{
+				Type:    RedHat,
+				Version: "9.4",
+			},
+			major: "9",
+			minor: "4",
+		},
+		{
+			name: "eus embedded in the version (never apply)",
+			release: linux.Release{
+				ID:      "rhel",
+				Version: "9.4+eus",
+			},
+			channels: []FixChannel{
+				{
+					Name:  "eus",
+					IDs:   []string{"rhel"},
+					Apply: ChannelNeverEnabled, // important!
+				},
+			},
+			expected: &Distro{
+				Type:    RedHat,
+				Version: "9.4",
+			},
+			major: "9",
+			minor: "4",
+		},
 	}
 
 	for _, tt := range tests {
@@ -123,7 +286,7 @@ func Test_NewDistroFromRelease(t *testing.T) {
 				tt.expectErr = require.NoError
 			}
 
-			distro, err := NewFromRelease(tt.release)
+			distro, err := NewFromRelease(tt.release, tt.channels)
 			tt.expectErr(t, err)
 			if err != nil {
 				return
@@ -316,7 +479,7 @@ func Test_NewDistroFromRelease_Coverage(t *testing.T) {
 			require.NotNil(t, release, "empty linux release info")
 
 			// craft a new distro from the syft raw info
-			d, err := NewFromRelease(*release)
+			d, err := NewFromRelease(*release, testFixChannels())
 			require.NoError(t, err)
 
 			observedDistros.Add(d.Type.String())
@@ -368,7 +531,7 @@ func TestDistro_FullVersion(t *testing.T) {
 			d, err := NewFromRelease(linux.Release{
 				ID:      "centos",
 				Version: test.version,
-			})
+			}, testFixChannels())
 			require.NoError(t, err)
 			assert.Equal(t, test.expected, d.Version)
 		})
@@ -405,7 +568,7 @@ func TestDistro_MajorVersion(t *testing.T) {
 			d, err := NewFromRelease(linux.Release{
 				ID:      "centos",
 				Version: test.version,
-			})
+			}, testFixChannels())
 			require.NoError(t, err)
 			assert.Equal(t, test.expected, d.MajorVersion())
 
@@ -413,3 +576,7 @@ func TestDistro_MajorVersion(t *testing.T) {
 	}
 
 }
+
+func names(ns ...string) []string {
+	return ns
+}
diff --git a/grype/distro/type_test.go b/grype/distro/type_test.go
new file mode 100644
index 00000000..12d2dbbc
--- /dev/null
+++ b/grype/distro/type_test.go
@@ -0,0 +1,127 @@
+package distro
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/anchore/syft/syft/linux"
+)
+
+func TestTypeFromRelease(t *testing.T) {
+
+	tests := []struct {
+		name    string
+		release linux.Release
+		want    Type
+	}{
+		{
+			name: "direct ID mapping",
+			release: linux.Release{
+				ID: "ubuntu",
+			},
+			want: Ubuntu,
+		},
+		{
+			name: "direct ID mapping rhel",
+			release: linux.Release{
+				ID: "rhel",
+			},
+			want: RedHat,
+		},
+		{
+			name: "alias mapping",
+			release: linux.Release{
+				ID: "Alpine Linux",
+			},
+			want: Alpine,
+		},
+		{
+			name: "alias mapping windows",
+			release: linux.Release{
+				ID: "windows",
+			},
+			want: Windows,
+		},
+		{
+			name: "ID_LIKE mapping",
+			release: linux.Release{
+				ID:     "unknown-distro",
+				IDLike: []string{"debian", "ubuntu"},
+			},
+			want: Debian,
+		},
+		{
+			name: "ID_LIKE alias mapping",
+			release: linux.Release{
+				ID:     "custom-alpine",
+				IDLike: []string{"Alpine Linux"},
+			},
+			want: Alpine,
+		},
+		{
+			name: "fallback to name ID mapping",
+			release: linux.Release{
+				ID:   "unrecognized",
+				Name: "fedora",
+			},
+			want: Fedora,
+		},
+		{
+			name: "fallback to name alias mapping",
+			release: linux.Release{
+				ID:   "unrecognized",
+				Name: "windows",
+			},
+			want: Windows,
+		},
+		{
+			name: "empty result when no matches",
+			release: linux.Release{
+				ID:   "totally-unknown",
+				Name: "also-unknown",
+			},
+			want: "",
+		},
+		{
+			name: "prefer ID over ID_LIKE",
+			release: linux.Release{
+				ID:     "ubuntu",
+				IDLike: []string{"debian"},
+			},
+			want: Ubuntu,
+		},
+		{
+			name: "prefer ID over name",
+			release: linux.Release{
+				ID:   "ubuntu",
+				Name: "fedora",
+			},
+			want: Ubuntu,
+		},
+		{
+			name: "prefer ID_LIKE over name",
+			release: linux.Release{
+				ID:     "unknown",
+				IDLike: []string{"centos"},
+				Name:   "fedora",
+			},
+			want: CentOS,
+		},
+		{
+			name: "multiple ID_LIKE entries use first match",
+			release: linux.Release{
+				ID:     "unknown",
+				IDLike: []string{"nonexistent", "alpine", "debian"},
+			},
+			want: Alpine,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := TypeFromRelease(tt.release)
+			assert.Equal(t, tt.want, got)
+		})
+	}
+}
diff --git a/grype/matcher/internal/cpe_test.go b/grype/matcher/internal/cpe_test.go
index c1335d5a..0d8f9f47 100644
--- a/grype/matcher/internal/cpe_test.go
+++ b/grype/matcher/internal/cpe_test.go
@@ -1064,7 +1064,7 @@ func TestFilterCPEsByVersion(t *testing.T) {
 
 			var versionObj *version.Version
 			if test.version != "" {
-				versionObj = version.NewVersion(test.version, version.UnknownFormat)
+				versionObj = version.New(test.version, version.UnknownFormat)
 			}
 
 			// run the test subject...
diff --git a/grype/pkg/java_metadata_test.go b/grype/pkg/java_metadata_test.go
index 1f93622e..8a9bb84e 100644
--- a/grype/pkg/java_metadata_test.go
+++ b/grype/pkg/java_metadata_test.go
@@ -108,7 +108,7 @@ func TestIsJvmPackage(t *testing.T) {
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			result := IsJvmPackage(tt.pkg)
+			result := isJvmPackage(tt.pkg)
 			assert.Equal(t, tt.expected, result)
 		})
 	}
diff --git a/grype/pkg/package_test.go b/grype/pkg/package_test.go
index f1303313..78e38c63 100644
--- a/grype/pkg/package_test.go
+++ b/grype/pkg/package_test.go
@@ -1074,7 +1074,7 @@ func Test_RemovePackagesByOverlap(t *testing.T) {
 	}
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			d := distro.FromRelease(test.sbom.Artifacts.LinuxDistribution)
+			d := distro.FromRelease(test.sbom.Artifacts.LinuxDistribution, distro.DefaultFixChannels())
 			catalog := removePackagesByOverlap(test.sbom.Artifacts.Packages, test.sbom.Relationships, d)
 			pkgs := FromCollection(catalog, SynthesisConfig{})
 			var pkgNames []string
diff --git a/grype/pkg/provider_test.go b/grype/pkg/provider_test.go
index 9b913a24..54cc39c8 100644
--- a/grype/pkg/provider_test.go
+++ b/grype/pkg/provider_test.go
@@ -5,6 +5,7 @@ import (
 
 	"github.com/stretchr/testify/assert"
 
+	"github.com/anchore/grype/grype/distro"
 	"github.com/anchore/stereoscope/pkg/imagetest"
 	"github.com/anchore/syft/syft"
 	"github.com/anchore/syft/syft/file"
@@ -241,3 +242,105 @@ func Test_matchesLocation(t *testing.T) {
 		})
 	}
 }
+
+func Test_getDistroChannelApplier(t *testing.T) {
+
+	defaultOSGen := func() *distro.Distro {
+		return distro.NewFromNameVersion("rhel", "8.4")
+	}
+
+	tests := []struct {
+		name     string
+		channels []distro.FixChannel
+		distro   func() *distro.Distro
+		want     []string
+	}{
+		{
+			name:     "nil distro",
+			channels: distro.DefaultFixChannels(),
+			distro:   func() *distro.Distro { return nil },
+			want:     nil,
+		},
+		{
+			name:     "no matching channel",
+			channels: distro.DefaultFixChannels(),
+			distro: func() *distro.Distro {
+				return distro.NewFromNameVersion("ubuntu", "20.04")
+			},
+			want: nil,
+		},
+		{
+			name: "channel never enabled",
+			channels: []distro.FixChannel{
+				{
+					Name:  "test-channel",
+					IDs:   []string{"rhel"},
+					Apply: distro.ChannelNeverEnabled,
+				},
+			},
+			distro: defaultOSGen,
+			want:   nil,
+		},
+		{
+			name: "channel always enabled",
+			channels: []distro.FixChannel{
+				{
+					Name:  "eus",
+					IDs:   []string{"rhel"},
+					Apply: distro.ChannelAlwaysEnabled,
+				},
+			},
+			distro: defaultOSGen,
+			want:   []string{"eus"},
+		},
+		{
+			name: "case insensitive matching",
+			channels: []distro.FixChannel{
+				{
+					Name:  "eus",
+					IDs:   []string{"RHEL"},
+					Apply: distro.ChannelAlwaysEnabled,
+				},
+			},
+			distro: defaultOSGen,
+			want:   []string{"eus"},
+		},
+		{
+			name: "multiple IDs in channel",
+			channels: []distro.FixChannel{
+				{
+					Name:  "test-channel",
+					IDs:   []string{"centos", "rhel", "fedora"},
+					Apply: distro.ChannelAlwaysEnabled,
+				},
+			},
+			distro: defaultOSGen,
+			want:   []string{"test-channel"},
+		},
+		{
+			name: "empty channel name skipped",
+			channels: []distro.FixChannel{
+				{
+					Name:  "",
+					IDs:   []string{"rhel"},
+					Apply: distro.ChannelAlwaysEnabled,
+				},
+			},
+			distro: defaultOSGen,
+			want:   nil,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			applier := getDistroChannelApplier(tt.channels)
+			d := tt.distro()
+
+			applier(d)
+
+			if d != nil {
+				assert.Equal(t, tt.want, d.Channels)
+			}
+		})
+	}
+}
diff --git a/grype/pkg/purl_provider_test.go b/grype/pkg/purl_provider_test.go
index ca8b7c37..4586fe10 100644
--- a/grype/pkg/purl_provider_test.go
+++ b/grype/pkg/purl_provider_test.go
@@ -12,25 +12,33 @@ import (
 	"github.com/anchore/syft/syft/source"
 )
 
+var diffOpts = []cmp.Option{
+	cmpopts.IgnoreFields(Package{}, "ID", "Locations", "Licenses", "Language", "CPEs"),
+	cmpopts.IgnoreUnexported(distro.Distro{}),
+}
+
 func Test_PurlProvider(t *testing.T) {
+
 	tests := []struct {
-		name      string
-		userInput string
-		context   Context
-		pkgs      []Package
-		wantErr   require.ErrorAssertionFunc
+		name        string
+		userInput   string
+		channels    []distro.FixChannel
+		wantContext Context
+		wantPkgs    []Package
+		wantErr     require.ErrorAssertionFunc
 	}{
 		{
 			name:      "takes a single purl",
 			userInput: "pkg:apk/curl@7.61.1",
-			context: Context{
+			channels:  testFixChannels(),
+			wantContext: Context{
 				Source: &source.Description{
 					Metadata: PURLLiteralMetadata{
 						PURL: "pkg:apk/curl@7.61.1",
 					},
 				},
 			},
-			pkgs: []Package{
+			wantPkgs: []Package{
 				{
 					Name:    "curl",
 					Version: "7.61.1",
@@ -42,14 +50,15 @@ func Test_PurlProvider(t *testing.T) {
 		{
 			name:      "java metadata decoded from purl",
 			userInput: "pkg:maven/org.apache.commons/commons-lang3@3.12.0",
-			context: Context{
+			channels:  testFixChannels(),
+			wantContext: Context{
 				Source: &source.Description{
 					Metadata: PURLLiteralMetadata{
 						PURL: "pkg:maven/org.apache.commons/commons-lang3@3.12.0",
 					},
 				},
 			},
-			pkgs: []Package{
+			wantPkgs: []Package{
 				{
 					Name:    "commons-lang3",
 					Version: "3.12.0",
@@ -65,19 +74,15 @@ func Test_PurlProvider(t *testing.T) {
 		{
 			name:      "os with codename",
 			userInput: "pkg:deb/debian/sysv-rc@2.88dsf-59?arch=all&distro=debian-jessie&upstream=sysvinit",
-			context: Context{
-				Distro: &distro.Distro{
-					Type:     "debian",
-					IDLike:   []string{"debian"},
-					Codename: "jessie", // important!
-				},
+			channels:  testFixChannels(),
+			wantContext: Context{
 				Source: &source.Description{
 					Metadata: PURLLiteralMetadata{
 						PURL: "pkg:deb/debian/sysv-rc@2.88dsf-59?arch=all&distro=debian-jessie&upstream=sysvinit",
 					},
 				},
 			},
-			pkgs: []Package{
+			wantPkgs: []Package{
 				{
 					Name:    "sysv-rc",
 					Version: "2.88dsf-59",
@@ -95,14 +100,15 @@ func Test_PurlProvider(t *testing.T) {
 		{
 			name:      "default upstream",
 			userInput: "pkg:apk/libcrypto3@3.3.2?upstream=openssl",
-			context: Context{
+			channels:  testFixChannels(),
+			wantContext: Context{
 				Source: &source.Description{
 					Metadata: PURLLiteralMetadata{
 						PURL: "pkg:apk/libcrypto3@3.3.2?upstream=openssl",
 					},
 				},
 			},
-			pkgs: []Package{
+			wantPkgs: []Package{
 				{
 					Name:    "libcrypto3",
 					Version: "3.3.2",
@@ -119,14 +125,15 @@ func Test_PurlProvider(t *testing.T) {
 		{
 			name:      "upstream with version",
 			userInput: "pkg:apk/libcrypto3@3.3.2?upstream=openssl%403.2.1", // %40 is @
-			context: Context{
+			channels:  testFixChannels(),
+			wantContext: Context{
 				Source: &source.Description{
 					Metadata: PURLLiteralMetadata{
 						PURL: "pkg:apk/libcrypto3@3.3.2?upstream=openssl%403.2.1",
 					},
 				},
 			},
-			pkgs: []Package{
+			wantPkgs: []Package{
 				{
 					Name:    "libcrypto3",
 					Version: "3.3.2",
@@ -144,19 +151,15 @@ func Test_PurlProvider(t *testing.T) {
 		{
 			name:      "upstream for source RPM",
 			userInput: "pkg:rpm/redhat/systemd-x@239-82.el8_10.2?arch=aarch64&distro=rhel-8.10&upstream=systemd-239-82.el8_10.2.src.rpm",
-			context: Context{
-				Distro: &distro.Distro{
-					Type:    "redhat",
-					IDLike:  []string{"redhat"},
-					Version: "8.10",
-				},
+			channels:  testFixChannels(),
+			wantContext: Context{
 				Source: &source.Description{
 					Metadata: PURLLiteralMetadata{
 						PURL: "pkg:rpm/redhat/systemd-x@239-82.el8_10.2?arch=aarch64&distro=rhel-8.10&upstream=systemd-239-82.el8_10.2.src.rpm",
 					},
 				},
 			},
-			pkgs: []Package{
+			wantPkgs: []Package{
 				{
 					Name:    "systemd-x",
 					Version: "239-82.el8_10.2",
@@ -175,19 +178,15 @@ func Test_PurlProvider(t *testing.T) {
 		{
 			name:      "RPM with epoch",
 			userInput: "pkg:rpm/redhat/dbus-common@1.12.8-26.el8?arch=noarch&distro=rhel-8.10&epoch=1&upstream=dbus-1.12.8-26.el8.src.rpm",
-			context: Context{
-				Distro: &distro.Distro{
-					Type:    "redhat",
-					IDLike:  []string{"redhat"},
-					Version: "8.10",
-				},
+			channels:  testFixChannels(),
+			wantContext: Context{
 				Source: &source.Description{
 					Metadata: PURLLiteralMetadata{
 						PURL: "pkg:rpm/redhat/dbus-common@1.12.8-26.el8?arch=noarch&distro=rhel-8.10&epoch=1&upstream=dbus-1.12.8-26.el8.src.rpm",
 					},
 				},
 			},
-			pkgs: []Package{
+			wantPkgs: []Package{
 				{
 					Name:    "dbus-common",
 					Version: "1:1.12.8-26.el8",
@@ -206,19 +205,15 @@ func Test_PurlProvider(t *testing.T) {
 		{
 			name:      "infer context when distro is present for single purl",
 			userInput: "pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.3",
-			context: Context{
-				Distro: &distro.Distro{
-					Type:    "alpine",
-					IDLike:  []string{"alpine"},
-					Version: "3.20.3",
-				},
+			channels:  testFixChannels(),
+			wantContext: Context{
 				Source: &source.Description{
 					Metadata: PURLLiteralMetadata{
 						PURL: "pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.3",
 					},
 				},
 			},
-			pkgs: []Package{
+			wantPkgs: []Package{
 				{
 					Name:    "curl",
 					Version: "7.61.1",
@@ -231,12 +226,13 @@ func Test_PurlProvider(t *testing.T) {
 		{
 			name:      "include namespace in name when purl is type Golang",
 			userInput: "pkg:golang/k8s.io/ingress-nginx@v1.11.2",
-			context: Context{
+			channels:  testFixChannels(),
+			wantContext: Context{
 				Source: &source.Description{
 					Metadata: PURLLiteralMetadata{PURL: "pkg:golang/k8s.io/ingress-nginx@v1.11.2"},
 				},
 			},
-			pkgs: []Package{
+			wantPkgs: []Package{
 				{
 					Name:    "k8s.io/ingress-nginx",
 					Version: "v1.11.2",
@@ -248,12 +244,13 @@ func Test_PurlProvider(t *testing.T) {
 		{
 			name:      "include complex namespace in name when purl is type Golang",
 			userInput: "pkg:golang/github.com/wazuh/wazuh@v4.5.0",
-			context: Context{
+			channels:  testFixChannels(),
+			wantContext: Context{
 				Source: &source.Description{
 					Metadata: PURLLiteralMetadata{PURL: "pkg:golang/github.com/wazuh/wazuh@v4.5.0"},
 				},
 			},
-			pkgs: []Package{
+			wantPkgs: []Package{
 				{
 					Name:    "github.com/wazuh/wazuh",
 					Version: "v4.5.0",
@@ -265,12 +262,13 @@ func Test_PurlProvider(t *testing.T) {
 		{
 			name:      "do not include namespace when given blank input blank",
 			userInput: "pkg:golang/wazuh@v4.5.0",
-			context: Context{
+			channels:  testFixChannels(),
+			wantContext: Context{
 				Source: &source.Description{
 					Metadata: PURLLiteralMetadata{PURL: "pkg:golang/wazuh@v4.5.0"},
 				},
 			},
-			pkgs: []Package{
+			wantPkgs: []Package{
 				{
 					Name:    "wazuh",
 					Version: "v4.5.0",
@@ -279,14 +277,91 @@ func Test_PurlProvider(t *testing.T) {
 				},
 			},
 		},
+		{
+			name:      "RPM with extended support (auto)",
+			userInput: "pkg:rpm/redhat/systemd-x@239-82.el8_10.2?distro=rhel-8.10+eus",
+			channels:  testFixChannels(), // important! auto applies EUS
+			wantContext: Context{
+				Source: &source.Description{
+					Metadata: PURLLiteralMetadata{
+						PURL: "pkg:rpm/redhat/systemd-x@239-82.el8_10.2?distro=rhel-8.10+eus",
+					},
+				},
+			},
+			wantPkgs: []Package{
+				{
+					Name:    "systemd-x",
+					Version: "239-82.el8_10.2",
+					Type:    pkg.RpmPkg,
+					PURL:    "pkg:rpm/redhat/systemd-x@239-82.el8_10.2?distro=rhel-8.10+eus",
+					Distro:  &distro.Distro{Type: distro.RedHat, Version: "8.10", Channels: names("eus"), IDLike: []string{"redhat"}},
+				},
+			},
+		},
+		{
+			name:      "RPM with extended support (never)",
+			userInput: "pkg:rpm/redhat/systemd-x@239-82.el8_10.2?distro=rhel-8.10+eus",
+			channels: []distro.FixChannel{
+				{
+					Name:  "eus",
+					IDs:   []string{"rhel"},
+					Apply: distro.ChannelNeverEnabled, // important!
+				},
+			},
+			wantContext: Context{
+				Source: &source.Description{
+					Metadata: PURLLiteralMetadata{
+						PURL: "pkg:rpm/redhat/systemd-x@239-82.el8_10.2?distro=rhel-8.10+eus", // the input did hint hat eus, so we leave it
+					},
+				},
+			},
+			wantPkgs: []Package{
+				{
+					Name:    "systemd-x",
+					Version: "239-82.el8_10.2",
+					Type:    pkg.RpmPkg,
+					PURL:    "pkg:rpm/redhat/systemd-x@239-82.el8_10.2?distro=rhel-8.10+eus",                                 // important! we are NOT patching the channel out of the PURL
+					Distro:  &distro.Distro{Type: distro.RedHat, Version: "8.10", Channels: nil, IDLike: []string{"redhat"}}, // important! no channel applied
+				},
+			},
+		},
+		{
+			name:      "RPM without extended support (always)",
+			userInput: "pkg:rpm/redhat/systemd-x@239-82.el8_10.2?distro=rhel-8.10", // important! no channel hint
+			channels: []distro.FixChannel{
+				{
+					Name:  "eus",
+					IDs:   []string{"rhel"},
+					Apply: distro.ChannelAlwaysEnabled, // important!
+				},
+			},
+			wantContext: Context{
+				Source: &source.Description{
+					Metadata: PURLLiteralMetadata{
+						PURL: "pkg:rpm/redhat/systemd-x@239-82.el8_10.2?distro=rhel-8.10",
+					},
+				},
+			},
+			wantPkgs: []Package{
+				{
+					Name:    "systemd-x",
+					Version: "239-82.el8_10.2",
+					Type:    pkg.RpmPkg,
+					PURL:    "pkg:rpm/redhat/systemd-x@239-82.el8_10.2?distro=rhel-8.10",                                              // important! we are NOT patching the channel into the PURL
+					Distro:  &distro.Distro{Type: distro.RedHat, Version: "8.10", Channels: names("eus"), IDLike: []string{"redhat"}}, // important! channel applied
+				},
+			},
+		},
 		{
 			name:      "fails on purl list input",
 			userInput: "purl:test-fixtures/purl/invalid-purl.txt",
+			channels:  testFixChannels(),
 			wantErr:   require.Error,
 		},
 		{
 			name:      "invalid prefix",
 			userInput: "dir:test-fixtures/purl",
+			channels:  testFixChannels(),
 			wantErr:   require.Error,
 		},
 	}
@@ -297,8 +372,7 @@ func Test_PurlProvider(t *testing.T) {
 				tc.wantErr = require.NoError
 			}
 
-			packages, ctx, _, err := purlProvider(tc.userInput, ProviderConfig{})
-			setContextDistro(packages, &ctx)
+			packages, ctx, _, err := purlProvider(tc.userInput, ProviderConfig{}, getDistroChannelApplier(tc.channels))
 
 			tc.wantErr(t, err)
 			if err != nil {
@@ -306,11 +380,11 @@ func Test_PurlProvider(t *testing.T) {
 				return
 			}
 
-			if d := cmp.Diff(tc.context, ctx, diffOpts...); d != "" {
+			if d := cmp.Diff(tc.wantContext, ctx, diffOpts...); d != "" {
 				t.Errorf("unexpected context (-want +got):\n%s", d)
 			}
-			require.Len(t, packages, len(tc.pkgs))
-			for idx, expected := range tc.pkgs {
+			require.Len(t, packages, len(tc.wantPkgs))
+			for idx, expected := range tc.wantPkgs {
 				if d := cmp.Diff(expected, packages[idx], diffOpts...); d != "" {
 					t.Errorf("unexpected context (-want +got):\n%s", d)
 				}
@@ -319,7 +393,6 @@ func Test_PurlProvider(t *testing.T) {
 	}
 }
 
-var diffOpts = []cmp.Option{
-	cmpopts.IgnoreFields(Package{}, "ID", "Locations", "Licenses", "Language", "CPEs"),
-	cmpopts.IgnoreUnexported(distro.Distro{}),
+func names(ns ...string) []string {
+	return ns
 }
diff --git a/grype/pkg/syft_sbom_provider_test.go b/grype/pkg/syft_sbom_provider_test.go
index 3c069a16..3774c648 100644
--- a/grype/pkg/syft_sbom_provider_test.go
+++ b/grype/pkg/syft_sbom_provider_test.go
@@ -18,6 +18,8 @@ import (
 )
 
 func TestParseSyftJSON(t *testing.T) {
+	applyChannel := getDistroChannelApplier(testFixChannels())
+
 	tests := []struct {
 		Fixture  string
 		Packages []Package
@@ -234,7 +236,7 @@ func TestParseSyftJSON(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.Fixture, func(t *testing.T) {
-			pkgs, context, _, err := syftSBOMProvider(test.Fixture, ProviderConfig{})
+			pkgs, context, _, err := syftSBOMProvider(test.Fixture, ProviderConfig{}, applyChannel)
 			if err != nil {
 				t.Fatalf("unable to parse: %+v", err)
 			}
@@ -267,7 +269,8 @@ func TestParseSyftJSON(t *testing.T) {
 }
 
 func TestParseSyftJSON_BadCPEs(t *testing.T) {
-	pkgs, _, _, err := syftSBOMProvider("test-fixtures/syft-java-bad-cpes.json", ProviderConfig{})
+	applyChannel := getDistroChannelApplier(testFixChannels())
+	pkgs, _, _, err := syftSBOMProvider("test-fixtures/syft-java-bad-cpes.json", ProviderConfig{}, applyChannel)
 	assert.NoError(t, err)
 	assert.Len(t, pkgs, 1)
 }
@@ -350,16 +353,17 @@ var springImageTestCase = struct {
 
 func Test_PurlList(t *testing.T) {
 	tests := []struct {
-		name      string
-		userInput string
-		context   Context
-		pkgs      []Package
-		wantErr   require.ErrorAssertionFunc
+		name        string
+		config      ProviderConfig
+		userInput   string
+		wantContext Context
+		wantPkgs    []Package
+		wantErr     require.ErrorAssertionFunc
 	}{
 		{
 			name:      "takes multiple purls",
 			userInput: "purl:test-fixtures/purl/valid-purl.txt",
-			context: Context{
+			wantContext: Context{
 				Distro: &distro.Distro{
 					Type:    "debian",
 					IDLike:  []string{"debian"},
@@ -371,12 +375,13 @@ func Test_PurlList(t *testing.T) {
 					},
 				},
 			},
-			pkgs: []Package{
+			wantPkgs: []Package{
 				{
 					Name:    "ant",
 					Version: "1.10.8",
 					Type:    pkg.JavaPkg,
 					PURL:    "pkg:maven/org.apache.ant/ant@1.10.8",
+					Distro:  &distro.Distro{Type: distro.Debian, Version: "8", IDLike: []string{"debian"}},
 					Metadata: JavaMetadata{
 						PomArtifactID: "ant",
 						PomGroupID:    "org.apache.ant",
@@ -387,6 +392,7 @@ func Test_PurlList(t *testing.T) {
 					Version: "2.14.1",
 					Type:    pkg.JavaPkg,
 					PURL:    "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1",
+					Distro:  &distro.Distro{Type: distro.Debian, Version: "8", IDLike: []string{"debian"}},
 					Metadata: JavaMetadata{
 						PomArtifactID: "log4j-core",
 						PomGroupID:    "org.apache.logging.log4j",
@@ -397,7 +403,7 @@ func Test_PurlList(t *testing.T) {
 					Version: "2.88dsf-59",
 					Type:    pkg.DebPkg,
 					PURL:    "pkg:deb/debian/sysv-rc@2.88dsf-59?arch=all&distro=debian-8&upstream=sysvinit",
-					Distro:  &distro.Distro{Type: distro.Debian, Version: "8", Codename: "", IDLike: []string{"debian"}},
+					Distro:  &distro.Distro{Type: distro.Debian, Version: "8", IDLike: []string{"debian"}},
 					Upstreams: []UpstreamPackage{
 						{
 							Name: "sysvinit",
@@ -409,7 +415,7 @@ func Test_PurlList(t *testing.T) {
 		{
 			name:      "infer context when distro is present for multiple similar purls",
 			userInput: "purl:test-fixtures/purl/homogeneous-os.txt",
-			context: Context{
+			wantContext: Context{
 				Distro: &distro.Distro{
 					Type:    "alpine",
 					IDLike:  []string{"alpine"},
@@ -421,27 +427,27 @@ func Test_PurlList(t *testing.T) {
 					},
 				},
 			},
-			pkgs: []Package{
+			wantPkgs: []Package{
 				{
 					Name:    "openssl",
 					Version: "3.2.1",
 					Type:    pkg.ApkPkg,
 					PURL:    "pkg:apk/openssl@3.2.1?arch=aarch64&distro=alpine-3.20.3",
-					Distro:  &distro.Distro{Type: distro.Alpine, Version: "3.20.3", Codename: "", IDLike: []string{"alpine"}},
+					Distro:  &distro.Distro{Type: distro.Alpine, Version: "3.20.3", IDLike: []string{"alpine"}},
 				},
 				{
 					Name:    "curl",
 					Version: "7.61.1",
 					Type:    pkg.ApkPkg,
 					PURL:    "pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.3",
-					Distro:  &distro.Distro{Type: distro.Alpine, Version: "3.20.3", Codename: "", IDLike: []string{"alpine"}},
+					Distro:  &distro.Distro{Type: distro.Alpine, Version: "3.20.3", IDLike: []string{"alpine"}},
 				},
 			},
 		},
 		{
 			name:      "different distro info in purls does not infer context",
 			userInput: "purl:test-fixtures/purl/different-os.txt",
-			context: Context{
+			wantContext: Context{
 				// important: no distro info inferred
 				Source: &source.Description{
 					Metadata: SBOMFileMetadata{
@@ -449,20 +455,20 @@ func Test_PurlList(t *testing.T) {
 					},
 				},
 			},
-			pkgs: []Package{
+			wantPkgs: []Package{
 				{
 					Name:    "openssl",
 					Version: "3.2.1",
 					Type:    pkg.ApkPkg,
 					PURL:    "pkg:apk/openssl@3.2.1?arch=aarch64&distro=alpine-3.20.3",
-					Distro:  &distro.Distro{Type: distro.Alpine, Version: "3.20.3", Codename: "", IDLike: []string{"alpine"}},
+					Distro:  &distro.Distro{Type: distro.Alpine, Version: "3.20.3", IDLike: []string{"alpine"}},
 				},
 				{
 					Name:    "curl",
 					Version: "7.61.1",
 					Type:    pkg.ApkPkg,
 					PURL:    "pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.2",
-					Distro:  &distro.Distro{Type: distro.Alpine, Version: "3.20.2", Codename: "", IDLike: []string{"alpine"}},
+					Distro:  &distro.Distro{Type: distro.Alpine, Version: "3.20.2", IDLike: []string{"alpine"}},
 				},
 			},
 		},
@@ -486,6 +492,71 @@ func Test_PurlList(t *testing.T) {
 			userInput: "purl:test-fixtures/purl/invalid-purl.txt",
 			wantErr:   require.Error,
 		},
+		{
+			name: "honors default channel configuration (no EUS)",
+			config: ProviderConfig{
+				SynthesisConfig: SynthesisConfig{
+					Distro: DistroConfig{
+						FixChannels: testFixChannels(),
+					},
+				},
+			},
+			userInput: "purl:test-fixtures/purl/valid-rhel-9.txt",
+			wantContext: Context{
+				Source: &source.Description{
+					Metadata: SBOMFileMetadata{
+						Path: "test-fixtures/purl/valid-rhel-9.txt",
+					},
+				},
+				Distro: &distro.Distro{
+					Type:    distro.RedHat,
+					IDLike:  []string{"redhat"},
+					Version: "9.4",
+				},
+			},
+			wantPkgs: []Package{
+				{
+					Name:    "kernel",
+					Version: "0:5.14.0-100",
+					Type:    pkg.RpmPkg,
+					Distro:  &distro.Distro{Type: distro.RedHat, Version: "9.4", IDLike: []string{"redhat"}},
+					PURL:    "pkg:rpm/redhat/kernel@0:5.14.0-100?distro=rhel-9.4",
+				},
+			},
+		},
+		{
+			name: "honors default channel configuration (EUS)",
+			config: ProviderConfig{
+				SynthesisConfig: SynthesisConfig{
+					Distro: DistroConfig{
+						FixChannels: testFixChannels(),
+					},
+				},
+			},
+			userInput: "purl:test-fixtures/purl/valid-rhel-9+eus.txt",
+			wantContext: Context{
+				Source: &source.Description{
+					Metadata: SBOMFileMetadata{
+						Path: "test-fixtures/purl/valid-rhel-9+eus.txt",
+					},
+				},
+				Distro: &distro.Distro{
+					Type:     distro.RedHat,
+					IDLike:   []string{"redhat"},
+					Channels: names("eus"), // important!
+					Version:  "9.4",
+				},
+			},
+			wantPkgs: []Package{
+				{
+					Name:    "kernel",
+					Version: "0:5.14.0-100",
+					Type:    pkg.RpmPkg,
+					Distro:  &distro.Distro{Type: distro.RedHat, Version: "9.4", Channels: names("eus"), IDLike: []string{"redhat"}},
+					PURL:    "pkg:rpm/redhat/kernel@0:5.14.0-100?distro=rhel-9.4+eus",
+				},
+			},
+		},
 	}
 
 	for _, tc := range tests {
@@ -494,7 +565,7 @@ func Test_PurlList(t *testing.T) {
 				tc.wantErr = require.NoError
 			}
 
-			packages, ctx, _, err := Provide(tc.userInput, ProviderConfig{})
+			packages, ctx, _, err := Provide(tc.userInput, tc.config)
 
 			tc.wantErr(t, err)
 			if err != nil {
@@ -502,19 +573,19 @@ func Test_PurlList(t *testing.T) {
 				return
 			}
 
-			if d := cmp.Diff(tc.context, ctx, diffOpts...); d != "" {
+			if d := cmp.Diff(tc.wantContext, ctx, diffOpts...); d != "" {
 				t.Errorf("unexpected context (-want +got):\n%s", d)
 			}
-			require.Len(t, packages, len(tc.pkgs))
+			require.Len(t, packages, len(tc.wantPkgs))
 
 			slices.SortFunc(packages, func(a, b Package) int {
 				return strings.Compare(a.Name, b.Name)
 			})
-			slices.SortFunc(tc.pkgs, func(a, b Package) int {
+			slices.SortFunc(tc.wantPkgs, func(a, b Package) int {
 				return strings.Compare(a.Name, b.Name)
 			})
 
-			for idx, expected := range tc.pkgs {
+			for idx, expected := range tc.wantPkgs {
 				if d := cmp.Diff(expected, packages[idx], diffOpts...); d != "" {
 					t.Errorf("unexpected context (-want +got):\n%s", d)
 				}
@@ -522,3 +593,15 @@ func Test_PurlList(t *testing.T) {
 		})
 	}
 }
+
+func testFixChannels() []distro.FixChannel {
+	// TODO: change this to distro.DefaultFixChannels() when EUS is configured "auto" by default
+	//return distro.DefaultFixChannels()
+	return []distro.FixChannel{
+		{
+			Name:  "eus",
+			IDs:   []string{"rhel"},
+			Apply: distro.ChannelConditionallyEnabled,
+		},
+	}
+}
diff --git a/grype/pkg/test-fixtures/purl/valid-rhel-9+eus.txt b/grype/pkg/test-fixtures/purl/valid-rhel-9+eus.txt
new file mode 100644
index 00000000..901fc7ac
--- /dev/null
+++ b/grype/pkg/test-fixtures/purl/valid-rhel-9+eus.txt
@@ -0,0 +1,1 @@
+pkg:rpm/redhat/kernel@0:5.14.0-100?distro=rhel-9.4+eus
\ No newline at end of file
diff --git a/grype/pkg/test-fixtures/purl/valid-rhel-9.txt b/grype/pkg/test-fixtures/purl/valid-rhel-9.txt
new file mode 100644
index 00000000..78af5ae2
--- /dev/null
+++ b/grype/pkg/test-fixtures/purl/valid-rhel-9.txt
@@ -0,0 +1,1 @@
+pkg:rpm/redhat/kernel@0:5.14.0-100?distro=rhel-9.4
\ No newline at end of file
diff --git a/grype/pkg/version_format_test.go b/grype/pkg/version_format_test.go
new file mode 100644
index 00000000..eec95e84
--- /dev/null
+++ b/grype/pkg/version_format_test.go
@@ -0,0 +1,95 @@
+package pkg
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/anchore/grype/grype/version"
+	syftPkg "github.com/anchore/syft/syft/pkg"
+)
+
+func TestVersionFormat(t *testing.T) {
+	tests := []struct {
+		name   string
+		p      Package
+		format version.Format
+	}{
+		{
+			name: "bitnami",
+			p: Package{
+				Type: syftPkg.BitnamiPkg,
+			},
+			format: version.BitnamiFormat,
+		},
+		{
+			name: "deb",
+			p: Package{
+				Type: syftPkg.DebPkg,
+			},
+			format: version.DebFormat,
+		},
+		{
+			name: "java jar",
+			p: Package{
+				Type: syftPkg.JavaPkg,
+			},
+			format: version.MavenFormat,
+		},
+		{
+			name: "gem",
+			p: Package{
+				Type: syftPkg.GemPkg,
+			},
+			format: version.GemFormat,
+		},
+		{
+			name: "jvm by metadata",
+			p: Package{
+				Metadata: JavaVMInstallationMetadata{},
+			},
+			format: version.JVMFormat,
+		},
+		{
+			name: "jvm by type and name (jdk)",
+			p: Package{
+				Type: syftPkg.BinaryPkg,
+				Name: "jdk",
+			},
+			format: version.JVMFormat,
+		},
+		{
+			name: "jvm by type and name (openjdk)",
+			p: Package{
+				Type: syftPkg.BinaryPkg,
+				Name: "openjdk",
+			},
+			format: version.JVMFormat,
+		},
+		{
+			name: "jvm by type and name (jre)",
+			p: Package{
+				Type: syftPkg.BinaryPkg,
+				Name: "jre",
+			},
+			format: version.JVMFormat,
+		},
+		{
+			name: "jvm by type and name (java_se)",
+			p: Package{
+				Type: syftPkg.BinaryPkg,
+				Name: "java_se",
+			},
+			format: version.JVMFormat,
+		},
+	}
+
+	for _, test := range tests {
+		name := fmt.Sprintf("pkgType[%s]->format[%s]", test.p.Type, test.format)
+		t.Run(name, func(t *testing.T) {
+			actual := VersionFormat(test.p)
+			if actual != test.format {
+				t.Errorf("mismatched pkgType->format mapping, pkgType='%s': '%s'!='%s'", test.p.Type, test.format, actual)
+			}
+		})
+	}
+}
diff --git a/grype/presenter/internal/test_helpers.go b/grype/presenter/internal/test_helpers.go
index 61581c6d..54be97ec 100644
--- a/grype/presenter/internal/test_helpers.go
+++ b/grype/presenter/internal/test_helpers.go
@@ -511,7 +511,8 @@ func generateContext(t *testing.T, scheme SyftSource) pkg.Context {
 			IDLike: []string{
 				"centos",
 			},
-			Version: "8.0",
+			Version:  "8.0",
+			Channels: []string{"eus"}, // a fake EUS-like channel for centOS
 		},
 	}
 }
diff --git a/grype/presenter/json/presenter_test.go b/grype/presenter/json/presenter_test.go
index dc2929ae..5a64191b 100644
--- a/grype/presenter/json/presenter_test.go
+++ b/grype/presenter/json/presenter_test.go
@@ -80,7 +80,7 @@ func TestJsonDirsPresenter(t *testing.T) {
 }
 
 func TestEmptyJsonPresenter(t *testing.T) {
-	// Expected to have an empty JSON array back
+	// expected to have an empty JSON array back
 	var buffer bytes.Buffer
 
 	ctx := pkg.Context{
diff --git a/grype/presenter/json/test-fixtures/snapshot/TestJsonDirsPresenter.golden b/grype/presenter/json/test-fixtures/snapshot/TestJsonDirsPresenter.golden
index db14134e..08b87fe3 100644
--- a/grype/presenter/json/test-fixtures/snapshot/TestJsonDirsPresenter.golden
+++ b/grype/presenter/json/test-fixtures/snapshot/TestJsonDirsPresenter.golden
@@ -166,6 +166,9 @@
   "version": "8.0",
   "idLike": [
    "centos"
+  ],
+  "channels": [
+   "eus"
   ]
  },
  "descriptor": {
diff --git a/grype/presenter/json/test-fixtures/snapshot/TestJsonImgsPresenter.golden b/grype/presenter/json/test-fixtures/snapshot/TestJsonImgsPresenter.golden
index 1e740f2a..d6d4ce6e 100644
--- a/grype/presenter/json/test-fixtures/snapshot/TestJsonImgsPresenter.golden
+++ b/grype/presenter/json/test-fixtures/snapshot/TestJsonImgsPresenter.golden
@@ -195,6 +195,9 @@
   "version": "8.0",
   "idLike": [
    "centos"
+  ],
+  "channels": [
+   "eus"
   ]
  },
  "descriptor": {
diff --git a/grype/presenter/models/document_test.go b/grype/presenter/models/document_test.go
index 0cb95916..4dd75dc6 100644
--- a/grype/presenter/models/document_test.go
+++ b/grype/presenter/models/document_test.go
@@ -7,7 +7,6 @@ import (
 	"github.com/stretchr/testify/assert"
 
 	"github.com/anchore/clio"
-	"github.com/anchore/grype/grype/distro"
 	"github.com/anchore/grype/grype/match"
 	"github.com/anchore/grype/grype/pkg"
 	"github.com/anchore/grype/grype/vulnerability"
@@ -75,11 +74,6 @@ func TestPackagesAreSorted(t *testing.T) {
 		Source: &syftSource.Description{
 			Metadata: syftSource.DirectoryMetadata{},
 		},
-		Distro: &distro.Distro{
-			Type:    "centos",
-			IDLike:  []string{"rhel"},
-			Version: "8.0",
-		},
 	}
 	doc, err := NewDocument(clio.Identification{}, packages, ctx, matches, nil, NewMetadataMock(), nil, nil, SortByPackage)
 	if err != nil {
@@ -136,11 +130,6 @@ func TestFixSuggestedVersion(t *testing.T) {
 		Source: &syftSource.Description{
 			Metadata: syftSource.DirectoryMetadata{},
 		},
-		Distro: &distro.Distro{
-			Type:    "centos",
-			IDLike:  []string{"rhel"},
-			Version: "8.0",
-		},
 	}
 	doc, err := NewDocument(clio.Identification{}, packages, ctx, matches, nil, NewMetadataMock(), nil, nil, SortByPackage)
 	if err != nil {
@@ -158,7 +147,6 @@ func TestTimestampValidFormat(t *testing.T) {
 
 	ctx := pkg.Context{
 		Source: nil,
-		Distro: nil,
 	}
 
 	doc, err := NewDocument(clio.Identification{}, nil, ctx, matches, nil, nil, nil, nil, SortByPackage)
diff --git a/grype/search/version_constraint_test.go b/grype/search/version_constraint_test.go
index d375f7bc..1f09a5d8 100644
--- a/grype/search/version_constraint_test.go
+++ b/grype/search/version_constraint_test.go
@@ -40,7 +40,7 @@ func Test_ByVersion(t *testing.T) {
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			v := version.NewVersion(tt.version, version.SemanticFormat)
+			v := version.New(tt.version, version.SemanticFormat)
 			constraint := ByVersion(*v)
 			matches, reason, err := constraint.MatchesVulnerability(tt.input)
 			wantErr := require.NoError
diff --git a/grype/version/apk_version_test.go b/grype/version/apk_version_test.go
index cf7be62e..2f8dd666 100644
--- a/grype/version/apk_version_test.go
+++ b/grype/version/apk_version_test.go
@@ -114,7 +114,7 @@ func TestApkVersion_Compare(t *testing.T) {
 			thisVer, err := newApkVersion(test.thisVersion)
 			require.NoError(t, err)
 
-			otherVer := NewVersion(test.otherVersion, test.otherFormat)
+			otherVer := New(test.otherVersion, test.otherFormat)
 
 			result, err := thisVer.Compare(otherVer)
 
@@ -142,7 +142,7 @@ func TestApkVersion_Compare_EdgeCases(t *testing.T) {
 		{
 			name: "nil version object",
 			setupFunc: func(t testing.TB) (*Version, *Version) {
-				thisVer := NewVersion("1.2.3-r4", ApkFormat)
+				thisVer := New("1.2.3-r4", ApkFormat)
 				return thisVer, nil
 			},
 			expectError:    true,
diff --git a/grype/version/bitnami_version_test.go b/grype/version/bitnami_version_test.go
index a7b24f5c..1e3a7b98 100644
--- a/grype/version/bitnami_version_test.go
+++ b/grype/version/bitnami_version_test.go
@@ -113,7 +113,7 @@ func TestBitnamiVersion_Compare(t *testing.T) {
 			thisVer, err := newBitnamiVersion(test.thisVersion)
 			require.NoError(t, err)
 
-			otherVer := NewVersion(test.otherVersion, test.otherFormat)
+			otherVer := New(test.otherVersion, test.otherFormat)
 
 			result, err := thisVer.Compare(otherVer)
 
@@ -141,7 +141,7 @@ func TestBitnamiVersion_Compare_EdgeCases(t *testing.T) {
 		{
 			name: "nil version object",
 			setupFunc: func(t testing.TB) (*Version, *Version) {
-				thisVer := NewVersion("1.2.3-4", BitnamiFormat)
+				thisVer := New("1.2.3-4", BitnamiFormat)
 
 				return thisVer, nil
 			},
diff --git a/grype/version/combined_constraint_test.go b/grype/version/combined_constraint_test.go
index 4b9338ce..39c24712 100644
--- a/grype/version/combined_constraint_test.go
+++ b/grype/version/combined_constraint_test.go
@@ -98,7 +98,7 @@ func TestCombinedConstraint_Methods(t *testing.T) {
 					MustGetConstraint(">= 1.0.0", SemanticFormat),
 				},
 			},
-			version:       NewVersion("1.5.0", SemanticFormat),
+			version:       New("1.5.0", SemanticFormat),
 			wantValue:     ">= 1.0.0",
 			wantString:    ">= 1.0.0 (semantic)",
 			wantFormat:    SemanticFormat,
@@ -111,7 +111,7 @@ func TestCombinedConstraint_Methods(t *testing.T) {
 					MustGetConstraint(">= 2.0.0", SemanticFormat),
 				},
 			},
-			version:       NewVersion("1.5.0", SemanticFormat),
+			version:       New("1.5.0", SemanticFormat),
 			wantValue:     ">= 2.0.0",
 			wantString:    ">= 2.0.0 (semantic)",
 			wantFormat:    SemanticFormat,
@@ -125,7 +125,7 @@ func TestCombinedConstraint_Methods(t *testing.T) {
 					MustGetConstraint(">= 3.0.0", SemanticFormat),
 				},
 			},
-			version:       NewVersion("1.5.0", SemanticFormat),
+			version:       New("1.5.0", SemanticFormat),
 			wantValue:     ">= 1.0.0 || >= 3.0.0",
 			wantString:    ">= 1.0.0 || >= 3.0.0 (semantic)",
 			wantFormat:    SemanticFormat,
@@ -139,7 +139,7 @@ func TestCombinedConstraint_Methods(t *testing.T) {
 					MustGetConstraint("< 2.0.0", SemanticFormat),
 				},
 			},
-			version:       NewVersion("1.5.0", SemanticFormat),
+			version:       New("1.5.0", SemanticFormat),
 			wantValue:     ">= 2.0.0 || < 2.0.0",
 			wantString:    ">= 2.0.0 || < 2.0.0 (semantic)",
 			wantFormat:    SemanticFormat,
@@ -153,7 +153,7 @@ func TestCombinedConstraint_Methods(t *testing.T) {
 					MustGetConstraint(">= 3.0.0", SemanticFormat),
 				},
 			},
-			version:       NewVersion("1.5.0", SemanticFormat),
+			version:       New("1.5.0", SemanticFormat),
 			wantValue:     ">= 2.0.0 || >= 3.0.0",
 			wantString:    ">= 2.0.0 || >= 3.0.0 (semantic)",
 			wantFormat:    SemanticFormat,
@@ -164,7 +164,7 @@ func TestCombinedConstraint_Methods(t *testing.T) {
 			constraint: combinedConstraint{
 				OrOperands: []Constraint{},
 			},
-			version:       NewVersion("1.5.0", SemanticFormat),
+			version:       New("1.5.0", SemanticFormat),
 			wantValue:     "",
 			wantString:    " (unknown)",
 			wantFormat:    UnknownFormat,
@@ -178,7 +178,7 @@ func TestCombinedConstraint_Methods(t *testing.T) {
 					MustGetConstraint("< 0.5.0", RpmFormat),
 				},
 			},
-			version:       NewVersion("1.5.0", RpmFormat),
+			version:       New("1.5.0", RpmFormat),
 			wantValue:     ">= 1.0.0 || < 0.5.0",
 			wantString:    ">= 1.0.0 || < 0.5.0 (rpm)",
 			wantFormat:    RpmFormat,
@@ -245,7 +245,7 @@ func TestCombinedConstraint_Satisfied_WithErrors(t *testing.T) {
 					mockConstraint{value: "< 2.0.0", format: SemanticFormat, satisfied: true},
 				},
 			},
-			version: NewVersion("1.5.0", SemanticFormat),
+			version: New("1.5.0", SemanticFormat),
 			wantErr: require.Error,
 		},
 		{
@@ -256,7 +256,7 @@ func TestCombinedConstraint_Satisfied_WithErrors(t *testing.T) {
 					mockConstraint{value: "< 2.0.0", format: SemanticFormat, returnErr: true},
 				},
 			},
-			version: NewVersion("1.5.0", SemanticFormat),
+			version: New("1.5.0", SemanticFormat),
 			wantErr: require.Error,
 		},
 		{
@@ -267,7 +267,7 @@ func TestCombinedConstraint_Satisfied_WithErrors(t *testing.T) {
 					mockConstraint{value: "< 2.0.0", format: SemanticFormat, returnErr: true},
 				},
 			},
-			version: NewVersion("1.5.0", SemanticFormat),
+			version: New("1.5.0", SemanticFormat),
 			wantErr: require.NoError,
 		},
 	}
diff --git a/grype/version/deb_version_test.go b/grype/version/deb_version_test.go
index cade7c5b..61ed6d88 100644
--- a/grype/version/deb_version_test.go
+++ b/grype/version/deb_version_test.go
@@ -112,7 +112,7 @@ func TestDebVersion_Compare(t *testing.T) {
 			thisVer, err := newDebVersion(test.thisVersion)
 			require.NoError(t, err)
 
-			otherVer := NewVersion(test.otherVersion, test.otherFormat)
+			otherVer := New(test.otherVersion, test.otherFormat)
 
 			result, err := thisVer.Compare(otherVer)
 
@@ -140,7 +140,7 @@ func TestDebVersion_Compare_EdgeCases(t *testing.T) {
 		{
 			name: "nil version object",
 			setupFunc: func(t testing.TB) (*Version, *Version) {
-				thisVer := NewVersion("1.2.3-1", DebFormat)
+				thisVer := New("1.2.3-1", DebFormat)
 				return thisVer, nil
 			},
 			expectError:    true,
@@ -149,7 +149,7 @@ func TestDebVersion_Compare_EdgeCases(t *testing.T) {
 		{
 			name: "empty debVersion in other object",
 			setupFunc: func(t testing.TB) (*Version, *Version) {
-				thisVer := NewVersion("1.2.3-1", DebFormat)
+				thisVer := New("1.2.3-1", DebFormat)
 				otherVer := &Version{
 					Raw:    "1.2.3-2",
 					Format: DebFormat,
diff --git a/grype/version/format_test.go b/grype/version/format_test.go
index 6194c3ae..1c1af6be 100644
--- a/grype/version/format_test.go
+++ b/grype/version/format_test.go
@@ -3,9 +3,6 @@ package version
 import (
 	"fmt"
 	"testing"
-
-	"github.com/anchore/grype/grype/pkg"
-	syftPkg "github.com/anchore/syft/syft/pkg"
 )
 
 func TestParseFormat(t *testing.T) {
@@ -53,89 +50,3 @@ func TestParseFormat(t *testing.T) {
 		})
 	}
 }
-
-func TestFormatFromPkgType(t *testing.T) {
-	tests := []struct {
-		name   string
-		p      pkg.Package
-		format Format
-	}{
-		{
-			name: "bitnami",
-			p: pkg.Package{
-				Type: syftPkg.BitnamiPkg,
-			},
-			format: BitnamiFormat,
-		},
-		{
-			name: "deb",
-			p: pkg.Package{
-				Type: syftPkg.DebPkg,
-			},
-			format: DebFormat,
-		},
-		{
-			name: "java jar",
-			p: pkg.Package{
-				Type: syftPkg.JavaPkg,
-			},
-			format: MavenFormat,
-		},
-		{
-			name: "gem",
-			p: pkg.Package{
-				Type: syftPkg.GemPkg,
-			},
-			format: GemFormat,
-		},
-		{
-			name: "jvm by metadata",
-			p: pkg.Package{
-				Metadata: pkg.JavaVMInstallationMetadata{},
-			},
-			format: JVMFormat,
-		},
-		{
-			name: "jvm by type and name (jdk)",
-			p: pkg.Package{
-				Type: syftPkg.BinaryPkg,
-				Name: "jdk",
-			},
-			format: JVMFormat,
-		},
-		{
-			name: "jvm by type and name (openjdk)",
-			p: pkg.Package{
-				Type: syftPkg.BinaryPkg,
-				Name: "openjdk",
-			},
-			format: JVMFormat,
-		},
-		{
-			name: "jvm by type and name (jre)",
-			p: pkg.Package{
-				Type: syftPkg.BinaryPkg,
-				Name: "jre",
-			},
-			format: JVMFormat,
-		},
-		{
-			name: "jvm by type and name (java_se)",
-			p: pkg.Package{
-				Type: syftPkg.BinaryPkg,
-				Name: "java_se",
-			},
-			format: JVMFormat,
-		},
-	}
-
-	for _, test := range tests {
-		name := fmt.Sprintf("pkgType[%s]->format[%s]", test.p.Type, test.format)
-		t.Run(name, func(t *testing.T) {
-			actual := FormatFromPkg(test.p)
-			if actual != test.format {
-				t.Errorf("mismatched pkgType->format mapping, pkgType='%s': '%s'!='%s'", test.p.Type, test.format, actual)
-			}
-		})
-	}
-}
diff --git a/grype/version/fuzzy_version_test.go b/grype/version/fuzzy_version_test.go
index 5b7315b1..c40ddfb8 100644
--- a/grype/version/fuzzy_version_test.go
+++ b/grype/version/fuzzy_version_test.go
@@ -56,9 +56,9 @@ func TestFuzzyVersion_Compare(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			thisVer := NewVersion(test.thisVersion, UnknownFormat) // explicitly use the fuzzy version format
+			thisVer := New(test.thisVersion, UnknownFormat) // explicitly use the fuzzy version format
 
-			otherVer := NewVersion(test.otherVersion, test.otherFormat)
+			otherVer := New(test.otherVersion, test.otherFormat)
 
 			result, err := thisVer.Compare(otherVer)
 
@@ -87,7 +87,7 @@ func TestFuzzyVersion_Compare_EdgeCases(t *testing.T) {
 		{
 			name: "nil version object",
 			setupFunc: func(t testing.TB) (*Version, *Version) {
-				thisVer := NewVersion("1.2.3", UnknownFormat)
+				thisVer := New("1.2.3", UnknownFormat)
 
 				return thisVer, nil
 			},
@@ -132,7 +132,7 @@ func TestFuzzyVersion_Compare_NilScenarios(t *testing.T) {
 					raw:    "abc123",
 				}
 
-				otherVer := NewVersion("def456", UnknownFormat)
+				otherVer := New("def456", UnknownFormat)
 
 				return fv, otherVer
 			},
@@ -147,7 +147,7 @@ func TestFuzzyVersion_Compare_NilScenarios(t *testing.T) {
 					raw:    "abc123",
 				}
 
-				otherVer := NewVersion("1.2.3", UnknownFormat)
+				otherVer := New("1.2.3", UnknownFormat)
 
 				return fv, otherVer
 			},
@@ -162,7 +162,7 @@ func TestFuzzyVersion_Compare_NilScenarios(t *testing.T) {
 					raw:    "abc123",
 				}
 
-				otherVer := NewVersion("1.2.3", UnknownFormat)
+				otherVer := New("1.2.3", UnknownFormat)
 
 				return fv, otherVer
 			},
@@ -181,7 +181,7 @@ func TestFuzzyVersion_Compare_NilScenarios(t *testing.T) {
 				}
 
 				// create other version that will result in nil semver from newFuzzySemver
-				otherVer := NewVersion("abc123", UnknownFormat)
+				otherVer := New("abc123", UnknownFormat)
 
 				return fv, otherVer
 			},
@@ -202,7 +202,7 @@ func TestFuzzyVersion_Compare_NilScenarios(t *testing.T) {
 				// this should create a version that when passed to newFuzzySemver
 				// results in a semanticVersion with nil obj (this might be hard to achieve
 				// but we'll test the logic path)
-				otherVer := NewVersion("not-semver-compliant", UnknownFormat)
+				otherVer := New("not-semver-compliant", UnknownFormat)
 
 				return fv, otherVer
 			},
@@ -220,7 +220,7 @@ func TestFuzzyVersion_Compare_NilScenarios(t *testing.T) {
 					raw:    "1.2.3",
 				}
 
-				otherVer := NewVersion("1.2.4", UnknownFormat)
+				otherVer := New("1.2.4", UnknownFormat)
 
 				return fv, otherVer
 			},
diff --git a/grype/version/gem_version_test.go b/grype/version/gem_version_test.go
index 588797d9..6086a001 100644
--- a/grype/version/gem_version_test.go
+++ b/grype/version/gem_version_test.go
@@ -81,8 +81,8 @@ func Test_cleanPlatformMakesEqualVersions(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.input, func(t *testing.T) {
-			original := NewVersion(tt.input, GemFormat)
-			trimmed := NewVersion(tt.trimmed, GemFormat)
+			original := New(tt.input, GemFormat)
+			trimmed := New(tt.trimmed, GemFormat)
 			comp, err := original.Compare(trimmed)
 			require.NoError(t, err)
 			assert.Equal(t, 0, comp)
@@ -222,8 +222,8 @@ func TestGemVersion_Compare(t *testing.T) {
 
 	for _, tt := range tests {
 		t.Run(fmt.Sprintf("%s_vs_%s", tt.v1, tt.v2), func(t *testing.T) {
-			ver1 := NewVersion(tt.v1, GemFormat)
-			ver2 := NewVersion(tt.v2, GemFormat)
+			ver1 := New(tt.v1, GemFormat)
+			ver2 := New(tt.v2, GemFormat)
 
 			// Test v1 vs v2
 			got1, err1 := ver1.Compare(ver2)
diff --git a/grype/version/generic_constraint_test.go b/grype/version/generic_constraint_test.go
index 15c9706f..f6cc7d12 100644
--- a/grype/version/generic_constraint_test.go
+++ b/grype/version/generic_constraint_test.go
@@ -67,7 +67,7 @@ func TestGenericConstraint_Satisfied_EmptyConstraint(t *testing.T) {
 	}{
 		{
 			name:    "with valid version",
-			version: NewVersion("1.2.3", SemanticFormat),
+			version: New("1.2.3", SemanticFormat),
 		},
 		{
 			name:    "with nil version",
@@ -75,7 +75,7 @@ func TestGenericConstraint_Satisfied_EmptyConstraint(t *testing.T) {
 		},
 		{
 			name:    "with different format version",
-			version: NewVersion("1.2.3-r1", ApkFormat),
+			version: New("1.2.3-r1", ApkFormat),
 		},
 	}
 
@@ -139,7 +139,7 @@ func TestGenericConstraint_Satisfied_WithConstraint(t *testing.T) {
 			constraint, err := newGenericConstraint(SemanticFormat, test.constraint)
 			require.NoError(t, err)
 
-			version := NewVersion(test.version, SemanticFormat)
+			version := New(test.version, SemanticFormat)
 
 			satisfied, err := constraint.Satisfied(version)
 			if test.shouldError {
diff --git a/grype/version/golang_version_test.go b/grype/version/golang_version_test.go
index ae199e75..787049ce 100644
--- a/grype/version/golang_version_test.go
+++ b/grype/version/golang_version_test.go
@@ -46,7 +46,7 @@ func TestGolangVersion_Constraint(t *testing.T) {
 		t.Run(tc.name, func(t *testing.T) {
 			c, err := GetConstraint(tc.constraint, GolangFormat)
 			require.NoError(t, err)
-			v := NewVersion(tc.version, GolangFormat)
+			v := New(tc.version, GolangFormat)
 			sat, err := c.Satisfied(v)
 			require.NoError(t, err)
 			assert.Equal(t, tc.satisfied, sat)
@@ -206,9 +206,9 @@ func TestGolangVersion_Compare(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			version1 := NewVersion(test.version1, GolangFormat)
+			version1 := New(test.version1, GolangFormat)
 
-			version2 := NewVersion(test.version2, GolangFormat)
+			version2 := New(test.version2, GolangFormat)
 
 			result, err := version1.Compare(version2)
 			assert.NoError(t, err)
@@ -218,7 +218,7 @@ func TestGolangVersion_Compare(t *testing.T) {
 }
 
 func TestGolangVersion_Compare_NilVersion(t *testing.T) {
-	version := NewVersion("v1.2.3", GolangFormat)
+	version := New("v1.2.3", GolangFormat)
 
 	result, err := version.Compare(nil)
 	require.Error(t, err)
@@ -230,7 +230,7 @@ func TestGolangVersion_Compare_DifferentFormat(t *testing.T) {
 	golangVer, err := newGolangVersion("v1.2.3")
 	require.NoError(t, err)
 
-	semanticVer := NewVersion("1.2.3", SemanticFormat)
+	semanticVer := New("1.2.3", SemanticFormat)
 
 	result, err := golangVer.Compare(semanticVer)
 	require.NoError(t, err)
diff --git a/grype/version/helper_test.go b/grype/version/helper_test.go
index 55bcbb6a..8729cce4 100644
--- a/grype/version/helper_test.go
+++ b/grype/version/helper_test.go
@@ -31,7 +31,7 @@ func (c *testCase) assertVersionConstraint(t *testing.T, format Format, constrai
 		c.wantError = require.NoError
 	}
 
-	version := NewVersion(c.version, format)
+	version := New(c.version, format)
 
 	isSatisfied, err := constraint.Satisfied(version)
 	c.wantError(t, err)
diff --git a/grype/version/jvm_version_test.go b/grype/version/jvm_version_test.go
index 330e0182..61505289 100644
--- a/grype/version/jvm_version_test.go
+++ b/grype/version/jvm_version_test.go
@@ -124,10 +124,10 @@ func TestJVMVersion_Compare(t *testing.T) {
 	for _, test := range tests {
 		name := test.v1 + "_vs_" + test.v2
 		t.Run(name, func(t *testing.T) {
-			v1 := NewVersion(test.v1, JVMFormat)
+			v1 := New(test.v1, JVMFormat)
 			require.NotNil(t, v1)
 
-			v2 := NewVersion(test.v2, JVMFormat)
+			v2 := New(test.v2, JVMFormat)
 			require.NotNil(t, v2)
 
 			actual, err := v1.Compare(v2)
@@ -264,7 +264,7 @@ func TestJvmVersion_Compare_Formats(t *testing.T) {
 			thisVer, err := newJvmVersion(test.thisVersion)
 			require.NoError(t, err)
 
-			otherVer := NewVersion(test.otherVersion, test.otherFormat)
+			otherVer := New(test.otherVersion, test.otherFormat)
 
 			result, err := thisVer.Compare(otherVer)
 
@@ -291,7 +291,7 @@ func TestJvmVersion_Compare_EdgeCases(t *testing.T) {
 		{
 			name: "nil version object",
 			setupFunc: func(t testing.TB) (*Version, *Version) {
-				thisVer := NewVersion("1.8.0_275", JVMFormat)
+				thisVer := New("1.8.0_275", JVMFormat)
 				return thisVer, nil
 			},
 			expectError:    true,
diff --git a/grype/version/kb_version_test.go b/grype/version/kb_version_test.go
index 17a55b6d..d90d2dec 100644
--- a/grype/version/kb_version_test.go
+++ b/grype/version/kb_version_test.go
@@ -44,7 +44,7 @@ func TestKbVersion_Compare(t *testing.T) {
 		t.Run(test.name, func(t *testing.T) {
 			thisVer := newKBVersion(test.thisVersion)
 
-			otherVer := NewVersion(test.otherVersion, test.otherFormat)
+			otherVer := New(test.otherVersion, test.otherFormat)
 
 			result, err := thisVer.Compare(otherVer)
 
@@ -72,7 +72,7 @@ func TestKbVersion_Compare_EdgeCases(t *testing.T) {
 		{
 			name: "nil version object",
 			setupFunc: func(t testing.TB) (*Version, *Version) {
-				v := NewVersion("KB4562562", KBFormat)
+				v := New("KB4562562", KBFormat)
 				return v, nil
 			},
 			expectError:    true,
diff --git a/grype/version/maven_version_test.go b/grype/version/maven_version_test.go
index 37bc1bb9..970a07e2 100644
--- a/grype/version/maven_version_test.go
+++ b/grype/version/maven_version_test.go
@@ -155,8 +155,8 @@ func TestMavenVersion_Compare(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.v1+" vs "+tt.v2, func(t *testing.T) {
-			v1 := NewVersion(tt.v1, MavenFormat)
-			v2 := NewVersion(tt.v2, MavenFormat)
+			v1 := New(tt.v1, MavenFormat)
+			v2 := New(tt.v2, MavenFormat)
 
 			if got, _ := v1.Compare(v2); got != tt.want {
 				t.Errorf("Compare() = %v, want %v", got, tt.want)
@@ -199,8 +199,8 @@ func TestMavenVersion_Compare_Format(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			thisVer := NewVersion(test.thisVersion, MavenFormat)
-			otherVer := NewVersion(test.otherVersion, test.otherFormat)
+			thisVer := New(test.thisVersion, MavenFormat)
+			otherVer := New(test.otherVersion, test.otherFormat)
 
 			result, err := thisVer.Compare(otherVer)
 
@@ -228,7 +228,7 @@ func TestMavenVersion_Compare_EdgeCases(t *testing.T) {
 		{
 			name: "nil version object",
 			setupFunc: func(t testing.TB) (*Version, *Version) {
-				thisVer := NewVersion("1.2.3", MavenFormat)
+				thisVer := New("1.2.3", MavenFormat)
 				return thisVer, nil
 			},
 			expectError:    true,
@@ -241,12 +241,12 @@ func TestMavenVersion_Compare_EdgeCases(t *testing.T) {
 				// version library handles most comparisons, but we can simulate the
 				// error condition by creating a mock that would trigger the last
 				// error condition in the Compare function
-				thisVer := NewVersion("1.2.3", MavenFormat)
+				thisVer := New("1.2.3", MavenFormat)
 
 				// We'd need to modify the otherVer manually to create a scenario
 				// where none of the comparison methods return true, which is unlikely
 				// in real usage but could be simulated for test coverage
-				otherVer := NewVersion("1.2.4", MavenFormat)
+				otherVer := New("1.2.4", MavenFormat)
 
 				return thisVer, otherVer
 			},
diff --git a/grype/version/pep440_version_test.go b/grype/version/pep440_version_test.go
index 6685df7f..2d13e3ac 100644
--- a/grype/version/pep440_version_test.go
+++ b/grype/version/pep440_version_test.go
@@ -212,7 +212,7 @@ func TestPep440Version_Constraint(t *testing.T) {
 		t.Run(tc.name, func(t *testing.T) {
 			c, err := GetConstraint(tc.constraint, PythonFormat)
 			require.NoError(t, err)
-			v := NewVersion(tc.version, PythonFormat)
+			v := New(tc.version, PythonFormat)
 			sat, err := c.Satisfied(v)
 			require.NoError(t, err)
 			assert.Equal(t, tc.satisfied, sat)
@@ -265,7 +265,7 @@ func TestPep440Version_Compare(t *testing.T) {
 			thisVer, err := newPep440Version(test.thisVersion)
 			require.NoError(t, err)
 
-			otherVer := NewVersion(test.otherVersion, test.otherFormat)
+			otherVer := New(test.otherVersion, test.otherFormat)
 
 			result, err := thisVer.Compare(otherVer)
 
@@ -293,7 +293,7 @@ func TestPep440Version_Compare_EdgeCases(t *testing.T) {
 		{
 			name: "nil version object",
 			setupFunc: func(t testing.TB) (*Version, *Version) {
-				thisVer := NewVersion("1.2.3", PythonFormat)
+				thisVer := New("1.2.3", PythonFormat)
 				return thisVer, nil
 			},
 			expectError:    true,
diff --git a/grype/version/portage_version_test.go b/grype/version/portage_version_test.go
index 9f2a1bb5..969894f4 100644
--- a/grype/version/portage_version_test.go
+++ b/grype/version/portage_version_test.go
@@ -151,7 +151,7 @@ func TestPortageVersion_Constraint_UnsupportedFormat(t *testing.T) {
 	assert.NoError(t, err)
 
 	// test with a semantic version (wrong format)
-	version := NewVersion("1.2.3", SemanticFormat)
+	version := New("1.2.3", SemanticFormat)
 
 	satisfied, err := c.Satisfied(version)
 	require.Error(t, err)
@@ -214,8 +214,8 @@ func TestPortageVersion_Compare(t *testing.T) {
 	for _, test := range tests {
 		name := test.v1 + "_vs_" + test.v2
 		t.Run(name, func(t *testing.T) {
-			v1 := NewVersion(test.v1, PortageFormat)
-			v2 := NewVersion(test.v2, PortageFormat)
+			v1 := New(test.v1, PortageFormat)
+			v2 := New(test.v2, PortageFormat)
 
 			actual, err := v1.Compare(v2)
 			require.NoError(t, err)
@@ -258,8 +258,8 @@ func TestPortageVersion_Compare_Format(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			thisVer := NewVersion(test.thisVersion, PortageFormat)
-			otherVer := NewVersion(test.otherVersion, test.otherFormat)
+			thisVer := New(test.thisVersion, PortageFormat)
+			otherVer := New(test.otherVersion, test.otherFormat)
 
 			result, err := thisVer.Compare(otherVer)
 
@@ -287,7 +287,7 @@ func TestPortageVersion_Compare_EdgeCases(t *testing.T) {
 		{
 			name: "nil version object",
 			setupFunc: func(t testing.TB) (*Version, *Version) {
-				thisVer := NewVersion("1.2.3", PortageFormat)
+				thisVer := New("1.2.3", PortageFormat)
 				return thisVer, nil
 			},
 			expectError:    true,
diff --git a/grype/version/rpm_version_test.go b/grype/version/rpm_version_test.go
index df4892d2..99afb56f 100644
--- a/grype/version/rpm_version_test.go
+++ b/grype/version/rpm_version_test.go
@@ -114,8 +114,8 @@ func TestRpmVersion_Compare(t *testing.T) {
 	for _, test := range tests {
 		name := test.v1 + "_vs_" + test.v2
 		t.Run(name, func(t *testing.T) {
-			v1 := NewVersion(test.v1, RpmFormat)
-			v2 := NewVersion(test.v2, RpmFormat)
+			v1 := New(test.v1, RpmFormat)
+			v2 := New(test.v2, RpmFormat)
 
 			actual, err := v1.Compare(v2)
 			require.NoError(t, err, "unexpected error comparing versions: %s vs %s", test.v1, test.v2)
@@ -158,9 +158,9 @@ func TestRpmVersion_Compare_Format(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			thisVer := NewVersion(test.thisVersion, RpmFormat)
+			thisVer := New(test.thisVersion, RpmFormat)
 
-			otherVer := NewVersion(test.otherVersion, test.otherFormat)
+			otherVer := New(test.otherVersion, test.otherFormat)
 
 			result, err := thisVer.Compare(otherVer)
 
@@ -188,7 +188,7 @@ func TestRpmVersion_Compare_EdgeCases(t *testing.T) {
 		{
 			name: "nil version object",
 			setupFunc: func(t testing.TB) (*Version, *Version) {
-				thisVer := NewVersion("1.2.3-1", RpmFormat)
+				thisVer := New("1.2.3-1", RpmFormat)
 				return thisVer, nil
 			},
 			expectError:    true,
diff --git a/grype/version/semantic_version_test.go b/grype/version/semantic_version_test.go
index ca4deef0..4a8db2d1 100644
--- a/grype/version/semantic_version_test.go
+++ b/grype/version/semantic_version_test.go
@@ -201,8 +201,8 @@ func TestSemanticVersion_PrereleaseNormalizer_WithGemFormat(t *testing.T) {
 	for _, version := range rubyStyleVersions {
 		t.Run(version, func(t *testing.T) {
 			// both semantic and gem formats should be able to handle these versions
-			semanticVer := NewVersion(version, SemanticFormat)
-			gemVer := NewVersion(version, GemFormat)
+			semanticVer := New(version, SemanticFormat)
+			gemVer := New(version, GemFormat)
 
 			// they might have different comparison behavior, but both should be valid
 			assert.NotNil(t, semanticVer)
@@ -263,7 +263,7 @@ func TestSemanticVersion_Compare_Format(t *testing.T) {
 			thisVer, err := newSemanticVersion(test.thisVersion, true)
 			require.NoError(t, err)
 
-			otherVer := NewVersion(test.otherVersion, test.otherFormat)
+			otherVer := New(test.otherVersion, test.otherFormat)
 
 			result, err := thisVer.Compare(otherVer)
 
@@ -291,7 +291,7 @@ func TestSemanticVersion_Compare_EdgeCases(t *testing.T) {
 		{
 			name: "nil version object",
 			setupFunc: func(t testing.TB) (*Version, *Version) {
-				thisVer := NewVersion("1.2.3", SemanticFormat)
+				thisVer := New("1.2.3", SemanticFormat)
 				return thisVer, nil
 			},
 			expectError:    true,
diff --git a/grype/version/set_test.go b/grype/version/set_test.go
index ac19de4c..01af3c49 100644
--- a/grype/version/set_test.go
+++ b/grype/version/set_test.go
@@ -24,8 +24,8 @@ func TestNewSet(t *testing.T) {
 			name:         "set with versions",
 			ignoreFormat: false,
 			versions: []*Version{
-				NewVersion("1.0.0", SemanticFormat),
-				NewVersion("2.0.0", SemanticFormat),
+				New("1.0.0", SemanticFormat),
+				New("2.0.0", SemanticFormat),
 			},
 			expectedSize: 2,
 		},
@@ -33,8 +33,8 @@ func TestNewSet(t *testing.T) {
 			name:         "set with duplicate versions ignoring format",
 			ignoreFormat: true,
 			versions: []*Version{
-				NewVersion("1.0.0", SemanticFormat),
-				NewVersion("1.0.0", ApkFormat),
+				New("1.0.0", SemanticFormat),
+				New("1.0.0", ApkFormat),
 			},
 			expectedSize: 1,
 		},
@@ -42,8 +42,8 @@ func TestNewSet(t *testing.T) {
 			name:         "set with duplicate versions not ignoring format",
 			ignoreFormat: false,
 			versions: []*Version{
-				NewVersion("1.0.0", SemanticFormat),
-				NewVersion("1.0.0", ApkFormat),
+				New("1.0.0", SemanticFormat),
+				New("1.0.0", ApkFormat),
 			},
 			expectedSize: 2,
 		},
@@ -51,9 +51,9 @@ func TestNewSet(t *testing.T) {
 			name:         "set with nil versions",
 			ignoreFormat: false,
 			versions: []*Version{
-				NewVersion("1.0.0", SemanticFormat),
+				New("1.0.0", SemanticFormat),
 				nil,
-				NewVersion("2.0.0", SemanticFormat),
+				New("2.0.0", SemanticFormat),
 			},
 			expectedSize: 2,
 		},
@@ -81,10 +81,10 @@ func TestSet_Add(t *testing.T) {
 			name:         "add to empty set",
 			ignoreFormat: false,
 			versionsToAdd: []*Version{
-				NewVersion("1.0.0", SemanticFormat),
+				New("1.0.0", SemanticFormat),
 			},
 			expectedSize:     1,
-			expectedContains: NewVersion("1.0.0", SemanticFormat),
+			expectedContains: New("1.0.0", SemanticFormat),
 		},
 		{
 			name:         "add nil version",
@@ -98,46 +98,46 @@ func TestSet_Add(t *testing.T) {
 			name:         "add duplicate version",
 			ignoreFormat: false,
 			initialVersions: []*Version{
-				NewVersion("1.0.0", SemanticFormat),
+				New("1.0.0", SemanticFormat),
 			},
 			versionsToAdd: []*Version{
-				NewVersion("1.0.0", SemanticFormat),
+				New("1.0.0", SemanticFormat),
 			},
 			expectedSize:     1,
-			expectedContains: NewVersion("1.0.0", SemanticFormat),
+			expectedContains: New("1.0.0", SemanticFormat),
 		},
 		{
 			name:         "add same version different format with ignoreFormat=true",
 			ignoreFormat: true,
 			initialVersions: []*Version{
-				NewVersion("1.0.0", SemanticFormat),
+				New("1.0.0", SemanticFormat),
 			},
 			versionsToAdd: []*Version{
-				NewVersion("1.0.0", ApkFormat),
+				New("1.0.0", ApkFormat),
 			},
 			expectedSize:     1,
-			expectedContains: NewVersion("1.0.0", ApkFormat), // latest added wins
+			expectedContains: New("1.0.0", ApkFormat), // latest added wins
 		},
 		{
 			name:         "add same version different format with ignoreFormat=false",
 			ignoreFormat: false,
 			initialVersions: []*Version{
-				NewVersion("1.0.0", SemanticFormat),
+				New("1.0.0", SemanticFormat),
 			},
 			versionsToAdd: []*Version{
-				NewVersion("1.0.0", ApkFormat),
+				New("1.0.0", ApkFormat),
 			},
 			expectedSize:     2,
-			expectedContains: NewVersion("1.0.0", SemanticFormat),
+			expectedContains: New("1.0.0", SemanticFormat),
 		},
 		{
 			name:         "add to set with nil versions map",
 			ignoreFormat: false,
 			versionsToAdd: []*Version{
-				NewVersion("1.0.0", SemanticFormat),
+				New("1.0.0", SemanticFormat),
 			},
 			expectedSize:     1,
-			expectedContains: NewVersion("1.0.0", SemanticFormat),
+			expectedContains: New("1.0.0", SemanticFormat),
 		},
 	}
 
@@ -176,7 +176,7 @@ func TestSet_Remove(t *testing.T) {
 			name:         "remove from empty set",
 			ignoreFormat: false,
 			versionsToRemove: []*Version{
-				NewVersion("1.0.0", SemanticFormat),
+				New("1.0.0", SemanticFormat),
 			},
 			expectedSize: 0,
 		},
@@ -184,45 +184,45 @@ func TestSet_Remove(t *testing.T) {
 			name:         "remove existing version",
 			ignoreFormat: false,
 			initialVersions: []*Version{
-				NewVersion("1.0.0", SemanticFormat),
-				NewVersion("2.0.0", SemanticFormat),
+				New("1.0.0", SemanticFormat),
+				New("2.0.0", SemanticFormat),
 			},
 			versionsToRemove: []*Version{
-				NewVersion("1.0.0", SemanticFormat),
+				New("1.0.0", SemanticFormat),
 			},
 			expectedSize:     1,
-			shouldContain:    NewVersion("2.0.0", SemanticFormat),
-			shouldNotContain: NewVersion("1.0.0", SemanticFormat),
+			shouldContain:    New("2.0.0", SemanticFormat),
+			shouldNotContain: New("1.0.0", SemanticFormat),
 		},
 		{
 			name:         "remove nil version",
 			ignoreFormat: false,
 			initialVersions: []*Version{
-				NewVersion("1.0.0", SemanticFormat),
+				New("1.0.0", SemanticFormat),
 			},
 			versionsToRemove: []*Version{
 				nil,
 			},
 			expectedSize:  1,
-			shouldContain: NewVersion("1.0.0", SemanticFormat),
+			shouldContain: New("1.0.0", SemanticFormat),
 		},
 		{
 			name:         "remove non-existing version",
 			ignoreFormat: false,
 			initialVersions: []*Version{
-				NewVersion("1.0.0", SemanticFormat),
+				New("1.0.0", SemanticFormat),
 			},
 			versionsToRemove: []*Version{
-				NewVersion("2.0.0", SemanticFormat),
+				New("2.0.0", SemanticFormat),
 			},
 			expectedSize:  1,
-			shouldContain: NewVersion("1.0.0", SemanticFormat),
+			shouldContain: New("1.0.0", SemanticFormat),
 		},
 		{
 			name:         "remove from set with nil versions map",
 			ignoreFormat: false,
 			versionsToRemove: []*Version{
-				NewVersion("1.0.0", SemanticFormat),
+				New("1.0.0", SemanticFormat),
 			},
 			expectedSize: 0,
 		},
@@ -261,26 +261,26 @@ func TestSet_Contains(t *testing.T) {
 			name:         "contains existing version",
 			ignoreFormat: false,
 			versions: []*Version{
-				NewVersion("1.0.0", SemanticFormat),
-				NewVersion("2.0.0", SemanticFormat),
+				New("1.0.0", SemanticFormat),
+				New("2.0.0", SemanticFormat),
 			},
-			checkVersion: NewVersion("1.0.0", SemanticFormat),
+			checkVersion: New("1.0.0", SemanticFormat),
 			expected:     true,
 		},
 		{
 			name:         "does not contain non-existing version",
 			ignoreFormat: false,
 			versions: []*Version{
-				NewVersion("1.0.0", SemanticFormat),
+				New("1.0.0", SemanticFormat),
 			},
-			checkVersion: NewVersion("2.0.0", SemanticFormat),
+			checkVersion: New("2.0.0", SemanticFormat),
 			expected:     false,
 		},
 		{
 			name:         "check nil version",
 			ignoreFormat: false,
 			versions: []*Version{
-				NewVersion("1.0.0", SemanticFormat),
+				New("1.0.0", SemanticFormat),
 			},
 			checkVersion: nil,
 			expected:     false,
@@ -289,32 +289,32 @@ func TestSet_Contains(t *testing.T) {
 			name:         "check version in empty set",
 			ignoreFormat: false,
 			versions:     nil,
-			checkVersion: NewVersion("1.0.0", SemanticFormat),
+			checkVersion: New("1.0.0", SemanticFormat),
 			expected:     false,
 		},
 		{
 			name:         "contains same version different format with ignoreFormat=true",
 			ignoreFormat: true,
 			versions: []*Version{
-				NewVersion("1.0.0", SemanticFormat),
+				New("1.0.0", SemanticFormat),
 			},
-			checkVersion: NewVersion("1.0.0", ApkFormat),
+			checkVersion: New("1.0.0", ApkFormat),
 			expected:     true,
 		},
 		{
 			name:         "does not contain same version different format with ignoreFormat=false",
 			ignoreFormat: false,
 			versions: []*Version{
-				NewVersion("1.0.0", SemanticFormat),
+				New("1.0.0", SemanticFormat),
 			},
-			checkVersion: NewVersion("1.0.0", ApkFormat),
+			checkVersion: New("1.0.0", ApkFormat),
 			expected:     false,
 		},
 		{
 			name:         "check version with nil versions map",
 			ignoreFormat: false,
 			versions:     []*Version{},
-			checkVersion: NewVersion("1.0.0", SemanticFormat),
+			checkVersion: New("1.0.0", SemanticFormat),
 			expected:     false,
 		},
 	}
@@ -353,9 +353,9 @@ func TestSet_Values(t *testing.T) {
 			name:         "set with versions returns sorted list",
 			ignoreFormat: false,
 			versions: []*Version{
-				NewVersion("2.0.0", SemanticFormat),
-				NewVersion("1.0.0", SemanticFormat),
-				NewVersion("3.0.0", SemanticFormat),
+				New("2.0.0", SemanticFormat),
+				New("1.0.0", SemanticFormat),
+				New("3.0.0", SemanticFormat),
 			},
 			expectedLength: 3,
 			checkSorted:    true,
@@ -370,7 +370,7 @@ func TestSet_Values(t *testing.T) {
 			name:         "set with single version",
 			ignoreFormat: false,
 			versions: []*Version{
-				NewVersion("1.0.0", SemanticFormat),
+				New("1.0.0", SemanticFormat),
 			},
 			expectedLength: 1,
 		},
@@ -422,8 +422,8 @@ func TestSet_Size(t *testing.T) {
 			name:         "set with versions",
 			ignoreFormat: false,
 			versions: []*Version{
-				NewVersion("1.0.0", SemanticFormat),
-				NewVersion("2.0.0", SemanticFormat),
+				New("1.0.0", SemanticFormat),
+				New("2.0.0", SemanticFormat),
 			},
 			expected: 2,
 		},
@@ -431,8 +431,8 @@ func TestSet_Size(t *testing.T) {
 			name:         "set with duplicate versions",
 			ignoreFormat: false,
 			versions: []*Version{
-				NewVersion("1.0.0", SemanticFormat),
-				NewVersion("1.0.0", SemanticFormat),
+				New("1.0.0", SemanticFormat),
+				New("1.0.0", SemanticFormat),
 			},
 			expected: 1,
 		},
@@ -468,8 +468,8 @@ func TestSet_Clear(t *testing.T) {
 			name:         "clear non-empty set",
 			ignoreFormat: false,
 			versions: []*Version{
-				NewVersion("1.0.0", SemanticFormat),
-				NewVersion("2.0.0", SemanticFormat),
+				New("1.0.0", SemanticFormat),
+				New("2.0.0", SemanticFormat),
 			},
 		},
 		{
@@ -505,9 +505,9 @@ func TestSet_Integration(t *testing.T) {
 	// test combining multiple operations
 	s := NewSet(false)
 
-	v1 := NewVersion("1.0.0", SemanticFormat)
-	v2 := NewVersion("2.0.0", SemanticFormat)
-	v3 := NewVersion("3.0.0", SemanticFormat)
+	v1 := New("1.0.0", SemanticFormat)
+	v2 := New("2.0.0", SemanticFormat)
+	v3 := New("3.0.0", SemanticFormat)
 
 	// add versions
 	s.Add(v1, v2, v3)
diff --git a/grype/version/version_test.go b/grype/version/version_test.go
index 9f4d007b..237925af 100644
--- a/grype/version/version_test.go
+++ b/grype/version/version_test.go
@@ -59,12 +59,12 @@ func TestVersionCompare(t *testing.T) {
 					if tc.expectErr == nil {
 						tc.expectErr = require.NoError
 					}
-					v1 := NewVersion(tc.v1, format)
+					v1 := New(tc.v1, format)
 					require.Equal(t, format, v1.Format)
 
 					var v2 *Version
 					if tc.v2 != "" {
-						v2 = NewVersion(tc.v2, format)
+						v2 = New(tc.v2, format)
 						require.Equal(t, format, v2.Format)
 					}
 
@@ -83,10 +83,10 @@ func TestVersionCompare(t *testing.T) {
 }
 
 func TestVersion_UpgradeUnknownRightSideComparison(t *testing.T) {
-	v1 := NewVersion("1.0.0", SemanticFormat)
+	v1 := New("1.0.0", SemanticFormat)
 
 	// test if we can upgrade an unknown format to a known format when the left hand side is known
-	v2 := NewVersion("1.0.0", UnknownFormat)
+	v2 := New("1.0.0", UnknownFormat)
 
 	result, err := v1.Compare(v2)
 	assert.NoError(t, err)
@@ -116,8 +116,8 @@ func TestVersionCompareSameFormat(t *testing.T) {
 		t.Run(fmt.name, func(t *testing.T) {
 			// just test that we can create and compare versions of this format
 			// without errors - not testing the actual comparison logic
-			v1 := NewVersion("1.0.0", fmt.format)
-			v2 := NewVersion("1.0.0", fmt.format)
+			v1 := New("1.0.0", fmt.format)
+			v2 := New("1.0.0", fmt.format)
 
 			result, err := v1.Compare(v2)
 			assert.NoError(t, err, "comparison error")
@@ -137,140 +137,140 @@ func TestVersion_Is(t *testing.T) {
 	}{
 		{
 			name:     "equal versions - EQ operator",
-			version:  NewVersion("1.0.0", SemanticFormat),
+			version:  New("1.0.0", SemanticFormat),
 			operator: EQ,
-			other:    NewVersion("1.0.0", SemanticFormat),
+			other:    New("1.0.0", SemanticFormat),
 			expected: true,
 		},
 		{
 			name:     "equal versions - empty operator (defaults to EQ)",
-			version:  NewVersion("1.0.0", SemanticFormat),
+			version:  New("1.0.0", SemanticFormat),
 			operator: "",
-			other:    NewVersion("1.0.0", SemanticFormat),
+			other:    New("1.0.0", SemanticFormat),
 			expected: true,
 		},
 		{
 			name:     "unequal versions - EQ operator",
-			version:  NewVersion("1.0.0", SemanticFormat),
+			version:  New("1.0.0", SemanticFormat),
 			operator: EQ,
-			other:    NewVersion("2.0.0", SemanticFormat),
+			other:    New("2.0.0", SemanticFormat),
 			expected: false,
 		},
 		{
 			name:     "greater than - GT operator true",
-			version:  NewVersion("2.0.0", SemanticFormat),
+			version:  New("2.0.0", SemanticFormat),
 			operator: GT,
-			other:    NewVersion("1.0.0", SemanticFormat),
+			other:    New("1.0.0", SemanticFormat),
 			expected: true,
 		},
 		{
 			name:     "greater than - GT operator false",
-			version:  NewVersion("1.0.0", SemanticFormat),
+			version:  New("1.0.0", SemanticFormat),
 			operator: GT,
-			other:    NewVersion("2.0.0", SemanticFormat),
+			other:    New("2.0.0", SemanticFormat),
 			expected: false,
 		},
 		{
 			name:     "greater than or equal - GTE operator true (greater)",
-			version:  NewVersion("2.0.0", SemanticFormat),
+			version:  New("2.0.0", SemanticFormat),
 			operator: GTE,
-			other:    NewVersion("1.0.0", SemanticFormat),
+			other:    New("1.0.0", SemanticFormat),
 			expected: true,
 		},
 		{
 			name:     "greater than or equal - GTE operator true (equal)",
-			version:  NewVersion("1.0.0", SemanticFormat),
+			version:  New("1.0.0", SemanticFormat),
 			operator: GTE,
-			other:    NewVersion("1.0.0", SemanticFormat),
+			other:    New("1.0.0", SemanticFormat),
 			expected: true,
 		},
 		{
 			name:     "greater than or equal - GTE operator false",
-			version:  NewVersion("1.0.0", SemanticFormat),
+			version:  New("1.0.0", SemanticFormat),
 			operator: GTE,
-			other:    NewVersion("2.0.0", SemanticFormat),
+			other:    New("2.0.0", SemanticFormat),
 			expected: false,
 		},
 		{
 			name:     "less than - LT operator true",
-			version:  NewVersion("1.0.0", SemanticFormat),
+			version:  New("1.0.0", SemanticFormat),
 			operator: LT,
-			other:    NewVersion("2.0.0", SemanticFormat),
+			other:    New("2.0.0", SemanticFormat),
 			expected: true,
 		},
 		{
 			name:     "less than - LT operator false",
-			version:  NewVersion("2.0.0", SemanticFormat),
+			version:  New("2.0.0", SemanticFormat),
 			operator: LT,
-			other:    NewVersion("1.0.0", SemanticFormat),
+			other:    New("1.0.0", SemanticFormat),
 			expected: false,
 		},
 		{
 			name:     "less than or equal - LTE operator true (less)",
-			version:  NewVersion("1.0.0", SemanticFormat),
+			version:  New("1.0.0", SemanticFormat),
 			operator: LTE,
-			other:    NewVersion("2.0.0", SemanticFormat),
+			other:    New("2.0.0", SemanticFormat),
 			expected: true,
 		},
 		{
 			name:     "less than or equal - LTE operator true (equal)",
-			version:  NewVersion("1.0.0", SemanticFormat),
+			version:  New("1.0.0", SemanticFormat),
 			operator: LTE,
-			other:    NewVersion("1.0.0", SemanticFormat),
+			other:    New("1.0.0", SemanticFormat),
 			expected: true,
 		},
 		{
 			name:     "less than or equal - LTE operator false",
-			version:  NewVersion("2.0.0", SemanticFormat),
+			version:  New("2.0.0", SemanticFormat),
 			operator: LTE,
-			other:    NewVersion("1.0.0", SemanticFormat),
+			other:    New("1.0.0", SemanticFormat),
 			expected: false,
 		},
 		{
 			name:     "nil other version should return ErrNoVersionProvided",
-			version:  NewVersion("1.0.0", SemanticFormat),
+			version:  New("1.0.0", SemanticFormat),
 			operator: EQ,
 			other:    nil,
 			wantErr:  require.Error,
 		},
 		{
 			name:     "unknown operator should return error",
-			version:  NewVersion("1.0.0", SemanticFormat),
+			version:  New("1.0.0", SemanticFormat),
 			operator: "unknown",
-			other:    NewVersion("1.0.0", SemanticFormat),
+			other:    New("1.0.0", SemanticFormat),
 			wantErr:  require.Error,
 		},
 		{
 			name:     "invalid version format should return error",
-			version:  NewVersion("invalid", SemanticFormat),
+			version:  New("invalid", SemanticFormat),
 			operator: EQ,
-			other:    NewVersion("1.0.0", SemanticFormat),
+			other:    New("1.0.0", SemanticFormat),
 			wantErr:  require.Error,
 		},
 		{
 			name:     "different formats - semantic vs apk",
-			version:  NewVersion("1.0.0", SemanticFormat),
+			version:  New("1.0.0", SemanticFormat),
 			operator: EQ,
-			other:    NewVersion("1.0.0", ApkFormat),
+			other:    New("1.0.0", ApkFormat),
 			expected: true,
 		},
 		{
 			name:     "complex semantic versions",
-			version:  NewVersion("1.2.3-alpha.1", SemanticFormat),
+			version:  New("1.2.3-alpha.1", SemanticFormat),
 			operator: LT,
-			other:    NewVersion("1.2.3", SemanticFormat),
+			other:    New("1.2.3", SemanticFormat),
 			expected: true,
 		},
 		{
 			name:     "version with v prefix",
-			version:  NewVersion("v1.0.0", SemanticFormat),
+			version:  New("v1.0.0", SemanticFormat),
 			operator: EQ,
-			other:    NewVersion("1.0.0", SemanticFormat),
+			other:    New("1.0.0", SemanticFormat),
 			expected: true,
 		},
 		{
 			name:     "nil other version is ErrNoVersionProvided",
-			version:  NewVersion("1.0.0", SemanticFormat),
+			version:  New("1.0.0", SemanticFormat),
 			operator: EQ,
 			other:    nil,
 			wantErr: func(t require.TestingT, err error, a ...interface{}) {
@@ -279,18 +279,18 @@ func TestVersion_Is(t *testing.T) {
 		},
 		{
 			name:     "unknown operator error",
-			version:  NewVersion("1.0.0", SemanticFormat),
+			version:  New("1.0.0", SemanticFormat),
 			operator: "!@#",
-			other:    NewVersion("1.0.0", SemanticFormat),
+			other:    New("1.0.0", SemanticFormat),
 			wantErr: func(t require.TestingT, err error, a ...interface{}) {
 				require.ErrorContains(t, err, "unknown operator !@#", a...)
 			},
 		},
 		{
 			name:     "invalid version format error contains format",
-			version:  NewVersion("not-a-valid-version", SemanticFormat),
+			version:  New("not-a-valid-version", SemanticFormat),
 			operator: EQ,
-			other:    NewVersion("1.0.0", SemanticFormat),
+			other:    New("1.0.0", SemanticFormat),
 			wantErr: func(t require.TestingT, err error, a ...interface{}) {
 				require.ErrorContains(t, err, "unable to get comparator for Semantic", a...)
 			},
@@ -316,9 +316,9 @@ func TestVersion_Is(t *testing.T) {
 }
 
 func TestVersion_Is_AllOperators(t *testing.T) {
-	v1 := NewVersion("1.0.0", SemanticFormat)
-	v2 := NewVersion("2.0.0", SemanticFormat)
-	v1dup := NewVersion("1.0.0", SemanticFormat)
+	v1 := New("1.0.0", SemanticFormat)
+	v2 := New("2.0.0", SemanticFormat)
+	v1dup := New("1.0.0", SemanticFormat)
 
 	tests := []struct {
 		name     string
diff --git a/grype/vex/processor_test.go b/grype/vex/processor_test.go
index 86a7d0d1..78ccab62 100644
--- a/grype/vex/processor_test.go
+++ b/grype/vex/processor_test.go
@@ -23,7 +23,6 @@ func TestProcessor_ApplyVEX(t *testing.T) {
 				},
 			},
 		},
-		Distro: nil,
 	}
 
 	libCryptoPackage := pkg.Package{
diff --git a/grype/vulnerability_matcher_test.go b/grype/vulnerability_matcher_test.go
index f782e8c1..ecd063eb 100644
--- a/grype/vulnerability_matcher_test.go
+++ b/grype/vulnerability_matcher_test.go
@@ -207,6 +207,10 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) {
 		Name:    "neutron",
 		Version: "2013.1.1-1",
 		Type:    syftPkg.DebPkg,
+		Distro: &distro.Distro{
+			Type:    "debian",
+			Version: "8",
+		},
 	}
 
 	mustCPE := func(c string) cpe.CPE {
@@ -262,12 +266,7 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) {
 						Type:    syftPkg.DebPkg,
 					},
 				},
-				context: pkg.Context{
-					Distro: &distro.Distro{
-						Type:    "debian",
-						Version: "8",
-					},
-				},
+				context: pkg.Context{},
 			},
 		},
 		{
@@ -279,12 +278,7 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) {
 				pkgs: []pkg.Package{
 					neutron2013Pkg,
 				},
-				context: pkg.Context{
-					Distro: &distro.Distro{
-						Type:    "debian",
-						Version: "8",
-					},
-				},
+				context: pkg.Context{},
 			},
 			wantMatches: match.NewMatches(
 				match.Match{
@@ -334,12 +328,7 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) {
 				pkgs: []pkg.Package{
 					neutron2013Pkg,
 				},
-				context: pkg.Context{
-					Distro: &distro.Distro{
-						Type:    "debian",
-						Version: "8",
-					},
-				},
+				context: pkg.Context{},
 			},
 			wantMatches: match.NewMatches(
 				match.Match{
@@ -409,10 +398,6 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) {
 							},
 						},
 					},
-					Distro: &distro.Distro{
-						Type:    "debian",
-						Version: "8",
-					},
 				},
 			},
 			wantMatches: match.NewMatches(),
@@ -1067,12 +1052,7 @@ func Test_fatalErrors(t *testing.T) {
 					Type:    syftPkg.JavaPkg,
 				},
 			},
-				pkg.Context{
-					Distro: &distro.Distro{
-						Type:    "debian",
-						Version: "8",
-					},
-				},
+				pkg.Context{},
 			)
 
 			tt.assertErr(t, err)
diff --git a/internal/stringutil/string_helpers_test.go b/internal/stringutil/string_helpers_test.go
index 89baa28f..8adb1b2a 100644
--- a/internal/stringutil/string_helpers_test.go
+++ b/internal/stringutil/string_helpers_test.go
@@ -154,3 +154,276 @@ func TestSplitCommaSeparatedString(t *testing.T) {
 		})
 	}
 }
+
+func TestSplitOnFirstString(t *testing.T) {
+	tests := []struct {
+		name       string
+		input      string
+		separators []string
+		wantBefore string
+		wantAfter  string
+	}{
+		// go cases
+		{
+			name:       "single separator found",
+			input:      "key=value",
+			separators: []string{"="},
+			wantBefore: "key",
+			wantAfter:  "value",
+		},
+		{
+			name:       "multiple separators, first one wins",
+			input:      "protocol://host:port",
+			separators: []string{"://", ":"},
+			wantBefore: "protocol",
+			wantAfter:  "host:port",
+		},
+		{
+			name:       "multiple separators, earlier position wins",
+			input:      "name:value=data",
+			separators: []string{"=", ":"},
+			wantBefore: "name",
+			wantAfter:  "value=data",
+		},
+		// edge cases
+		{
+			name:       "no separator found",
+			input:      "noseparator",
+			separators: []string{"=", ":"},
+			wantBefore: "noseparator",
+			wantAfter:  "",
+		},
+		{
+			name:       "empty input",
+			input:      "",
+			separators: []string{"="},
+			wantBefore: "",
+			wantAfter:  "",
+		},
+		{
+			name:       "separator at beginning",
+			input:      "=value",
+			separators: []string{"="},
+			wantBefore: "",
+			wantAfter:  "value",
+		},
+		{
+			name:       "separator at end",
+			input:      "key=",
+			separators: []string{"="},
+			wantBefore: "key",
+			wantAfter:  "",
+		},
+		{
+			name:       "only separator",
+			input:      "=",
+			separators: []string{"="},
+			wantBefore: "",
+			wantAfter:  "",
+		},
+		// multiple occurrences
+		{
+			name:       "multiple occurrences of same separator",
+			input:      "a=b=c=d",
+			separators: []string{"="},
+			wantBefore: "a",
+			wantAfter:  "b=c=d",
+		},
+		{
+			name:       "multiple different separators, choose earliest",
+			input:      "a:b=c:d",
+			separators: []string{"=", ":"},
+			wantBefore: "a",
+			wantAfter:  "b=c:d",
+		},
+
+		// multi-character separators
+		{
+			name:       "multi-character separator",
+			input:      "before::after",
+			separators: []string{"::"},
+			wantBefore: "before",
+			wantAfter:  "after",
+		},
+		{
+			name:       "overlapping separators",
+			input:      "test:::data",
+			separators: []string{"::", ":::"},
+			wantBefore: "test",
+			wantAfter:  ":data",
+		},
+		{
+			name:       "longer separator wins when at same position",
+			input:      "test:::data",
+			separators: []string{":::", "::"},
+			wantBefore: "test",
+			wantAfter:  "data",
+		},
+		// more realistic cases
+		{
+			name:       "URL parsing",
+			input:      "https://user:pass@host:8080/path?query=value",
+			separators: []string{"://", "@", ":", "/", "?", "="},
+			wantBefore: "https",
+			wantAfter:  "user:pass@host:8080/path?query=value",
+		},
+		{
+			name:       "environment variable",
+			input:      "PATH=/usr/bin:/bin",
+			separators: []string{"=", ":"},
+			wantBefore: "PATH",
+			wantAfter:  "/usr/bin:/bin",
+		},
+		{
+			name:       "docker image tag",
+			input:      "registry.example.com/namespace/image:v1.0",
+			separators: []string{":", "/"},
+			wantBefore: "registry.example.com",
+			wantAfter:  "namespace/image:v1.0",
+		},
+
+		// special characters
+		{
+			name:       "unicode separators",
+			input:      "hello→world",
+			separators: []string{"→"},
+			wantBefore: "hello",
+			wantAfter:  "world",
+		},
+		{
+			name:       "whitespace separators",
+			input:      "word1 word2\tword3",
+			separators: []string{" ", "\t"},
+			wantBefore: "word1",
+			wantAfter:  "word2\tword3",
+		},
+		// co separators provided
+		{
+			name:       "no separators provided",
+			input:      "test=data",
+			separators: []string{},
+			wantBefore: "test=data",
+			wantAfter:  "",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			gotBefore, gotAfter := SplitOnFirstString(tt.input, tt.separators...)
+
+			if gotBefore != tt.wantBefore {
+				t.Errorf("SplitOnFirstString() gotBefore = %q, want %q", gotBefore, tt.wantBefore)
+			}
+			if gotAfter != tt.wantAfter {
+				t.Errorf("SplitOnFirstString() gotAfter = %q, want %q", gotAfter, tt.wantAfter)
+			}
+		})
+	}
+}
+
+func TestSplitOnAny(t *testing.T) {
+	tests := []struct {
+		name       string
+		input      string
+		separators []string
+		expected   []string
+	}{
+		{
+			name:       "empty string",
+			input:      "",
+			separators: []string{","},
+			expected:   nil,
+		},
+		{
+			name:       "single separator",
+			input:      "a,b,c",
+			separators: []string{","},
+			expected:   []string{"a", "b", "c"},
+		},
+		{
+			name:       "multiple separators",
+			input:      "a,b;c:d",
+			separators: []string{",", ";", ":"},
+			expected:   []string{"a", "b", "c", "d"},
+		},
+		{
+			name:       "no separators found",
+			input:      "hello",
+			separators: []string{",", ";"},
+			expected:   []string{"hello"},
+		},
+		{
+			name:       "consecutive separators",
+			input:      "a,,b",
+			separators: []string{","},
+			expected:   []string{"a", "", "b"},
+		},
+		{
+			name:       "separator at beginning",
+			input:      ",a,b",
+			separators: []string{","},
+			expected:   []string{"", "a", "b"},
+		},
+		{
+			name:       "separator at end",
+			input:      "a,b,",
+			separators: []string{","},
+			expected:   []string{"a", "b", ""},
+		},
+		{
+			name:       "only separators",
+			input:      ",,",
+			separators: []string{","},
+			expected:   []string{"", "", ""},
+		},
+		{
+			name:       "overlapping separators",
+			input:      "a,b;c,d",
+			separators: []string{",", ";"},
+			expected:   []string{"a", "b", "c", "d"},
+		},
+		{
+			name:       "separator is substring of another",
+			input:      "a::b:c",
+			separators: []string{"::", ":"},
+			expected:   []string{"a", "b", "c"},
+		},
+		{
+			name:       "order does not matter for overlapping",
+			input:      "a::b:c",
+			separators: []string{":", "::"},
+			expected:   []string{"a", "b", "c"},
+		},
+		{
+			name:       "no separators provided",
+			input:      "hello",
+			separators: []string{},
+			expected:   []string{"hello"},
+		},
+		{
+			name:       "multi-character separator",
+			input:      "a<->b<->c",
+			separators: []string{"<->"},
+			expected:   []string{"a", "b", "c"},
+		},
+		{
+			name:       "mixed single and multi-character separators",
+			input:      "a,b<->c;d",
+			separators: []string{",", "<->", ";"},
+			expected:   []string{"a", "b", "c", "d"},
+		},
+		{
+			name:       "space separator",
+			input:      "hello world test",
+			separators: []string{" "},
+			expected:   []string{"hello", "world", "test"},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := SplitOnAny(tt.input, tt.separators...)
+			assert.Equal(t, tt.expected, result)
+		})
+	}
+}
diff --git a/test/integration/match_by_image_test.go b/test/integration/match_by_image_test.go
index 90f5cb9c..8bfbfbb2 100644
--- a/test/integration/match_by_image_test.go
+++ b/test/integration/match_by_image_test.go
@@ -787,7 +787,11 @@ func TestMatchByImage(t *testing.T) {
 				},
 			})
 
-			actualResults := grype.FindVulnerabilitiesForPackage(theProvider, distro.FromRelease(s.Artifacts.LinuxDistribution), matchers, pkg.FromCollection(s.Artifacts.Packages, pkg.SynthesisConfig{}))
+			actualResults := grype.FindVulnerabilitiesForPackage(theProvider, matchers, pkg.FromCollection(s.Artifacts.Packages, pkg.SynthesisConfig{
+				Distro: pkg.DistroConfig{
+					Override: distro.FromRelease(s.Artifacts.LinuxDistribution, distro.DefaultFixChannels()),
+				},
+			}))
 			for _, m := range actualResults.Sorted() {
 				for _, d := range m.Details {
 					observedMatchers.Add(string(d.Matcher))