Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Add grype db providers command #2174

Merged
merged 10 commits into from
Oct 28, 2024
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions cmd/grype/cli/commands/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ func DB(app clio.Application) *cobra.Command {
DBStatus(app),
DBUpdate(app),
DBSearch(app),
DBProviders(app),
)

return db
Expand Down
155 changes: 155 additions & 0 deletions cmd/grype/cli/commands/db_providers.go
Original file line number Diff line number Diff line change
@@ -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
}
151 changes: 151 additions & 0 deletions cmd/grype/cli/commands/db_providers_test.go
Original file line number Diff line number Diff line change
@@ -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")
}

})
}
}
12 changes: 12 additions & 0 deletions cmd/grype/cli/commands/test-fixtures/provider-metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"providers": [
{
"name": "provider1",
"lastSuccessfulRun": "2024-10-16T01:33:16.844201Z"
},
{
"name": "provider2",
"lastSuccessfulRun": "2024-10-16T01:32:43.516596Z"
}
]
}
Loading
Loading