Repository: anchore/grype
Pull request #2219 (2024-10-28 18:24:42)

bump syft to v1.15.0, sterescope to v0.0.5
Bumping to newly released syft


Patch:
diff --git a/README.md b/README.md
index 9a37eb2c..3752941a 100644
--- a/README.md
+++ b/README.md
@@ -567,6 +567,8 @@ Grype provides database-specific CLI commands for users that want to control the
 
 `grype db import` — provide grype with a database archive to explicitly use (useful for offline DB updates)
 
+`grype db providers` - provides a detailed list of database providers
+
 Find complete information on Grype's database commands by running `grype db --help`.
 
 ## Shell completion
diff --git a/cmd/grype/cli/commands/db.go b/cmd/grype/cli/commands/db.go
index ce1ed09b..41a81b2c 100644
--- a/cmd/grype/cli/commands/db.go
+++ b/cmd/grype/cli/commands/db.go
@@ -35,6 +35,7 @@ func DB(app clio.Application) *cobra.Command {
 		DBStatus(app),
 		DBUpdate(app),
 		DBSearch(app),
+		DBProviders(app),
 	)
 
 	return db
diff --git a/cmd/grype/cli/commands/db_providers.go b/cmd/grype/cli/commands/db_providers.go
new file mode 100644
index 00000000..d25de084
--- /dev/null
+++ b/cmd/grype/cli/commands/db_providers.go
@@ -0,0 +1,155 @@
+package commands
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"os"
+	"path"
+	"strings"
+
+	"github.com/olekukonko/tablewriter"
+	"github.com/spf13/cobra"
+
+	"github.com/anchore/clio"
+	"github.com/anchore/grype/grype/db/legacy/distribution"
+	"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"`
+}
+
+var _ clio.FlagAdder = (*dbProvidersOptions)(nil)
+
+func (d *dbProvidersOptions) AddFlags(flags clio.FlagSet) {
+	flags.StringVarP(&d.Output, "output", "o", "format to display results (available=[table, json])")
+}
+
+func DBProviders(app clio.Application) *cobra.Command {
+	opts := &dbProvidersOptions{
+		Output: tableOutputFormat,
+	}
+
+	return app.SetupCommand(&cobra.Command{
+		Use:   "providers",
+		Short: "list vulnerability database providers",
+		Args:  cobra.ExactArgs(0),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return runDBProviders(opts, app)
+		},
+	}, opts)
+}
+
+func runDBProviders(opts *dbProvidersOptions, app clio.Application) error {
+	metadataFileLocation, err := getMetadataFileLocation(app)
+	if err != nil {
+		return nil
+	}
+	providers, err := getDBProviders(*metadataFileLocation)
+	if err != nil {
+		return err
+	}
+
+	sb := &strings.Builder{}
+
+	switch opts.Output {
+	case tableOutputFormat:
+		displayDBProvidersTable(providers.Providers, sb)
+	case jsonOutputFormat:
+		err = displayDBProvidersJSON(providers, sb)
+		if err != nil {
+			return err
+		}
+	default:
+		return fmt.Errorf("unsupported output format: %s", opts.Output)
+	}
+	bus.Report(sb.String())
+
+	return nil
+}
+
+func getMetadataFileLocation(app clio.Application) (*string, error) {
+	dbCurator, err := distribution.NewCurator(dbOptionsDefault(app.ID()).DB.ToCuratorConfig())
+	if err != nil {
+		return nil, err
+	}
+
+	location := dbCurator.Status().Location
+
+	return &location, nil
+}
+
+func getDBProviders(metadataFileLocation string) (*dbProviders, error) {
+	metadataFile := path.Join(metadataFileLocation, metadataFileName)
+
+	file, err := os.Open(metadataFile)
+	if err != nil {
+		if os.IsNotExist(err) {
+			return nil, fmt.Errorf("file not found: %w", err)
+		}
+		return nil, fmt.Errorf("error opening file: %w", err)
+	}
+	defer file.Close()
+
+	var providers dbProviders
+	fileBytes, err := io.ReadAll(file)
+	if err != nil {
+		return nil, fmt.Errorf("error reading file: %w", err)
+	}
+	err = json.Unmarshal(fileBytes, &providers)
+	if err != nil {
+		return nil, fmt.Errorf("cannot unmarshal providers: %w", err)
+	}
+
+	return &providers, nil
+}
+
+func displayDBProvidersTable(providers []dbProviderMetadata, output io.Writer) {
+	rows := [][]string{}
+	for _, provider := range providers {
+		rows = append(rows, []string{provider.Name, provider.LastSuccessfulRun})
+	}
+
+	table := tablewriter.NewWriter(output)
+	table.SetHeader([]string{"Name", "Last Successful Run"})
+
+	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 *dbProviders, 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
+}
diff --git a/go.mod b/go.mod
index a95b0884..58426abd 100644
--- a/go.mod
+++ b/go.mod
@@ -14,8 +14,8 @@ require (
 	github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
 	github.com/anchore/go-version v1.2.2-0.20210903204242-51efa5b487c4
 	github.com/anchore/packageurl-go v0.1.1-0.20241018175412-5c22e6360c4f
-	github.com/anchore/stereoscope v0.0.5-0.20241018131503-a38c93517fc7
-	github.com/anchore/syft v1.14.2
+	github.com/anchore/stereoscope v0.0.5
+	github.com/anchore/syft v1.15.0
 	github.com/aquasecurity/go-pep440-version v0.0.0-20210121094942-22b2f8951d46
 	github.com/bmatcuk/doublestar/v2 v2.0.4
 	github.com/charmbracelet/bubbletea v1.1.2
@@ -58,7 +58,7 @@ require (
 	github.com/wagoodman/go-presenter v0.0.0-20211015174752-f9c01afc824b
 	github.com/wagoodman/go-progress v0.0.0-20230925121702-07e42b3cdba0
 	github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8
-	golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8
+	golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
 	gorm.io/gorm v1.25.12
 )
 
@@ -109,7 +109,7 @@ require (
 	github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect
 	github.com/containerd/ttrpc v1.2.5 // indirect
 	github.com/containerd/typeurl/v2 v2.1.1 // indirect
-	github.com/cyphar/filepath-securejoin v0.2.4 // indirect
+	github.com/cyphar/filepath-securejoin v0.2.5 // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da // indirect
 	github.com/distribution/reference v0.6.0 // indirect
@@ -132,7 +132,7 @@ require (
 	github.com/gkampitakis/go-diff v1.3.2 // indirect
 	github.com/glebarez/go-sqlite v1.21.2 // indirect
 	github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
-	github.com/go-git/go-billy/v5 v5.5.0 // indirect
+	github.com/go-git/go-billy/v5 v5.6.0 // indirect
 	github.com/go-git/go-git/v5 v5.12.0 // indirect
 	github.com/go-logr/logr v1.4.1 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
@@ -222,7 +222,7 @@ require (
 	github.com/spf13/viper v1.19.0 // indirect
 	github.com/stretchr/objx v0.5.2 // indirect
 	github.com/subosito/gotenv v1.6.0 // indirect
-	github.com/sylabs/sif/v2 v2.19.1 // indirect
+	github.com/sylabs/sif/v2 v2.19.2 // indirect
 	github.com/sylabs/squashfs v1.0.0 // indirect
 	github.com/therootcompany/xz v1.0.1 // indirect
 	github.com/tidwall/gjson v1.17.0 // indirect
diff --git a/go.sum b/go.sum
index cc9d72f7..2b12c283 100644
--- a/go.sum
+++ b/go.sum
@@ -256,10 +256,10 @@ github.com/anchore/go-version v1.2.2-0.20210903204242-51efa5b487c4 h1:rmZG77uXgE
 github.com/anchore/go-version v1.2.2-0.20210903204242-51efa5b487c4/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E=
 github.com/anchore/packageurl-go v0.1.1-0.20241018175412-5c22e6360c4f h1:dAQPIrQ3a5PBqZeZ+B9NGZsGmodk4NO9OjDIsQmQyQM=
 github.com/anchore/packageurl-go v0.1.1-0.20241018175412-5c22e6360c4f/go.mod h1:KoYIv7tdP5+CC9VGkeZV4/vGCKsY55VvoG+5dadg4YI=
-github.com/anchore/stereoscope v0.0.5-0.20241018131503-a38c93517fc7 h1:CBKDfuMWKRt12EnKtgT/an9FQSQI83mOU1KT57z81IU=
-github.com/anchore/stereoscope v0.0.5-0.20241018131503-a38c93517fc7/go.mod h1:wH79ZauZcd3erKPocg9+qhDC6Q/z125sD+Isi4d61a4=
-github.com/anchore/syft v1.14.2 h1:y/1QIsSUaVDzbT1Q29BkKAAyNivt+2wgWzpCxI0b5yc=
-github.com/anchore/syft v1.14.2/go.mod h1:tyGQPeUSS9498A10nUF1kEVIObsvsnmrWt6hP25EjXE=
+github.com/anchore/stereoscope v0.0.5 h1:PILlvsQS3+dT5rNsDudRhi91jukR65y2itG1lQOLn0s=
+github.com/anchore/stereoscope v0.0.5/go.mod h1:jwK34VB049/iRE1DyWUv4ZWraOaFQ+FpurgvkWMGQzQ=
+github.com/anchore/syft v1.15.0 h1:V2PKilik4ChuvQZ3kRGc41w62uVex+qDE3TxIR+lsoM=
+github.com/anchore/syft v1.15.0/go.mod h1:z062WmfN0BMCDGIY8112PtxDgAmlWSDxgTOgpHpjxOM=
 github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
 github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
 github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
@@ -370,8 +370,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
 github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
-github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
-github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
+github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo=
+github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
 github.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo=
 github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -460,8 +460,8 @@ github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE=
 github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8=
 github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
 github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
-github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
-github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
+github.com/go-git/go-billy/v5 v5.6.0 h1:w2hPNtoehvJIxR00Vb4xX94qHQi/ApZfX+nBE2Cjio8=
+github.com/go-git/go-billy/v5 v5.6.0/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM=
 github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
 github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
 github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys=
@@ -811,8 +811,8 @@ github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7
 github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
 github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
 github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
-github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
-github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
+github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
+github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
 github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
 github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
@@ -961,8 +961,8 @@ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8
 github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
 github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
-github.com/sylabs/sif/v2 v2.19.1 h1:1eeMmFc8elqJe60ZiWwXgL3gMheb0IP4GmNZ4q0IEA0=
-github.com/sylabs/sif/v2 v2.19.1/go.mod h1:U1SUhvl8X1JIxAylC0DYz1fa/Xba6EMZD1dGPGBH83E=
+github.com/sylabs/sif/v2 v2.19.2 h1:KKcUKnbnT69rN1WWHRYoAVKFpqnXpNJ36kmQLpp86Uc=
+github.com/sylabs/sif/v2 v2.19.2/go.mod h1:nhX6D/CJntHDWspNLXLe+yct0cd5lm8HJ7VIW6hgKrw=
 github.com/sylabs/squashfs v1.0.0 h1:xAyMS21ogglkuR5HaY55PCfqY3H32ma9GkasTYo28Zg=
 github.com/sylabs/squashfs v1.0.0/go.mod h1:rhWzvgefq1X+R+LZdts10hfMsTg3g74OfGunW8tvg/4=
 github.com/terminalstatic/go-xsd-validate v0.1.5 h1:RqpJnf6HGE2CB/lZB1A8BYguk8uRtcvYAPLCF15qguo=
@@ -1089,8 +1089,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
 golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
-golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw=
-golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ=
+golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
+golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -1402,8 +1402,8 @@ golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
-golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
+golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
+golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=


Test patch:
diff --git a/cmd/grype/cli/commands/db_providers_test.go b/cmd/grype/cli/commands/db_providers_test.go
new file mode 100644
index 00000000..734ff127
--- /dev/null
+++ b/cmd/grype/cli/commands/db_providers_test.go
@@ -0,0 +1,151 @@
+package commands
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"os"
+	"reflect"
+	"testing"
+)
+
+func TestGetDBProviders(t *testing.T) {
+
+	tests := []struct {
+		name              string
+		fileLocation      string
+		expectedProviders dbProviders
+		expectedError     error
+	}{
+		{
+			name:         "test provider metadata file",
+			fileLocation: "./test-fixtures",
+			expectedProviders: dbProviders{
+				Providers: []dbProviderMetadata{
+					{
+						Name:              "provider1",
+						LastSuccessfulRun: "2024-10-16T01:33:16.844201Z",
+					},
+					{
+						Name:              "provider2",
+						LastSuccessfulRun: "2024-10-16T01:32:43.516596Z",
+					},
+				},
+			},
+			expectedError: nil,
+		},
+		{
+			name:              "no metadata file found",
+			fileLocation:      "./",
+			expectedProviders: dbProviders{},
+			expectedError:     os.ErrNotExist,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			providers, err := getDBProviders(test.fileLocation)
+			if err != nil {
+				if errors.Is(err, test.expectedError) {
+					return
+				}
+				t.Errorf("getDBProviders() expected list of providers, got error:  %v", err)
+				return
+			}
+			if !reflect.DeepEqual(*providers, test.expectedProviders) {
+				t.Error("getDBProviders() providers comparison failed, got error")
+			}
+		})
+	}
+
+}
+
+func TestDisplayDBProvidersTable(t *testing.T) {
+	tests := []struct {
+		name           string
+		providers      dbProviders
+		expectedOutput string
+	}{
+		{
+			name: "display providers table",
+			providers: dbProviders{
+				Providers: []dbProviderMetadata{
+					{
+						Name:              "provider1",
+						LastSuccessfulRun: "2024-10-16T01:33:16.844201Z",
+					},
+					{
+						Name:              "provider2",
+						LastSuccessfulRun: "2024-10-16T01:32:43.516596Z",
+					},
+				},
+			},
+			expectedOutput: "NAME       LAST SUCCESSFUL RUN         \nprovider1  2024-10-16T01:33:16.844201Z  \nprovider2  2024-10-16T01:32:43.516596Z  \n",
+		},
+		{
+			name: "empty list of providers",
+			providers: dbProviders{
+				Providers: []dbProviderMetadata{},
+			},
+			expectedOutput: "NAME  LAST SUCCESSFUL RUN \n",
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+
+			var out bytes.Buffer
+			displayDBProvidersTable(test.providers.Providers, &out)
+			outputString := out.String()
+			if outputString != test.expectedOutput {
+				t.Errorf("displayDBProvidersTable() = %v, want %v", out.String(), test.expectedOutput)
+			}
+		})
+	}
+}
+
+func TestDisplayDBProvidersJSON(t *testing.T) {
+	tests := []struct {
+		name      string
+		providers dbProviders
+	}{
+
+		{
+			name: "display providers table",
+			providers: dbProviders{
+				Providers: []dbProviderMetadata{
+					{
+						Name:              "provider1",
+						LastSuccessfulRun: "2024-10-16T01:33:16.844201Z",
+					},
+					{
+						Name:              "provider2",
+						LastSuccessfulRun: "2024-10-16T01:32:43.516596Z",
+					},
+				},
+			},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+
+			var out bytes.Buffer
+			err := displayDBProvidersJSON(&test.providers, &out)
+			if err != nil {
+				t.Error(err)
+			}
+			var providers dbProviders
+
+			err = json.Unmarshal(out.Bytes(), &providers)
+			if err != nil {
+				t.Error(err)
+			}
+
+			if !reflect.DeepEqual(providers, test.providers) {
+				t.Error("DBProvidersJSON() providers comparison failed, got error")
+			}
+
+		})
+	}
+}
diff --git a/cmd/grype/cli/commands/test-fixtures/provider-metadata.json b/cmd/grype/cli/commands/test-fixtures/provider-metadata.json
new file mode 100644
index 00000000..015f6914
--- /dev/null
+++ b/cmd/grype/cli/commands/test-fixtures/provider-metadata.json
@@ -0,0 +1,12 @@
+{
+  "providers": [
+    {
+      "name": "provider1",
+      "lastSuccessfulRun": "2024-10-16T01:33:16.844201Z"
+    },
+    {
+      "name": "provider2",
+      "lastSuccessfulRun": "2024-10-16T01:32:43.516596Z"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/test/cli/db_providers_test.go b/test/cli/db_providers_test.go
new file mode 100644
index 00000000..3437aa88
--- /dev/null
+++ b/test/cli/db_providers_test.go
@@ -0,0 +1,65 @@
+package cli
+
+import (
+	"strings"
+	"testing"
+)
+
+func TestDBProviders(t *testing.T) {
+	tests := []struct {
+		name       string
+		args       []string
+		env        map[string]string
+		assertions []traitAssertion
+	}{
+		{
+			name: "db providers command",
+			args: []string{"db", "providers"},
+			assertions: []traitAssertion{
+				assertInOutput("LAST SUCCESSFUL RUN"),
+				assertNoStderr,
+				assertTableReport,
+			},
+		},
+		{
+			name: "db providers command help",
+			args: []string{"db", "providers", "-h"},
+			assertions: []traitAssertion{
+				assertInOutput("list vulnerability database providers"),
+				assertNoStderr,
+			},
+		},
+		{
+			name: "db providers command with table output flag",
+			args: []string{"db", "providers", "-o", "table"},
+			assertions: []traitAssertion{
+				assertInOutput("LAST SUCCESSFUL RUN"),
+				assertNoStderr,
+				assertTableReport,
+			},
+		},
+		{
+			name: "db providers command with json output flag",
+			args: []string{"db", "providers", "-o", "json"},
+			assertions: []traitAssertion{
+				assertInOutput("providers"),
+				assertNoStderr,
+				assertJsonReport,
+			},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			cmd, stdout, stderr := runGrype(t, test.env, test.args...)
+			for _, traitAssertionFn := range test.assertions {
+				traitAssertionFn(t, stdout, stderr, cmd.ProcessState.ExitCode())
+			}
+			if t.Failed() {
+				t.Log("STDOUT:\n", stdout)
+				t.Log("STDERR:\n", stderr)
+				t.Log("COMMAND:", strings.Join(cmd.Args, " "))
+			}
+		})
+	}
+}
diff --git a/test/cli/trait_assertions_test.go b/test/cli/trait_assertions_test.go
index 8aa14b09..11e30f18 100644
--- a/test/cli/trait_assertions_test.go
+++ b/test/cli/trait_assertions_test.go
@@ -1,6 +1,7 @@
 package cli
 
 import (
+	"encoding/json"
 	"strings"
 	"testing"
 
@@ -70,3 +71,19 @@ func assertNotInOutput(notWanted string) traitAssertion {
 		}
 	}
 }
+
+func assertJsonReport(tb testing.TB, stdout, _ string, _ int) {
+	tb.Helper()
+	var data interface{}
+
+	if err := json.Unmarshal([]byte(stdout), &data); err != nil {
+		tb.Errorf("expected to find a JSON report, but was unmarshalable: %+v", err)
+	}
+}
+
+func assertTableReport(tb testing.TB, stdout, _ string, _ int) {
+	tb.Helper()
+	if !strings.Contains(stdout, "NAME") || !strings.Contains(stdout, "LAST SUCCESSFUL RUN") {
+		tb.Errorf("expected to find a table report, but did not")
+	}
+}