Repository: anchore/grype
Pull request #2444 (2025-02-12 20:55:51)

Add more logging and fix search by CPE
This PR makes the following adjustments:
- adds counts to v6 store searches
- adds stringer functions to models and blobs that are being written to the logger
- when searching for packages with CPEs, if no distro is provided assume this means we should be restricting to records that are not associated with any distros (not search for records that relate to any distro)
- change parameters to `logDroppedVulnerability` to note more detailed reasons and use `logger.Field` for arbitrary key-values (better log presentation)
- adds a new `UnsupportedFormatError` that version constraints can raise up, allowing for the version constraint criteria to not log errors for this case
- exposes a new `dev.db.debug` option to show sql with trace logs


Patch:
diff --git a/cmd/grype/cli/commands/db.go b/cmd/grype/cli/commands/db.go
index d17a762d..63ef1818 100644
--- a/cmd/grype/cli/commands/db.go
+++ b/cmd/grype/cli/commands/db.go
@@ -4,7 +4,6 @@ import (
 	"github.com/spf13/cobra"
 
 	"github.com/anchore/clio"
-	"github.com/anchore/grype/cmd/grype/cli/options"
 )
 
 const (
@@ -13,20 +12,6 @@ const (
 	textOutputFormat  = "text"
 )
 
-type DBOptions struct {
-	DB           options.Database     `yaml:"db" json:"db" mapstructure:"db"`
-	Experimental options.Experimental `yaml:"exp" json:"exp" mapstructure:"exp"`
-}
-
-func dbOptionsDefault(id clio.Identification) *DBOptions {
-	dbDefaults := options.DefaultDatabase(id)
-	// by default, require update check success for db operations which check for updates
-	dbDefaults.RequireUpdateCheck = true
-	return &DBOptions{
-		DB: dbDefaults,
-	}
-}
-
 func DB(app clio.Application) *cobra.Command {
 	db := &cobra.Command{
 		Use:   "db",
diff --git a/cmd/grype/cli/commands/db_check.go b/cmd/grype/cli/commands/db_check.go
index 5474f381..6f61a068 100644
--- a/cmd/grype/cli/commands/db_check.go
+++ b/cmd/grype/cli/commands/db_check.go
@@ -9,6 +9,7 @@ import (
 	"github.com/spf13/cobra"
 
 	"github.com/anchore/clio"
+	"github.com/anchore/grype/cmd/grype/cli/options"
 	legacyDistribution "github.com/anchore/grype/grype/db/v5/distribution"
 	db "github.com/anchore/grype/grype/db/v6"
 	"github.com/anchore/grype/grype/db/v6/distribution"
@@ -20,8 +21,8 @@ const (
 )
 
 type dbCheckOptions struct {
-	Output    string `yaml:"output" json:"output" mapstructure:"output"`
-	DBOptions `yaml:",inline" mapstructure:",squash"`
+	Output                  string `yaml:"output" json:"output" mapstructure:"output"`
+	options.DatabaseCommand `yaml:",inline" mapstructure:",squash"`
 }
 
 var _ clio.FlagAdder = (*dbCheckOptions)(nil)
@@ -32,8 +33,8 @@ func (d *dbCheckOptions) AddFlags(flags clio.FlagSet) {
 
 func DBCheck(app clio.Application) *cobra.Command {
 	opts := &dbCheckOptions{
-		Output:    textOutputFormat,
-		DBOptions: *dbOptionsDefault(app.ID()),
+		Output:          textOutputFormat,
+		DatabaseCommand: *options.DefaultDatabaseCommand(app.ID()),
 	}
 
 	cmd := &cobra.Command{
@@ -52,27 +53,27 @@ func DBCheck(app clio.Application) *cobra.Command {
 
 	// prevent from being shown in the grype config
 	type configWrapper struct {
-		Hidden     *dbCheckOptions `json:"-" yaml:"-" mapstructure:"-"`
-		*DBOptions `yaml:",inline" mapstructure:",squash"`
+		Hidden                   *dbCheckOptions `json:"-" yaml:"-" mapstructure:"-"`
+		*options.DatabaseCommand `yaml:",inline" mapstructure:",squash"`
 	}
 
-	return app.SetupCommand(cmd, &configWrapper{Hidden: opts, DBOptions: &opts.DBOptions})
+	return app.SetupCommand(cmd, &configWrapper{Hidden: opts, DatabaseCommand: &opts.DatabaseCommand})
 }
 
 func runDBCheck(opts dbCheckOptions) error {
-	if opts.DBOptions.Experimental.DBv6 {
+	if opts.DatabaseCommand.Experimental.DBv6 {
 		return newDBCheck(opts)
 	}
 	return legacyDBCheck(opts)
 }
 
 func newDBCheck(opts dbCheckOptions) error {
-	client, err := distribution.NewClient(opts.DB.ToClientConfig())
+	client, err := distribution.NewClient(opts.ToClientConfig())
 	if err != nil {
 		return fmt.Errorf("unable to create distribution client: %w", err)
 	}
 
-	cfg := opts.DB.ToCuratorConfig()
+	cfg := opts.ToCuratorConfig()
 
 	current, err := db.ReadDescription(cfg.DBFilePath())
 	if err != nil {
@@ -148,7 +149,7 @@ type legacyDBCheckJSON struct {
 }
 
 func legacyDBCheck(opts dbCheckOptions) error {
-	dbCurator, err := legacyDistribution.NewCurator(opts.DB.ToLegacyCuratorConfig())
+	dbCurator, err := legacyDistribution.NewCurator(opts.ToLegacyCuratorConfig())
 	if err != nil {
 		return err
 	}
diff --git a/cmd/grype/cli/commands/db_delete.go b/cmd/grype/cli/commands/db_delete.go
index 2c4cc553..1eaed76b 100644
--- a/cmd/grype/cli/commands/db_delete.go
+++ b/cmd/grype/cli/commands/db_delete.go
@@ -13,7 +13,7 @@ import (
 )
 
 func DBDelete(app clio.Application) *cobra.Command {
-	opts := dbOptionsDefault(app.ID())
+	opts := options.DefaultDatabaseCommand(app.ID())
 
 	cmd := &cobra.Command{
 		Use:     "delete",
@@ -27,20 +27,20 @@ func DBDelete(app clio.Application) *cobra.Command {
 
 	// prevent from being shown in the grype config
 	type configWrapper struct {
-		*DBOptions `yaml:",inline" mapstructure:",squash"`
+		*options.DatabaseCommand `yaml:",inline" mapstructure:",squash"`
 	}
 
 	return app.SetupCommand(cmd, &configWrapper{opts})
 }
 
-func runDBDelete(opts DBOptions) error {
+func runDBDelete(opts options.DatabaseCommand) error {
 	if opts.Experimental.DBv6 {
-		return newDBDelete(opts.DB)
+		return newDBDelete(opts)
 	}
-	return legacyDBDelete(opts.DB)
+	return legacyDBDelete(opts)
 }
 
-func newDBDelete(opts options.Database) error {
+func newDBDelete(opts options.DatabaseCommand) error {
 	client, err := distribution.NewClient(opts.ToClientConfig())
 	if err != nil {
 		return fmt.Errorf("unable to create distribution client: %w", err)
@@ -57,7 +57,7 @@ func newDBDelete(opts options.Database) error {
 	return stderrPrintLnf("Vulnerability database deleted")
 }
 
-func legacyDBDelete(opts options.Database) error {
+func legacyDBDelete(opts options.DatabaseCommand) error {
 	dbCurator, err := legacyDistribution.NewCurator(opts.ToLegacyCuratorConfig())
 	if err != nil {
 		return err
diff --git a/cmd/grype/cli/commands/db_diff.go b/cmd/grype/cli/commands/db_diff.go
index 3565d9e6..e41c4199 100644
--- a/cmd/grype/cli/commands/db_diff.go
+++ b/cmd/grype/cli/commands/db_diff.go
@@ -15,9 +15,9 @@ import (
 )
 
 type dbDiffOptions struct {
-	Output    string `yaml:"output" json:"output" mapstructure:"output"`
-	Delete    bool   `yaml:"delete" json:"delete" mapstructure:"delete"`
-	DBOptions `yaml:",inline" mapstructure:",squash"`
+	Output                  string `yaml:"output" json:"output" mapstructure:"output"`
+	Delete                  bool   `yaml:"delete" json:"delete" mapstructure:"delete"`
+	options.DatabaseCommand `yaml:",inline" mapstructure:",squash"`
 }
 
 var _ clio.FlagAdder = (*dbDiffOptions)(nil)
@@ -29,8 +29,8 @@ func (d *dbDiffOptions) AddFlags(flags clio.FlagSet) {
 
 func DBDiff(app clio.Application) *cobra.Command {
 	opts := &dbDiffOptions{
-		Output:    tableOutputFormat,
-		DBOptions: *dbOptionsDefault(app.ID()),
+		Output:          tableOutputFormat,
+		DatabaseCommand: *options.DefaultDatabaseCommand(app.ID()),
 	}
 
 	cmd := &cobra.Command{
@@ -43,14 +43,14 @@ func DBDiff(app clio.Application) *cobra.Command {
 			switch len(args) {
 			case 0:
 				log.Info("base_db_url and target_db_url not provided; fetching most recent")
-				base, target, err = getDefaultURLs(opts.DB)
+				base, target, err = getDefaultURLs(opts.DatabaseCommand)
 				if err != nil {
 					return err
 				}
 			case 1:
 				log.Info("target_db_url not provided; fetching most recent")
 				base = args[0]
-				_, target, err = getDefaultURLs(opts.DB)
+				_, target, err = getDefaultURLs(opts.DatabaseCommand)
 				if err != nil {
 					return err
 				}
@@ -65,15 +65,15 @@ func DBDiff(app clio.Application) *cobra.Command {
 
 	// prevent from being shown in the grype config
 	type configWrapper struct {
-		Hidden     *dbDiffOptions `json:"-" yaml:"-" mapstructure:"-"`
-		*DBOptions `yaml:",inline" mapstructure:",squash"`
+		Hidden                   *dbDiffOptions `json:"-" yaml:"-" mapstructure:"-"`
+		*options.DatabaseCommand `yaml:",inline" mapstructure:",squash"`
 	}
 
-	return app.SetupCommand(cmd, &configWrapper{Hidden: opts, DBOptions: &opts.DBOptions})
+	return app.SetupCommand(cmd, &configWrapper{Hidden: opts, DatabaseCommand: &opts.DatabaseCommand})
 }
 
 func runDBDiff(opts *dbDiffOptions, base string, target string) (errs error) {
-	d, err := differ.NewDiffer(opts.DB.ToLegacyCuratorConfig())
+	d, err := differ.NewDiffer(opts.ToLegacyCuratorConfig())
 	if err != nil {
 		return err
 	}
@@ -111,7 +111,7 @@ func runDBDiff(opts *dbDiffOptions, base string, target string) (errs error) {
 	return errs
 }
 
-func getDefaultURLs(opts options.Database) (baseURL string, targetURL string, err error) {
+func getDefaultURLs(opts options.DatabaseCommand) (baseURL string, targetURL string, err error) {
 	dbCurator, err := distribution.NewCurator(opts.ToLegacyCuratorConfig())
 	if err != nil {
 		return "", "", err
diff --git a/cmd/grype/cli/commands/db_import.go b/cmd/grype/cli/commands/db_import.go
index d5af0c00..42182f23 100644
--- a/cmd/grype/cli/commands/db_import.go
+++ b/cmd/grype/cli/commands/db_import.go
@@ -15,7 +15,7 @@ import (
 )
 
 func DBImport(app clio.Application) *cobra.Command {
-	opts := dbOptionsDefault(app.ID())
+	opts := options.DefaultDatabaseCommand(app.ID())
 
 	cmd := &cobra.Command{
 		Use:   "import FILE",
@@ -29,23 +29,23 @@ func DBImport(app clio.Application) *cobra.Command {
 
 	// prevent from being shown in the grype config
 	type configWrapper struct {
-		*DBOptions `yaml:",inline" mapstructure:",squash"`
+		*options.DatabaseCommand `yaml:",inline" mapstructure:",squash"`
 	}
 
 	return app.SetupCommand(cmd, &configWrapper{opts})
 }
 
-func runDBImport(opts DBOptions, dbArchivePath string) error {
+func runDBImport(opts options.DatabaseCommand, dbArchivePath string) error {
 	// TODO: tui update? better logging?
 
 	// TODO: we will only support v6 after development is complete
 	if opts.Experimental.DBv6 {
-		return newDBImport(opts.DB, dbArchivePath)
+		return newDBImport(opts, dbArchivePath)
 	}
-	return legacyDBImport(opts.DB, dbArchivePath)
+	return legacyDBImport(opts, dbArchivePath)
 }
 
-func newDBImport(opts options.Database, dbArchivePath string) error {
+func newDBImport(opts options.DatabaseCommand, dbArchivePath string) error {
 	client, err := distribution.NewClient(opts.ToClientConfig())
 	if err != nil {
 		return fmt.Errorf("unable to create distribution client: %w", err)
@@ -65,7 +65,7 @@ func newDBImport(opts options.Database, dbArchivePath string) error {
 	return nil
 }
 
-func legacyDBImport(opts options.Database, dbArchivePath string) error {
+func legacyDBImport(opts options.DatabaseCommand, dbArchivePath string) error {
 	dbCurator, err := legacyDistribution.NewCurator(opts.ToLegacyCuratorConfig())
 	if err != nil {
 		return err
diff --git a/cmd/grype/cli/commands/db_list.go b/cmd/grype/cli/commands/db_list.go
index ed7a736f..0569dc49 100644
--- a/cmd/grype/cli/commands/db_list.go
+++ b/cmd/grype/cli/commands/db_list.go
@@ -11,13 +11,14 @@ import (
 	"github.com/spf13/cobra"
 
 	"github.com/anchore/clio"
+	"github.com/anchore/grype/cmd/grype/cli/options"
 	legacyDistribution "github.com/anchore/grype/grype/db/v5/distribution"
 	"github.com/anchore/grype/grype/db/v6/distribution"
 )
 
 type dbListOptions struct {
-	Output    string `yaml:"output" json:"output" mapstructure:"output"`
-	DBOptions `yaml:",inline" mapstructure:",squash"`
+	Output                  string `yaml:"output" json:"output" mapstructure:"output"`
+	options.DatabaseCommand `yaml:",inline" mapstructure:",squash"`
 }
 
 var _ clio.FlagAdder = (*dbListOptions)(nil)
@@ -28,8 +29,8 @@ func (d *dbListOptions) AddFlags(flags clio.FlagSet) {
 
 func DBList(app clio.Application) *cobra.Command {
 	opts := &dbListOptions{
-		Output:    textOutputFormat,
-		DBOptions: *dbOptionsDefault(app.ID()),
+		Output:          textOutputFormat,
+		DatabaseCommand: *options.DefaultDatabaseCommand(app.ID()),
 	}
 
 	cmd := &cobra.Command{
@@ -44,11 +45,11 @@ func DBList(app clio.Application) *cobra.Command {
 
 	// prevent from being shown in the grype config
 	type configWrapper struct {
-		Hidden     *dbListOptions `json:"-" yaml:"-" mapstructure:"-"`
-		*DBOptions `yaml:",inline" mapstructure:",squash"`
+		Hidden                   *dbListOptions `json:"-" yaml:"-" mapstructure:"-"`
+		*options.DatabaseCommand `yaml:",inline" mapstructure:",squash"`
 	}
 
-	return app.SetupCommand(cmd, &configWrapper{Hidden: opts, DBOptions: &opts.DBOptions})
+	return app.SetupCommand(cmd, &configWrapper{Hidden: opts, DatabaseCommand: &opts.DatabaseCommand})
 }
 
 func runDBList(opts dbListOptions) error {
@@ -59,7 +60,7 @@ func runDBList(opts dbListOptions) error {
 }
 
 func newDBList(opts dbListOptions) error {
-	c, err := distribution.NewClient(opts.DB.ToClientConfig())
+	c, err := distribution.NewClient(opts.ToClientConfig())
 	if err != nil {
 		return fmt.Errorf("unable to create distribution client: %w", err)
 	}
@@ -109,7 +110,7 @@ func presentNewDBList(format string, u string, writer io.Writer, latest *distrib
 // all legacy processing below ////////////////////////////////////////////////////////////////////////////////////////
 
 func legacyDBList(opts dbListOptions) error {
-	dbCurator, err := legacyDistribution.NewCurator(opts.DB.ToLegacyCuratorConfig())
+	dbCurator, err := legacyDistribution.NewCurator(opts.ToLegacyCuratorConfig())
 	if err != nil {
 		return err
 	}
diff --git a/cmd/grype/cli/commands/db_providers.go b/cmd/grype/cli/commands/db_providers.go
index 9107959a..7ed5569d 100644
--- a/cmd/grype/cli/commands/db_providers.go
+++ b/cmd/grype/cli/commands/db_providers.go
@@ -13,6 +13,7 @@ import (
 	"github.com/spf13/cobra"
 
 	"github.com/anchore/clio"
+	"github.com/anchore/grype/cmd/grype/cli/options"
 	legacyDistribution "github.com/anchore/grype/grype/db/v5/distribution"
 	v6 "github.com/anchore/grype/grype/db/v6"
 	"github.com/anchore/grype/grype/db/v6/distribution"
@@ -21,8 +22,8 @@ import (
 )
 
 type dbProvidersOptions struct {
-	Output    string `yaml:"output" json:"output"`
-	DBOptions `yaml:",inline" mapstructure:",squash"`
+	Output                  string `yaml:"output" json:"output"`
+	options.DatabaseCommand `yaml:",inline" mapstructure:",squash"`
 }
 
 var _ clio.FlagAdder = (*dbProvidersOptions)(nil)
@@ -33,8 +34,8 @@ func (d *dbProvidersOptions) AddFlags(flags clio.FlagSet) {
 
 func DBProviders(app clio.Application) *cobra.Command {
 	opts := &dbProvidersOptions{
-		Output:    tableOutputFormat,
-		DBOptions: *dbOptionsDefault(app.ID()),
+		Output:          tableOutputFormat,
+		DatabaseCommand: *options.DefaultDatabaseCommand(app.ID()),
 	}
 
 	cmd := &cobra.Command{
@@ -48,11 +49,11 @@ func DBProviders(app clio.Application) *cobra.Command {
 
 	// prevent from being shown in the grype config
 	type configWrapper struct {
-		Hidden     *dbProvidersOptions `json:"-" yaml:"-" mapstructure:"-"`
-		*DBOptions `yaml:",inline" mapstructure:",squash"`
+		Hidden                   *dbProvidersOptions `json:"-" yaml:"-" mapstructure:"-"`
+		*options.DatabaseCommand `yaml:",inline" mapstructure:",squash"`
 	}
 
-	return app.SetupCommand(cmd, &configWrapper{Hidden: opts, DBOptions: &opts.DBOptions})
+	return app.SetupCommand(cmd, &configWrapper{Hidden: opts, DatabaseCommand: &opts.DatabaseCommand})
 }
 
 func runDBProviders(opts *dbProvidersOptions, app clio.Application) error {
@@ -63,11 +64,11 @@ func runDBProviders(opts *dbProvidersOptions, app clio.Application) error {
 }
 
 func newDBProviders(opts *dbProvidersOptions) error {
-	client, err := distribution.NewClient(opts.DB.ToClientConfig())
+	client, err := distribution.NewClient(opts.ToClientConfig())
 	if err != nil {
 		return fmt.Errorf("unable to create distribution client: %w", err)
 	}
-	c, err := installation.NewCurator(opts.DB.ToCuratorConfig(), client)
+	c, err := installation.NewCurator(opts.ToCuratorConfig(), client)
 	if err != nil {
 		return fmt.Errorf("unable to create curator: %w", err)
 	}
@@ -199,7 +200,7 @@ func legacyDBProviders(opts *dbProvidersOptions, app clio.Application) error {
 }
 
 func getLegacyMetadataFileLocation(app clio.Application) (*string, error) {
-	dbCurator, err := legacyDistribution.NewCurator(dbOptionsDefault(app.ID()).DB.ToLegacyCuratorConfig())
+	dbCurator, err := legacyDistribution.NewCurator(options.DefaultDatabaseCommand(app.ID()).ToLegacyCuratorConfig())
 	if err != nil {
 		return nil, err
 	}
diff --git a/cmd/grype/cli/commands/db_search.go b/cmd/grype/cli/commands/db_search.go
index de58f6e1..873bc1ad 100644
--- a/cmd/grype/cli/commands/db_search.go
+++ b/cmd/grype/cli/commands/db_search.go
@@ -31,7 +31,7 @@ type dbSearchMatchOptions struct {
 	OS            options.DBSearchOSs             `yaml:",inline" mapstructure:",squash"`
 	Bounds        options.DBSearchBounds          `yaml:",inline" mapstructure:",squash"`
 
-	DBOptions `yaml:",inline" mapstructure:",squash"`
+	options.DatabaseCommand `yaml:",inline" mapstructure:",squash"`
 }
 
 var alasPattern = regexp.MustCompile(`^alas[\w]*-\d+-\d+$`)
@@ -81,8 +81,8 @@ func DBSearch(app clio.Application) *cobra.Command {
 		Vulnerability: options.DBSearchVulnerabilities{
 			UseVulnIDFlag: true,
 		},
-		Bounds:    options.DefaultDBSearchBounds(),
-		DBOptions: *dbOptionsDefault(app.ID()),
+		Bounds:          options.DefaultDBSearchBounds(),
+		DatabaseCommand: *options.DefaultDatabaseCommand(app.ID()),
 	}
 
 	cmd := &cobra.Command{
@@ -144,20 +144,20 @@ func DBSearch(app clio.Application) *cobra.Command {
 
 	// prevent from being shown in the grype config
 	type configWrapper struct {
-		Hidden     *dbSearchMatchOptions `json:"-" yaml:"-" mapstructure:"-"`
-		*DBOptions `yaml:",inline" mapstructure:",squash"`
+		Hidden                   *dbSearchMatchOptions `json:"-" yaml:"-" mapstructure:"-"`
+		*options.DatabaseCommand `yaml:",inline" mapstructure:",squash"`
 	}
 
-	return app.SetupCommand(cmd, &configWrapper{Hidden: opts, DBOptions: &opts.DBOptions})
+	return app.SetupCommand(cmd, &configWrapper{Hidden: opts, DatabaseCommand: &opts.DatabaseCommand})
 }
 
 func runDBSearchMatches(opts dbSearchMatchOptions) error {
-	client, err := distribution.NewClient(opts.DB.ToClientConfig())
+	client, err := distribution.NewClient(opts.ToClientConfig())
 	if err != nil {
 		return fmt.Errorf("unable to create distribution client: %w", err)
 	}
 
-	curator, err := installation.NewCurator(opts.DB.ToCuratorConfig(), client)
+	curator, err := installation.NewCurator(opts.ToCuratorConfig(), client)
 	if err != nil {
 		return fmt.Errorf("unable to create curator: %w", err)
 	}
@@ -234,7 +234,7 @@ func legacyDBSearchPackages(opts dbSearchMatchOptions, vulnerabilityIDs []string
 	}
 
 	log.Debug("loading DB")
-	str, status, err := grype.LoadVulnerabilityDB(opts.DB.ToLegacyCuratorConfig(), opts.DB.AutoUpdate)
+	str, status, err := grype.LoadVulnerabilityDB(opts.ToLegacyCuratorConfig(), opts.DB.AutoUpdate)
 	err = validateDBLoad(err, status)
 	if err != nil {
 		return err
diff --git a/cmd/grype/cli/commands/db_search_vuln.go b/cmd/grype/cli/commands/db_search_vuln.go
index 0fd3385d..b2049aea 100644
--- a/cmd/grype/cli/commands/db_search_vuln.go
+++ b/cmd/grype/cli/commands/db_search_vuln.go
@@ -26,7 +26,7 @@ type dbSearchVulnerabilityOptions struct {
 	Vulnerability options.DBSearchVulnerabilities `yaml:",inline" mapstructure:",squash"`
 	Bounds        options.DBSearchBounds          `yaml:",inline" mapstructure:",squash"`
 
-	DBOptions `yaml:",inline" mapstructure:",squash"`
+	options.DatabaseCommand `yaml:",inline" mapstructure:",squash"`
 }
 
 func DBSearchVulnerabilities(app clio.Application) *cobra.Command {
@@ -35,8 +35,8 @@ func DBSearchVulnerabilities(app clio.Application) *cobra.Command {
 		Vulnerability: options.DBSearchVulnerabilities{
 			UseVulnIDFlag: false, // we input this through the args
 		},
-		Bounds:    options.DefaultDBSearchBounds(),
-		DBOptions: *dbOptionsDefault(app.ID()),
+		Bounds:          options.DefaultDBSearchBounds(),
+		DatabaseCommand: *options.DefaultDatabaseCommand(app.ID()),
 	}
 
 	cmd := &cobra.Command{
@@ -60,20 +60,20 @@ func DBSearchVulnerabilities(app clio.Application) *cobra.Command {
 
 	// prevent from being shown in the grype config
 	type configWrapper struct {
-		Hidden     *dbSearchVulnerabilityOptions `json:"-" yaml:"-" mapstructure:"-"`
-		*DBOptions `yaml:",inline" mapstructure:",squash"`
+		Hidden                   *dbSearchVulnerabilityOptions `json:"-" yaml:"-" mapstructure:"-"`
+		*options.DatabaseCommand `yaml:",inline" mapstructure:",squash"`
 	}
 
-	return app.SetupCommand(cmd, &configWrapper{Hidden: opts, DBOptions: &opts.DBOptions})
+	return app.SetupCommand(cmd, &configWrapper{Hidden: opts, DatabaseCommand: &opts.DatabaseCommand})
 }
 
 func runDBSearchVulnerabilities(opts dbSearchVulnerabilityOptions) error {
-	client, err := distribution.NewClient(opts.DB.ToClientConfig())
+	client, err := distribution.NewClient(opts.ToClientConfig())
 	if err != nil {
 		return fmt.Errorf("unable to create distribution client: %w", err)
 	}
 
-	c, err := installation.NewCurator(opts.DB.ToCuratorConfig(), client)
+	c, err := installation.NewCurator(opts.ToCuratorConfig(), client)
 	if err != nil {
 		return fmt.Errorf("unable to create curator: %w", err)
 	}
diff --git a/cmd/grype/cli/commands/db_status.go b/cmd/grype/cli/commands/db_status.go
index f9cf68ca..f13b5d17 100644
--- a/cmd/grype/cli/commands/db_status.go
+++ b/cmd/grype/cli/commands/db_status.go
@@ -9,6 +9,7 @@ import (
 	"github.com/spf13/cobra"
 
 	"github.com/anchore/clio"
+	"github.com/anchore/grype/cmd/grype/cli/options"
 	legacyDistribution "github.com/anchore/grype/grype/db/v5/distribution"
 	v6 "github.com/anchore/grype/grype/db/v6"
 	"github.com/anchore/grype/grype/db/v6/distribution"
@@ -16,8 +17,8 @@ import (
 )
 
 type dbStatusOptions struct {
-	Output    string `yaml:"output" json:"output" mapstructure:"output"`
-	DBOptions `yaml:",inline" mapstructure:",squash"`
+	Output                  string `yaml:"output" json:"output" mapstructure:"output"`
+	options.DatabaseCommand `yaml:",inline" mapstructure:",squash"`
 }
 
 var _ clio.FlagAdder = (*dbStatusOptions)(nil)
@@ -28,8 +29,8 @@ func (d *dbStatusOptions) AddFlags(flags clio.FlagSet) {
 
 func DBStatus(app clio.Application) *cobra.Command {
 	opts := &dbStatusOptions{
-		Output:    textOutputFormat,
-		DBOptions: *dbOptionsDefault(app.ID()),
+		Output:          textOutputFormat,
+		DatabaseCommand: *options.DefaultDatabaseCommand(app.ID()),
 	}
 
 	cmd := &cobra.Command{
@@ -44,11 +45,11 @@ func DBStatus(app clio.Application) *cobra.Command {
 
 	// prevent from being shown in the grype config
 	type configWrapper struct {
-		Hidden     *dbStatusOptions `json:"-" yaml:"-" mapstructure:"-"`
-		*DBOptions `yaml:",inline" mapstructure:",squash"`
+		Hidden                   *dbStatusOptions `json:"-" yaml:"-" mapstructure:"-"`
+		*options.DatabaseCommand `yaml:",inline" mapstructure:",squash"`
 	}
 
-	return app.SetupCommand(cmd, &configWrapper{Hidden: opts, DBOptions: &opts.DBOptions})
+	return app.SetupCommand(cmd, &configWrapper{Hidden: opts, DatabaseCommand: &opts.DatabaseCommand})
 }
 
 func runDBStatus(opts dbStatusOptions) error {
@@ -59,11 +60,11 @@ func runDBStatus(opts dbStatusOptions) error {
 }
 
 func newDBStatus(opts dbStatusOptions) error {
-	client, err := distribution.NewClient(opts.DB.ToClientConfig())
+	client, err := distribution.NewClient(opts.ToClientConfig())
 	if err != nil {
 		return fmt.Errorf("unable to create distribution client: %w", err)
 	}
-	c, err := installation.NewCurator(opts.DB.ToCuratorConfig(), client)
+	c, err := installation.NewCurator(opts.ToCuratorConfig(), client)
 	if err != nil {
 		return fmt.Errorf("unable to create distribution client: %w", err)
 	}
@@ -103,7 +104,7 @@ func presentDBStatus(format string, writer io.Writer, status v6.Status) error {
 // all legacy processing below ////////////////////////////////////////////////////////////////////////////////////////
 
 func legacyDBStatus(opts dbStatusOptions) error {
-	dbCurator, err := legacyDistribution.NewCurator(opts.DB.ToLegacyCuratorConfig())
+	dbCurator, err := legacyDistribution.NewCurator(opts.ToLegacyCuratorConfig())
 	if err != nil {
 		return err
 	}
diff --git a/cmd/grype/cli/commands/db_update.go b/cmd/grype/cli/commands/db_update.go
index ba851ccd..8f515e5b 100644
--- a/cmd/grype/cli/commands/db_update.go
+++ b/cmd/grype/cli/commands/db_update.go
@@ -15,7 +15,7 @@ import (
 )
 
 func DBUpdate(app clio.Application) *cobra.Command {
-	opts := dbOptionsDefault(app.ID())
+	opts := options.DefaultDatabaseCommand(app.ID())
 
 	cmd := &cobra.Command{
 		Use:   "update",
@@ -27,26 +27,26 @@ func DBUpdate(app clio.Application) *cobra.Command {
 			return nil
 		},
 		RunE: func(_ *cobra.Command, _ []string) error {
-			return runDBUpdate(opts.DB, opts.Experimental.DBv6)
+			return runDBUpdate(*opts, opts.Experimental.DBv6)
 		},
 	}
 
 	// prevent from being shown in the grype config
 	type configWrapper struct {
-		*DBOptions `yaml:",inline" mapstructure:",squash"`
+		*options.DatabaseCommand `yaml:",inline" mapstructure:",squash"`
 	}
 
 	return app.SetupCommand(cmd, &configWrapper{opts})
 }
 
-func runDBUpdate(opts options.Database, expUseV6 bool) error {
+func runDBUpdate(opts options.DatabaseCommand, expUseV6 bool) error {
 	if expUseV6 {
 		return newDBUpdate(opts)
 	}
 	return legacyDBUpdate(opts)
 }
 
-func newDBUpdate(opts options.Database) error {
+func newDBUpdate(opts options.DatabaseCommand) error {
 	client, err := distribution.NewClient(opts.ToClientConfig())
 	if err != nil {
 		return fmt.Errorf("unable to create distribution client: %w", err)
@@ -76,7 +76,7 @@ func newDBUpdate(opts options.Database) error {
 ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
 // all legacy processing below ////////////////////////////////////////////////////////////////////////////////////////
 
-func legacyDBUpdate(opts options.Database) error {
+func legacyDBUpdate(opts options.DatabaseCommand) error {
 	dbCurator, err := legacyDistribution.NewCurator(opts.ToLegacyCuratorConfig())
 	if err != nil {
 		return err
diff --git a/cmd/grype/cli/commands/root.go b/cmd/grype/cli/commands/root.go
index a7f36807..c5edb823 100644
--- a/cmd/grype/cli/commands/root.go
+++ b/cmd/grype/cli/commands/root.go
@@ -153,10 +153,10 @@ func runGrype(app clio.Application, opts *options.Grype, userInput string) (errs
 		func() (err error) {
 			log.Debug("loading DB")
 			if opts.Experimental.DBv6 {
-				vp, status, err = grype.LoadVulnerabilityDBv6(opts.DB.ToClientConfig(), opts.DB.ToCuratorConfig(), opts.DB.AutoUpdate)
+				vp, status, err = grype.LoadVulnerabilityDBv6(opts.ToClientConfig(), opts.ToCuratorConfig(), opts.DB.AutoUpdate)
 				return err
 			}
-			vp, status, err = grype.LoadVulnerabilityDB(opts.DB.ToLegacyCuratorConfig(), opts.DB.AutoUpdate)
+			vp, status, err = grype.LoadVulnerabilityDB(opts.ToLegacyCuratorConfig(), opts.DB.AutoUpdate)
 			return validateDBLoad(err, status)
 		},
 		func() (err error) {
diff --git a/cmd/grype/cli/options/database.go b/cmd/grype/cli/options/database.go
index 3263e945..9488fc02 100644
--- a/cmd/grype/cli/options/database.go
+++ b/cmd/grype/cli/options/database.go
@@ -7,9 +7,6 @@ import (
 	"github.com/adrg/xdg"
 
 	"github.com/anchore/clio"
-	legacyDistribution "github.com/anchore/grype/grype/db/v5/distribution"
-	"github.com/anchore/grype/grype/db/v6/distribution"
-	"github.com/anchore/grype/grype/db/v6/installation"
 	"github.com/anchore/grype/internal"
 )
 
@@ -55,43 +52,6 @@ func DefaultDatabase(id clio.Identification) Database {
 	}
 }
 
-func (cfg Database) ToClientConfig() distribution.Config {
-	return distribution.Config{
-		ID:                 cfg.ID,
-		LatestURL:          cfg.UpdateURL,
-		CACert:             cfg.CACert,
-		RequireUpdateCheck: cfg.RequireUpdateCheck,
-		CheckTimeout:       cfg.UpdateAvailableTimeout,
-		UpdateTimeout:      cfg.UpdateDownloadTimeout,
-	}
-}
-
-func (cfg Database) ToCuratorConfig() installation.Config {
-	return installation.Config{
-		DBRootDir:               cfg.Dir,
-		ValidateAge:             cfg.ValidateAge,
-		ValidateChecksum:        cfg.ValidateByHashOnStart,
-		MaxAllowedBuiltAge:      cfg.MaxAllowedBuiltAge,
-		UpdateCheckMaxFrequency: cfg.MaxUpdateCheckFrequency,
-	}
-}
-
-func (cfg Database) ToLegacyCuratorConfig() legacyDistribution.Config {
-	return legacyDistribution.Config{
-		ID:                      cfg.ID,
-		DBRootDir:               cfg.Dir,
-		ListingURL:              cfg.UpdateURL,
-		CACert:                  cfg.CACert,
-		ValidateByHashOnGet:     cfg.ValidateByHashOnStart,
-		ValidateAge:             cfg.ValidateAge,
-		MaxAllowedBuiltAge:      cfg.MaxAllowedBuiltAge,
-		RequireUpdateCheck:      cfg.RequireUpdateCheck,
-		ListingFileTimeout:      cfg.UpdateAvailableTimeout,
-		UpdateTimeout:           cfg.UpdateDownloadTimeout,
-		UpdateCheckMaxFrequency: cfg.MaxUpdateCheckFrequency,
-	}
-}
-
 func (cfg *Database) DescribeFields(descriptions clio.FieldDescriptionSet) {
 	descriptions.Add(&cfg.Dir, `location to write the vulnerability database cache`)
 	descriptions.Add(&cfg.UpdateURL, `URL of the vulnerability database`)
diff --git a/cmd/grype/cli/options/database_command.go b/cmd/grype/cli/options/database_command.go
new file mode 100644
index 00000000..561671e4
--- /dev/null
+++ b/cmd/grype/cli/options/database_command.go
@@ -0,0 +1,61 @@
+package options
+
+import (
+	"github.com/anchore/clio"
+	legacyDistribution "github.com/anchore/grype/grype/db/v5/distribution"
+	"github.com/anchore/grype/grype/db/v6/distribution"
+	"github.com/anchore/grype/grype/db/v6/installation"
+)
+
+type DatabaseCommand struct {
+	DB           Database     `yaml:"db" json:"db" mapstructure:"db"`
+	Experimental Experimental `yaml:"exp" json:"exp" mapstructure:"exp"`
+	Developer    developer    `yaml:"dev" json:"dev" mapstructure:"dev"`
+}
+
+func DefaultDatabaseCommand(id clio.Identification) *DatabaseCommand {
+	dbDefaults := DefaultDatabase(id)
+	// by default, require update check success for db operations which check for updates
+	dbDefaults.RequireUpdateCheck = true
+	return &DatabaseCommand{
+		DB: dbDefaults,
+	}
+}
+
+func (cfg DatabaseCommand) ToCuratorConfig() installation.Config {
+	return installation.Config{
+		DBRootDir:               cfg.DB.Dir,
+		ValidateAge:             cfg.DB.ValidateAge,
+		ValidateChecksum:        cfg.DB.ValidateByHashOnStart,
+		MaxAllowedBuiltAge:      cfg.DB.MaxAllowedBuiltAge,
+		UpdateCheckMaxFrequency: cfg.DB.MaxUpdateCheckFrequency,
+		Debug:                   cfg.Developer.DB.Debug,
+	}
+}
+
+func (cfg DatabaseCommand) ToClientConfig() distribution.Config {
+	return distribution.Config{
+		ID:                 cfg.DB.ID,
+		LatestURL:          cfg.DB.UpdateURL,
+		CACert:             cfg.DB.CACert,
+		RequireUpdateCheck: cfg.DB.RequireUpdateCheck,
+		CheckTimeout:       cfg.DB.UpdateAvailableTimeout,
+		UpdateTimeout:      cfg.DB.UpdateDownloadTimeout,
+	}
+}
+
+func (cfg DatabaseCommand) ToLegacyCuratorConfig() legacyDistribution.Config {
+	return legacyDistribution.Config{
+		ID:                      cfg.DB.ID,
+		DBRootDir:               cfg.DB.Dir,
+		ListingURL:              cfg.DB.UpdateURL,
+		CACert:                  cfg.DB.CACert,
+		ValidateByHashOnGet:     cfg.DB.ValidateByHashOnStart,
+		ValidateAge:             cfg.DB.ValidateAge,
+		MaxAllowedBuiltAge:      cfg.DB.MaxAllowedBuiltAge,
+		RequireUpdateCheck:      cfg.DB.RequireUpdateCheck,
+		ListingFileTimeout:      cfg.DB.UpdateAvailableTimeout,
+		UpdateTimeout:           cfg.DB.UpdateDownloadTimeout,
+		UpdateCheckMaxFrequency: cfg.DB.MaxUpdateCheckFrequency,
+	}
+}
diff --git a/cmd/grype/cli/options/grype.go b/cmd/grype/cli/options/grype.go
index 43074875..4bd5b3f6 100644
--- a/cmd/grype/cli/options/grype.go
+++ b/cmd/grype/cli/options/grype.go
@@ -25,7 +25,6 @@ type Grype struct {
 	Search                     search             `yaml:"search" json:"search" mapstructure:"search"`
 	Ignore                     []match.IgnoreRule `yaml:"ignore" json:"ignore" mapstructure:"ignore"`
 	Exclusions                 []string           `yaml:"exclude" json:"exclude" mapstructure:"exclude"`
-	DB                         Database           `yaml:"db" json:"db" mapstructure:"db"`
 	ExternalSources            externalSources    `yaml:"external-sources" json:"externalSources" mapstructure:"external-sources"`
 	Match                      matchConfig        `yaml:"match" json:"match" mapstructure:"match"`
 	FailOn                     string             `yaml:"fail-on-severity" json:"fail-on-severity" mapstructure:"fail-on-severity"`
@@ -37,7 +36,15 @@ type Grype struct {
 	VexDocuments               []string           `yaml:"vex-documents" json:"vex-documents" mapstructure:"vex-documents"`
 	VexAdd                     []string           `yaml:"vex-add" json:"vex-add" mapstructure:"vex-add"`                                                                   // GRYPE_VEX_ADD
 	MatchUpstreamKernelHeaders bool               `yaml:"match-upstream-kernel-headers" json:"match-upstream-kernel-headers" mapstructure:"match-upstream-kernel-headers"` // Show matches on kernel-headers packages where the match is on kernel upstream instead of marking them as ignored, default=false
-	Experimental               Experimental       `yaml:"exp" json:"exp" mapstructure:"exp"`
+	DatabaseCommand            `yaml:",inline" json:",inline" mapstructure:",squash"`
+}
+
+type developer struct {
+	DB databaseDeveloper `yaml:"db" json:"db" mapstructure:"db"`
+}
+
+type databaseDeveloper struct {
+	Debug bool `yaml:"debug" json:"debug" mapstructure:"debug"`
 }
 
 var _ interface {
@@ -48,8 +55,10 @@ var _ interface {
 
 func DefaultGrype(id clio.Identification) *Grype {
 	return &Grype{
-		Search:                     defaultSearch(source.SquashedScope),
-		DB:                         DefaultDatabase(id),
+		Search: defaultSearch(source.SquashedScope),
+		DatabaseCommand: DatabaseCommand{
+			DB: DefaultDatabase(id),
+		},
 		Match:                      defaultMatchConfig(),
 		ExternalSources:            defaultExternalSources(),
 		CheckForAppUpdate:          true,
diff --git a/grype/db/v6/affected_cpe_store.go b/grype/db/v6/affected_cpe_store.go
index e01dff1a..24171d69 100644
--- a/grype/db/v6/affected_cpe_store.go
+++ b/grype/db/v6/affected_cpe_store.go
@@ -117,6 +117,7 @@ func (s *affectedCPEStore) GetAffectedCPEs(cpe *cpe.Attributes, config *GetAffec
 	}
 
 	fields := make(logger.Fields)
+	count := 0
 	if cpe == nil {
 		fields["cpe"] = "any"
 	} else {
@@ -168,6 +169,8 @@ func (s *affectedCPEStore) GetAffectedCPEs(cpe *cpe.Attributes, config *GetAffec
 			models = append(models, *r)
 		}
 
+		count += len(results)
+
 		if config.Limit > 0 && len(models) >= config.Limit {
 			return ErrLimitReached
 		}
@@ -177,6 +180,8 @@ func (s *affectedCPEStore) GetAffectedCPEs(cpe *cpe.Attributes, config *GetAffec
 		return models, fmt.Errorf("unable to fetch affected CPE records: %w", err)
 	}
 
+	fields["records"] = count
+
 	return models, nil
 }
 
diff --git a/grype/db/v6/affected_package_store.go b/grype/db/v6/affected_package_store.go
index b6fb7279..824af95b 100644
--- a/grype/db/v6/affected_package_store.go
+++ b/grype/db/v6/affected_package_store.go
@@ -361,14 +361,23 @@ func (s *affectedPackageStore) addOs(packages ...*AffectedPackageHandle) error {
 	return nil
 }
 
-func (s *affectedPackageStore) GetAffectedPackages(pkg *PackageSpecifier, config *GetAffectedPackageOptions) ([]AffectedPackageHandle, error) {
+func (s *affectedPackageStore) GetAffectedPackages(pkg *PackageSpecifier, config *GetAffectedPackageOptions) ([]AffectedPackageHandle, error) { // nolint:funlen
 	if config == nil {
 		config = &GetAffectedPackageOptions{}
 	}
 
 	start := time.Now()
+	count := 0
 	defer func() {
-		log.WithFields("pkg", pkg.String(), "distro", config.OSs, "vulns", config.Vulnerabilities, "duration", time.Since(start)).Trace("fetched affected package record")
+		log.
+			WithFields(
+				"pkg", pkg.String(),
+				"distro", config.OSs,
+				"vulns", config.Vulnerabilities,
+				"duration", time.Since(start),
+				"records", count,
+			).
+			Trace("fetched affected package record")
 	}()
 
 	query := s.handlePackage(s.db, pkg)
@@ -416,6 +425,8 @@ func (s *affectedPackageStore) GetAffectedPackages(pkg *PackageSpecifier, config
 			models = append(models, *r)
 		}
 
+		count += len(results)
+
 		if config.Limit > 0 && len(models) >= config.Limit {
 			return ErrLimitReached
 		}
diff --git a/grype/db/v6/blobs.go b/grype/db/v6/blobs.go
index 474ab58d..f70332db 100644
--- a/grype/db/v6/blobs.go
+++ b/grype/db/v6/blobs.go
@@ -28,6 +28,10 @@ type VulnerabilityBlob struct {
 	Severities []Severity `json:"severities,omitempty"`
 }
 
+func (v VulnerabilityBlob) String() string {
+	return v.ID
+}
+
 // Reference represents a single external URL and string tags to use for organizational purposes
 type Reference struct {
 	// URL is the external resource
@@ -114,6 +118,24 @@ type AffectedPackageBlob struct {
 	Ranges []AffectedRange `json:"ranges,omitempty"`
 }
 
+func (a AffectedPackageBlob) String() string {
+	var fields []string
+
+	if len(a.Ranges) > 0 {
+		var ranges []string
+		for _, r := range a.Ranges {
+			ranges = append(ranges, r.String())
+		}
+		fields = append(fields, fmt.Sprintf("ranges=%s", strings.Join(ranges, ", ")))
+	}
+
+	if len(a.CVEs) > 0 {
+		fields = append(fields, fmt.Sprintf("cves=%s", strings.Join(a.CVEs, ", ")))
+	}
+
+	return strings.Join(fields, ", ")
+}
+
 // AffectedPackageQualifiers contains package attributes that confirm the package is affected by the vulnerability.
 type AffectedPackageQualifiers struct {
 	// RpmModularity indicates if the package follows RPM modularity for versioning.
@@ -132,6 +154,10 @@ type AffectedRange struct {
 	Fix *Fix `json:"fix,omitempty"`
 }
 
+func (a AffectedRange) String() string {
+	return fmt.Sprintf("%s (%s)", a.Version, a.Fix)
+}
+
 // Fix conveys availability of a fix for a vulnerability.
 type Fix struct {
 	// Version is the version number of the fix.
@@ -144,6 +170,16 @@ type Fix struct {
 	Detail *FixDetail `json:"detail,omitempty"`
 }
 
+func (f Fix) String() string {
+	switch f.State {
+	case FixedStatus:
+		return fmt.Sprintf("fixed in %s", f.Version)
+	case NotAffectedFixStatus:
+		return fmt.Sprintf("%s is not affected", f.Version)
+	}
+	return string(f.State)
+}
+
 // FixDetail is additional information about a fix, such as commit details and patch URLs.
 type FixDetail struct {
 	// GitCommit is the identifier for the Git commit associated with the fix.
diff --git a/grype/db/v6/db.go b/grype/db/v6/db.go
index 255c1825..b10609c8 100644
--- a/grype/db/v6/db.go
+++ b/grype/db/v6/db.go
@@ -60,6 +60,7 @@ type Curator interface {
 
 type Config struct {
 	DBDirPath string
+	Debug     bool
 }
 
 func (c Config) DBFilePath() string {
@@ -86,8 +87,10 @@ func Hydrater() func(string) error {
 
 // NewLowLevelDB creates a new empty DB for writing or opens an existing one for reading from the given path. This is
 // not recommended for typical interactions with the vulnerability DB, use NewReader and NewWriter instead.
-func NewLowLevelDB(dbFilePath string, empty, writable bool) (*gorm.DB, error) {
-	var opts []gormadapter.Option
+func NewLowLevelDB(dbFilePath string, empty, writable, debug bool) (*gorm.DB, error) {
+	opts := []gormadapter.Option{
+		gormadapter.WithDebug(debug),
+	}
 
 	if empty && !writable {
 		return nil, fmt.Errorf("cannot open an empty database for reading only")
diff --git a/grype/db/v6/installation/curator.go b/grype/db/v6/installation/curator.go
index 7557e197..a4e9a41d 100644
--- a/grype/db/v6/installation/curator.go
+++ b/grype/db/v6/installation/curator.go
@@ -35,6 +35,7 @@ type monitor struct {
 
 type Config struct {
 	DBRootDir string
+	Debug     bool
 
 	// validations
 	ValidateAge             bool
@@ -81,6 +82,7 @@ func (c curator) Reader() (db.Reader, error) {
 	s, err := db.NewReader(
 		db.Config{
 			DBDirPath: c.config.DBDirectoryPath(),
+			Debug:     c.config.Debug,
 		},
 	)
 	if err != nil {
diff --git a/grype/db/v6/log_dropped.go b/grype/db/v6/log_dropped.go
index 704dda44..d843ba67 100644
--- a/grype/db/v6/log_dropped.go
+++ b/grype/db/v6/log_dropped.go
@@ -1,13 +1,15 @@
 package v6
 
-import "github.com/anchore/grype/internal/log"
+import (
+	"github.com/anchore/go-logger"
+	"github.com/anchore/grype/internal/log"
+)
 
 // logDroppedVulnerability is a hook called when vulnerabilities are dropped from consideration in a vulnerability Provider,
 // this offers a convenient location to set a breakpoint
-func logDroppedVulnerability(vulnerabilityID string, reason any, context ...any) {
-	log.WithFields(
-		"vulnerability", vulnerabilityID,
-		"reason", reason,
-		"context", context,
-	).Trace("dropped vulnerability")
+func logDroppedVulnerability(vuln string, reason any, fields logger.Fields) {
+	fields["reason"] = reason
+	fields["vulnerability"] = vuln
+
+	log.WithFields(fields).Trace("dropped vulnerability")
 }
diff --git a/grype/db/v6/models.go b/grype/db/v6/models.go
index bb0aaf0b..3054163d 100644
--- a/grype/db/v6/models.go
+++ b/grype/db/v6/models.go
@@ -162,6 +162,10 @@ type VulnerabilityHandle struct {
 	BlobValue *VulnerabilityBlob `gorm:"-"`
 }
 
+func (v VulnerabilityHandle) String() string {
+	return fmt.Sprintf("%s/%s", v.Provider, v.Name)
+}
+
 func (v VulnerabilityHandle) getBlobValue() any {
 	if v.BlobValue == nil {
 		return nil // must return untyped nil or getBlobValue() == nil will always be false
@@ -256,6 +260,48 @@ type AffectedPackageHandle struct {
 	BlobValue *AffectedPackageBlob `gorm:"-"`
 }
 
+func (aph AffectedPackageHandle) vulnerability() string {
+	if aph.Vulnerability != nil {
+		return aph.Vulnerability.Name
+	}
+	if aph.BlobValue != nil {
+		if len(aph.BlobValue.CVEs) > 0 {
+			return aph.BlobValue.CVEs[0]
+		}
+	}
+	return ""
+}
+
+func (aph AffectedPackageHandle) String() string {
+	var fields []string
+
+	if aph.BlobValue != nil {
+		v := aph.BlobValue.String()
+		if v != "" {
+			fields = append(fields, v)
+		}
+	}
+	if aph.OperatingSystem != nil {
+		fields = append(fields, fmt.Sprintf("os=%q", aph.OperatingSystem.String()))
+	} else {
+		fields = append(fields, fmt.Sprintf("os=%d", aph.OperatingSystemID))
+	}
+
+	if aph.Package != nil {
+		fields = append(fields, fmt.Sprintf("pkg=%q", aph.Package.String()))
+	} else {
+		fields = append(fields, fmt.Sprintf("pkg=%d", aph.PackageID))
+	}
+
+	if aph.Vulnerability != nil {
+		fields = append(fields, fmt.Sprintf("vuln=%q", aph.Vulnerability.String()))
+	} else {
+		fields = append(fields, fmt.Sprintf("vuln=%d", aph.VulnerabilityID))
+	}
+
+	return fmt.Sprintf("affectedPackage(%s)", strings.Join(fields, ", "))
+}
+
 func (aph AffectedPackageHandle) getBlobValue() any {
 	if aph.BlobValue == nil {
 		return nil // must return untyped nil or getBlobValue() == nil will always be false
@@ -553,28 +599,65 @@ type AffectedCPEHandle struct {
 	BlobValue *AffectedPackageBlob `gorm:"-"`
 }
 
-func (v AffectedCPEHandle) getBlobID() ID {
-	return v.BlobID
+func (ach AffectedCPEHandle) vulnerability() string {
+	if ach.Vulnerability != nil {
+		return ach.Vulnerability.Name
+	}
+	if ach.BlobValue != nil {
+		if len(ach.BlobValue.CVEs) > 0 {
+			return ach.BlobValue.CVEs[0]
+		}
+	}
+	return ""
 }
 
-func (v AffectedCPEHandle) getBlobValue() any {
-	if v.BlobValue == nil {
+func (ach AffectedCPEHandle) String() string {
+	var fields []string
+
+	if ach.BlobValue != nil {
+		v := ach.BlobValue.String()
+		if v != "" {
+			fields = append(fields, v)
+		}
+	}
+
+	if ach.CPE != nil {
+		fields = append(fields, fmt.Sprintf("cpe=%q", ach.CPE.String()))
+	} else {
+		fields = append(fields, fmt.Sprintf("cpe=%d", ach.CpeID))
+	}
+
+	if ach.Vulnerability != nil {
+		fields = append(fields, fmt.Sprintf("vuln=%q", ach.Vulnerability.String()))
+	} else {
+		fields = append(fields, fmt.Sprintf("vuln=%d", ach.VulnerabilityID))
+	}
+
+	return fmt.Sprintf("affectedCPE(%s)", strings.Join(fields, ", "))
+}
+
+func (ach AffectedCPEHandle) getBlobID() ID {
+	return ach.BlobID
+}
+
+func (ach AffectedCPEHandle) getBlobValue() any {
+	if ach.BlobValue == nil {
 		return nil // must return untyped nil or getBlobValue() == nil will always be false
 	}
-	return v.BlobValue
+	return ach.BlobValue
 }
 
-func (v *AffectedCPEHandle) setBlobID(id ID) {
-	v.BlobID = id
+func (ach *AffectedCPEHandle) setBlobID(id ID) {
+	ach.BlobID = id
 }
 
-func (v *AffectedCPEHandle) setBlob(rawBlobValue []byte) error {
+func (ach *AffectedCPEHandle) setBlob(rawBlobValue []byte) error {
 	var blobValue AffectedPackageBlob
 	if err := json.Unmarshal(rawBlobValue, &blobValue); err != nil {
 		return fmt.Errorf("unable to unmarshal affected cpe blob value: %w", err)
 	}
 
-	v.BlobValue = &blobValue
+	ach.BlobValue = &blobValue
 	return nil
 }
 
diff --git a/grype/db/v6/store.go b/grype/db/v6/store.go
index 6f7eaccf..17404edc 100644
--- a/grype/db/v6/store.go
+++ b/grype/db/v6/store.go
@@ -50,7 +50,7 @@ func newStore(cfg Config, empty, writable bool) (*store, error) {
 		path = cfg.DBFilePath()
 	}
 
-	db, err := NewLowLevelDB(path, empty, writable)
+	db, err := NewLowLevelDB(path, empty, writable, cfg.Debug)
 	if err != nil {
 		return nil, fmt.Errorf("failed to open db: %w", err)
 	}
diff --git a/grype/db/v6/vulnerability_provider.go b/grype/db/v6/vulnerability_provider.go
index f2e11689..592df0d9 100644
--- a/grype/db/v6/vulnerability_provider.go
+++ b/grype/db/v6/vulnerability_provider.go
@@ -6,6 +6,7 @@ import (
 
 	"github.com/iancoleman/strcase"
 
+	"github.com/anchore/go-logger"
 	"github.com/anchore/grype/grype/search"
 	"github.com/anchore/grype/grype/version"
 	"github.com/anchore/grype/grype/vulnerability"
@@ -119,6 +120,12 @@ func (s vulnerabilityProvider) FindVulnerabilities(criteria ...vulnerability.Cri
 			}
 		}
 
+		if len(osSpecs) == 0 {
+			// we don't want to search across all distros, instead if the user did not specify a distro we should assume that
+			// they want to search across affected packages not associated with any distro.
+			osSpecs = append(osSpecs, NoOSSpecified)
+		}
+
 		versionMatcher, remainingCriteria := splitConstraintMatcher(criteriaSet...)
 
 		var affectedPackages []AffectedPackageHandle
@@ -185,7 +192,10 @@ func (s vulnerabilityProvider) filterVulnerabilities(vulns []vulnerability.Vulne
 			}
 			matches, err := c.MatchesVulnerability(v)
 			if !matches || err != nil {
-				logDroppedVulnerability(v.Reference.ID, err, c)
+				logDroppedVulnerability(v.ID, err, logger.Fields{
+					"vulnerability": v,
+					"criteria":      c,
+				})
 				return false, err
 			}
 		}
@@ -275,13 +285,9 @@ func filterAffectedPackageVersions(constraintMatcher search.VersionConstraintMat
 			continue // keep this handle
 		}
 
-		id := ""
-		if handle.Vulnerability != nil {
-			id = handle.Vulnerability.Name
-		} else if len(handle.BlobValue.CVEs) > 0 {
-			id = handle.BlobValue.CVEs[0]
-		}
-		logDroppedVulnerability(id, "constraints", handle, constraintMatcher)
+		logDroppedVulnerability(handle.vulnerability(), "package version not within vulnerability constraints", logger.Fields{
+			"affectedPackage": handle,
+		})
 
 		// if we haven't matched a constraint, remove the package
 		packages = append(packages[0:packageIdx], packages[packageIdx+1:]...)
@@ -303,13 +309,9 @@ func filterAffectedCPEVersions(constraintMatcher search.VersionConstraintMatcher
 			continue // keep this handle
 		}
 
-		id := ""
-		if handle.Vulnerability != nil {
-			id = handle.Vulnerability.Name
-		} else if len(handle.BlobValue.CVEs) > 0 {
-			id = handle.BlobValue.CVEs[0]
-		}
-		logDroppedVulnerability(id, "constraints", handle, constraintMatcher)
+		logDroppedVulnerability(handle.vulnerability(), "package version not within vulnerability constraints", logger.Fields{
+			"affectedCPE": handle,
+		})
 	}
 	return out
 }
diff --git a/grype/search/version_constraint.go b/grype/search/version_constraint.go
index 9757a1a4..c3d41e6d 100644
--- a/grype/search/version_constraint.go
+++ b/grype/search/version_constraint.go
@@ -25,6 +25,13 @@ func ByVersion(v version.Version) vulnerability.Criteria {
 	return ByConstraintFunc(func(constraint version.Constraint) (bool, error) {
 		satisfied, err := constraint.Satisfied(&v)
 		if err != nil {
+			var formatErr *version.UnsupportedFormatError
+			if errors.As(err, &formatErr) {
+				// if the format is unsupported, then the constraint is not satisfied, but this should not be conveyed as an error
+				log.WithFields("reason", err).Trace("unsatisfied constraint")
+				return false, nil
+			}
+
 			var e *version.NonFatalConstraintError
 			if errors.As(err, &e) {
 				log.Warn(e)
diff --git a/grype/version/apk_constraint.go b/grype/version/apk_constraint.go
index 37e69cad..679a4058 100644
--- a/grype/version/apk_constraint.go
+++ b/grype/version/apk_constraint.go
@@ -54,7 +54,7 @@ func (c apkConstraint) Satisfied(version *Version) (bool, error) {
 	}
 
 	if !c.supported(version.Format) {
-		return false, fmt.Errorf("(apk) unsupported format: %s", version.Format)
+		return false, NewUnsupportedFormatError(ApkFormat, version.Format)
 	}
 
 	if version.rich.apkVer == nil {
diff --git a/grype/version/deb_constraint.go b/grype/version/deb_constraint.go
index 5950bf17..d70125f1 100644
--- a/grype/version/deb_constraint.go
+++ b/grype/version/deb_constraint.go
@@ -49,7 +49,7 @@ func (c debConstraint) Satisfied(version *Version) (bool, error) {
 	}
 
 	if !c.supported(version.Format) {
-		return false, fmt.Errorf("(deb) unsupported format: %s", version.Format)
+		return false, NewUnsupportedFormatError(DebFormat, version.Format)
 	}
 
 	if version.rich.debVer == nil {
diff --git a/grype/version/error.go b/grype/version/error.go
new file mode 100644
index 00000000..f6775fd3
--- /dev/null
+++ b/grype/version/error.go
@@ -0,0 +1,34 @@
+package version
+
+import (
+	"errors"
+	"fmt"
+)
+
+// UnsupportedFormatError represents an error when a format doesn't match the expected format
+type UnsupportedFormatError struct {
+	Left  Format
+	Right Format
+}
+
+// NewUnsupportedFormatError creates a new UnsupportedFormatError
+func NewUnsupportedFormatError(left, right Format) *UnsupportedFormatError {
+	return &UnsupportedFormatError{
+		Left:  left,
+		Right: right,
+	}
+}
+
+func (e *UnsupportedFormatError) Error() string {
+	return fmt.Sprintf("(%s) unsupported format: %s", e.Left, e.Right)
+}
+
+func (e *UnsupportedFormatError) Is(target error) bool {
+	var t *UnsupportedFormatError
+	ok := errors.As(target, &t)
+	if !ok {
+		return false
+	}
+	return (t.Left == UnknownFormat || t.Left == e.Left) &&
+		(t.Right == UnknownFormat || t.Right == e.Right)
+}
diff --git a/grype/version/golang_version.go b/grype/version/golang_version.go
index a9c21005..a52256ee 100644
--- a/grype/version/golang_version.go
+++ b/grype/version/golang_version.go
@@ -16,7 +16,7 @@ type golangVersion struct {
 
 func (g golangVersion) Compare(version *Version) (int, error) {
 	if version.Format != GolangFormat {
-		return -1, fmt.Errorf("cannot compare %v to golang version", version.Format)
+		return -1, NewUnsupportedFormatError(GolangFormat, version.Format)
 	}
 	if version.rich.golangVersion == nil {
 		return -1, fmt.Errorf("cannot compare version with nil golang version to golang version")
diff --git a/grype/version/jvm_version.go b/grype/version/jvm_version.go
index f07a15d1..91c296d0 100644
--- a/grype/version/jvm_version.go
+++ b/grype/version/jvm_version.go
@@ -57,7 +57,7 @@ func (v *jvmVersion) Compare(other *Version) (int, error) {
 		return other.rich.semVer.verObj.Compare(v.semVer), nil
 	}
 
-	return -1, fmt.Errorf("unable to compare JVM to given format: %s", other.Format)
+	return -1, NewUnsupportedFormatError(JVMFormat, other.Format)
 }
 
 func (v jvmVersion) compare(other jvmVersion) int {
diff --git a/grype/version/kb_contraint.go b/grype/version/kb_contraint.go
index dc8d2e92..c2367d7e 100644
--- a/grype/version/kb_contraint.go
+++ b/grype/version/kb_contraint.go
@@ -51,7 +51,7 @@ func (c kbConstraint) Satisfied(version *Version) (bool, error) {
 	}
 
 	if !c.supported(version.Format) {
-		return false, fmt.Errorf("(kb) unsupported format: %s", version.Format)
+		return false, NewUnsupportedFormatError(KBFormat, version.Format)
 	}
 
 	return c.expression.satisfied(version)
diff --git a/grype/version/maven_constraint.go b/grype/version/maven_constraint.go
index 8a191d67..77c4e95e 100644
--- a/grype/version/maven_constraint.go
+++ b/grype/version/maven_constraint.go
@@ -53,7 +53,7 @@ func (c mavenConstraint) Satisfied(version *Version) (satisfied bool, err error)
 	}
 
 	if !c.supported(version.Format) {
-		return false, fmt.Errorf("(maven) unsupported format: %s", version.Format)
+		return false, NewUnsupportedFormatError(MavenFormat, version.Format)
 	}
 
 	if version.rich.mavenVer == nil {
diff --git a/grype/version/pep440_constraint.go b/grype/version/pep440_constraint.go
index 2aa39caa..cc0c0b31 100644
--- a/grype/version/pep440_constraint.go
+++ b/grype/version/pep440_constraint.go
@@ -26,7 +26,7 @@ func (p pep440Constraint) Satisfied(version *Version) (bool, error) {
 		return true, nil
 	}
 	if version.Format != PythonFormat {
-		return false, fmt.Errorf("(python) unsupported format: %s", version.Format)
+		return false, NewUnsupportedFormatError(PythonFormat, version.Format)
 	}
 
 	if version.rich.pep440version == nil {
diff --git a/grype/version/portage_constraint.go b/grype/version/portage_constraint.go
index dd34d5e5..d04dc56b 100644
--- a/grype/version/portage_constraint.go
+++ b/grype/version/portage_constraint.go
@@ -48,7 +48,7 @@ func (c portageConstraint) Satisfied(version *Version) (bool, error) {
 	}
 
 	if !c.supported(version.Format) {
-		return false, fmt.Errorf("(portage) unsupported format: %s", version.Format)
+		return false, NewUnsupportedFormatError(PortageFormat, version.Format)
 	}
 
 	if version.rich.portVer == nil {
diff --git a/grype/version/rpm_constraint.go b/grype/version/rpm_constraint.go
index 0565f3e8..4973139b 100644
--- a/grype/version/rpm_constraint.go
+++ b/grype/version/rpm_constraint.go
@@ -51,7 +51,7 @@ func (c rpmConstraint) Satisfied(version *Version) (bool, error) {
 	}
 
 	if !c.supported(version.Format) {
-		return false, fmt.Errorf("(rpm) unsupported format: %s", version.Format)
+		return false, NewUnsupportedFormatError(RpmFormat, version.Format)
 	}
 
 	if version.rich.rpmVer == nil {
diff --git a/grype/version/semantic_constraint.go b/grype/version/semantic_constraint.go
index 00fe9523..69192dff 100644
--- a/grype/version/semantic_constraint.go
+++ b/grype/version/semantic_constraint.go
@@ -56,7 +56,7 @@ func (c semanticConstraint) Satisfied(version *Version) (bool, error) {
 	}
 
 	if !c.supported(version.Format) {
-		return false, fmt.Errorf("(semantic) unsupported format: %s", version.Format)
+		return false, NewUnsupportedFormatError(SemanticFormat, version.Format)
 	}
 
 	if version.rich.semVer == nil {


Test patch:
diff --git a/cmd/grype/cli/commands/db_list_test.go b/cmd/grype/cli/commands/db_list_test.go
index 77fa488b..e5b9d9f9 100644
--- a/cmd/grype/cli/commands/db_list_test.go
+++ b/cmd/grype/cli/commands/db_list_test.go
@@ -12,6 +12,7 @@ import (
 	"github.com/stretchr/testify/require"
 
 	"github.com/anchore/clio"
+	"github.com/anchore/grype/cmd/grype/cli/options"
 	db "github.com/anchore/grype/grype/db/v6"
 	"github.com/anchore/grype/grype/db/v6/distribution"
 	"github.com/anchore/grype/internal/schemaver"
@@ -34,7 +35,7 @@ func Test_ListingUserAgent(t *testing.T) {
 		mockSrv := httptest.NewServer(handler)
 		defer mockSrv.Close()
 
-		dbOptions := *dbOptionsDefault(clio.Identification{
+		dbOptions := *options.DefaultDatabaseCommand(clio.Identification{
 			Name:    "the-app",
 			Version: "v3.2.1",
 		})
@@ -42,8 +43,8 @@ func Test_ListingUserAgent(t *testing.T) {
 		dbOptions.DB.UpdateURL = mockSrv.URL + listingFile
 
 		_ = legacyDBList(dbListOptions{
-			Output:    "",
-			DBOptions: dbOptions,
+			Output:          "",
+			DatabaseCommand: dbOptions,
 		})
 
 		if got != "the-app v3.2.1" {
@@ -76,7 +77,7 @@ func Test_ListingUserAgent(t *testing.T) {
 		mockSrv := httptest.NewServer(handler)
 		defer mockSrv.Close()
 
-		dbOptions := *dbOptionsDefault(clio.Identification{
+		dbOptions := *options.DefaultDatabaseCommand(clio.Identification{
 			Name:    "new-app",
 			Version: "v4.0.0",
 		})
@@ -84,8 +85,8 @@ func Test_ListingUserAgent(t *testing.T) {
 		dbOptions.DB.UpdateURL = mockSrv.URL + listingFile
 
 		err := newDBList(dbListOptions{
-			Output:    textOutputFormat,
-			DBOptions: dbOptions,
+			Output:          textOutputFormat,
+			DatabaseCommand: dbOptions,
 		})
 		require.NoError(t, err)
 
diff --git a/grype/db/v6/installation/curator_test.go b/grype/db/v6/installation/curator_test.go
index 10898f8f..1954dd2b 100644
--- a/grype/db/v6/installation/curator_test.go
+++ b/grype/db/v6/installation/curator_test.go
@@ -117,7 +117,7 @@ func writeTestChecksumsFile(t *testing.T, fs afero.Fs, dir string, checksums str
 
 func writeTestDescriptionToDB(t *testing.T, dir string, desc db.Description) string {
 	c := db.Config{DBDirPath: dir}
-	d, err := db.NewLowLevelDB(c.DBFilePath(), false, false)
+	d, err := db.NewLowLevelDB(c.DBFilePath(), false, false, true)
 	require.NoError(t, err)
 
 	if err := d.Unscoped().Where("true").Delete(&db.DBMetadata{}).Error; err != nil {