Repository: anchore/grype
Pull request #2291 (2024-12-01 16:06:01)

test: update quality gate db to latest version
This is an auto-generated pull request to update the quality gate db to latest version


Patch:
diff --git a/.binny.yaml b/.binny.yaml
index a3cf53c2..fa7baebb 100644
--- a/.binny.yaml
+++ b/.binny.yaml
@@ -98,7 +98,7 @@ tools:
   # used for triggering a release
   - name: gh
     version:
-      want: v2.62.0
+      want: v2.63.0
     method: github-release
     with:
       repo: cli/cli
diff --git a/cmd/grype/cli/commands/db.go b/cmd/grype/cli/commands/db.go
index 41a81b2c..d17a762d 100644
--- a/cmd/grype/cli/commands/db.go
+++ b/cmd/grype/cli/commands/db.go
@@ -7,8 +7,15 @@ import (
 	"github.com/anchore/grype/cmd/grype/cli/options"
 )
 
+const (
+	jsonOutputFormat  = "json"
+	tableOutputFormat = "table"
+	textOutputFormat  = "text"
+)
+
 type DBOptions struct {
-	DB options.Database `yaml:"db" json:"db" mapstructure:"db"`
+	DB           options.Database     `yaml:"db" json:"db" mapstructure:"db"`
+	Experimental options.Experimental `yaml:"exp" json:"exp" mapstructure:"exp"`
 }
 
 func dbOptionsDefault(id clio.Identification) *DBOptions {
diff --git a/cmd/grype/cli/commands/db_check.go b/cmd/grype/cli/commands/db_check.go
index 0f5fedd8..a5b8a02a 100644
--- a/cmd/grype/cli/commands/db_check.go
+++ b/cmd/grype/cli/commands/db_check.go
@@ -1,22 +1,40 @@
 package commands
 
 import (
+	"encoding/json"
 	"fmt"
+	"io"
 	"os"
 
 	"github.com/spf13/cobra"
 
 	"github.com/anchore/clio"
-	"github.com/anchore/grype/cmd/grype/cli/options"
-	"github.com/anchore/grype/grype/db/legacy/distribution"
+	legacyDistribution "github.com/anchore/grype/grype/db/legacy/distribution"
+	db "github.com/anchore/grype/grype/db/v6"
+	"github.com/anchore/grype/grype/db/v6/distribution"
+	"github.com/anchore/grype/internal/log"
 )
 
 const (
 	exitCodeOnDBUpgradeAvailable = 100
 )
 
+type dbCheckOptions struct {
+	Output    string `yaml:"output" json:"output" mapstructure:"output"`
+	DBOptions `yaml:",inline" mapstructure:",squash"`
+}
+
+var _ clio.FlagAdder = (*dbCheckOptions)(nil)
+
+func (d *dbCheckOptions) AddFlags(flags clio.FlagSet) {
+	flags.StringVarP(&d.Output, "output", "o", "format to display results (available=[text, json])")
+}
+
 func DBCheck(app clio.Application) *cobra.Command {
-	opts := dbOptionsDefault(app.ID())
+	opts := &dbCheckOptions{
+		Output:    textOutputFormat,
+		DBOptions: *dbOptionsDefault(app.ID()),
+	}
 
 	return app.SetupCommand(&cobra.Command{
 		Use:   "check",
@@ -28,13 +46,101 @@ func DBCheck(app clio.Application) *cobra.Command {
 		},
 		Args: cobra.ExactArgs(0),
 		RunE: func(_ *cobra.Command, _ []string) error {
-			return runDBCheck(opts.DB)
+			return runDBCheck(*opts)
 		},
 	}, opts)
 }
 
-func runDBCheck(opts options.Database) error {
-	dbCurator, err := distribution.NewCurator(opts.ToLegacyCuratorConfig())
+func runDBCheck(opts dbCheckOptions) error {
+	if opts.DBOptions.Experimental.DBv6 {
+		return newDBCheck(opts)
+	}
+	return legacyDBCheck(opts)
+}
+
+func newDBCheck(opts dbCheckOptions) error {
+	client, err := distribution.NewClient(opts.DB.ToClientConfig())
+	if err != nil {
+		return fmt.Errorf("unable to create distribution client: %w", err)
+	}
+
+	cfg := opts.DB.ToCuratorConfig()
+
+	current, err := db.ReadDescription(cfg.DBFilePath())
+	if err != nil {
+		log.WithFields("error", err).Debug("unable to read current database metadata")
+		current = nil
+	}
+
+	archive, err := client.IsUpdateAvailable(current)
+	if err != nil {
+		return fmt.Errorf("unable to check for vulnerability database update: %w", err)
+	}
+
+	updateAvailable := archive != nil
+
+	if err := presentNewDBCheck(opts.Output, os.Stdout, updateAvailable, current, archive); err != nil {
+		return err
+	}
+
+	if updateAvailable {
+		os.Exit(exitCodeOnDBUpgradeAvailable) //nolint:gocritic
+	}
+	return nil
+}
+
+type dbCheckJSON struct {
+	CurrentDB       *db.Description       `json:"currentDB"`
+	CandidateDB     *distribution.Archive `json:"candidateDB"`
+	UpdateAvailable bool                  `json:"updateAvailable"`
+}
+
+func presentNewDBCheck(format string, writer io.Writer, updateAvailable bool, current *db.Description, candidate *distribution.Archive) error {
+	switch format {
+	case textOutputFormat:
+		if current != nil {
+			fmt.Fprintf(writer, "Installed DB version %s was built on %s\n", current.SchemaVersion, current.Built.String())
+		} else {
+			fmt.Fprintln(writer, "No installed DB version found")
+		}
+
+		if !updateAvailable {
+			fmt.Fprintln(writer, "No update available")
+			return nil
+		}
+
+		fmt.Fprintf(writer, "Updated DB version %s was built on %s\n", candidate.SchemaVersion, candidate.Built.String())
+		fmt.Fprintln(writer, "You can run 'grype db update' to update to the latest db")
+	case jsonOutputFormat:
+		data := dbCheckJSON{
+			CurrentDB:       current,
+			CandidateDB:     candidate,
+			UpdateAvailable: updateAvailable,
+		}
+
+		enc := json.NewEncoder(writer)
+		enc.SetEscapeHTML(false)
+		enc.SetIndent("", " ")
+		if err := enc.Encode(&data); err != nil {
+			return fmt.Errorf("failed to db listing information: %+v", err)
+		}
+	default:
+		return fmt.Errorf("unsupported output format: %s", format)
+	}
+	return nil
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// all legacy processing below ////////////////////////////////////////////////////////////////////////////////////////
+
+type legacyDBCheckJSON struct {
+	CurrentDB       *legacyDistribution.Metadata     `json:"currentDB"`
+	CandidateDB     *legacyDistribution.ListingEntry `json:"candidateDB"`
+	UpdateAvailable bool                             `json:"updateAvailable"`
+}
+
+func legacyDBCheck(opts dbCheckOptions) error {
+	dbCurator, err := legacyDistribution.NewCurator(opts.DB.ToLegacyCuratorConfig())
 	if err != nil {
 		return err
 	}
@@ -44,21 +150,40 @@ func runDBCheck(opts options.Database) error {
 		return fmt.Errorf("unable to check for vulnerability database update: %+v", err)
 	}
 
-	if !updateAvailable {
-		return stderrPrintLnf("No update available")
-	}
+	switch opts.Output {
+	case textOutputFormat:
+		if currentDBMetadata != nil {
+			fmt.Printf("Current DB version %d was built on %s\n", currentDBMetadata.Version, currentDBMetadata.Built.String())
+		}
 
-	fmt.Println("Update available!")
+		if !updateAvailable {
+			fmt.Println("No update available")
+			return nil
+		}
 
-	if currentDBMetadata != nil {
-		fmt.Printf("Current DB version %d was built on %s\n", currentDBMetadata.Version, currentDBMetadata.Built.String())
-	}
+		fmt.Printf("Updated DB version %d was built on %s\n", updateDBEntry.Version, updateDBEntry.Built.String())
+		fmt.Printf("Updated DB URL: %s\n", updateDBEntry.URL.String())
+		fmt.Println("You can run 'grype db update' to update to the latest db")
+	case jsonOutputFormat:
+		data := legacyDBCheckJSON{
+			CurrentDB:       currentDBMetadata,
+			CandidateDB:     updateDBEntry,
+			UpdateAvailable: updateAvailable,
+		}
 
-	fmt.Printf("Updated DB version %d was built on %s\n", updateDBEntry.Version, updateDBEntry.Built.String())
-	fmt.Printf("Updated DB URL: %s\n", updateDBEntry.URL.String())
-	fmt.Println("You can run 'grype db update' to update to the latest db")
+		enc := json.NewEncoder(os.Stdout)
+		enc.SetEscapeHTML(false)
+		enc.SetIndent("", " ")
+		if err := enc.Encode(&data); err != nil {
+			return fmt.Errorf("failed to db listing information: %+v", err)
+		}
+	default:
+		return fmt.Errorf("unsupported output format: %s", opts.Output)
+	}
 
-	os.Exit(exitCodeOnDBUpgradeAvailable) //nolint:gocritic
+	if updateAvailable {
+		os.Exit(exitCodeOnDBUpgradeAvailable) //nolint:gocritic
+	}
 
 	return nil
 }
diff --git a/cmd/grype/cli/commands/db_delete.go b/cmd/grype/cli/commands/db_delete.go
index 0bb4b655..4d4ef142 100644
--- a/cmd/grype/cli/commands/db_delete.go
+++ b/cmd/grype/cli/commands/db_delete.go
@@ -7,7 +7,9 @@ import (
 
 	"github.com/anchore/clio"
 	"github.com/anchore/grype/cmd/grype/cli/options"
-	"github.com/anchore/grype/grype/db/legacy/distribution"
+	legacyDistribution "github.com/anchore/grype/grype/db/legacy/distribution"
+	"github.com/anchore/grype/grype/db/v6/distribution"
+	"github.com/anchore/grype/grype/db/v6/installation"
 )
 
 func DBDelete(app clio.Application) *cobra.Command {
@@ -19,17 +21,40 @@ func DBDelete(app clio.Application) *cobra.Command {
 		Args:    cobra.ExactArgs(0),
 		PreRunE: disableUI(app),
 		RunE: func(_ *cobra.Command, _ []string) error {
-			return runDBDelete(opts.DB)
+			return runDBDelete(*opts)
 		},
 	}, opts)
 }
 
-func runDBDelete(opts options.Database) error {
-	dbCurator, err := distribution.NewCurator(opts.ToLegacyCuratorConfig())
+func runDBDelete(opts DBOptions) error {
+	if opts.Experimental.DBv6 {
+		return newDBDelete(opts.DB)
+	}
+	return legacyDBDelete(opts.DB)
+}
+
+func newDBDelete(opts options.Database) error {
+	client, err := distribution.NewClient(opts.ToClientConfig())
 	if err != nil {
-		return err
+		return fmt.Errorf("unable to create distribution client: %w", err)
+	}
+	c, err := installation.NewCurator(opts.ToCuratorConfig(), client)
+	if err != nil {
+		return fmt.Errorf("unable to create curator: %w", err)
 	}
 
+	if err := c.Delete(); err != nil {
+		return fmt.Errorf("unable to delete vulnerability database: %+v", err)
+	}
+
+	return stderrPrintLnf("Vulnerability database deleted")
+}
+
+func legacyDBDelete(opts options.Database) error {
+	dbCurator, err := legacyDistribution.NewCurator(opts.ToLegacyCuratorConfig())
+	if err != nil {
+		return err
+	}
 	if err := dbCurator.Delete(); err != nil {
 		return fmt.Errorf("unable to delete vulnerability database: %+v", err)
 	}
diff --git a/cmd/grype/cli/commands/db_diff.go b/cmd/grype/cli/commands/db_diff.go
index ab9b7f31..c41a8496 100644
--- a/cmd/grype/cli/commands/db_diff.go
+++ b/cmd/grype/cli/commands/db_diff.go
@@ -29,7 +29,7 @@ func (d *dbDiffOptions) AddFlags(flags clio.FlagSet) {
 
 func DBDiff(app clio.Application) *cobra.Command {
 	opts := &dbDiffOptions{
-		Output:    "table",
+		Output:    tableOutputFormat,
 		DBOptions: *dbOptionsDefault(app.ID()),
 	}
 
diff --git a/cmd/grype/cli/commands/db_import.go b/cmd/grype/cli/commands/db_import.go
index 52820b07..2d4948f0 100644
--- a/cmd/grype/cli/commands/db_import.go
+++ b/cmd/grype/cli/commands/db_import.go
@@ -2,8 +2,6 @@ package commands
 
 import (
 	"fmt"
-	"path/filepath"
-	"strings"
 
 	"github.com/spf13/cobra"
 
@@ -25,22 +23,22 @@ func DBImport(app clio.Application) *cobra.Command {
 		Args:    cobra.ExactArgs(1),
 		PreRunE: disableUI(app),
 		RunE: func(_ *cobra.Command, args []string) error {
-			return runDBImport(opts.DB, args[0])
+			return runDBImport(*opts, args[0])
 		},
 	}, opts)
 }
 
-func runDBImport(opts options.Database, dbArchivePath string) error {
+func runDBImport(opts DBOptions, dbArchivePath string) error {
 	// TODO: tui update? better logging?
 
 	// TODO: we will only support v6 after development is complete
-	if isV6DB(dbArchivePath) {
-		return importDB(opts, dbArchivePath)
+	if opts.Experimental.DBv6 {
+		return newDBImport(opts.DB, dbArchivePath)
 	}
-	return legacyDBImport(opts, dbArchivePath)
+	return legacyDBImport(opts.DB, dbArchivePath)
 }
 
-func importDB(opts options.Database, dbArchivePath string) error {
+func newDBImport(opts options.Database, dbArchivePath string) error {
 	client, err := distribution.NewClient(opts.ToClientConfig())
 	if err != nil {
 		return fmt.Errorf("unable to create distribution client: %w", err)
@@ -68,7 +66,3 @@ func legacyDBImport(opts options.Database, dbArchivePath string) error {
 
 	return stderrPrintLnf("Vulnerability database imported")
 }
-
-func isV6DB(path string) bool {
-	return strings.Contains(filepath.Base(path), "vulnerability-db_v6")
-}
diff --git a/cmd/grype/cli/commands/db_list.go b/cmd/grype/cli/commands/db_list.go
index 16a77e5d..e02f4de5 100644
--- a/cmd/grype/cli/commands/db_list.go
+++ b/cmd/grype/cli/commands/db_list.go
@@ -3,12 +3,16 @@ package commands
 import (
 	"encoding/json"
 	"fmt"
+	"io"
+	"net/url"
 	"os"
+	"path"
 
 	"github.com/spf13/cobra"
 
 	"github.com/anchore/clio"
-	"github.com/anchore/grype/grype/db/legacy/distribution"
+	legacyDistribution "github.com/anchore/grype/grype/db/legacy/distribution"
+	"github.com/anchore/grype/grype/db/v6/distribution"
 )
 
 type dbListOptions struct {
@@ -24,7 +28,7 @@ func (d *dbListOptions) AddFlags(flags clio.FlagSet) {
 
 func DBList(app clio.Application) *cobra.Command {
 	opts := &dbListOptions{
-		Output:    "text",
+		Output:    textOutputFormat,
 		DBOptions: *dbOptionsDefault(app.ID()),
 	}
 
@@ -34,13 +38,70 @@ func DBList(app clio.Application) *cobra.Command {
 		PreRunE: disableUI(app),
 		Args:    cobra.ExactArgs(0),
 		RunE: func(_ *cobra.Command, _ []string) error {
-			return runDBList(opts)
+			return runDBList(*opts)
 		},
 	}, opts)
 }
 
-func runDBList(opts *dbListOptions) error {
-	dbCurator, err := distribution.NewCurator(opts.DB.ToLegacyCuratorConfig())
+func runDBList(opts dbListOptions) error {
+	if opts.Experimental.DBv6 {
+		return newDBList(opts)
+	}
+	return legacyDBList(opts)
+}
+
+func newDBList(opts dbListOptions) error {
+	c, err := distribution.NewClient(opts.DB.ToClientConfig())
+	if err != nil {
+		return fmt.Errorf("unable to create distribution client: %w", err)
+	}
+
+	latest, err := c.Latest()
+	if err != nil {
+		return fmt.Errorf("unable to get database listing: %w", err)
+	}
+
+	return presentNewDBList(opts.Output, opts.DB.UpdateURL, os.Stdout, latest)
+}
+
+func presentNewDBList(format string, u string, writer io.Writer, latest *distribution.LatestDocument) error {
+	if latest == nil {
+		return fmt.Errorf("no database listing found")
+	}
+
+	parsedURL, err := url.Parse(u)
+	if err != nil {
+		return fmt.Errorf("failed to parse base URL: %w", err)
+	}
+
+	parsedURL.Path = path.Join(path.Dir(parsedURL.Path), latest.Path)
+
+	switch format {
+	case textOutputFormat:
+		fmt.Fprintf(writer, "Status:   %s\n", latest.Status)
+		fmt.Fprintf(writer, "Schema:   %s\n", latest.SchemaVersion.String())
+		fmt.Fprintf(writer, "Built:    %s\n", latest.Built.String())
+		fmt.Fprintf(writer, "Listing:  %s\n", u)
+		fmt.Fprintf(writer, "DB URL:   %s\n", parsedURL.String())
+		fmt.Fprintf(writer, "Checksum: %s\n", latest.Checksum)
+	case jsonOutputFormat, "raw":
+		enc := json.NewEncoder(writer)
+		enc.SetEscapeHTML(false)
+		enc.SetIndent("", " ")
+		if err := enc.Encode(&latest); err != nil {
+			return fmt.Errorf("failed to db listing information: %+v", err)
+		}
+	default:
+		return fmt.Errorf("unsupported output format: %s", format)
+	}
+	return nil
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// all legacy processing below ////////////////////////////////////////////////////////////////////////////////////////
+
+func legacyDBList(opts dbListOptions) error {
+	dbCurator, err := legacyDistribution.NewCurator(opts.DB.ToLegacyCuratorConfig())
 	if err != nil {
 		return err
 	}
@@ -58,7 +119,7 @@ func runDBList(opts *dbListOptions) error {
 	}
 
 	switch opts.Output {
-	case "text":
+	case textOutputFormat:
 		// summarize each listing entry for the current DB schema
 		for _, l := range available {
 			fmt.Printf("Built:    %s\n", l.Built)
@@ -67,7 +128,7 @@ func runDBList(opts *dbListOptions) error {
 		}
 
 		fmt.Printf("%d databases available for schema %d\n", len(available), supportedSchema)
-	case "json":
+	case jsonOutputFormat:
 		// show entries for the current schema
 		enc := json.NewEncoder(os.Stdout)
 		enc.SetEscapeHTML(false)
diff --git a/cmd/grype/cli/commands/db_providers.go b/cmd/grype/cli/commands/db_providers.go
index 92410502..c59bf6fe 100644
--- a/cmd/grype/cli/commands/db_providers.go
+++ b/cmd/grype/cli/commands/db_providers.go
@@ -7,30 +7,22 @@ import (
 	"os"
 	"path"
 	"strings"
+	"time"
 
 	"github.com/olekukonko/tablewriter"
 	"github.com/spf13/cobra"
 
 	"github.com/anchore/clio"
-	"github.com/anchore/grype/grype/db/legacy/distribution"
+	legacyDistribution "github.com/anchore/grype/grype/db/legacy/distribution"
+	v6 "github.com/anchore/grype/grype/db/v6"
+	"github.com/anchore/grype/grype/db/v6/distribution"
+	"github.com/anchore/grype/grype/db/v6/installation"
 	"github.com/anchore/grype/internal/bus"
 )
 
-const metadataFileName = "provider-metadata.json"
-const jsonOutputFormat = "json"
-const tableOutputFormat = "table"
-
-type dbProviderMetadata struct {
-	Name              string `json:"name"`
-	LastSuccessfulRun string `json:"lastSuccessfulRun"`
-}
-
-type dbProviders struct {
-	Providers []dbProviderMetadata `json:"providers"`
-}
-
 type dbProvidersOptions struct {
-	Output string `yaml:"output" json:"output"`
+	Output    string `yaml:"output" json:"output"`
+	DBOptions `yaml:",inline" mapstructure:",squash"`
 }
 
 var _ clio.FlagAdder = (*dbProvidersOptions)(nil)
@@ -41,7 +33,8 @@ func (d *dbProvidersOptions) AddFlags(flags clio.FlagSet) {
 
 func DBProviders(app clio.Application) *cobra.Command {
 	opts := &dbProvidersOptions{
-		Output: tableOutputFormat,
+		Output:    tableOutputFormat,
+		DBOptions: *dbOptionsDefault(app.ID()),
 	}
 
 	return app.SetupCommand(&cobra.Command{
@@ -55,11 +48,126 @@ func DBProviders(app clio.Application) *cobra.Command {
 }
 
 func runDBProviders(opts *dbProvidersOptions, app clio.Application) error {
-	metadataFileLocation, err := getMetadataFileLocation(app)
+	if opts.Experimental.DBv6 {
+		return newDBProviders(opts)
+	}
+	return legacyDBProviders(opts, app)
+}
+
+func newDBProviders(opts *dbProvidersOptions) error {
+	client, err := distribution.NewClient(opts.DB.ToClientConfig())
+	if err != nil {
+		return fmt.Errorf("unable to create distribution client: %w", err)
+	}
+	c, err := installation.NewCurator(opts.DB.ToCuratorConfig(), client)
+	if err != nil {
+		return fmt.Errorf("unable to create curator: %w", err)
+	}
+
+	reader, err := c.Reader()
+	if err != nil {
+		return fmt.Errorf("unable to get providers: %w", err)
+	}
+
+	providerModels, err := reader.AllProviders()
+	if err != nil {
+		return fmt.Errorf("unable to get providers: %w", err)
+	}
+
+	sb := &strings.Builder{}
+
+	switch opts.Output {
+	case tableOutputFormat, textOutputFormat:
+		displayDBProvidersTable(toProviders(providerModels), sb)
+	case jsonOutputFormat:
+		err = displayDBProvidersJSON(toProviders(providerModels), sb)
+		if err != nil {
+			return err
+		}
+	default:
+		return fmt.Errorf("unsupported output format: %s", opts.Output)
+	}
+	bus.Report(sb.String())
+
+	return nil
+}
+
+type provider struct {
+	Name         string     `json:"name"`
+	Version      string     `json:"version"`
+	Processor    string     `json:"processor"`
+	DateCaptured *time.Time `json:"dateCaptured"`
+	InputDigest  string     `json:"inputDigest"`
+}
+
+func toProviders(providers []v6.Provider) []provider {
+	var res []provider
+	for _, p := range providers {
+		res = append(res, provider{
+			Name:         p.ID,
+			Version:      p.Version,
+			Processor:    p.Processor,
+			DateCaptured: p.DateCaptured,
+			InputDigest:  p.InputDigest,
+		})
+	}
+	return res
+}
+
+func displayDBProvidersTable(providers []provider, output io.Writer) {
+	rows := [][]string{}
+	for _, provider := range providers {
+		rows = append(rows, []string{provider.Name, provider.Version, provider.Processor, provider.DateCaptured.String(), provider.InputDigest})
+	}
+
+	table := tablewriter.NewWriter(output)
+	table.SetHeader([]string{"Name", "Version", "Processor", "Date Captured", "Input Digest"})
+
+	table.SetHeaderLine(false)
+	table.SetBorder(false)
+	table.SetAutoWrapText(false)
+	table.SetAutoFormatHeaders(true)
+	table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
+	table.SetAlignment(tablewriter.ALIGN_LEFT)
+	table.SetCenterSeparator("")
+	table.SetColumnSeparator("")
+	table.SetRowSeparator("")
+	table.SetTablePadding("  ")
+	table.SetNoWhiteSpace(true)
+
+	table.AppendBulk(rows)
+	table.Render()
+}
+
+func displayDBProvidersJSON(providers []provider, output io.Writer) error {
+	encoder := json.NewEncoder(output)
+	encoder.SetEscapeHTML(false)
+	encoder.SetIndent("", " ")
+	err := encoder.Encode(providers)
+	if err != nil {
+		return fmt.Errorf("cannot display json: %w", err)
+	}
+	return nil
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// all legacy processing below ////////////////////////////////////////////////////////////////////////////////////////
+
+type legacyProviderMetadata struct {
+	Name              string `json:"name"`
+	LastSuccessfulRun string `json:"lastSuccessfulRun"`
+}
+
+type dbProviders struct {
+	Providers []legacyProviderMetadata `json:"providers"`
+}
+
+func legacyDBProviders(opts *dbProvidersOptions, app clio.Application) error {
+	metadataFileLocation, err := getLegacyMetadataFileLocation(app)
 	if err != nil {
 		return nil
 	}
-	providers, err := getDBProviders(*metadataFileLocation)
+	providers, err := getLegacyProviders(*metadataFileLocation)
 	if err != nil {
 		return err
 	}
@@ -67,10 +175,10 @@ func runDBProviders(opts *dbProvidersOptions, app clio.Application) error {
 	sb := &strings.Builder{}
 
 	switch opts.Output {
-	case tableOutputFormat:
-		displayDBProvidersTable(providers.Providers, sb)
+	case tableOutputFormat, textOutputFormat:
+		displayLegacyProvidersTable(providers.Providers, sb)
 	case jsonOutputFormat:
-		err = displayDBProvidersJSON(providers, sb)
+		err = displayLegacyProvidersJSON(providers, sb)
 		if err != nil {
 			return err
 		}
@@ -82,8 +190,8 @@ func runDBProviders(opts *dbProvidersOptions, app clio.Application) error {
 	return nil
 }
 
-func getMetadataFileLocation(app clio.Application) (*string, error) {
-	dbCurator, err := distribution.NewCurator(dbOptionsDefault(app.ID()).DB.ToLegacyCuratorConfig())
+func getLegacyMetadataFileLocation(app clio.Application) (*string, error) {
+	dbCurator, err := legacyDistribution.NewCurator(dbOptionsDefault(app.ID()).DB.ToLegacyCuratorConfig())
 	if err != nil {
 		return nil, err
 	}
@@ -93,8 +201,8 @@ func getMetadataFileLocation(app clio.Application) (*string, error) {
 	return &location, nil
 }
 
-func getDBProviders(metadataFileLocation string) (*dbProviders, error) {
-	metadataFile := path.Join(metadataFileLocation, metadataFileName)
+func getLegacyProviders(metadataFileLocation string) (*dbProviders, error) {
+	metadataFile := path.Join(metadataFileLocation, "provider-metadata.json")
 
 	file, err := os.Open(metadataFile)
 	if err != nil {
@@ -118,7 +226,7 @@ func getDBProviders(metadataFileLocation string) (*dbProviders, error) {
 	return &providers, nil
 }
 
-func displayDBProvidersTable(providers []dbProviderMetadata, output io.Writer) {
+func displayLegacyProvidersTable(providers []legacyProviderMetadata, output io.Writer) {
 	rows := [][]string{}
 	for _, provider := range providers {
 		rows = append(rows, []string{provider.Name, provider.LastSuccessfulRun})
@@ -143,7 +251,7 @@ func displayDBProvidersTable(providers []dbProviderMetadata, output io.Writer) {
 	table.Render()
 }
 
-func displayDBProvidersJSON(providers *dbProviders, output io.Writer) error {
+func displayLegacyProvidersJSON(providers *dbProviders, output io.Writer) error {
 	encoder := json.NewEncoder(output)
 	encoder.SetEscapeHTML(false)
 	encoder.SetIndent("", " ")
diff --git a/cmd/grype/cli/commands/db_search.go b/cmd/grype/cli/commands/db_search.go
index adcbc64a..4bc9a277 100644
--- a/cmd/grype/cli/commands/db_search.go
+++ b/cmd/grype/cli/commands/db_search.go
@@ -11,6 +11,9 @@ import (
 
 	"github.com/anchore/clio"
 	"github.com/anchore/grype/grype"
+	v6 "github.com/anchore/grype/grype/db/v6"
+	"github.com/anchore/grype/grype/db/v6/distribution"
+	"github.com/anchore/grype/grype/db/v6/installation"
 	"github.com/anchore/grype/grype/vulnerability"
 	"github.com/anchore/grype/internal/bus"
 	"github.com/anchore/grype/internal/log"
@@ -29,7 +32,7 @@ func (c *dbQueryOptions) AddFlags(flags clio.FlagSet) {
 
 func DBSearch(app clio.Application) *cobra.Command {
 	opts := &dbQueryOptions{
-		Output:    "table",
+		Output:    tableOutputFormat,
 		DBOptions: *dbOptionsDefault(app.ID()),
 	}
 
@@ -39,12 +42,53 @@ func DBSearch(app clio.Application) *cobra.Command {
 		Args:  cobra.ExactArgs(1),
 		RunE: func(_ *cobra.Command, args []string) (err error) {
 			id := args[0]
-			return runDBSearch(opts, id)
+			return runDBSearch(*opts, id)
 		},
 	}, opts)
 }
 
-func runDBSearch(opts *dbQueryOptions, vulnerabilityID string) error {
+func runDBSearch(opts dbQueryOptions, vulnerabilityID string) error {
+	if opts.Experimental.DBv6 {
+		return newDBSearch(opts, vulnerabilityID)
+	}
+	return legacyDBSearch(opts, vulnerabilityID)
+}
+
+func newDBSearch(opts dbQueryOptions, vulnerabilityID string) error {
+	client, err := distribution.NewClient(opts.DB.ToClientConfig())
+	if err != nil {
+		return fmt.Errorf("unable to create distribution client: %w", err)
+	}
+
+	c, err := installation.NewCurator(opts.DB.ToCuratorConfig(), client)
+	if err != nil {
+		return fmt.Errorf("unable to create curator: %w", err)
+	}
+
+	reader, err := c.Reader()
+	if err != nil {
+		return fmt.Errorf("unable to get providers: %w", err)
+	}
+
+	vh, err := reader.GetVulnerabilitiesByName(vulnerabilityID, &v6.GetVulnerabilityOptions{
+		Preload: true,
+	})
+	if err != nil {
+		return fmt.Errorf("unable to get vulnerability: %w", err)
+	}
+
+	if len(vh) == 0 {
+		return fmt.Errorf("vulnerability doesn't exist in the DB: %s", vulnerabilityID)
+	}
+
+	// TODO: we need to implement the functions that inflate models to the grype vulnerability.Vulnerability struct
+	panic("not implemented")
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// all legacy processing below ////////////////////////////////////////////////////////////////////////////////////////
+
+func legacyDBSearch(opts dbQueryOptions, vulnerabilityID string) error {
 	log.Debug("loading DB")
 	str, status, dbCloser, err := grype.LoadVulnerabilityDB(opts.DB.ToLegacyCuratorConfig(), opts.DB.AutoUpdate)
 	err = validateDBLoad(err, status)
@@ -60,24 +104,24 @@ func runDBSearch(opts *dbQueryOptions, vulnerabilityID string) error {
 		return err
 	}
 
-	sb := &strings.Builder{}
 	if len(vulnerabilities) == 0 {
 		return fmt.Errorf("vulnerability doesn't exist in the DB: %s", vulnerabilityID)
 	}
 
-	err = present(opts.Output, vulnerabilities, sb)
+	sb := &strings.Builder{}
+	err = presentLegacy(opts.Output, vulnerabilities, sb)
 	bus.Report(sb.String())
 
 	return err
 }
 
-func present(outputFormat string, vulnerabilities []vulnerability.Vulnerability, output io.Writer) error {
+func presentLegacy(outputFormat string, vulnerabilities []vulnerability.Vulnerability, output io.Writer) error {
 	if vulnerabilities == nil {
 		return nil
 	}
 
 	switch outputFormat {
-	case "table":
+	case tableOutputFormat:
 		rows := [][]string{}
 		for _, v := range vulnerabilities {
 			rows = append(rows, []string{v.ID, v.PackageName, v.Namespace, v.Constraint.String()})
@@ -102,7 +146,7 @@ func present(outputFormat string, vulnerabilities []vulnerability.Vulnerability,
 
 		table.AppendBulk(rows)
 		table.Render()
-	case "json":
+	case jsonOutputFormat:
 		enc := json.NewEncoder(output)
 		enc.SetEscapeHTML(false)
 		enc.SetIndent("", " ")
diff --git a/cmd/grype/cli/commands/db_status.go b/cmd/grype/cli/commands/db_status.go
index 04834b27..6a3585f0 100644
--- a/cmd/grype/cli/commands/db_status.go
+++ b/cmd/grype/cli/commands/db_status.go
@@ -1,17 +1,36 @@
 package commands
 
 import (
+	"encoding/json"
 	"fmt"
+	"io"
+	"os"
 
 	"github.com/spf13/cobra"
 
 	"github.com/anchore/clio"
-	"github.com/anchore/grype/cmd/grype/cli/options"
-	"github.com/anchore/grype/grype/db/legacy/distribution"
+	legacyDistribution "github.com/anchore/grype/grype/db/legacy/distribution"
+	v6 "github.com/anchore/grype/grype/db/v6"
+	"github.com/anchore/grype/grype/db/v6/distribution"
+	"github.com/anchore/grype/grype/db/v6/installation"
 )
 
+type dbStatusOptions struct {
+	Output    string `yaml:"output" json:"output" mapstructure:"output"`
+	DBOptions `yaml:",inline" mapstructure:",squash"`
+}
+
+var _ clio.FlagAdder = (*dbStatusOptions)(nil)
+
+func (d *dbStatusOptions) AddFlags(flags clio.FlagSet) {
+	flags.StringVarP(&d.Output, "output", "o", "format to display results (available=[text, json])")
+}
+
 func DBStatus(app clio.Application) *cobra.Command {
-	opts := dbOptionsDefault(app.ID())
+	opts := &dbStatusOptions{
+		Output:    textOutputFormat,
+		DBOptions: *dbOptionsDefault(app.ID()),
+	}
 
 	return app.SetupCommand(&cobra.Command{
 		Use:     "status",
@@ -19,13 +38,69 @@ func DBStatus(app clio.Application) *cobra.Command {
 		Args:    cobra.ExactArgs(0),
 		PreRunE: disableUI(app),
 		RunE: func(_ *cobra.Command, _ []string) error {
-			return runDBStatus(opts.DB)
+			return runDBStatus(*opts)
 		},
 	}, opts)
 }
 
-func runDBStatus(opts options.Database) error {
-	dbCurator, err := distribution.NewCurator(opts.ToLegacyCuratorConfig())
+func runDBStatus(opts dbStatusOptions) error {
+	if opts.Experimental.DBv6 {
+		return newDBStatus(opts)
+	}
+	return legacyDBStatus(opts)
+}
+
+func newDBStatus(opts dbStatusOptions) error {
+	client, err := distribution.NewClient(opts.DB.ToClientConfig())
+	if err != nil {
+		return fmt.Errorf("unable to create distribution client: %w", err)
+	}
+	c, err := installation.NewCurator(opts.DB.ToCuratorConfig(), client)
+	if err != nil {
+		return fmt.Errorf("unable to create distribution client: %w", err)
+	}
+
+	status := c.Status()
+
+	if err := presentDBStatus(opts.Output, os.Stdout, status); err != nil {
+		return fmt.Errorf("failed to present db status information: %+v", err)
+	}
+
+	return status.Err
+}
+
+func presentDBStatus(format string, writer io.Writer, status v6.Status) error {
+	statusStr := "valid"
+	if status.Err != nil {
+		statusStr = "invalid"
+	}
+
+	switch format {
+	case textOutputFormat:
+		fmt.Fprintln(writer, "Path:     ", status.Path)
+		fmt.Fprintln(writer, "Schema:   ", status.SchemaVersion)
+		fmt.Fprintln(writer, "Built:    ", status.Built.String())
+		fmt.Fprintln(writer, "Checksum: ", status.Checksum)
+		fmt.Fprintln(writer, "Status:   ", statusStr)
+	case jsonOutputFormat:
+		enc := json.NewEncoder(writer)
+		enc.SetEscapeHTML(false)
+		enc.SetIndent("", " ")
+		if err := enc.Encode(&status); err != nil {
+			return fmt.Errorf("failed to db status information: %+v", err)
+		}
+	default:
+		return fmt.Errorf("unsupported output format: %s", format)
+	}
+
+	return nil
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// all legacy processing below ////////////////////////////////////////////////////////////////////////////////////////
+
+func legacyDBStatus(opts dbStatusOptions) error {
+	dbCurator, err := legacyDistribution.NewCurator(opts.DB.ToLegacyCuratorConfig())
 	if err != nil {
 		return err
 	}
@@ -37,11 +112,23 @@ func runDBStatus(opts options.Database) error {
 		statusStr = "invalid"
 	}
 
-	fmt.Println("Location: ", status.Location)
-	fmt.Println("Built:    ", status.Built.String())
-	fmt.Println("Schema:   ", status.SchemaVersion)
-	fmt.Println("Checksum: ", status.Checksum)
-	fmt.Println("Status:   ", statusStr)
+	switch opts.Output {
+	case textOutputFormat:
+		fmt.Println("Location: ", status.Location)
+		fmt.Println("Built:    ", status.Built.String())
+		fmt.Println("Schema:   ", status.SchemaVersion)
+		fmt.Println("Checksum: ", status.Checksum)
+		fmt.Println("Status:   ", statusStr)
+	case jsonOutputFormat:
+		enc := json.NewEncoder(os.Stdout)
+		enc.SetEscapeHTML(false)
+		enc.SetIndent("", " ")
+		if err := enc.Encode(&status); err != nil {
+			return fmt.Errorf("failed to db status information: %+v", err)
+		}
+	default:
+		return fmt.Errorf("unsupported output format: %s", opts.Output)
+	}
 
 	return status.Err
 }
diff --git a/cmd/grype/cli/commands/db_update.go b/cmd/grype/cli/commands/db_update.go
index d9515b94..aa153aea 100644
--- a/cmd/grype/cli/commands/db_update.go
+++ b/cmd/grype/cli/commands/db_update.go
@@ -7,7 +7,9 @@ import (
 
 	"github.com/anchore/clio"
 	"github.com/anchore/grype/cmd/grype/cli/options"
-	"github.com/anchore/grype/grype/db/legacy/distribution"
+	legacyDistribution "github.com/anchore/grype/grype/db/legacy/distribution"
+	"github.com/anchore/grype/grype/db/v6/distribution"
+	"github.com/anchore/grype/grype/db/v6/installation"
 	"github.com/anchore/grype/internal/bus"
 	"github.com/anchore/grype/internal/log"
 )
@@ -25,13 +27,50 @@ func DBUpdate(app clio.Application) *cobra.Command {
 			return nil
 		},
 		RunE: func(_ *cobra.Command, _ []string) error {
-			return runDBUpdate(opts.DB)
+			return runDBUpdate(opts.DB, opts.Experimental.DBv6)
 		},
 	}, opts)
 }
 
-func runDBUpdate(opts options.Database) error {
-	dbCurator, err := distribution.NewCurator(opts.ToLegacyCuratorConfig())
+func runDBUpdate(opts options.Database, expUseV6 bool) error {
+	if expUseV6 {
+		return newDBUpdate(opts)
+	}
+	return legacyDBUpdate(opts)
+}
+
+func newDBUpdate(opts options.Database) error {
+	client, err := distribution.NewClient(opts.ToClientConfig())
+	if err != nil {
+		return fmt.Errorf("unable to create distribution client: %w", err)
+	}
+	c, err := installation.NewCurator(opts.ToCuratorConfig(), client)
+	if err != nil {
+		return fmt.Errorf("unable to create curator: %w", err)
+	}
+
+	updated, err := c.Update()
+	if err != nil {
+		return fmt.Errorf("unable to update vulnerability database: %w", err)
+	}
+
+	result := "No vulnerability database update available\n"
+	if updated {
+		result = "Vulnerability database updated to latest version!\n"
+	}
+
+	log.Debugf("completed db update check with result: %s", result)
+
+	bus.Report(result)
+
+	return nil
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// all legacy processing below ////////////////////////////////////////////////////////////////////////////////////////
+
+func legacyDBUpdate(opts options.Database) error {
+	dbCurator, err := legacyDistribution.NewCurator(opts.ToLegacyCuratorConfig())
 	if err != nil {
 		return err
 	}
diff --git a/cmd/grype/cli/options/experimental.go b/cmd/grype/cli/options/experimental.go
new file mode 100644
index 00000000..34498ad7
--- /dev/null
+++ b/cmd/grype/cli/options/experimental.go
@@ -0,0 +1,23 @@
+package options
+
+import (
+	"github.com/anchore/clio"
+)
+
+// Experimental options are opt-in features that are...
+// ...not stable
+// ...not yet fully supported
+// ...not necessarily tested
+// ...not ready for production use
+// these may go away at any moment, do not depend on them
+type Experimental struct {
+	DBv6 bool `yaml:"dbv6" json:"dbv6" mapstructure:"dbv6"`
+}
+
+var _ interface {
+	clio.FieldDescriber
+} = (*Experimental)(nil)
+
+func (cfg *Experimental) DescribeFields(descriptions clio.FieldDescriptionSet) {
+	descriptions.Add(&cfg.DBv6, `use the v6 database schema`)
+}
diff --git a/cmd/grype/cli/options/grype.go b/cmd/grype/cli/options/grype.go
index ca822d4b..10fb9ec8 100644
--- a/cmd/grype/cli/options/grype.go
+++ b/cmd/grype/cli/options/grype.go
@@ -36,6 +36,7 @@ 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"`
 }
 
 var _ interface {
diff --git a/grype/db/v6/db_metadata_store.go b/grype/db/v6/db_metadata_store.go
index cbcc9e7a..c5cfa853 100644
--- a/grype/db/v6/db_metadata_store.go
+++ b/grype/db/v6/db_metadata_store.go
@@ -28,7 +28,7 @@ func newDBMetadataStore(db *gorm.DB) *dbMetadataStore {
 }
 
 func (s *dbMetadataStore) GetDBMetadata() (*DBMetadata, error) {
-	log.Trace("fetching DB metadata record")
+	log.Trace("fetching DB metadata")
 
 	var model DBMetadata
 
@@ -37,13 +37,17 @@ func (s *dbMetadataStore) GetDBMetadata() (*DBMetadata, error) {
 }
 
 func (s *dbMetadataStore) SetDBMetadata() error {
-	log.Trace("writing DB metadata record")
+	log.Trace("writing DB metadata")
 
 	if err := s.db.Unscoped().Where("true").Delete(&DBMetadata{}).Error; err != nil {
 		return fmt.Errorf("failed to delete existing DB metadata record: %w", err)
 	}
 
-	ts := time.Now().UTC()
+	// note: it is important to round the time to the second to avoid issues with the database update check.
+	// since we are comparing timestamps that are RFC3339 formatted, it's possible that milliseconds will
+	// be rounded up, causing a slight difference in candidate timestamps vs current DB timestamps.
+	ts := time.Now().UTC().Round(time.Second)
+
 	instance := &DBMetadata{
 		BuildTimestamp: &ts,
 		Model:          ModelVersion,
diff --git a/grype/db/v6/description.go b/grype/db/v6/description.go
index 9b8c57bd..d3ad18f1 100644
--- a/grype/db/v6/description.go
+++ b/grype/db/v6/description.go
@@ -14,8 +14,8 @@ import (
 	"github.com/OneOfOne/xxhash"
 	"github.com/spf13/afero"
 
-	"github.com/anchore/grype/grype/db/internal/schemaver"
 	"github.com/anchore/grype/internal/file"
+	"github.com/anchore/grype/internal/schemaver"
 )
 
 const ChecksumFileName = VulnerabilityDBFileName + ".checksum"
@@ -56,6 +56,16 @@ func (t Time) String() string {
 	return t.Time.UTC().Round(time.Second).Format(time.RFC3339)
 }
 
+func DescriptionFromMetadata(m *DBMetadata) *Description {
+	if m == nil {
+		return nil
+	}
+	return &Description{
+		SchemaVersion: schemaver.New(m.Model, m.Revision, m.Addition),
+		Built:         Time{Time: *m.BuildTimestamp},
+	}
+}
+
 func ReadDescription(dbFilePath string) (*Description, error) {
 	// check if exists
 	if _, err := os.Stat(dbFilePath); err != nil {
diff --git a/grype/db/v6/distribution/client.go b/grype/db/v6/distribution/client.go
index 6f83b9d1..87c7960d 100644
--- a/grype/db/v6/distribution/client.go
+++ b/grype/db/v6/distribution/client.go
@@ -37,6 +37,7 @@ type Config struct {
 }
 
 type Client interface {
+	Latest() (*LatestDocument, error)
 	IsUpdateAvailable(current *v6.Description) (*Archive, error)
 	Download(archive Archive, dest string, downloadProgress *progress.Manual) (string, error)
 }
@@ -59,12 +60,12 @@ func DefaultConfig() Config {
 
 func NewClient(cfg Config) (Client, error) {
 	fs := afero.NewOsFs()
-	latestClient, err := defaultHTTPClient(fs, cfg.CACert, withClientTimeout(cfg.CheckTimeout))
+	latestClient, err := defaultHTTPClient(fs, cfg.CACert, withClientTimeout(cfg.CheckTimeout), withUserAgent(cfg.ID))
 	if err != nil {
 		return client{}, err
 	}
 
-	dbClient, err := defaultHTTPClient(fs, cfg.CACert, withClientTimeout(cfg.UpdateTimeout))
+	dbClient, err := defaultHTTPClient(fs, cfg.CACert, withClientTimeout(cfg.UpdateTimeout), withUserAgent(cfg.ID))
 	if err != nil {
 		return client{}, err
 	}
@@ -82,7 +83,7 @@ func NewClient(cfg Config) (Client, error) {
 func (c client) IsUpdateAvailable(current *v6.Description) (*Archive, error) {
 	log.Debugf("checking for available database updates")
 
-	latestDoc, err := c.latestFromURL()
+	latestDoc, err := c.Latest()
 	if err != nil {
 		if c.config.RequireUpdateCheck {
 			return nil, fmt.Errorf("check for vulnerability database update failed: %+v", err)
@@ -160,8 +161,8 @@ func (c client) Download(archive Archive, dest string, downloadProgress *progres
 	return tempDir, nil
 }
 
-// latestFromURL loads a LatestDocument from a URL.
-func (c client) latestFromURL() (*LatestDocument, error) {
+// Latest loads a LatestDocument from the configured URL.
+func (c client) Latest() (*LatestDocument, error) {
 	resp, err := c.latestHTTPClient.Get(c.config.LatestURL)
 	if err != nil {
 		return nil, fmt.Errorf("unable to fetch latest.json: %w", err)
@@ -181,6 +182,12 @@ func withClientTimeout(timeout time.Duration) func(*http.Client) {
 	}
 }
 
+func withUserAgent(id clio.Identification) func(*http.Client) {
+	return func(c *http.Client) {
+		*(c) = *newHTTPClientWithDefaultUserAgent(c.Transport, fmt.Sprintf("%s %s", id.Name, id.Version))
+	}
+}
+
 func defaultHTTPClient(fs afero.Fs, caCertPath string, postProcessor ...func(*http.Client)) (*http.Client, error) {
 	httpClient := cleanhttp.DefaultClient()
 	httpClient.Timeout = 30 * time.Second
@@ -238,7 +245,8 @@ func isSupersededBy(current *v6.Description, candidate v6.Description) bool {
 	}
 
 	if candidate.Built.After(current.Built.Time) {
-		log.WithFields("existing", current.Built.String(), "candidate", candidate.Built.String()).Debug("existing database is older than candidate update, using update...")
+		d := candidate.Built.Sub(current.Built.Time).String()
+		log.WithFields("existing", current.Built.String(), "candidate", candidate.Built.String(), "delta", d).Debug("existing database is older than candidate update, using update...")
 		// the listing is newer than the existing db, use it!
 		return true
 	}
@@ -246,3 +254,27 @@ func isSupersededBy(current *v6.Description, candidate v6.Description) bool {
 	log.Debugf("existing database is already up to date")
 	return false
 }
+
+func newHTTPClientWithDefaultUserAgent(baseTransport http.RoundTripper, userAgent string) *http.Client {
+	return &http.Client{
+		Transport: roundTripperWithUserAgent{
+			transport: baseTransport,
+			userAgent: userAgent,
+		},
+	}
+}
+
+type roundTripperWithUserAgent struct {
+	transport http.RoundTripper
+	userAgent string
+}
+
+func (r roundTripperWithUserAgent) RoundTrip(req *http.Request) (*http.Response, error) {
+	clonedReq := req.Clone(req.Context())
+
+	if clonedReq.Header.Get("User-Agent") == "" {
+		clonedReq.Header.Set("User-Agent", r.userAgent)
+	}
+
+	return r.transport.RoundTrip(clonedReq)
+}
diff --git a/grype/db/v6/installation/curator.go b/grype/db/v6/installation/curator.go
index f0b43fa9..e76de040 100644
--- a/grype/db/v6/installation/curator.go
+++ b/grype/db/v6/installation/curator.go
@@ -83,15 +83,19 @@ func (c curator) Reader() (db.Reader, error) {
 		return nil, err
 	}
 
-	_, err = c.validate(c.config.ValidateChecksum)
+	m, err := s.GetDBMetadata()
+	if err != nil {
+		return nil, fmt.Errorf("unable to get vulnerability store metadata: %w", err)
+	}
+
+	_, err = c.validate(db.DescriptionFromMetadata(m), c.config.ValidateChecksum)
 
 	return s, err
 }
 
 func (c curator) Status() db.Status {
-	dbDir := c.config.DBDirectoryPath()
-
-	d, err := db.ReadDescription(c.config.DBFilePath())
+	dbFile := c.config.DBFilePath()
+	d, err := db.ReadDescription(dbFile)
 	if err != nil {
 		return db.Status{
 			Err: err,
@@ -99,17 +103,17 @@ func (c curator) Status() db.Status {
 	}
 	if d == nil {
 		return db.Status{
-			Err: fmt.Errorf("database not found at %q", dbDir),
+			Err: fmt.Errorf("database not found at %q", dbFile),
 		}
 	}
 
 	// override the checksum validation setting to ensure the checksum is always validated
-	digest, validateErr := c.validate(true)
+	digest, validateErr := c.validate(d, true)
 
 	return db.Status{
 		Built:         db.Time{Time: d.Built.Time},
 		SchemaVersion: d.SchemaVersion.String(),
-		Location:      dbDir,
+		Path:          dbFile,
 		Checksum:      digest,
 		Err:           validateErr,
 	}
@@ -122,7 +126,21 @@ func (c curator) Delete() error {
 
 // Update the existing DB, returning an indication if any action was taken.
 func (c curator) Update() (bool, error) {
-	if !c.isUpdateCheckAllowed() {
+	current, err := db.ReadDescription(c.config.DBFilePath())
+	if err != nil {
+		log.WithFields("error", err).Warn("unable to read current database metadata (continuing with update)")
+		// downstream any non-existent DB should always be replaced with any best-candidate found
+		current = nil
+	} else {
+		_, err := c.validate(current, true)
+		if err != nil {
+			// even if we are not allowed to check for an update, we should still attempt to update the DB if it is invalid
+			log.WithFields("error", err).Warn("current database is invalid")
+			current = nil
+		}
+	}
+
+	if current != nil && !c.isUpdateCheckAllowed() {
 		// we should not notify the user of an update check if the current configuration and state
 		// indicates we're should be in a low-pass filter mode (and the check frequency is too high).
 		// this should appear to the user as if we never attempted to check for an update at all.
@@ -132,13 +150,6 @@ func (c curator) Update() (bool, error) {
 	mon := newMonitor()
 	defer mon.SetCompleted()
 
-	current, err := db.ReadDescription(c.config.DBDirectoryPath())
-	if err != nil {
-		log.WithFields("error", err).Warn("unable to read current database metadata (continuing with update)")
-		// downstream any non-existent DB should always be replaced with any best-candidate found
-		current = nil
-	}
-
 	mon.Set("checking for update")
 	update, checkErr := c.client.IsUpdateAvailable(current)
 	if checkErr != nil {
@@ -261,8 +272,8 @@ func (c curator) setLastSuccessfulUpdateCheck() {
 }
 
 // validate checks the current database to ensure file integrity and if it can be used by this version of the application.
-func (c curator) validate(validateChecksum bool) (string, error) {
-	metadata, digest, err := c.validateIntegrity(c.config.DBFilePath(), validateChecksum)
+func (c curator) validate(current *db.Description, validateChecksum bool) (string, error) {
+	metadata, digest, err := c.validateIntegrity(current, c.config.DBFilePath(), validateChecksum)
 	if err != nil {
 		return "", err
 	}
@@ -312,7 +323,14 @@ func (c curator) activate(dbDirPath string, mon monitor) error {
 	}
 
 	checksumsFilePath := filepath.Join(dbDirPath, db.ChecksumFileName)
-	if err := afero.WriteFile(c.fs, checksumsFilePath, []byte(digest), 0644); err != nil {
+	fh, err := c.fs.OpenFile(checksumsFilePath, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0644)
+	if err != nil {
+		return fmt.Errorf("failed to open checksums file: %w", err)
+	}
+	if err := db.WriteChecksums(fh, digest); err != nil {
+		return fmt.Errorf("failed to write checksums file: %w", err)
+	}
+	if err := fh.Close(); err != nil {
 		return err
 	}
 
@@ -332,12 +350,8 @@ func (c curator) activate(dbDirPath string, mon monitor) error {
 	return os.Rename(dbDirPath, dbDir)
 }
 
-func (c curator) validateIntegrity(dbFilePath string, validateChecksum bool) (*db.Description, string, error) {
+func (c curator) validateIntegrity(metadata *db.Description, dbFilePath string, validateChecksum bool) (*db.Description, string, error) {
 	// check that the disk checksum still matches the db payload
-	metadata, err := db.ReadDescription(dbFilePath)
-	if err != nil {
-		return nil, "", fmt.Errorf("failed to parse database metadata (%s): %w", dbFilePath, err)
-	}
 	if metadata == nil {
 		return nil, "", fmt.Errorf("database not found: %s", dbFilePath)
 	}
@@ -347,8 +361,16 @@ func (c curator) validateIntegrity(dbFilePath string, validateChecksum bool) (*d
 		return nil, "", fmt.Errorf("unsupported database version: have=%d want=%d", gotModel, db.ModelVersion)
 	}
 
+	if _, err := c.fs.Stat(dbFilePath); err != nil {
+		if os.IsNotExist(err) {
+			return nil, "", fmt.Errorf("database does not exist: %s", dbFilePath)
+		}
+		return nil, "", fmt.Errorf("failed to access database file: %w", err)
+	}
+
 	var digest string
 	if validateChecksum {
+		var err error
 		digest, err = db.ReadDBChecksum(filepath.Dir(dbFilePath))
 		if err != nil {
 			return nil, "", err
diff --git a/grype/db/v6/status.go b/grype/db/v6/status.go
index 3d275bf5..4d4f04f7 100644
--- a/grype/db/v6/status.go
+++ b/grype/db/v6/status.go
@@ -1,9 +1,32 @@
 package v6
 
+import "encoding/json"
+
 type Status struct {
-	Built         Time   `json:"built"`
 	SchemaVersion string `json:"schemaVersion"`
-	Location      string `json:"location"`
+	Built         Time   `json:"built"`
+	Path          string `json:"path"`
 	Checksum      string `json:"checksum"`
 	Err           error  `json:"error"`
 }
+
+func (s Status) MarshalJSON() ([]byte, error) {
+	errStr := ""
+	if s.Err != nil {
+		errStr = s.Err.Error()
+	}
+
+	return json.Marshal(&struct {
+		SchemaVersion string `json:"schemaVersion"`
+		Built         Time   `json:"built"`
+		Path          string `json:"path"`
+		Checksum      string `json:"checksum"`
+		Err           string `json:"error"`
+	}{
+		SchemaVersion: s.SchemaVersion,
+		Built:         s.Built,
+		Path:          s.Path,
+		Checksum:      s.Checksum,
+		Err:           errStr,
+	})
+}
diff --git a/grype/db/v6/store.go b/grype/db/v6/store.go
index c0ab5523..d96d94a8 100644
--- a/grype/db/v6/store.go
+++ b/grype/db/v6/store.go
@@ -2,8 +2,6 @@ package v6
 
 import (
 	"fmt"
-	"os"
-	"path/filepath"
 
 	"gorm.io/gorm"
 
@@ -63,15 +61,5 @@ func (s *store) Close() error {
 		return fmt.Errorf("failed to vacuum: %w", err)
 	}
 
-	digest, err := CalculateDBDigest(filepath.Join(s.config.DBDirPath, VulnerabilityDBFileName))
-	if err != nil {
-		return fmt.Errorf("failed to create description from dir: %w", err)
-	}
-
-	fh, err := os.OpenFile(filepath.Join(s.config.DBDirPath, ChecksumFileName), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
-	if err != nil {
-		return fmt.Errorf("failed to open description file: %w", err)
-	}
-
-	return WriteChecksums(fh, digest)
+	return nil
 }


Test patch:
diff --git a/cmd/grype/cli/commands/db_check_test.go b/cmd/grype/cli/commands/db_check_test.go
new file mode 100644
index 00000000..ff95a386
--- /dev/null
+++ b/cmd/grype/cli/commands/db_check_test.go
@@ -0,0 +1,131 @@
+package commands
+
+import (
+	"bytes"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	db "github.com/anchore/grype/grype/db/v6"
+	"github.com/anchore/grype/grype/db/v6/distribution"
+)
+
+func TestPresentNewDBCheck(t *testing.T) {
+	currentDB := &db.Description{
+		SchemaVersion: "v6.0.0",
+		Built:         db.Time{Time: time.Date(2023, 11, 25, 12, 0, 0, 0, time.UTC)},
+	}
+
+	candidateDB := &distribution.Archive{
+		Description: db.Description{
+			SchemaVersion: "v6.0.1",
+			Built:         db.Time{Time: time.Date(2023, 11, 26, 12, 0, 0, 0, time.UTC)},
+		},
+		Path:     "vulnerability-db_6.0.1_2023-11-26T12:00:00Z_6238463.tar.gz",
+		Checksum: "sha256:1234561234567890345674561234567890345678",
+	}
+	tests := []struct {
+		name            string
+		format          string
+		updateAvailable bool
+		current         *db.Description
+		candidate       *distribution.Archive
+		expectedText    string
+		expectErr       require.ErrorAssertionFunc
+	}{
+		{
+			name:            "text format with update available",
+			format:          textOutputFormat,
+			updateAvailable: true,
+			current:         currentDB,
+			candidate:       candidateDB,
+			expectedText: `
+Installed DB version v6.0.0 was built on 2023-11-25T12:00:00Z
+Updated DB version v6.0.1 was built on 2023-11-26T12:00:00Z
+You can run 'grype db update' to update to the latest db
+`,
+		},
+		{
+			name:            "text format without update available",
+			format:          textOutputFormat,
+			updateAvailable: false,
+			current:         currentDB,
+			candidate:       nil,
+			expectedText: `
+Installed DB version v6.0.0 was built on 2023-11-25T12:00:00Z
+No update available
+`,
+		},
+		{
+			name:            "json format with update available",
+			format:          jsonOutputFormat,
+			updateAvailable: true,
+			current:         currentDB,
+			candidate:       candidateDB,
+			expectedText: `
+{
+ "currentDB": {
+  "schemaVersion": "v6.0.0",
+  "built": "2023-11-25T12:00:00Z"
+ },
+ "candidateDB": {
+  "schemaVersion": "v6.0.1",
+  "built": "2023-11-26T12:00:00Z",
+  "path": "vulnerability-db_6.0.1_2023-11-26T12:00:00Z_6238463.tar.gz",
+  "checksum": "sha256:1234561234567890345674561234567890345678"
+ },
+ "updateAvailable": true
+}
+`,
+		},
+		{
+			name:            "json format without update available",
+			format:          jsonOutputFormat,
+			updateAvailable: false,
+			current:         currentDB,
+			candidate:       nil,
+			expectedText: `
+{
+ "currentDB": {
+  "schemaVersion": "v6.0.0",
+  "built": "2023-11-25T12:00:00Z"
+ },
+ "candidateDB": null,
+ "updateAvailable": false
+}
+`,
+		},
+		{
+			name:      "unsupported format",
+			format:    "xml",
+			expectErr: requireErrorContains("unsupported output format: xml"),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if tt.expectErr == nil {
+				tt.expectErr = require.NoError
+			}
+			buf := &bytes.Buffer{}
+			err := presentNewDBCheck(tt.format, buf, tt.updateAvailable, tt.current, tt.candidate)
+
+			tt.expectErr(t, err)
+			if err != nil {
+				return
+			}
+
+			assert.Equal(t, strings.TrimSpace(tt.expectedText), strings.TrimSpace(buf.String()))
+		})
+	}
+}
+
+func requireErrorContains(expected string) require.ErrorAssertionFunc {
+	return func(t require.TestingT, err error, msgAndArgs ...interface{}) {
+		require.Error(t, err)
+		assert.Contains(t, err.Error(), expected)
+	}
+}
diff --git a/cmd/grype/cli/commands/db_list_test.go b/cmd/grype/cli/commands/db_list_test.go
new file mode 100644
index 00000000..77fa488b
--- /dev/null
+++ b/cmd/grype/cli/commands/db_list_test.go
@@ -0,0 +1,178 @@
+package commands
+
+import (
+	"bytes"
+	"encoding/json"
+	"net/http"
+	"net/http/httptest"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/anchore/clio"
+	db "github.com/anchore/grype/grype/db/v6"
+	"github.com/anchore/grype/grype/db/v6/distribution"
+	"github.com/anchore/grype/internal/schemaver"
+)
+
+func Test_ListingUserAgent(t *testing.T) {
+
+	t.Run("legacy", func(t *testing.T) {
+		listingFile := "/listing.json"
+
+		got := ""
+
+		// setup mock
+		handler := http.NewServeMux()
+		handler.HandleFunc(listingFile, func(w http.ResponseWriter, r *http.Request) {
+			got = r.Header.Get("User-Agent")
+			w.WriteHeader(http.StatusOK)
+			_, _ = w.Write([]byte("1.0.0"))
+		})
+		mockSrv := httptest.NewServer(handler)
+		defer mockSrv.Close()
+
+		dbOptions := *dbOptionsDefault(clio.Identification{
+			Name:    "the-app",
+			Version: "v3.2.1",
+		})
+		dbOptions.DB.RequireUpdateCheck = true
+		dbOptions.DB.UpdateURL = mockSrv.URL + listingFile
+
+		_ = legacyDBList(dbListOptions{
+			Output:    "",
+			DBOptions: dbOptions,
+		})
+
+		if got != "the-app v3.2.1" {
+			t.Errorf("expected User-Agent header to match, got: %v", got)
+		}
+	})
+
+	t.Run("new", func(t *testing.T) {
+		listingFile := "/latest.json"
+
+		got := ""
+
+		// setup mock
+		handler := http.NewServeMux()
+		handler.HandleFunc(listingFile, func(w http.ResponseWriter, r *http.Request) {
+			got = r.Header.Get("User-Agent")
+			w.WriteHeader(http.StatusOK)
+			_ = json.NewEncoder(w).Encode(&distribution.LatestDocument{
+				Status: "active",
+				Archive: distribution.Archive{
+					Description: db.Description{
+						SchemaVersion: schemaver.New(6, 0, 0),
+						Built:         db.Time{Time: time.Now()},
+					},
+					Path:     "vulnerability-db_v6.0.0.tar.gz",
+					Checksum: "sha256:dummychecksum",
+				},
+			})
+		})
+		mockSrv := httptest.NewServer(handler)
+		defer mockSrv.Close()
+
+		dbOptions := *dbOptionsDefault(clio.Identification{
+			Name:    "new-app",
+			Version: "v4.0.0",
+		})
+		dbOptions.DB.RequireUpdateCheck = true
+		dbOptions.DB.UpdateURL = mockSrv.URL + listingFile
+
+		err := newDBList(dbListOptions{
+			Output:    textOutputFormat,
+			DBOptions: dbOptions,
+		})
+		require.NoError(t, err)
+
+		if got != "new-app v4.0.0" {
+			t.Errorf("expected User-Agent header to match, got: %v", got)
+		}
+	})
+
+}
+
+func TestPresentNewDBList(t *testing.T) {
+	baseURL := "http://localhost:8000/latest.json"
+	latestDoc := &distribution.LatestDocument{
+		Status: "active",
+		Archive: distribution.Archive{
+			Description: db.Description{
+				SchemaVersion: schemaver.New(6, 0, 0),
+				Built:         db.Time{Time: time.Date(2024, 11, 27, 14, 43, 17, 0, time.UTC)},
+			},
+			Path:     "vulnerability-db_v6.0.0_2024-11-25T01:31:56Z_1732718597.tar.zst",
+			Checksum: "sha256:16bcb6551c748056f752f299fcdb4fa50fe61589d086be3889e670261ff21ca4",
+		},
+	}
+
+	tests := []struct {
+		name         string
+		format       string
+		latest       *distribution.LatestDocument
+		expectedText string
+		expectedErr  require.ErrorAssertionFunc
+	}{
+		{
+			name:   "valid text format",
+			format: textOutputFormat,
+			latest: latestDoc,
+			expectedText: `Status:   active
+Schema:   6.0.0
+Built:    2024-11-27T14:43:17Z
+Listing:  http://localhost:8000/latest.json
+DB URL:   http://localhost:8000/vulnerability-db_v6.0.0_2024-11-25T01:31:56Z_1732718597.tar.zst
+Checksum: sha256:16bcb6551c748056f752f299fcdb4fa50fe61589d086be3889e670261ff21ca4
+`,
+			expectedErr: require.NoError,
+		},
+		{
+			name:   "valid JSON format",
+			format: jsonOutputFormat,
+			latest: latestDoc,
+			expectedText: `{
+ "status": "active",
+ "schemaVersion": "6.0.0",
+ "built": "2024-11-27T14:43:17Z",
+ "path": "vulnerability-db_v6.0.0_2024-11-25T01:31:56Z_1732718597.tar.zst",
+ "checksum": "sha256:16bcb6551c748056f752f299fcdb4fa50fe61589d086be3889e670261ff21ca4"
+}
+`,
+			expectedErr: require.NoError,
+		},
+		{
+			name:        "nil latest document",
+			format:      textOutputFormat,
+			latest:      nil,
+			expectedErr: requireErrorContains("no database listing found"),
+		},
+		{
+			name:        "unsupported format",
+			format:      "unsupported",
+			latest:      latestDoc,
+			expectedErr: requireErrorContains("unsupported output format"),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			writer := &bytes.Buffer{}
+
+			err := presentNewDBList(tt.format, baseURL, writer, tt.latest)
+			if tt.expectedErr == nil {
+				tt.expectedErr = require.NoError
+			}
+			tt.expectedErr(t, err)
+
+			if err != nil {
+				return
+			}
+
+			require.Equal(t, strings.TrimSpace(tt.expectedText), strings.TrimSpace(writer.String()))
+		})
+	}
+}
diff --git a/cmd/grype/cli/commands/db_providers_test.go b/cmd/grype/cli/commands/db_providers_test.go
index 734ff127..44208696 100644
--- a/cmd/grype/cli/commands/db_providers_test.go
+++ b/cmd/grype/cli/commands/db_providers_test.go
@@ -7,9 +7,84 @@ import (
 	"os"
 	"reflect"
 	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
 )
 
-func TestGetDBProviders(t *testing.T) {
+func TestDisplayDBProvidersTable(t *testing.T) {
+	providers := []provider{
+		{
+			Name:         "provider1",
+			Version:      "1.0.0",
+			Processor:    "vunnel@3.2",
+			DateCaptured: timeRef(time.Date(2024, 11, 25, 14, 30, 0, 0, time.UTC)),
+			InputDigest:  "xxh64:1234567834567",
+		},
+		{
+			Name:         "provider2",
+			Version:      "2.0.0",
+			Processor:    "vunnel@3.2",
+			DateCaptured: timeRef(time.Date(2024, 11, 26, 10, 15, 0, 0, time.UTC)),
+			InputDigest:  "xxh64:9876543212345",
+		},
+	}
+
+	expectedOutput := `NAME       VERSION  PROCESSOR   DATE CAPTURED                  INPUT DIGEST        
+provider1  1.0.0    vunnel@3.2  2024-11-25 14:30:00 +0000 UTC  xxh64:1234567834567  
+provider2  2.0.0    vunnel@3.2  2024-11-26 10:15:00 +0000 UTC  xxh64:9876543212345  
+`
+
+	var output bytes.Buffer
+	displayDBProvidersTable(providers, &output)
+
+	require.Equal(t, expectedOutput, output.String())
+}
+
+func TestDisplayDBProvidersJSON(t *testing.T) {
+	providers := []provider{
+		{
+			Name:         "provider1",
+			Version:      "1.0.0",
+			Processor:    "vunnel@3.2",
+			DateCaptured: timeRef(time.Date(2024, 11, 25, 14, 30, 0, 0, time.UTC)),
+			InputDigest:  "xxh64:1234567834567",
+		},
+		{
+			Name:         "provider2",
+			Version:      "2.0.0",
+			Processor:    "vunnel@3.2",
+			DateCaptured: timeRef(time.Date(2024, 11, 26, 10, 15, 0, 0, time.UTC)),
+			InputDigest:  "xxh64:9876543212345",
+		},
+	}
+
+	expectedJSON := `[
+ {
+  "name": "provider1",
+  "version": "1.0.0",
+  "processor": "vunnel@3.2",
+  "dateCaptured": "2024-11-25T14:30:00Z",
+  "inputDigest": "xxh64:1234567834567"
+ },
+ {
+  "name": "provider2",
+  "version": "2.0.0",
+  "processor": "vunnel@3.2",
+  "dateCaptured": "2024-11-26T10:15:00Z",
+  "inputDigest": "xxh64:9876543212345"
+ }
+]
+`
+
+	var output bytes.Buffer
+	err := displayDBProvidersJSON(providers, &output)
+	require.NoError(t, err)
+
+	require.JSONEq(t, expectedJSON, output.String())
+}
+
+func TestGetLegacyProviders(t *testing.T) {
 
 	tests := []struct {
 		name              string
@@ -21,7 +96,7 @@ func TestGetDBProviders(t *testing.T) {
 			name:         "test provider metadata file",
 			fileLocation: "./test-fixtures",
 			expectedProviders: dbProviders{
-				Providers: []dbProviderMetadata{
+				Providers: []legacyProviderMetadata{
 					{
 						Name:              "provider1",
 						LastSuccessfulRun: "2024-10-16T01:33:16.844201Z",
@@ -44,23 +119,23 @@ func TestGetDBProviders(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			providers, err := getDBProviders(test.fileLocation)
+			providers, err := getLegacyProviders(test.fileLocation)
 			if err != nil {
 				if errors.Is(err, test.expectedError) {
 					return
 				}
-				t.Errorf("getDBProviders() expected list of providers, got error:  %v", err)
+				t.Errorf("getLegacyProviders() expected list of providers, got error:  %v", err)
 				return
 			}
 			if !reflect.DeepEqual(*providers, test.expectedProviders) {
-				t.Error("getDBProviders() providers comparison failed, got error")
+				t.Error("getLegacyProviders() providers comparison failed, got error")
 			}
 		})
 	}
 
 }
 
-func TestDisplayDBProvidersTable(t *testing.T) {
+func TestDisplayLegacyProvidersTable(t *testing.T) {
 	tests := []struct {
 		name           string
 		providers      dbProviders
@@ -69,7 +144,7 @@ func TestDisplayDBProvidersTable(t *testing.T) {
 		{
 			name: "display providers table",
 			providers: dbProviders{
-				Providers: []dbProviderMetadata{
+				Providers: []legacyProviderMetadata{
 					{
 						Name:              "provider1",
 						LastSuccessfulRun: "2024-10-16T01:33:16.844201Z",
@@ -85,7 +160,7 @@ func TestDisplayDBProvidersTable(t *testing.T) {
 		{
 			name: "empty list of providers",
 			providers: dbProviders{
-				Providers: []dbProviderMetadata{},
+				Providers: []legacyProviderMetadata{},
 			},
 			expectedOutput: "NAME  LAST SUCCESSFUL RUN \n",
 		},
@@ -95,16 +170,16 @@ func TestDisplayDBProvidersTable(t *testing.T) {
 		t.Run(test.name, func(t *testing.T) {
 
 			var out bytes.Buffer
-			displayDBProvidersTable(test.providers.Providers, &out)
+			displayLegacyProvidersTable(test.providers.Providers, &out)
 			outputString := out.String()
 			if outputString != test.expectedOutput {
-				t.Errorf("displayDBProvidersTable() = %v, want %v", out.String(), test.expectedOutput)
+				t.Errorf("displayLegacyProvidersTable() = %v, want %v", out.String(), test.expectedOutput)
 			}
 		})
 	}
 }
 
-func TestDisplayDBProvidersJSON(t *testing.T) {
+func TestDisplayLegacyProvidersJSON(t *testing.T) {
 	tests := []struct {
 		name      string
 		providers dbProviders
@@ -113,7 +188,7 @@ func TestDisplayDBProvidersJSON(t *testing.T) {
 		{
 			name: "display providers table",
 			providers: dbProviders{
-				Providers: []dbProviderMetadata{
+				Providers: []legacyProviderMetadata{
 					{
 						Name:              "provider1",
 						LastSuccessfulRun: "2024-10-16T01:33:16.844201Z",
@@ -131,7 +206,7 @@ func TestDisplayDBProvidersJSON(t *testing.T) {
 		t.Run(test.name, func(t *testing.T) {
 
 			var out bytes.Buffer
-			err := displayDBProvidersJSON(&test.providers, &out)
+			err := displayLegacyProvidersJSON(&test.providers, &out)
 			if err != nil {
 				t.Error(err)
 			}
@@ -149,3 +224,7 @@ func TestDisplayDBProvidersJSON(t *testing.T) {
 		})
 	}
 }
+
+func timeRef(t time.Time) *time.Time {
+	return &t
+}
diff --git a/cmd/grype/cli/commands/db_status_test.go b/cmd/grype/cli/commands/db_status_test.go
new file mode 100644
index 00000000..dbfbcc73
--- /dev/null
+++ b/cmd/grype/cli/commands/db_status_test.go
@@ -0,0 +1,116 @@
+package commands
+
+import (
+	"bytes"
+	"errors"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	v6 "github.com/anchore/grype/grype/db/v6"
+)
+
+func TestPresentDBStatus(t *testing.T) {
+	validStatus := v6.Status{
+		Path:          "/Users/test/Library/Caches/grype/db/6/vulnerability.db",
+		SchemaVersion: "6.0.0",
+		Built:         v6.Time{Time: time.Date(2024, 11, 27, 14, 43, 17, 0, time.UTC)},
+		Checksum:      "xxh64:89d3ae128f6e718e",
+		Err:           nil,
+	}
+
+	invalidStatus := v6.Status{
+		Path:          "/Users/test/Library/Caches/grype/db/6/vulnerability.db",
+		SchemaVersion: "6.0.0",
+		Built:         v6.Time{Time: time.Date(2024, 11, 27, 14, 43, 17, 0, time.UTC)},
+		Checksum:      "xxh64:89d3ae128f6e718e",
+		Err:           errors.New("checksum mismatch"),
+	}
+
+	tests := []struct {
+		name         string
+		format       string
+		status       v6.Status
+		expectedText string
+		expectedErr  require.ErrorAssertionFunc
+	}{
+		{
+			name:   "valid status, text format",
+			format: textOutputFormat,
+			status: validStatus,
+			expectedText: `Path:      /Users/test/Library/Caches/grype/db/6/vulnerability.db
+Schema:    6.0.0
+Built:     2024-11-27T14:43:17Z
+Checksum:  xxh64:89d3ae128f6e718e
+Status:    valid
+`,
+			expectedErr: require.NoError,
+		},
+		{
+			name:   "invalid status, text format",
+			format: textOutputFormat,
+			status: invalidStatus,
+			expectedText: `Path:      /Users/test/Library/Caches/grype/db/6/vulnerability.db
+Schema:    6.0.0
+Built:     2024-11-27T14:43:17Z
+Checksum:  xxh64:89d3ae128f6e718e
+Status:    invalid
+`,
+			expectedErr: require.NoError,
+		},
+		{
+			name:   "valid status, JSON format",
+			format: jsonOutputFormat,
+			status: validStatus,
+			expectedText: `{
+ "schemaVersion": "6.0.0",
+ "built": "2024-11-27T14:43:17Z",
+ "path": "/Users/test/Library/Caches/grype/db/6/vulnerability.db",
+ "checksum": "xxh64:89d3ae128f6e718e",
+ "error": ""
+}
+`,
+			expectedErr: require.NoError,
+		},
+		{
+			name:   "invalid status, JSON format",
+			format: jsonOutputFormat,
+			status: invalidStatus,
+			expectedText: `{
+ "schemaVersion": "6.0.0",
+ "built": "2024-11-27T14:43:17Z",
+ "path": "/Users/test/Library/Caches/grype/db/6/vulnerability.db",
+ "checksum": "xxh64:89d3ae128f6e718e",
+ "error": "checksum mismatch"
+}
+`,
+			expectedErr: require.NoError,
+		},
+		{
+			name:        "unsupported format",
+			format:      "unsupported",
+			status:      validStatus,
+			expectedErr: requireErrorContains("unsupported output format"),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if tt.expectedErr == nil {
+				tt.expectedErr = require.NoError
+			}
+			writer := &bytes.Buffer{}
+
+			err := presentDBStatus(tt.format, writer, tt.status)
+			tt.expectedErr(t, err)
+			if err != nil {
+				return
+			}
+
+			assert.Equal(t, strings.TrimSpace(tt.expectedText), strings.TrimSpace(writer.String()))
+		})
+	}
+}
diff --git a/cmd/grype/cli/commands/listing_test.go b/cmd/grype/cli/commands/listing_test.go
deleted file mode 100644
index fa879f3d..00000000
--- a/cmd/grype/cli/commands/listing_test.go
+++ /dev/null
@@ -1,41 +0,0 @@
-package commands
-
-import (
-	"net/http"
-	"net/http/httptest"
-	"testing"
-
-	"github.com/anchore/clio"
-)
-
-func Test_ListingUserAgent(t *testing.T) {
-	listingFile := "/listing.json"
-
-	got := ""
-
-	// setup mock
-	handler := http.NewServeMux()
-	handler.HandleFunc(listingFile, func(w http.ResponseWriter, r *http.Request) {
-		got = r.Header.Get("User-Agent")
-		w.WriteHeader(http.StatusOK)
-		_, _ = w.Write([]byte("1.0.0"))
-	})
-	mockSrv := httptest.NewServer(handler)
-	defer mockSrv.Close()
-
-	dbOptions := *dbOptionsDefault(clio.Identification{
-		Name:    "the-app",
-		Version: "v3.2.1",
-	})
-	dbOptions.DB.RequireUpdateCheck = true
-	dbOptions.DB.UpdateURL = mockSrv.URL + listingFile
-
-	_ = runDBList(&dbListOptions{
-		Output:    "",
-		DBOptions: dbOptions,
-	})
-
-	if got != "the-app v3.2.1" {
-		t.Errorf("expected User-Agent header to match, got: %v", got)
-	}
-}
diff --git a/grype/db/v6/description_test.go b/grype/db/v6/description_test.go
index 6fca037e..4e42709d 100644
--- a/grype/db/v6/description_test.go
+++ b/grype/db/v6/description_test.go
@@ -12,7 +12,7 @@ import (
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 
-	"github.com/anchore/grype/grype/db/internal/schemaver"
+	"github.com/anchore/grype/internal/schemaver"
 )
 
 func TestReadDescription(t *testing.T) {
diff --git a/grype/db/v6/distribution/client_test.go b/grype/db/v6/distribution/client_test.go
index d96fb44d..7240a911 100644
--- a/grype/db/v6/distribution/client_test.go
+++ b/grype/db/v6/distribution/client_test.go
@@ -104,7 +104,7 @@ func TestClient_LatestFromURL(t *testing.T) {
 
 			cl := c.(client)
 
-			doc, err := cl.latestFromURL()
+			doc, err := cl.Latest()
 			tt.expectedErr(t, err)
 			if err != nil {
 				return
diff --git a/grype/db/v6/distribution/latest.go b/grype/db/v6/distribution/latest.go
index ca3127cd..c171fa4c 100644
--- a/grype/db/v6/distribution/latest.go
+++ b/grype/db/v6/distribution/latest.go
@@ -8,8 +8,8 @@ import (
 	"sort"
 	"time"
 
-	"github.com/anchore/grype/grype/db/internal/schemaver"
 	db "github.com/anchore/grype/grype/db/v6"
+	"github.com/anchore/grype/internal/schemaver"
 )
 
 const LatestFileName = "latest.json"
diff --git a/grype/db/v6/distribution/latest_test.go b/grype/db/v6/distribution/latest_test.go
index c37ffd02..a28b7c59 100644
--- a/grype/db/v6/distribution/latest_test.go
+++ b/grype/db/v6/distribution/latest_test.go
@@ -12,8 +12,8 @@ import (
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 
-	"github.com/anchore/grype/grype/db/internal/schemaver"
 	db "github.com/anchore/grype/grype/db/v6"
+	"github.com/anchore/grype/internal/schemaver"
 )
 
 func TestNewLatestDocument(t *testing.T) {
diff --git a/grype/db/v6/installation/curator_test.go b/grype/db/v6/installation/curator_test.go
index 1b2ba2f7..f2a656b0 100644
--- a/grype/db/v6/installation/curator_test.go
+++ b/grype/db/v6/installation/curator_test.go
@@ -13,10 +13,10 @@ import (
 	"github.com/stretchr/testify/require"
 	"github.com/wagoodman/go-progress"
 
-	"github.com/anchore/grype/grype/db/internal/schemaver"
 	db "github.com/anchore/grype/grype/db/v6"
 	"github.com/anchore/grype/grype/db/v6/distribution"
 	"github.com/anchore/grype/grype/db/v6/internal"
+	"github.com/anchore/grype/internal/schemaver"
 )
 
 type mockClient struct {
@@ -40,6 +40,11 @@ func (m *mockClient) Download(archive distribution.Archive, dest string, downloa
 	return args.String(0), args.Error(1)
 }
 
+func (m *mockClient) Latest() (*distribution.LatestDocument, error) {
+	args := m.Called()
+	return args.Get(0).(*distribution.LatestDocument), args.Error(1)
+}
+
 func newTestCurator(t *testing.T) curator {
 	tempDir := t.TempDir()
 	cfg := DefaultConfig()
@@ -469,7 +474,7 @@ func TestCurator_EnsureNotStale(t *testing.T) {
 }
 
 func TestCurator_ValidateIntegrity(t *testing.T) {
-	newCurator := func(t *testing.T) curator {
+	newCurator := func(t *testing.T) (curator, *db.Description) {
 		tempDir := t.TempDir()
 		cfg := DefaultConfig()
 		cfg.DBRootDir = tempDir
@@ -480,57 +485,64 @@ func TestCurator_ValidateIntegrity(t *testing.T) {
 		require.NoError(t, s.SetDBMetadata())
 		require.NoError(t, s.Close())
 
+		// assume that we already have a valid checksum file
+		digest, err := db.CalculateDBDigest(cfg.DBFilePath())
+		require.NoError(t, err)
+
+		checksumsFilePath := filepath.Join(cfg.DBDirectoryPath(), db.ChecksumFileName)
+		require.NoError(t, os.WriteFile(checksumsFilePath, []byte(digest), 0644))
+
 		ci, err := NewCurator(cfg, new(mockClient))
 		require.NoError(t, err)
 
-		return ci.(curator)
+		m, err := s.GetDBMetadata()
+		require.NoError(t, err)
+
+		return ci.(curator), db.DescriptionFromMetadata(m)
 	}
 
 	t.Run("valid metadata with correct checksum", func(t *testing.T) {
-		c := newCurator(t)
+		c, d := newCurator(t)
 
-		result, digest, err := c.validateIntegrity(c.config.DBFilePath(), true)
+		result, digest, err := c.validateIntegrity(d, c.config.DBFilePath(), true)
 		require.NoError(t, err)
 		require.NotNil(t, result)
 		require.NotEmpty(t, digest)
 	})
 
 	t.Run("db does not exist", func(t *testing.T) {
-		c := newCurator(t)
+		c, d := newCurator(t)
+
+		require.NoError(t, os.Remove(c.config.DBFilePath()))
 
-		_, _, err := c.validateIntegrity("non/existent/path", true)
+		_, _, err := c.validateIntegrity(d, c.config.DBFilePath(), true)
 		require.ErrorContains(t, err, "database does not exist")
 	})
 
 	t.Run("checksum file does not exist", func(t *testing.T) {
-		c := newCurator(t)
+		c, d := newCurator(t)
 		dbDir := c.config.DBDirectoryPath()
 		require.NoError(t, os.Remove(filepath.Join(dbDir, db.ChecksumFileName)))
-		_, _, err := c.validateIntegrity(c.config.DBFilePath(), true)
+		_, _, err := c.validateIntegrity(d, c.config.DBFilePath(), true)
 		require.ErrorContains(t, err, "no such file or directory")
 	})
 
 	t.Run("invalid checksum", func(t *testing.T) {
-		c := newCurator(t)
+		c, d := newCurator(t)
 		dbDir := c.config.DBDirectoryPath()
 
 		writeTestChecksumsFile(t, c.fs, dbDir, "xxh64:invalidchecksum")
 
-		_, _, err := c.validateIntegrity(c.config.DBFilePath(), true)
+		_, _, err := c.validateIntegrity(d, c.config.DBFilePath(), true)
 		require.ErrorContains(t, err, "bad db checksum")
 	})
 
 	t.Run("unsupported database version", func(t *testing.T) {
-		c := newCurator(t)
-
-		oldDescription := db.Description{
-			SchemaVersion: schemaver.New(db.ModelVersion-1, 0, 0),
-			Built:         db.Time{Time: time.Now().Add(-48 * time.Hour)},
-		}
+		c, d := newCurator(t)
 
-		writeTestDescriptionToDB(t, c.config.DBDirectoryPath(), oldDescription)
+		d.SchemaVersion = schemaver.New(db.ModelVersion-1, 0, 0)
 
-		_, _, err := c.validateIntegrity(c.config.DBFilePath(), true)
+		_, _, err := c.validateIntegrity(d, c.config.DBFilePath(), true)
 		require.ErrorContains(t, err, "unsupported database version")
 	})
 }
diff --git a/grype/db/internal/schemaver/schema_ver.go b/internal/schemaver/schema_ver.go
similarity index 100%
rename from grype/db/internal/schemaver/schema_ver.go
rename to internal/schemaver/schema_ver.go
diff --git a/grype/db/internal/schemaver/schema_ver_test.go b/internal/schemaver/schema_ver_test.go
similarity index 100%
rename from grype/db/internal/schemaver/schema_ver_test.go
rename to internal/schemaver/schema_ver_test.go
diff --git a/test/quality/test-db-url b/test/quality/test-db-url
index 5f28ed2b..e5200568 100644
--- a/test/quality/test-db-url
+++ b/test/quality/test-db-url
@@ -1,1 +1,1 @@
-https://grype.anchore.io/databases/vulnerability-db_v5_2024-11-01T01:31:51Z_1730434939.tar.gz
+https://grype.anchore.io/databases/vulnerability-db_v5_2024-12-01T01:32:10Z_1733027094.tar.gz