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:
Owen Rumney
2025-05-28 09:09:16 +01:00
committed by GitHub
parent 7ca656d54b
commit 5a0bf9ed31
20 changed files with 703 additions and 10 deletions

View File

@@ -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.

View 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
```

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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}

View 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))
}

View 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
View 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)
}

View 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())
})
}
}

View 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
}
}