Skip to content

Commit

Permalink
add db v6 feature flag and wire to db commands
Browse files Browse the repository at this point in the history
Signed-off-by: Alex Goodman <[email protected]>
  • Loading branch information
wagoodman committed Nov 27, 2024
1 parent cbcf174 commit e3c46df
Show file tree
Hide file tree
Showing 30 changed files with 1,286 additions and 218 deletions.
9 changes: 8 additions & 1 deletion cmd/grype/cli/commands/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
159 changes: 142 additions & 17 deletions cmd/grype/cli/commands/db_check.go
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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
}
Expand All @@ -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
}
131 changes: 131 additions & 0 deletions cmd/grype/cli/commands/db_check_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading

0 comments on commit e3c46df

Please sign in to comment.