From d442176405260caa04fce9ac093a62c52ab361aa Mon Sep 17 00:00:00 2001 From: Nikita Pivkin Date: Sun, 20 Aug 2023 13:12:31 +0700 Subject: [PATCH] feat(server): add version endpoint (#4869) * feat(server): add version endpoint * fix panic and test * move version.go * move version variable * add docs about endpoints * move testdata * refactor * update build command * refactor --- cmd/trivy/main.go | 6 +- docs/docs/references/modes/client-server.md | 42 ++++++ goreleaser-canary.yml | 2 +- goreleaser.yml | 8 +- integration/integration_test.go | 2 +- magefiles/docs.go | 8 +- magefiles/magefile.go | 2 +- pkg/commands/app.go | 136 ++++-------------- pkg/commands/app_test.go | 20 +-- pkg/flag/options.go | 5 +- pkg/policy/policy.go | 7 + pkg/rpc/server/listen.go | 15 +- pkg/rpc/server/listen_test.go | 41 +++++- .../testdata/testcache/db/metadata.json | 1 + .../testdata/testcache/policy/metadata.json | 1 + .../testdata/testcache/db/metadata.json | 1 + .../testdata/testcache/java-db/metadata.json | 1 + .../testdata/testcache/policy/metadata.json | 1 + pkg/version/version.go | 107 ++++++++++++++ pkg/version/version_test.go | 54 +++++++ 20 files changed, 319 insertions(+), 141 deletions(-) create mode 100644 pkg/rpc/server/testdata/testcache/db/metadata.json create mode 100644 pkg/rpc/server/testdata/testcache/policy/metadata.json create mode 100644 pkg/version/testdata/testcache/db/metadata.json create mode 100644 pkg/version/testdata/testcache/java-db/metadata.json create mode 100644 pkg/version/testdata/testcache/policy/metadata.json create mode 100644 pkg/version/version.go create mode 100644 pkg/version/version_test.go diff --git a/cmd/trivy/main.go b/cmd/trivy/main.go index f2f196f744e4..d07ec31e817e 100644 --- a/cmd/trivy/main.go +++ b/cmd/trivy/main.go @@ -13,10 +13,6 @@ import ( _ "modernc.org/sqlite" // sqlite driver for RPM DB and Java DB ) -var ( - version = "dev" -) - func main() { if err := run(); err != nil { log.Fatal(err) @@ -35,7 +31,7 @@ func run() error { return nil } - app := commands.NewApp(version) + app := commands.NewApp() if err := app.Execute(); err != nil { return err } diff --git a/docs/docs/references/modes/client-server.md b/docs/docs/references/modes/client-server.md index a9e1900e99f3..4e812bd48ab3 100644 --- a/docs/docs/references/modes/client-server.md +++ b/docs/docs/references/modes/client-server.md @@ -288,6 +288,48 @@ $ trivy server --listen localhost:8080 --token dummy $ trivy image --server http://localhost:8080 --token dummy alpine:3.10 ``` +## Endpoints + +### Health +Checks whether the Trivy server is running. Authentication is not required. + +Example request: +```bash +curl -s 0.0.0.0:8080/healthz +ok +``` + +Returns the `200 OK` status if the request was successful. +### Version + +Returns the version of the Trivy and all components (db, policy). Authentication is not required. + +Example request: +```bash +curl -s 0.0.0.0:8080/version | jq +{ + "Version": "dev", + "VulnerabilityDB": { + "Version": 2, + "NextUpdate": "2023-07-25T14:15:29.876639806Z", + "UpdatedAt": "2023-07-25T08:15:29.876640206Z", + "DownloadedAt": "2023-07-25T09:36:25.599004Z" + }, + "JavaDB": { + "Version": 1, + "NextUpdate": "2023-07-28T01:03:52.169192565Z", + "UpdatedAt": "2023-07-25T01:03:52.169192765Z", + "DownloadedAt": "2023-07-25T09:37:48.906152Z" + }, + "PolicyBundle": { + "Digest": "sha256:829832357626da2677955e3b427191212978ba20012b6eaa03229ca28569ae43", + "DownloadedAt": "2023-07-23T11:40:33.122462Z" + } +} +``` + +Returns the `200 OK` status if the request was successful. + ## Architecture ![architecture](../../../imgs/client-server.png) diff --git a/goreleaser-canary.yml b/goreleaser-canary.yml index 8c60f70fa458..da395ab53e9d 100644 --- a/goreleaser-canary.yml +++ b/goreleaser-canary.yml @@ -6,7 +6,7 @@ builds: ldflags: - -s -w - "-extldflags '-static'" - - -X main.version={{.Version}} + - -X github.com/aquasecurity/trivy/pkg/version.ver={{.Version}} env: - CGO_ENABLED=0 goos: diff --git a/goreleaser.yml b/goreleaser.yml index b8a8a08a1a50..3891f0249789 100644 --- a/goreleaser.yml +++ b/goreleaser.yml @@ -6,7 +6,7 @@ builds: ldflags: - -s -w - "-extldflags '-static'" - - -X main.version={{.Version}} + - -X github.com/aquasecurity/trivy/pkg/version.ver={{.Version}} env: - CGO_ENABLED=0 goos: @@ -26,7 +26,7 @@ builds: ldflags: - -s -w - "-extldflags '-static'" - - -X main.version={{.Version}} + - -X github.com/aquasecurity/trivy/pkg/version.ver={{.Version}} env: - CGO_ENABLED=0 goos: @@ -41,7 +41,7 @@ builds: ldflags: - -s -w - "-extldflags '-static'" - - -X main.version={{.Version}} + - -X github.com/aquasecurity/trivy/pkg/version.ver={{.Version}} env: - CGO_ENABLED=0 goos: @@ -57,7 +57,7 @@ builds: ldflags: - -s -w - "-extldflags '-static'" - - -X main.version={{.Version}} + - -X github.com/aquasecurity/trivy/pkg/version.ver={{.Version}} env: - CGO_ENABLED=0 goos: diff --git a/integration/integration_test.go b/integration/integration_test.go index c9dd6c260a68..04e1f423c927 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -184,7 +184,7 @@ func readSpdxJson(t *testing.T, filePath string) *spdx.Document { func execute(osArgs []string) error { // Setup CLI App - app := commands.NewApp("dev") + app := commands.NewApp() app.SetOut(io.Discard) // Run Trivy diff --git a/magefiles/docs.go b/magefiles/docs.go index e44b7147c00b..37d04343ecac 100644 --- a/magefiles/docs.go +++ b/magefiles/docs.go @@ -12,17 +12,13 @@ import ( // Generate CLI references func main() { - ver, err := version() - if err != nil { - log.Fatal(err) - } // Set a dummy path for the documents flag.CacheDirFlag.Default = "/path/to/cache" flag.ModuleDirFlag.Default = "$HOME/.trivy/modules" - cmd := commands.NewApp(ver) + cmd := commands.NewApp() cmd.DisableAutoGenTag = true - if err = doc.GenMarkdownTree(cmd, "./docs/docs/references/configuration/cli"); err != nil { + if err := doc.GenMarkdownTree(cmd, "./docs/docs/references/configuration/cli"); err != nil { log.Fatal(err) } } diff --git a/magefiles/magefile.go b/magefiles/magefile.go index 316ee2d25df2..44d12c1c8bb4 100644 --- a/magefiles/magefile.go +++ b/magefiles/magefile.go @@ -37,7 +37,7 @@ func buildLdflags() (string, error) { if err != nil { return "", err } - return fmt.Sprintf("-s -w -X=main.version=%s", ver), nil + return fmt.Sprintf("-s -w -X=github.com/aquasecurity/trivy/pkg/version.ver=%s", ver), nil } type Tool mg.Namespace diff --git a/pkg/commands/app.go b/pkg/commands/app.go index e8a5e2d44b52..78d1e6cd7423 100644 --- a/pkg/commands/app.go +++ b/pkg/commands/app.go @@ -6,7 +6,6 @@ import ( "fmt" "io" "os" - "path/filepath" "strings" "time" @@ -15,8 +14,7 @@ import ( "golang.org/x/xerrors" awsScanner "github.com/aquasecurity/defsec/pkg/scanners/cloud/aws" - "github.com/aquasecurity/trivy-db/pkg/metadata" - javadb "github.com/aquasecurity/trivy-java-db/pkg/db" + awscommands "github.com/aquasecurity/trivy/pkg/cloud/aws/commands" "github.com/aquasecurity/trivy/pkg/commands/artifact" "github.com/aquasecurity/trivy/pkg/commands/convert" @@ -27,19 +25,11 @@ import ( "github.com/aquasecurity/trivy/pkg/log" "github.com/aquasecurity/trivy/pkg/module" "github.com/aquasecurity/trivy/pkg/plugin" - "github.com/aquasecurity/trivy/pkg/policy" "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/version" xstrings "github.com/aquasecurity/trivy/pkg/x/strings" ) -// VersionInfo holds the trivy DB version Info -type VersionInfo struct { - Version string `json:",omitempty"` - VulnerabilityDB *metadata.Metadata `json:",omitempty"` - JavaDB *metadata.Metadata `json:",omitempty"` - PolicyBundle *policy.Metadata `json:",omitempty"` -} - const ( usageTemplate = `Usage:{{if .Runnable}} {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} @@ -72,9 +62,9 @@ Use "{{.CommandPath}} [command] --help" for more information about a command.{{e ) // NewApp is the factory method to return Trivy CLI -func NewApp(version string) *cobra.Command { +func NewApp() *cobra.Command { globalFlags := flag.NewGlobalFlagGroup() - rootCmd := NewRootCommand(version, globalFlags) + rootCmd := NewRootCommand(globalFlags) rootCmd.AddGroup( &cobra.Group{ ID: groupScanning, @@ -161,7 +151,7 @@ func initConfig(configFile string) error { return nil } -func NewRootCommand(version string, globalFlags *flag.GlobalFlagGroup) *cobra.Command { +func NewRootCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { var versionFormat string cmd := &cobra.Command{ Use: "trivy [global flags] command [flags] target", @@ -181,7 +171,7 @@ func NewRootCommand(version string, globalFlags *flag.GlobalFlagGroup) *cobra.Co Args: cobra.NoArgs, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { // Set the Trivy version here so that we can override version printer. - cmd.Version = version + cmd.Version = version.AppVersion() // viper.BindPFlag cannot be called in init(). // cf. https://github.com/spf13/cobra/issues/875 @@ -213,7 +203,7 @@ func NewRootCommand(version string, globalFlags *flag.GlobalFlagGroup) *cobra.Co globalOptions := globalFlags.ToOptions() if globalOptions.ShowVersion { // Customize version output - return showVersion(globalOptions.CacheDir, versionFormat, version, cmd.OutOrStdout()) + return showVersion(globalOptions.CacheDir, versionFormat, cmd.OutOrStdout()) } else { return cmd.Help() } @@ -298,7 +288,7 @@ func NewImageCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { return validateArgs(cmd, args) }, RunE: func(cmd *cobra.Command, args []string) error { - options, err := imageFlags.ToOptions(cmd.Version, args, globalFlags) + options, err := imageFlags.ToOptions(args, globalFlags) if err != nil { return xerrors.Errorf("flag error: %w", err) } @@ -357,7 +347,7 @@ func NewFilesystemCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { if err := fsFlags.Bind(cmd); err != nil { return xerrors.Errorf("flag bind error: %w", err) } - options, err := fsFlags.ToOptions(cmd.Version, args, globalFlags) + options, err := fsFlags.ToOptions(args, globalFlags) if err != nil { return xerrors.Errorf("flag error: %w", err) } @@ -416,7 +406,7 @@ func NewRootfsCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { if err := rootfsFlags.Bind(cmd); err != nil { return xerrors.Errorf("flag bind error: %w", err) } - options, err := rootfsFlags.ToOptions(cmd.Version, args, globalFlags) + options, err := rootfsFlags.ToOptions(args, globalFlags) if err != nil { return xerrors.Errorf("flag error: %w", err) } @@ -469,7 +459,7 @@ func NewRepositoryCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { if err := repoFlags.Bind(cmd); err != nil { return xerrors.Errorf("flag bind error: %w", err) } - options, err := repoFlags.ToOptions(cmd.Version, args, globalFlags) + options, err := repoFlags.ToOptions(args, globalFlags) if err != nil { return xerrors.Errorf("flag error: %w", err) } @@ -509,7 +499,7 @@ func NewConvertCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { if err := convertFlags.Bind(cmd); err != nil { return xerrors.Errorf("flag bind error: %w", err) } - opts, err := convertFlags.ToOptions(cmd.Version, args, globalFlags) + opts, err := convertFlags.ToOptions(args, globalFlags) if err != nil { return xerrors.Errorf("flag error: %w", err) } @@ -566,7 +556,7 @@ func NewClientCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { if err := clientFlags.Bind(cmd); err != nil { return xerrors.Errorf("flag bind error: %w", err) } - options, err := clientFlags.ToOptions(cmd.Version, args, globalFlags) + options, err := clientFlags.ToOptions(args, globalFlags) if err != nil { return xerrors.Errorf("flag error: %w", err) } @@ -607,7 +597,7 @@ func NewServerCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { if err := serverFlags.Bind(cmd); err != nil { return xerrors.Errorf("flag bind error: %w", err) } - options, err := serverFlags.ToOptions(cmd.Version, args, globalFlags) + options, err := serverFlags.ToOptions(args, globalFlags) if err != nil { return xerrors.Errorf("flag error: %w", err) } @@ -669,7 +659,7 @@ func NewConfigCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { if err := configFlags.Bind(cmd); err != nil { return xerrors.Errorf("flag bind error: %w", err) } - options, err := configFlags.ToOptions(cmd.Version, args, globalFlags) + options, err := configFlags.ToOptions(args, globalFlags) if err != nil { return xerrors.Errorf("flag error: %w", err) } @@ -742,7 +732,7 @@ func NewPluginCommand() *cobra.Command { if err != nil { return xerrors.Errorf("plugin list display error: %w", err) } - if _, err = fmt.Fprintf(os.Stdout, info); err != nil { + if _, err := fmt.Fprint(os.Stdout, info); err != nil { return xerrors.Errorf("print error: %w", err) } return nil @@ -759,7 +749,7 @@ func NewPluginCommand() *cobra.Command { if err != nil { return xerrors.Errorf("plugin information display error: %w", err) } - if _, err = fmt.Fprintf(os.Stdout, info); err != nil { + if _, err := fmt.Fprint(os.Stdout, info); err != nil { return xerrors.Errorf("print error: %w", err) } return nil @@ -827,7 +817,7 @@ func NewModuleCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { } repo := args[0] - opts, err := moduleFlags.ToOptions(cmd.Version, args, globalFlags) + opts, err := moduleFlags.ToOptions(args, globalFlags) if err != nil { return xerrors.Errorf("flag error: %w", err) } @@ -851,7 +841,7 @@ func NewModuleCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { } repo := args[0] - opts, err := moduleFlags.ToOptions(cmd.Version, args, globalFlags) + opts, err := moduleFlags.ToOptions(args, globalFlags) if err != nil { return xerrors.Errorf("flag error: %w", err) } @@ -940,7 +930,7 @@ func NewKubernetesCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { if err := k8sFlags.Bind(cmd); err != nil { return xerrors.Errorf("flag bind error: %w", err) } - opts, err := k8sFlags.ToOptions(cmd.Version, args, globalFlags) + opts, err := k8sFlags.ToOptions(args, globalFlags) if err != nil { return xerrors.Errorf("flag error: %w", err) } @@ -1007,7 +997,7 @@ The following services are supported: return nil }, RunE: func(cmd *cobra.Command, args []string) error { - opts, err := awsFlags.ToOptions(cmd.Version, args, globalFlags) + opts, err := awsFlags.ToOptions(args, globalFlags) if err != nil { return xerrors.Errorf("flag error: %w", err) } @@ -1071,7 +1061,7 @@ func NewVMCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { if err := vmFlags.Bind(cmd); err != nil { return xerrors.Errorf("flag bind error: %w", err) } - options, err := vmFlags.ToOptions(cmd.Version, args, globalFlags) + options, err := vmFlags.ToOptions(args, globalFlags) if err != nil { return xerrors.Errorf("flag error: %w", err) } @@ -1130,7 +1120,7 @@ func NewSBOMCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { if err := sbomFlags.Bind(cmd); err != nil { return xerrors.Errorf("flag bind error: %w", err) } - options, err := sbomFlags.ToOptions(cmd.Version, args, globalFlags) + options, err := sbomFlags.ToOptions(args, globalFlags) if err != nil { return xerrors.Errorf("flag error: %w", err) } @@ -1159,7 +1149,7 @@ func NewVersionCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { options := globalFlags.ToOptions() - return showVersion(options.CacheDir, versionFormat, cmd.Version, cmd.OutOrStdout()) + return showVersion(options.CacheDir, versionFormat, cmd.OutOrStdout()) }, SilenceErrors: true, SilenceUsage: true, @@ -1172,85 +1162,15 @@ func NewVersionCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { return cmd } -func showVersion(cacheDir, outputFormat, version string, w io.Writer) error { - var dbMeta *metadata.Metadata - var javadbMeta *metadata.Metadata - - mc := metadata.NewClient(cacheDir) - meta, err := mc.Get() - if err != nil { - log.Logger.Debugw("Failed to get DB metadata", "error", err) - } - if !meta.UpdatedAt.IsZero() && !meta.NextUpdate.IsZero() && meta.Version != 0 { - dbMeta = &metadata.Metadata{ - Version: meta.Version, - NextUpdate: meta.NextUpdate.UTC(), - UpdatedAt: meta.UpdatedAt.UTC(), - DownloadedAt: meta.DownloadedAt.UTC(), - } - } - - mcJava := javadb.NewMetadata(filepath.Join(cacheDir, "java-db")) - metaJava, err := mcJava.Get() - if err != nil { - log.Logger.Debugw("Failed to get Java DB metadata", "error", err) - } - if !metaJava.UpdatedAt.IsZero() && !metaJava.NextUpdate.IsZero() && metaJava.Version != 0 { - javadbMeta = &metadata.Metadata{ - Version: metaJava.Version, - NextUpdate: metaJava.NextUpdate.UTC(), - UpdatedAt: metaJava.UpdatedAt.UTC(), - DownloadedAt: metaJava.DownloadedAt.UTC(), - } - } - - var pbMeta *policy.Metadata - pc, err := policy.NewClient(cacheDir, false, "") - if pc != nil && err == nil { - pbMeta, err = pc.GetMetadata() - if err != nil { - log.Logger.Debugw("Failed to get policy metadata", "error", err) - } - } - +func showVersion(cacheDir, outputFormat string, w io.Writer) error { + versionInfo := version.NewVersionInfo(cacheDir) switch outputFormat { case "json": - err = json.NewEncoder(w).Encode(VersionInfo{ - Version: version, - VulnerabilityDB: dbMeta, - JavaDB: javadbMeta, - PolicyBundle: pbMeta, - }) - if err != nil { + if err := json.NewEncoder(w).Encode(versionInfo); err != nil { return xerrors.Errorf("json encode error: %w", err) } default: - output := fmt.Sprintf("Version: %s\n", version) - if dbMeta != nil { - output += fmt.Sprintf(`Vulnerability DB: - Version: %d - UpdatedAt: %s - NextUpdate: %s - DownloadedAt: %s -`, dbMeta.Version, dbMeta.UpdatedAt.UTC(), dbMeta.NextUpdate.UTC(), dbMeta.DownloadedAt.UTC()) - } - - if javadbMeta != nil { - output += fmt.Sprintf(`Java DB: - Version: %d - UpdatedAt: %s - NextUpdate: %s - DownloadedAt: %s -`, javadbMeta.Version, javadbMeta.UpdatedAt.UTC(), javadbMeta.NextUpdate.UTC(), javadbMeta.DownloadedAt.UTC()) - } - - if pbMeta != nil { - output += fmt.Sprintf(`Policy Bundle: - Digest: %s - DownloadedAt: %s -`, pbMeta.Digest, pbMeta.DownloadedAt.UTC()) - } - fmt.Fprintf(w, output) + fmt.Fprint(w, versionInfo.String()) } return nil } diff --git a/pkg/commands/app_test.go b/pkg/commands/app_test.go index 52cd1c513335..a8e94882ff9e 100644 --- a/pkg/commands/app_test.go +++ b/pkg/commands/app_test.go @@ -29,10 +29,10 @@ func Test_showVersion(t *testing.T) { name: "happy path, table output", args: args{ outputFormat: "table", - version: "v1.2.3", + version: "dev", cacheDir: "testdata", }, - want: `Version: v1.2.3 + want: `Version: dev Vulnerability DB: Version: 2 UpdatedAt: 2022-03-02 06:07:07.99504083 +0000 UTC @@ -52,17 +52,17 @@ Policy Bundle: name: "sad path, bogus cache dir", args: args{ outputFormat: "json", - version: "1.2.3", + version: "dev", cacheDir: "/foo/bar/bogus", }, - want: `{"Version":"1.2.3"} + want: `{"Version":"dev"} `, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := new(bytes.Buffer) - showVersion(tt.args.cacheDir, tt.args.outputFormat, tt.args.version, got) + showVersion(tt.args.cacheDir, tt.args.outputFormat, got) assert.Equal(t, tt.want, got.String(), tt.name) }) } @@ -70,7 +70,7 @@ Policy Bundle: // Check flag and command for print version func TestPrintVersion(t *testing.T) { - tableOutput := `Version: test + tableOutput := `Version: dev Vulnerability DB: Version: 2 UpdatedAt: 2022-03-02 06:07:07.99504083 +0000 UTC @@ -85,7 +85,7 @@ Policy Bundle: Digest: sha256:19a017cdc798631ad42f6f4dce823d77b2989128f0e1a7f9bc83ae3c59024edd DownloadedAt: 2023-03-02 01:06:08.191725 +0000 UTC ` - jsonOutput := `{"Version":"test","VulnerabilityDB":{"Version":2,"NextUpdate":"2022-03-02T12:07:07.99504023Z","UpdatedAt":"2022-03-02T06:07:07.99504083Z","DownloadedAt":"2022-03-02T10:03:38.383312Z"},"JavaDB":{"Version":1,"NextUpdate":"2023-03-17T00:47:02.774253254Z","UpdatedAt":"2023-03-14T00:47:02.774253754Z","DownloadedAt":"2023-03-14T03:04:55.058541039Z"},"PolicyBundle":{"Digest":"sha256:19a017cdc798631ad42f6f4dce823d77b2989128f0e1a7f9bc83ae3c59024edd","DownloadedAt":"2023-03-01T17:06:08.191725-08:00"}} + jsonOutput := `{"Version":"dev","VulnerabilityDB":{"Version":2,"NextUpdate":"2022-03-02T12:07:07.99504023Z","UpdatedAt":"2022-03-02T06:07:07.99504083Z","DownloadedAt":"2022-03-02T10:03:38.383312Z"},"JavaDB":{"Version":1,"NextUpdate":"2023-03-17T00:47:02.774253254Z","UpdatedAt":"2023-03-14T00:47:02.774253754Z","DownloadedAt":"2023-03-14T03:04:55.058541039Z"},"PolicyBundle":{"Digest":"sha256:19a017cdc798631ad42f6f4dce823d77b2989128f0e1a7f9bc83ae3c59024edd","DownloadedAt":"2023-03-02T01:06:08.191725Z"}} ` tests := []struct { name string @@ -157,7 +157,7 @@ Policy Bundle: for _, test := range tests { t.Run(test.name, func(t *testing.T) { got := new(bytes.Buffer) - app := NewApp("test") + app := NewApp() app.SetOut(got) app.SetArgs(test.arguments) @@ -257,7 +257,7 @@ func TestFlags(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { globalFlags := flag.NewGlobalFlagGroup() - rootCmd := NewRootCommand("dev", globalFlags) + rootCmd := NewRootCommand(globalFlags) rootCmd.SetErr(io.Discard) rootCmd.SetOut(io.Discard) @@ -270,7 +270,7 @@ func TestFlags(t *testing.T) { // Bind require.NoError(t, flags.Bind(cmd)) - options, err := flags.ToOptions("dev", args, globalFlags) + options, err := flags.ToOptions(args, globalFlags) require.NoError(t, err) assert.Equal(t, tt.want.format, options.Format) diff --git a/pkg/flag/options.go b/pkg/flag/options.go index 760438ba7947..caf723fa5bb7 100644 --- a/pkg/flag/options.go +++ b/pkg/flag/options.go @@ -19,6 +19,7 @@ import ( "github.com/aquasecurity/trivy/pkg/log" "github.com/aquasecurity/trivy/pkg/result" "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/version" xio "github.com/aquasecurity/trivy/pkg/x/io" xstrings "github.com/aquasecurity/trivy/pkg/x/strings" ) @@ -439,10 +440,10 @@ func (f *Flags) Bind(cmd *cobra.Command) error { } // nolint: gocyclo -func (f *Flags) ToOptions(appVersion string, args []string, globalFlags *GlobalFlagGroup) (Options, error) { +func (f *Flags) ToOptions(args []string, globalFlags *GlobalFlagGroup) (Options, error) { var err error opts := Options{ - AppVersion: appVersion, + AppVersion: version.AppVersion(), GlobalOptions: globalFlags.ToOptions(), } diff --git a/pkg/policy/policy.go b/pkg/policy/policy.go index ad154fc3eeb6..9ed95e6230e0 100644 --- a/pkg/policy/policy.go +++ b/pkg/policy/policy.go @@ -60,6 +60,13 @@ type Metadata struct { DownloadedAt time.Time } +func (m Metadata) String() string { + return fmt.Sprintf(`Policy Bundle: + Digest: %s + DownloadedAt: %s +`, m.Digest, m.DownloadedAt.UTC()) +} + // NewClient is the factory method for policy client func NewClient(cacheDir string, quiet bool, policyBundleRepo string, opts ...Option) (*Client, error) { o := &options{ diff --git a/pkg/rpc/server/listen.go b/pkg/rpc/server/listen.go index 8d03e7f73ba6..7a6d4c46eeb6 100644 --- a/pkg/rpc/server/listen.go +++ b/pkg/rpc/server/listen.go @@ -2,6 +2,7 @@ package server import ( "context" + "encoding/json" "net/http" "os" "sync" @@ -18,6 +19,7 @@ import ( "github.com/aquasecurity/trivy/pkg/fanal/types" "github.com/aquasecurity/trivy/pkg/log" "github.com/aquasecurity/trivy/pkg/utils/fsutils" + "github.com/aquasecurity/trivy/pkg/version" rpcCache "github.com/aquasecurity/trivy/rpc/cache" rpcScanner "github.com/aquasecurity/trivy/rpc/scanner" ) @@ -66,13 +68,14 @@ func (s Server) ListenAndServe(serverCache cache.Cache, skipDBUpdate bool) error } }() - mux := newServeMux(serverCache, dbUpdateWg, requestWg, s.token, s.tokenHeader) + mux := newServeMux(serverCache, dbUpdateWg, requestWg, s.token, s.tokenHeader, s.cacheDir) log.Logger.Infof("Listening %s...", s.addr) return http.ListenAndServe(s.addr, mux) } -func newServeMux(serverCache cache.Cache, dbUpdateWg, requestWg *sync.WaitGroup, token, tokenHeader string) *http.ServeMux { +func newServeMux(serverCache cache.Cache, dbUpdateWg, requestWg *sync.WaitGroup, + token, tokenHeader, cacheDir string) *http.ServeMux { withWaitGroup := func(base http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Stop processing requests during DB update @@ -103,6 +106,14 @@ func newServeMux(serverCache cache.Cache, dbUpdateWg, requestWg *sync.WaitGroup, } }) + mux.HandleFunc("/version", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + + if err := json.NewEncoder(w).Encode(version.NewVersionInfo(cacheDir)); err != nil { + log.Logger.Errorf("get version error: %s", err) + } + }) + return mux } diff --git a/pkg/rpc/server/listen_test.go b/pkg/rpc/server/listen_test.go index cebc41496962..834627b92762 100644 --- a/pkg/rpc/server/listen_test.go +++ b/pkg/rpc/server/listen_test.go @@ -2,6 +2,7 @@ package server import ( "context" + "encoding/json" "net/http" "net/http/httptest" "os" @@ -20,7 +21,9 @@ import ( dbFile "github.com/aquasecurity/trivy/pkg/db" "github.com/aquasecurity/trivy/pkg/fanal/cache" ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/policy" "github.com/aquasecurity/trivy/pkg/utils/fsutils" + "github.com/aquasecurity/trivy/pkg/version" rpcCache "github.com/aquasecurity/trivy/rpc/cache" ) @@ -251,7 +254,7 @@ func Test_newServeMux(t *testing.T) { defer func() { _ = c.Close() }() ts := httptest.NewServer(newServeMux( - c, dbUpdateWg, requestWg, tt.args.token, tt.args.tokenHeader), + c, dbUpdateWg, requestWg, tt.args.token, tt.args.tokenHeader, ""), ) defer ts.Close() @@ -274,3 +277,39 @@ func Test_newServeMux(t *testing.T) { }) } } + +func Test_VersionEndpoint(t *testing.T) { + dbUpdateWg, requestWg := &sync.WaitGroup{}, &sync.WaitGroup{} + c, err := cache.NewFSCache(t.TempDir()) + require.NoError(t, err) + defer func() { _ = c.Close() }() + + ts := httptest.NewServer(newServeMux( + c, dbUpdateWg, requestWg, "", "", "testdata/testcache"), + ) + defer ts.Close() + + resp, err := http.Get(ts.URL + "/version") + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var versionInfo version.VersionInfo + require.NoError(t, json.NewDecoder(resp.Body).Decode(&versionInfo)) + + expected := version.VersionInfo{ + Version: "dev", + VulnerabilityDB: &metadata.Metadata{ + Version: 2, + NextUpdate: time.Date(2023, 7, 20, 18, 11, 37, 696263532, time.UTC), + UpdatedAt: time.Date(2023, 7, 20, 12, 11, 37, 696263932, time.UTC), + DownloadedAt: time.Date(2023, 7, 25, 7, 1, 41, 239158000, time.UTC), + }, + PolicyBundle: &policy.Metadata{ + Digest: "sha256:829832357626da2677955e3b427191212978ba20012b6eaa03229ca28569ae43", + DownloadedAt: time.Date(2023, 7, 23, 16, 40, 33, 122462000, time.UTC), + }, + } + assert.Equal(t, expected, versionInfo) +} diff --git a/pkg/rpc/server/testdata/testcache/db/metadata.json b/pkg/rpc/server/testdata/testcache/db/metadata.json new file mode 100644 index 000000000000..e9a4157ed5c5 --- /dev/null +++ b/pkg/rpc/server/testdata/testcache/db/metadata.json @@ -0,0 +1 @@ +{"Version":2,"NextUpdate":"2023-07-20T18:11:37.696263532Z","UpdatedAt":"2023-07-20T12:11:37.696263932Z","DownloadedAt":"2023-07-25T07:01:41.239158Z"} \ No newline at end of file diff --git a/pkg/rpc/server/testdata/testcache/policy/metadata.json b/pkg/rpc/server/testdata/testcache/policy/metadata.json new file mode 100644 index 000000000000..18f6f4801597 --- /dev/null +++ b/pkg/rpc/server/testdata/testcache/policy/metadata.json @@ -0,0 +1 @@ +{"Digest":"sha256:829832357626da2677955e3b427191212978ba20012b6eaa03229ca28569ae43","DownloadedAt":"2023-07-23T17:40:33.122462+01:00"} \ No newline at end of file diff --git a/pkg/version/testdata/testcache/db/metadata.json b/pkg/version/testdata/testcache/db/metadata.json new file mode 100644 index 000000000000..e9a4157ed5c5 --- /dev/null +++ b/pkg/version/testdata/testcache/db/metadata.json @@ -0,0 +1 @@ +{"Version":2,"NextUpdate":"2023-07-20T18:11:37.696263532Z","UpdatedAt":"2023-07-20T12:11:37.696263932Z","DownloadedAt":"2023-07-25T07:01:41.239158Z"} \ No newline at end of file diff --git a/pkg/version/testdata/testcache/java-db/metadata.json b/pkg/version/testdata/testcache/java-db/metadata.json new file mode 100644 index 000000000000..b8c43fa523c6 --- /dev/null +++ b/pkg/version/testdata/testcache/java-db/metadata.json @@ -0,0 +1 @@ +{"Version":1,"NextUpdate":"2023-07-28T01:03:52.169192565Z","UpdatedAt":"2023-07-25T01:03:52.169192765Z","DownloadedAt":"2023-07-25T09:37:48.906152Z"} diff --git a/pkg/version/testdata/testcache/policy/metadata.json b/pkg/version/testdata/testcache/policy/metadata.json new file mode 100644 index 000000000000..18f6f4801597 --- /dev/null +++ b/pkg/version/testdata/testcache/policy/metadata.json @@ -0,0 +1 @@ +{"Digest":"sha256:829832357626da2677955e3b427191212978ba20012b6eaa03229ca28569ae43","DownloadedAt":"2023-07-23T17:40:33.122462+01:00"} \ No newline at end of file diff --git a/pkg/version/version.go b/pkg/version/version.go new file mode 100644 index 000000000000..421fff6f1e6e --- /dev/null +++ b/pkg/version/version.go @@ -0,0 +1,107 @@ +package version + +import ( + "fmt" + "path/filepath" + + "github.com/aquasecurity/trivy-db/pkg/metadata" + javadb "github.com/aquasecurity/trivy-java-db/pkg/db" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/policy" +) + +var ( + ver = "dev" +) + +func AppVersion() string { + return ver +} + +type VersionInfo struct { + Version string `json:",omitempty"` + VulnerabilityDB *metadata.Metadata `json:",omitempty"` + JavaDB *metadata.Metadata `json:",omitempty"` + PolicyBundle *policy.Metadata `json:",omitempty"` +} + +func formatDBMetadata(title string, meta metadata.Metadata) string { + return fmt.Sprintf(`%s: + Version: %d + UpdatedAt: %s + NextUpdate: %s + DownloadedAt: %s +`, title, meta.Version, meta.UpdatedAt.UTC(), meta.NextUpdate.UTC(), meta.DownloadedAt.UTC()) +} + +func (v *VersionInfo) String() string { + output := fmt.Sprintf("Version: %s\n", v.Version) + if v.VulnerabilityDB != nil { + output += formatDBMetadata("Vulnerability DB", *v.VulnerabilityDB) + } + if v.JavaDB != nil { + output += formatDBMetadata("Java DB", *v.JavaDB) + } + if v.PolicyBundle != nil { + output += v.PolicyBundle.String() + } + return output +} + +func NewVersionInfo(cacheDir string) VersionInfo { + var dbMeta *metadata.Metadata + var javadbMeta *metadata.Metadata + + mc := metadata.NewClient(cacheDir) + meta, err := mc.Get() + if err != nil { + log.Logger.Debugw("Failed to get DB metadata", "error", err) + } + if !meta.UpdatedAt.IsZero() && !meta.NextUpdate.IsZero() && meta.Version != 0 { + dbMeta = &metadata.Metadata{ + Version: meta.Version, + NextUpdate: meta.NextUpdate.UTC(), + UpdatedAt: meta.UpdatedAt.UTC(), + DownloadedAt: meta.DownloadedAt.UTC(), + } + } + + mcJava := javadb.NewMetadata(filepath.Join(cacheDir, "java-db")) + metaJava, err := mcJava.Get() + if err != nil { + log.Logger.Debugw("Failed to get Java DB metadata", "error", err) + } + if !metaJava.UpdatedAt.IsZero() && !metaJava.NextUpdate.IsZero() && metaJava.Version != 0 { + javadbMeta = &metadata.Metadata{ + Version: metaJava.Version, + NextUpdate: metaJava.NextUpdate.UTC(), + UpdatedAt: metaJava.UpdatedAt.UTC(), + DownloadedAt: metaJava.DownloadedAt.UTC(), + } + } + + var pbMeta *policy.Metadata + pc, err := policy.NewClient(cacheDir, false, "") + if err != nil { + log.Logger.Debugw("Failed to instantiate policy client", "error", err) + } + if pc != nil && err == nil { + pbMetaRaw, err := pc.GetMetadata() + + if err != nil { + log.Logger.Debugw("Failed to get policy metadata", "error", err) + } else { + pbMeta = &policy.Metadata{ + Digest: pbMetaRaw.Digest, + DownloadedAt: pbMetaRaw.DownloadedAt.UTC(), + } + } + } + + return VersionInfo{ + Version: ver, + VulnerabilityDB: dbMeta, + JavaDB: javadbMeta, + PolicyBundle: pbMeta, + } +} diff --git a/pkg/version/version_test.go b/pkg/version/version_test.go new file mode 100644 index 000000000000..458b5f7b89a6 --- /dev/null +++ b/pkg/version/version_test.go @@ -0,0 +1,54 @@ +package version + +import ( + "testing" + "time" + + "github.com/aquasecurity/trivy-db/pkg/metadata" + "github.com/aquasecurity/trivy/pkg/policy" + "github.com/stretchr/testify/assert" +) + +func Test_BuildVersionInfo(t *testing.T) { + + expected := VersionInfo{ + Version: "dev", + VulnerabilityDB: &metadata.Metadata{ + Version: 2, + NextUpdate: time.Date(2023, 7, 20, 18, 11, 37, 696263532, time.UTC), + UpdatedAt: time.Date(2023, 7, 20, 12, 11, 37, 696263932, time.UTC), + DownloadedAt: time.Date(2023, 7, 25, 7, 1, 41, 239158000, time.UTC), + }, + JavaDB: &metadata.Metadata{ + Version: 1, + NextUpdate: time.Date(2023, 7, 28, 1, 3, 52, 169192565, time.UTC), + UpdatedAt: time.Date(2023, 7, 25, 1, 3, 52, 169192765, time.UTC), + DownloadedAt: time.Date(2023, 7, 25, 9, 37, 48, 906152000, time.UTC), + }, + PolicyBundle: &policy.Metadata{ + Digest: "sha256:829832357626da2677955e3b427191212978ba20012b6eaa03229ca28569ae43", + DownloadedAt: time.Date(2023, 7, 23, 16, 40, 33, 122462000, time.UTC), + }, + } + assert.Equal(t, expected, NewVersionInfo("testdata/testcache")) +} + +func Test_VersionInfoString(t *testing.T) { + expected := `Version: dev +Vulnerability DB: + Version: 2 + UpdatedAt: 2023-07-20 12:11:37.696263932 +0000 UTC + NextUpdate: 2023-07-20 18:11:37.696263532 +0000 UTC + DownloadedAt: 2023-07-25 07:01:41.239158 +0000 UTC +Java DB: + Version: 1 + UpdatedAt: 2023-07-25 01:03:52.169192765 +0000 UTC + NextUpdate: 2023-07-28 01:03:52.169192565 +0000 UTC + DownloadedAt: 2023-07-25 09:37:48.906152 +0000 UTC +Policy Bundle: + Digest: sha256:829832357626da2677955e3b427191212978ba20012b6eaa03229ca28569ae43 + DownloadedAt: 2023-07-23 16:40:33.122462 +0000 UTC +` + versionInfo := NewVersionInfo("testdata/testcache") + assert.Equal(t, expected, versionInfo.String()) +}