Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add DB v6 feature flag #2288

Merged
merged 1 commit into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading