mirror of
https://github.com/aquasecurity/trivy.git
synced 2026-01-31 13:53:14 +08:00
feat(cli): Add available version checking (#8553)
Signed-off-by: Owen Rumney <owen.rumney@aquasec.com> Co-authored-by: Teppei Fukuda <knqyf263@gmail.com> Co-authored-by: Itay <itay@itaysk.com> Co-authored-by: simar7 <1254783+simar7@users.noreply.github.com>
This commit is contained in:
@@ -75,3 +75,8 @@ Trivy might attempt to connect (over HTTPS) to the following URLs:
|
||||
### Offline mode
|
||||
|
||||
There's no way to leverage Maven Central in a network-restricted environment, but you can prevent Trivy from trying to connect to it by using the `--offline-scan` flag.
|
||||
|
||||
## Check updates service
|
||||
|
||||
Trivy [checks for updates](../configuration/others.md#check-for-updates) and [collects usage telemetry](../advanced/telemetry.md) by connecting to the following domain: `https://check.trivy.dev`.
|
||||
Connectivity with this domain is entirely optional and is not necessary for the normal operation of Trivy.
|
||||
|
||||
33
docs/docs/advanced/telemetry.md
Normal file
33
docs/docs/advanced/telemetry.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Usage Telemetry
|
||||
|
||||
Trivy collect anonymous usage data in order to help us improve the product. This document explains what is collected and how you can control it.
|
||||
|
||||
## Data collected
|
||||
|
||||
The following information could be collected:
|
||||
|
||||
- Environmental information
|
||||
- Installation identifier
|
||||
- Trivy version
|
||||
- Operating system
|
||||
- Scan
|
||||
- Non-revealing scan options
|
||||
|
||||
## Privacy
|
||||
|
||||
No personal information, scan results, or sensitive data is specifically collected. We take the following measures to ensure that:
|
||||
|
||||
- Installation identifier: one-way hash of machine fingerprint, resulting in opaque string.
|
||||
- Scaner: any option that is user controlled is omitted (never collected). For example, file paths, image names, etc are never collected.
|
||||
|
||||
Trivy is an Aqua Security product and adheres to the company's privacy policy: <https://aquasec.com/privacy>.
|
||||
|
||||
## Disabling telemetry
|
||||
|
||||
You can disable telemetry altogether using the `--disable-telemetry` flag. Like other Trivy flags, this can be set on the command line, YAML configuration file, or environment variable. For more details see [here](../configuration/index.md).
|
||||
|
||||
For example:
|
||||
|
||||
```bash
|
||||
trivy image --disable-metrics alpine
|
||||
```
|
||||
@@ -160,3 +160,14 @@ When we want to get the image `alpine` with the settings above. The logic will b
|
||||
1. Try to get the image from `mirror.with.bad.auth/library/alpine`, but we get an error because there are no credentials for this registry.
|
||||
2. Try to get the image from `mirror.without.image/library/alpine`, but we get an error because this registry doesn't have this image (but most likely it will be an error about authorization).
|
||||
3. Get the image from `index.docker.io` (the original registry).
|
||||
|
||||
## Check for updates
|
||||
|
||||
Trivy periodically checks for updates and notices, and displays a message to the user with recommendations.
|
||||
Updates checking is non-blocking and has no impact on scanning time, performance, results, or any user experience aspect besides displaying the message.
|
||||
You can disable updates checking by specifying the `--skip-version-check` flag.
|
||||
|
||||
## Telemetry
|
||||
|
||||
Trivy collected usage data for product improvement. More details in the [Telemetry document](../advanced/telemetry.md).
|
||||
You can disable telemetry collection using the `--disable-telemetry` flag.
|
||||
|
||||
@@ -35,6 +35,7 @@ trivy filesystem [flags] PATH
|
||||
- "precise": Prioritizes precise by minimizing false positives.
|
||||
- "comprehensive": Aims to detect more security findings at the cost of potential false positives.
|
||||
(allowed values: precise,comprehensive) (default "precise")
|
||||
--disable-telemetry disable sending anonymous usage data to Aqua
|
||||
--distro string [EXPERIMENTAL] specify a distribution, <family>/<version>
|
||||
--download-db-only download/update vulnerability database but don't run a scan
|
||||
--download-java-db-only download/update Java index database but don't run a scan
|
||||
@@ -126,6 +127,7 @@ trivy filesystem [flags] PATH
|
||||
--skip-dirs strings specify the directories or glob patterns to skip
|
||||
--skip-files strings specify the files or glob patterns to skip
|
||||
--skip-java-db-update skip updating Java index database
|
||||
--skip-version-check suppress notices about version updates and Trivy announcements
|
||||
--skip-vex-repo-update [EXPERIMENTAL] Skip VEX Repository update
|
||||
--table-mode strings [EXPERIMENTAL] tables that will be displayed in 'table' format (allowed values: summary,detailed) (default [summary,detailed])
|
||||
-t, --template string output template
|
||||
|
||||
@@ -49,6 +49,7 @@ trivy image [flags] IMAGE_NAME
|
||||
- "precise": Prioritizes precise by minimizing false positives.
|
||||
- "comprehensive": Aims to detect more security findings at the cost of potential false positives.
|
||||
(allowed values: precise,comprehensive) (default "precise")
|
||||
--disable-telemetry disable sending anonymous usage data to Aqua
|
||||
--distro string [EXPERIMENTAL] specify a distribution, <family>/<version>
|
||||
--docker-host string unix domain socket path to use for docker scanning
|
||||
--download-db-only download/update vulnerability database but don't run a scan
|
||||
@@ -148,6 +149,7 @@ trivy image [flags] IMAGE_NAME
|
||||
--skip-dirs strings specify the directories or glob patterns to skip
|
||||
--skip-files strings specify the files or glob patterns to skip
|
||||
--skip-java-db-update skip updating Java index database
|
||||
--skip-version-check suppress notices about version updates and Trivy announcements
|
||||
--skip-vex-repo-update [EXPERIMENTAL] Skip VEX Repository update
|
||||
--table-mode strings [EXPERIMENTAL] tables that will be displayed in 'table' format (allowed values: summary,detailed) (default [summary,detailed])
|
||||
-t, --template string output template
|
||||
|
||||
@@ -52,6 +52,7 @@ trivy kubernetes [flags] [CONTEXT]
|
||||
- "comprehensive": Aims to detect more security findings at the cost of potential false positives.
|
||||
(allowed values: precise,comprehensive) (default "precise")
|
||||
--disable-node-collector When the flag is activated, the node-collector job will not be executed, thus skipping misconfiguration findings on the node.
|
||||
--disable-telemetry disable sending anonymous usage data to Aqua
|
||||
--distro string [EXPERIMENTAL] specify a distribution, <family>/<version>
|
||||
--download-db-only download/update vulnerability database but don't run a scan
|
||||
--download-java-db-only download/update Java index database but don't run a scan
|
||||
@@ -138,6 +139,7 @@ trivy kubernetes [flags] [CONTEXT]
|
||||
--skip-files strings specify the files or glob patterns to skip
|
||||
--skip-images skip the downloading and scanning of images (vulnerabilities and secrets) in the cluster resources
|
||||
--skip-java-db-update skip updating Java index database
|
||||
--skip-version-check suppress notices about version updates and Trivy announcements
|
||||
--skip-vex-repo-update [EXPERIMENTAL] Skip VEX Repository update
|
||||
-t, --template string output template
|
||||
--tf-exclude-downloaded-modules exclude misconfigurations for downloaded terraform modules
|
||||
|
||||
@@ -35,6 +35,7 @@ trivy repository [flags] (REPO_PATH | REPO_URL)
|
||||
- "precise": Prioritizes precise by minimizing false positives.
|
||||
- "comprehensive": Aims to detect more security findings at the cost of potential false positives.
|
||||
(allowed values: precise,comprehensive) (default "precise")
|
||||
--disable-telemetry disable sending anonymous usage data to Aqua
|
||||
--download-db-only download/update vulnerability database but don't run a scan
|
||||
--download-java-db-only download/update Java index database but don't run a scan
|
||||
--enable-modules strings [EXPERIMENTAL] module names to enable
|
||||
@@ -124,6 +125,7 @@ trivy repository [flags] (REPO_PATH | REPO_URL)
|
||||
--skip-dirs strings specify the directories or glob patterns to skip
|
||||
--skip-files strings specify the files or glob patterns to skip
|
||||
--skip-java-db-update skip updating Java index database
|
||||
--skip-version-check suppress notices about version updates and Trivy announcements
|
||||
--skip-vex-repo-update [EXPERIMENTAL] Skip VEX Repository update
|
||||
--table-mode strings [EXPERIMENTAL] tables that will be displayed in 'table' format (allowed values: summary,detailed) (default [summary,detailed])
|
||||
--tag string pass the tag name to be scanned
|
||||
|
||||
@@ -37,6 +37,7 @@ trivy rootfs [flags] ROOTDIR
|
||||
- "precise": Prioritizes precise by minimizing false positives.
|
||||
- "comprehensive": Aims to detect more security findings at the cost of potential false positives.
|
||||
(allowed values: precise,comprehensive) (default "precise")
|
||||
--disable-telemetry disable sending anonymous usage data to Aqua
|
||||
--distro string [EXPERIMENTAL] specify a distribution, <family>/<version>
|
||||
--download-db-only download/update vulnerability database but don't run a scan
|
||||
--download-java-db-only download/update Java index database but don't run a scan
|
||||
@@ -127,6 +128,7 @@ trivy rootfs [flags] ROOTDIR
|
||||
--skip-dirs strings specify the directories or glob patterns to skip
|
||||
--skip-files strings specify the files or glob patterns to skip
|
||||
--skip-java-db-update skip updating Java index database
|
||||
--skip-version-check suppress notices about version updates and Trivy announcements
|
||||
--skip-vex-repo-update [EXPERIMENTAL] Skip VEX Repository update
|
||||
--table-mode strings [EXPERIMENTAL] tables that will be displayed in 'table' format (allowed values: summary,detailed) (default [summary,detailed])
|
||||
-t, --template string output template
|
||||
|
||||
@@ -29,6 +29,7 @@ trivy sbom [flags] SBOM_PATH
|
||||
- "precise": Prioritizes precise by minimizing false positives.
|
||||
- "comprehensive": Aims to detect more security findings at the cost of potential false positives.
|
||||
(allowed values: precise,comprehensive) (default "precise")
|
||||
--disable-telemetry disable sending anonymous usage data to Aqua
|
||||
--distro string [EXPERIMENTAL] specify a distribution, <family>/<version>
|
||||
--download-db-only download/update vulnerability database but don't run a scan
|
||||
--download-java-db-only download/update Java index database but don't run a scan
|
||||
@@ -99,6 +100,7 @@ trivy sbom [flags] SBOM_PATH
|
||||
--show-suppressed [EXPERIMENTAL] show suppressed vulnerabilities
|
||||
--skip-db-update skip updating vulnerability database
|
||||
--skip-java-db-update skip updating Java index database
|
||||
--skip-version-check suppress notices about version updates and Trivy announcements
|
||||
--skip-vex-repo-update [EXPERIMENTAL] Skip VEX Repository update
|
||||
--table-mode strings [EXPERIMENTAL] tables that will be displayed in 'table' format (allowed values: summary,detailed) (default [summary,detailed])
|
||||
-t, --template string output template
|
||||
|
||||
@@ -33,6 +33,7 @@ trivy vm [flags] VM_IMAGE
|
||||
- "precise": Prioritizes precise by minimizing false positives.
|
||||
- "comprehensive": Aims to detect more security findings at the cost of potential false positives.
|
||||
(allowed values: precise,comprehensive) (default "precise")
|
||||
--disable-telemetry disable sending anonymous usage data to Aqua
|
||||
--distro string [EXPERIMENTAL] specify a distribution, <family>/<version>
|
||||
--download-db-only download/update vulnerability database but don't run a scan
|
||||
--download-java-db-only download/update Java index database but don't run a scan
|
||||
@@ -115,6 +116,7 @@ trivy vm [flags] VM_IMAGE
|
||||
--skip-dirs strings specify the directories or glob patterns to skip
|
||||
--skip-files strings specify the files or glob patterns to skip
|
||||
--skip-java-db-update skip updating Java index database
|
||||
--skip-version-check suppress notices about version updates and Trivy announcements
|
||||
--skip-vex-repo-update [EXPERIMENTAL] Skip VEX Repository update
|
||||
--table-mode strings [EXPERIMENTAL] tables that will be displayed in 'table' format (allowed values: summary,detailed) (default [summary,detailed])
|
||||
-t, --template string output template
|
||||
|
||||
@@ -586,6 +586,9 @@ scan:
|
||||
# Same as '--detection-priority'
|
||||
detection-priority: "precise"
|
||||
|
||||
# Same as '--disable-telemetry'
|
||||
disable-telemetry: false
|
||||
|
||||
# Same as '--distro'
|
||||
distro: ""
|
||||
|
||||
@@ -615,6 +618,9 @@ scan:
|
||||
# Same as '--skip-files'
|
||||
skip-files: []
|
||||
|
||||
# Same as '--skip-version-check'
|
||||
skip-version-check: false
|
||||
|
||||
```
|
||||
## Secret options
|
||||
|
||||
|
||||
@@ -158,6 +158,7 @@ nav:
|
||||
- GCR (Google Container Registry): docs/advanced/private-registries/gcr.md
|
||||
- ACR (Azure Container Registry): docs/advanced/private-registries/acr.md
|
||||
- Self-Hosted: docs/advanced/private-registries/self.md
|
||||
- Usage Telemetry: docs/advanced/telemetry.md
|
||||
- References:
|
||||
- Configuration:
|
||||
- CLI:
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"github.com/aquasecurity/trivy/pkg/log"
|
||||
"github.com/aquasecurity/trivy/pkg/misconf"
|
||||
"github.com/aquasecurity/trivy/pkg/module"
|
||||
"github.com/aquasecurity/trivy/pkg/notification"
|
||||
"github.com/aquasecurity/trivy/pkg/policy"
|
||||
pkgReport "github.com/aquasecurity/trivy/pkg/report"
|
||||
"github.com/aquasecurity/trivy/pkg/result"
|
||||
@@ -92,6 +93,7 @@ type Runner interface {
|
||||
|
||||
type runner struct {
|
||||
initializeScanService InitializeScanService
|
||||
versionChecker *notification.VersionChecker
|
||||
dbOpen bool
|
||||
|
||||
// WASM modules
|
||||
@@ -116,6 +118,13 @@ func NewRunner(ctx context.Context, cliOptions flag.Options, opts ...RunnerOptio
|
||||
opt(r)
|
||||
}
|
||||
|
||||
// If the user has not disabled notices or is running in quiet mode
|
||||
r.versionChecker = notification.NewVersionChecker(
|
||||
notification.WithSkipVersionCheck(cliOptions.SkipVersionCheck),
|
||||
notification.WithQuietMode(cliOptions.Quiet),
|
||||
notification.WithTelemetryDisabled(cliOptions.DisableTelemetry),
|
||||
)
|
||||
|
||||
// Update the vulnerability database if needed.
|
||||
if err := r.initDB(ctx, cliOptions); err != nil {
|
||||
return nil, xerrors.Errorf("DB error: %w", err)
|
||||
@@ -137,6 +146,13 @@ func NewRunner(ctx context.Context, cliOptions flag.Options, opts ...RunnerOptio
|
||||
m.Register()
|
||||
r.module = m
|
||||
|
||||
// Make a silent attempt to check for updates in the background
|
||||
// only do this if the user has not disabled notices or is running
|
||||
// in quiet mode
|
||||
if r.versionChecker != nil {
|
||||
r.versionChecker.RunUpdateCheck(ctx, os.Args[1:])
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
@@ -152,6 +168,12 @@ func (r *runner) Close(ctx context.Context) error {
|
||||
if err := r.module.Close(ctx); err != nil {
|
||||
errs = multierror.Append(errs, err)
|
||||
}
|
||||
|
||||
// silently check if there is notifications
|
||||
if r.versionChecker != nil {
|
||||
r.versionChecker.PrintNotices(os.Stderr)
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
|
||||
@@ -118,6 +118,16 @@ var (
|
||||
ConfigName: "scan.distro",
|
||||
Usage: "[EXPERIMENTAL] specify a distribution, <family>/<version>",
|
||||
}
|
||||
SkipVersionCheckFlag = Flag[bool]{
|
||||
Name: "skip-version-check",
|
||||
ConfigName: "scan.skip-version-check",
|
||||
Usage: "suppress notices about version updates and Trivy announcements",
|
||||
}
|
||||
DisableTelemetryFlag = Flag[bool]{
|
||||
Name: "disable-telemetry",
|
||||
ConfigName: "scan.disable-telemetry",
|
||||
Usage: "disable sending anonymous usage data to Aqua",
|
||||
}
|
||||
)
|
||||
|
||||
type ScanFlagGroup struct {
|
||||
@@ -132,6 +142,8 @@ type ScanFlagGroup struct {
|
||||
RekorURL *Flag[string]
|
||||
DetectionPriority *Flag[string]
|
||||
DistroFlag *Flag[string]
|
||||
SkipVersionCheck *Flag[bool]
|
||||
DisableTelemetry *Flag[bool]
|
||||
}
|
||||
|
||||
type ScanOptions struct {
|
||||
@@ -146,6 +158,8 @@ type ScanOptions struct {
|
||||
RekorURL string
|
||||
DetectionPriority ftypes.DetectionPriority
|
||||
Distro ftypes.OS
|
||||
SkipVersionCheck bool
|
||||
DisableTelemetry bool
|
||||
}
|
||||
|
||||
func NewScanFlagGroup() *ScanFlagGroup {
|
||||
@@ -161,6 +175,8 @@ func NewScanFlagGroup() *ScanFlagGroup {
|
||||
Slow: SlowFlag.Clone(),
|
||||
DetectionPriority: DetectionPriority.Clone(),
|
||||
DistroFlag: DistroFlag.Clone(),
|
||||
SkipVersionCheck: SkipVersionCheckFlag.Clone(),
|
||||
DisableTelemetry: DisableTelemetryFlag.Clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,6 +197,8 @@ func (f *ScanFlagGroup) Flags() []Flagger {
|
||||
f.RekorURL,
|
||||
f.DetectionPriority,
|
||||
f.DistroFlag,
|
||||
f.SkipVersionCheck,
|
||||
f.DisableTelemetry,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,6 +238,8 @@ func (f *ScanFlagGroup) ToOptions(opts *Options) error {
|
||||
RekorURL: f.RekorURL.Value(),
|
||||
DetectionPriority: ftypes.DetectionPriority(f.DetectionPriority.Value()),
|
||||
Distro: distro,
|
||||
SkipVersionCheck: f.SkipVersionCheck.Value(),
|
||||
DisableTelemetry: f.DisableTelemetry.Value(),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -14,11 +14,12 @@ import (
|
||||
|
||||
func TestScanFlagGroup_ToOptions(t *testing.T) {
|
||||
type fields struct {
|
||||
skipDirs []string
|
||||
skipFiles []string
|
||||
offlineScan bool
|
||||
scanners string
|
||||
distro string
|
||||
skipDirs []string
|
||||
skipFiles []string
|
||||
offlineScan bool
|
||||
scanners string
|
||||
distro string
|
||||
skipVersionCheck bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -127,6 +128,16 @@ func TestScanFlagGroup_ToOptions(t *testing.T) {
|
||||
},
|
||||
assertion: require.Error,
|
||||
},
|
||||
{
|
||||
name: "skip version check flag",
|
||||
fields: fields{
|
||||
skipVersionCheck: true,
|
||||
},
|
||||
want: flag.ScanOptions{
|
||||
SkipVersionCheck: true,
|
||||
},
|
||||
assertion: require.NoError,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -137,14 +148,16 @@ func TestScanFlagGroup_ToOptions(t *testing.T) {
|
||||
setValue(flag.OfflineScanFlag.ConfigName, tt.fields.offlineScan)
|
||||
setValue(flag.ScannersFlag.ConfigName, tt.fields.scanners)
|
||||
setValue(flag.DistroFlag.ConfigName, tt.fields.distro)
|
||||
setValue(flag.SkipVersionCheckFlag.ConfigName, tt.fields.skipVersionCheck)
|
||||
|
||||
// Assert options
|
||||
f := &flag.ScanFlagGroup{
|
||||
SkipDirs: flag.SkipDirsFlag.Clone(),
|
||||
SkipFiles: flag.SkipFilesFlag.Clone(),
|
||||
OfflineScan: flag.OfflineScanFlag.Clone(),
|
||||
Scanners: flag.ScannersFlag.Clone(),
|
||||
DistroFlag: flag.DistroFlag.Clone(),
|
||||
SkipDirs: flag.SkipDirsFlag.Clone(),
|
||||
SkipFiles: flag.SkipFilesFlag.Clone(),
|
||||
OfflineScan: flag.OfflineScanFlag.Clone(),
|
||||
Scanners: flag.ScannersFlag.Clone(),
|
||||
DistroFlag: flag.DistroFlag.Clone(),
|
||||
SkipVersionCheck: flag.SkipVersionCheckFlag.Clone(),
|
||||
}
|
||||
|
||||
flags := flag.Flags{f}
|
||||
|
||||
47
pkg/notification/identifier.go
Normal file
47
pkg/notification/identifier.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package notification
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func getMachineIdentifier() (string, error) {
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
interfaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var macAddr string
|
||||
for _, iface := range interfaces {
|
||||
if iface.HardwareAddr.String() != "" {
|
||||
macAddr = iface.HardwareAddr.String()
|
||||
break
|
||||
}
|
||||
}
|
||||
identifier := fmt.Sprintf("%s-%s-%s", hostname, macAddr, strings.ToLower(hostname))
|
||||
|
||||
return identifier, nil
|
||||
}
|
||||
|
||||
func generateMachineHash(identifier string) string {
|
||||
hash := sha256.New()
|
||||
hash.Write([]byte(identifier))
|
||||
return hex.EncodeToString(hash.Sum(nil))
|
||||
}
|
||||
|
||||
func uniqueIdentifier() string {
|
||||
identifier, err := getMachineIdentifier()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return generateMachineHash(fmt.Sprintf("trivy-%s", identifier))
|
||||
}
|
||||
28
pkg/notification/identifier_test.go
Normal file
28
pkg/notification/identifier_test.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package notification
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGenerateMachineHash(t *testing.T) {
|
||||
// Test with known input
|
||||
identifier := "test-identifier"
|
||||
hash := generateMachineHash(identifier)
|
||||
|
||||
// Known hash for "test-identifier"
|
||||
expectedHash := "115ae872eb1d3e23f9de03f7ab344193b21068812ee52eb37e8169e6d093c7ae"
|
||||
assert.Equal(t, expectedHash, hash)
|
||||
}
|
||||
|
||||
// This test requires some modification to the original code to make it testable
|
||||
// by injecting mocked network interfaces
|
||||
func TestGetMachineIdentifier(t *testing.T) {
|
||||
// This is a basic test that at least ensures the function returns without error
|
||||
// A more complete test would mock os.Hostname and net.Interfaces
|
||||
identifier, err := getMachineIdentifier()
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, identifier)
|
||||
}
|
||||
214
pkg/notification/notice.go
Normal file
214
pkg/notification/notice.go
Normal file
@@ -0,0 +1,214 @@
|
||||
package notification
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aquasecurity/trivy/pkg/log"
|
||||
"github.com/aquasecurity/trivy/pkg/version/app"
|
||||
)
|
||||
|
||||
// flexibleTime is a custom time type that can handle
|
||||
// different date formats in JSON. It implements the
|
||||
// UnmarshalJSON method to parse the date string into a time.Time object.
|
||||
type flexibleTime struct {
|
||||
time.Time
|
||||
}
|
||||
|
||||
type versionInfo struct {
|
||||
LatestVersion string `json:"latest_version"`
|
||||
LatestDate flexibleTime `json:"latest_date"`
|
||||
}
|
||||
|
||||
type announcement struct {
|
||||
FromDate time.Time `json:"from_date"`
|
||||
ToDate time.Time `json:"to_date"`
|
||||
Announcement string `json:"announcement"`
|
||||
}
|
||||
|
||||
type updateResponse struct {
|
||||
Trivy versionInfo `json:"trivy"`
|
||||
Announcements []announcement `json:"announcements"`
|
||||
Warnings []string `json:"warnings"`
|
||||
}
|
||||
|
||||
type VersionChecker struct {
|
||||
updatesApi string
|
||||
skipUpdateCheck bool
|
||||
quiet bool
|
||||
telemetryDisabled bool
|
||||
|
||||
done bool
|
||||
responseReceived bool
|
||||
currentVersion string
|
||||
latestVersion updateResponse
|
||||
}
|
||||
|
||||
// NewVersionChecker creates a new VersionChecker with the default
|
||||
// updates API URL. The URL can be overridden by passing an Option
|
||||
// to the NewVersionChecker function.
|
||||
func NewVersionChecker(opts ...Option) *VersionChecker {
|
||||
v := &VersionChecker{
|
||||
updatesApi: "https://check.trivy.dev/updates",
|
||||
currentVersion: app.Version(),
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(v)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// RunUpdateCheck makes a best efforts request to determine the
|
||||
// latest version and any announcements
|
||||
// Logic:
|
||||
// 1. if skipUpdateCheck is true AND telemetryDisabled are both true, skip the request
|
||||
// 2. if skipUpdateCheck is true AND telemetryDisabled is false, run check with metric details but suppress output
|
||||
// 3. if skipUpdateCheck is false AND telemetryDisabled is true, run update check but don't send any metric identifiers
|
||||
func (v *VersionChecker) RunUpdateCheck(ctx context.Context, args []string) {
|
||||
logger := log.WithPrefix("notification")
|
||||
|
||||
if v.skipUpdateCheck && v.telemetryDisabled {
|
||||
logger.Debug("Skipping update check and metric ping")
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
logger.Debug("Running version check")
|
||||
args = getFlags(args)
|
||||
client := &http.Client{
|
||||
Timeout: 3 * time.Second,
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, v.updatesApi, http.NoBody)
|
||||
if err != nil {
|
||||
logger.Warn("Failed to create a request for Trivy api", log.Err(err))
|
||||
return
|
||||
}
|
||||
|
||||
// if the user hasn't disabled metrics, send the anonymous information as headers
|
||||
if !v.telemetryDisabled {
|
||||
req.Header.Set("Trivy-Identifier", uniqueIdentifier())
|
||||
req.Header.Set("Trivy-Command", strings.Join(args, " "))
|
||||
req.Header.Set("Trivy-OS", runtime.GOOS)
|
||||
req.Header.Set("Trivy-Arch", runtime.GOARCH)
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", fmt.Sprintf("trivy/%s", v.currentVersion))
|
||||
resp, err := client.Do(req)
|
||||
if err != nil || resp.StatusCode != http.StatusOK {
|
||||
logger.Debug("Failed getting response from Trivy api", log.Err(err))
|
||||
return
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
if err := json.NewDecoder(resp.Body).Decode(&v.latestVersion); err != nil {
|
||||
logger.Debug("Failed to decode the Trivy response", log.Err(err))
|
||||
return
|
||||
}
|
||||
|
||||
// enable priting if update allowed and quiet mode is not set
|
||||
if !v.skipUpdateCheck && !v.quiet {
|
||||
v.responseReceived = true
|
||||
}
|
||||
logger.Debug("Version check completed", log.String("latest_version", v.latestVersion.Trivy.LatestVersion))
|
||||
v.done = true
|
||||
}()
|
||||
}
|
||||
|
||||
// PrintNotices prints any announcements or warnings
|
||||
// to the output writer, most likely stderr
|
||||
func (v *VersionChecker) PrintNotices(output io.Writer) {
|
||||
if !v.responseReceived {
|
||||
return
|
||||
}
|
||||
|
||||
logger := log.WithPrefix("notification")
|
||||
logger.Debug("Printing notices")
|
||||
var notices []string
|
||||
|
||||
notices = append(notices, v.Warnings()...)
|
||||
for _, announcement := range v.Announcements() {
|
||||
if time.Now().Before(announcement.ToDate) && time.Now().After(announcement.FromDate) {
|
||||
notices = append(notices, announcement.Announcement)
|
||||
}
|
||||
}
|
||||
|
||||
if v.currentVersion != v.LatestVersion() {
|
||||
notices = append(notices, fmt.Sprintf("Version %s of Trivy is now available, current version is %s", v.latestVersion.Trivy.LatestVersion, v.currentVersion))
|
||||
}
|
||||
|
||||
if len(notices) > 0 {
|
||||
fmt.Fprintf(output, "\n📣 \x1b[34mNotices:\x1b[0m\n")
|
||||
for _, notice := range notices {
|
||||
fmt.Fprintf(output, " - %s\n", notice)
|
||||
}
|
||||
fmt.Fprintln(output)
|
||||
fmt.Fprintln(output, "To suppress version checks, run Trivy scans with the --skip-version-check flag")
|
||||
fmt.Fprintln(output)
|
||||
}
|
||||
}
|
||||
|
||||
func (v *VersionChecker) LatestVersion() string {
|
||||
if v.responseReceived {
|
||||
return v.latestVersion.Trivy.LatestVersion
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (v *VersionChecker) Announcements() []announcement {
|
||||
if v.responseReceived {
|
||||
return v.latestVersion.Announcements
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *VersionChecker) Warnings() []string {
|
||||
if v.responseReceived {
|
||||
return v.latestVersion.Warnings
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getFlags returns the just the flag portion without the values
|
||||
func getFlags(args []string) []string {
|
||||
var flags []string
|
||||
for _, arg := range args {
|
||||
if strings.HasPrefix(arg, "-") {
|
||||
flags = append(flags, strings.Split(arg, "=")[0])
|
||||
}
|
||||
}
|
||||
return flags
|
||||
}
|
||||
|
||||
func (fd *flexibleTime) UnmarshalJSON(b []byte) error {
|
||||
s := strings.Trim(string(b), `"`)
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Try parsing with time component
|
||||
layouts := []string{
|
||||
time.RFC3339,
|
||||
"2006-01-02",
|
||||
time.RFC1123,
|
||||
}
|
||||
|
||||
var err error
|
||||
for _, layout := range layouts {
|
||||
var t time.Time
|
||||
t, err = time.Parse(layout, s)
|
||||
if err == nil {
|
||||
fd.Time = t
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("unable to parse date: %s", s)
|
||||
}
|
||||
242
pkg/notification/notice_test.go
Normal file
242
pkg/notification/notice_test.go
Normal file
@@ -0,0 +1,242 @@
|
||||
package notification
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPrintNotices(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
options []Option
|
||||
latestVersion string
|
||||
announcements []announcement
|
||||
responseExpected bool
|
||||
expectedOutput string
|
||||
}{
|
||||
{
|
||||
name: "New version with no announcements",
|
||||
options: []Option{WithCurrentVersion("0.58.0")},
|
||||
latestVersion: "0.60.0",
|
||||
responseExpected: true,
|
||||
expectedOutput: "\n📣 \x1b[34mNotices:\x1b[0m\n - Version 0.60.0 of Trivy is now available, current version is 0.58.0\n\nTo suppress version checks, run Trivy scans with the --skip-version-check flag\n\n",
|
||||
},
|
||||
{
|
||||
name: "new version available but --quiet mode enabled",
|
||||
options: []Option{
|
||||
WithCurrentVersion("0.58.0"),
|
||||
WithQuietMode(true),
|
||||
},
|
||||
latestVersion: "0.60.0",
|
||||
responseExpected: false,
|
||||
expectedOutput: "",
|
||||
},
|
||||
{
|
||||
name: "new version available but --skip-update-check mode enabled",
|
||||
options: []Option{
|
||||
WithCurrentVersion("0.58.0"),
|
||||
WithSkipVersionCheck(true),
|
||||
},
|
||||
latestVersion: "0.60.0",
|
||||
responseExpected: false,
|
||||
expectedOutput: "",
|
||||
},
|
||||
{
|
||||
name: "New version with announcements",
|
||||
options: []Option{WithCurrentVersion("0.58.0")},
|
||||
latestVersion: "0.60.0",
|
||||
announcements: []announcement{
|
||||
{
|
||||
FromDate: time.Date(2025, 2, 2, 12, 0, 0, 0, time.UTC),
|
||||
ToDate: time.Date(2999, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
Announcement: "There are some amazing things happening right now!",
|
||||
},
|
||||
},
|
||||
responseExpected: true,
|
||||
expectedOutput: "\n📣 \x1b[34mNotices:\x1b[0m\n - There are some amazing things happening right now!\n - Version 0.60.0 of Trivy is now available, current version is 0.58.0\n\nTo suppress version checks, run Trivy scans with the --skip-version-check flag\n\n",
|
||||
},
|
||||
{
|
||||
name: "No new version with announcements",
|
||||
options: []Option{WithCurrentVersion("0.60.0")},
|
||||
latestVersion: "0.60.0",
|
||||
announcements: []announcement{
|
||||
{
|
||||
FromDate: time.Date(2025, 2, 2, 12, 0, 0, 0, time.UTC),
|
||||
ToDate: time.Date(2999, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
Announcement: "There are some amazing things happening right now!",
|
||||
},
|
||||
},
|
||||
responseExpected: true,
|
||||
expectedOutput: "\n📣 \x1b[34mNotices:\x1b[0m\n - There are some amazing things happening right now!\n\nTo suppress version checks, run Trivy scans with the --skip-version-check flag\n\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
updates := newUpdatesServer(t, tt.latestVersion, tt.announcements)
|
||||
server := httptest.NewServer(http.HandlerFunc(updates.handler))
|
||||
defer server.Close()
|
||||
tt.options = append(tt.options, WithUpdatesApi(server.URL))
|
||||
v := NewVersionChecker(tt.options...)
|
||||
|
||||
v.RunUpdateCheck(t.Context(), nil)
|
||||
require.Eventually(t, func() bool { return v.done }, time.Second*5, 500)
|
||||
require.Eventually(t, func() bool { return v.responseReceived == tt.responseExpected }, time.Second*5, 500)
|
||||
|
||||
sb := bytes.NewBufferString("")
|
||||
v.PrintNotices(sb)
|
||||
assert.Equal(t, tt.expectedOutput, sb.String())
|
||||
|
||||
// check metrics are sent
|
||||
require.NotNil(t, updates.lastRequest)
|
||||
require.NotEmpty(t, updates.lastRequest.Header.Get("Trivy-Identifier"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckForNotices(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
options []Option
|
||||
expectedVersion string
|
||||
expectedAnnouncements []announcement
|
||||
expectNoMetrics bool
|
||||
}{
|
||||
{
|
||||
name: "new version with no announcements",
|
||||
options: []Option{
|
||||
WithCurrentVersion("0.58.0"),
|
||||
},
|
||||
expectedVersion: "0.60.0",
|
||||
},
|
||||
{
|
||||
name: "new version with disabled metrics",
|
||||
options: []Option{
|
||||
WithCurrentVersion("0.58.0"),
|
||||
WithTelemetryDisabled(true),
|
||||
},
|
||||
expectedVersion: "0.60.0",
|
||||
expectNoMetrics: true,
|
||||
},
|
||||
{
|
||||
name: "new version and a new announcement",
|
||||
options: []Option{
|
||||
WithCurrentVersion("0.58.0"),
|
||||
},
|
||||
expectedVersion: "0.60.0",
|
||||
expectedAnnouncements: []announcement{
|
||||
{
|
||||
FromDate: time.Date(2025, 2, 2, 12, 0, 0, 0, time.UTC),
|
||||
ToDate: time.Date(2999, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
Announcement: "There are some amazing things happening right now!",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
updates := newUpdatesServer(t, tt.expectedVersion, tt.expectedAnnouncements)
|
||||
server := httptest.NewServer(http.HandlerFunc(updates.handler))
|
||||
defer server.Close()
|
||||
|
||||
tt.options = append(tt.options, WithUpdatesApi(server.URL))
|
||||
v := NewVersionChecker(tt.options...)
|
||||
|
||||
v.RunUpdateCheck(t.Context(), nil)
|
||||
require.Eventually(t, func() bool { return v.done }, time.Second*5, 500)
|
||||
require.Eventually(t, func() bool { return v.responseReceived }, time.Second*5, 500)
|
||||
assert.Equal(t, tt.expectedVersion, v.LatestVersion())
|
||||
assert.ElementsMatch(t, tt.expectedAnnouncements, v.Announcements())
|
||||
|
||||
if tt.expectNoMetrics {
|
||||
assert.True(t, v.telemetryDisabled)
|
||||
require.NotNil(t, updates.lastRequest)
|
||||
assert.Empty(t, updates.lastRequest.Header.Get("Trivy-Identifier"))
|
||||
} else {
|
||||
assert.False(t, v.telemetryDisabled)
|
||||
require.NotNil(t, updates.lastRequest)
|
||||
assert.NotEmpty(t, updates.lastRequest.Header.Get("Trivy-Identifier"))
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type updatesServer struct {
|
||||
t *testing.T
|
||||
lastRequest *http.Request
|
||||
expectedVersion string
|
||||
expectedAnnouncements []announcement
|
||||
}
|
||||
|
||||
func newUpdatesServer(t *testing.T, expectedVersion string, expectedAnnouncements []announcement) *updatesServer {
|
||||
return &updatesServer{
|
||||
t: t,
|
||||
expectedVersion: expectedVersion,
|
||||
expectedAnnouncements: expectedAnnouncements,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *updatesServer) handler(w http.ResponseWriter, r *http.Request) {
|
||||
if !strings.HasPrefix(r.Header.Get("User-Agent"), "trivy") {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
}
|
||||
|
||||
u.lastRequest = r
|
||||
|
||||
response := updateResponse{
|
||||
Trivy: versionInfo{
|
||||
LatestVersion: u.expectedVersion,
|
||||
LatestDate: flexibleTime{Time: time.Now()},
|
||||
},
|
||||
Announcements: u.expectedAnnouncements,
|
||||
}
|
||||
|
||||
out, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
u.t.Fail()
|
||||
}
|
||||
w.Write(out)
|
||||
}
|
||||
|
||||
func TestFlexibleDate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
dateStr string
|
||||
expected time.Time
|
||||
}{
|
||||
{
|
||||
name: "RFC3339 date format",
|
||||
dateStr: `"2023-10-01T12:00:00Z"`,
|
||||
expected: time.Date(2023, 10, 1, 12, 0, 0, 0, time.UTC),
|
||||
},
|
||||
{
|
||||
name: "RFC1123 date format",
|
||||
dateStr: `"Sun, 01 Oct 2023 12:00:00 GMT"`,
|
||||
expected: time.Date(2023, 10, 1, 12, 0, 0, 0, time.UTC),
|
||||
},
|
||||
{
|
||||
name: "RFC3339 date only format",
|
||||
dateStr: `"2023-10-01"`,
|
||||
expected: time.Date(2023, 10, 1, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var ft flexibleTime
|
||||
err := json.Unmarshal([]byte(tt.dateStr), &ft)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.expected.Unix(), ft.Unix())
|
||||
})
|
||||
}
|
||||
}
|
||||
37
pkg/notification/option.go
Normal file
37
pkg/notification/option.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package notification
|
||||
|
||||
type Option func(*VersionChecker)
|
||||
|
||||
// WithUpdatesApi sets the updates API URL
|
||||
func WithUpdatesApi(updatesApi string) Option {
|
||||
return func(v *VersionChecker) {
|
||||
v.updatesApi = updatesApi
|
||||
}
|
||||
}
|
||||
|
||||
// WithCurrentVersion sets the current version
|
||||
func WithCurrentVersion(version string) Option {
|
||||
return func(v *VersionChecker) {
|
||||
v.currentVersion = version
|
||||
}
|
||||
}
|
||||
|
||||
func WithSkipVersionCheck(skipVersionCheck bool) Option {
|
||||
return func(v *VersionChecker) {
|
||||
v.skipUpdateCheck = skipVersionCheck
|
||||
}
|
||||
}
|
||||
|
||||
// WithQuietMode sets the quiet mode when the user is using the --quiet flag
|
||||
func WithQuietMode(quiet bool) Option {
|
||||
return func(v *VersionChecker) {
|
||||
v.quiet = quiet
|
||||
}
|
||||
}
|
||||
|
||||
// WithTelemetryDisabled sets the telemetry disabled flag
|
||||
func WithTelemetryDisabled(telemetryDisabled bool) Option {
|
||||
return func(v *VersionChecker) {
|
||||
v.telemetryDisabled = telemetryDisabled
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user