fix(cli): add some values to the telemetry call (#9056)

This commit is contained in:
Owen Rumney
2025-06-27 08:14:25 +01:00
committed by GitHub
parent 367564a3be
commit fd2bc91e13
16 changed files with 479 additions and 238 deletions

View File

@@ -0,0 +1,19 @@
```
--debug
--detection-priority
--format
--ignore-status
--include-dev-deps
--insecure
--list-all-pkgs
--misconfig-scanners
--pkg-relationships
--pkg-types
--quiet
--report
--scanners
--severity
--show-suppressed
--timeout
--vuln-severity-source
```

View File

@@ -1,24 +1,30 @@
# 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.
Trivy collects 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
- Environmental information:
- Installation identifier
- Trivy version
- Operating system
- Scan
- Non-revealing scan options
- Scan:
- Non-revealing scan options (see below for comprehensive list)
### Captured scan options
The following flags will be included with their value:
--8<-- "./docs/docs/advanced/telemetry-flags.md"
## 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.
- Installation identifier: one-way hash of machine fingerprint, resulting in opaque ID.
- Scan: 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>.

View File

@@ -7,13 +7,13 @@ import (
"fmt"
"os"
"slices"
"sort"
"strings"
"github.com/spf13/cobra/doc"
"github.com/aquasecurity/trivy/pkg/commands"
"github.com/aquasecurity/trivy/pkg/flag"
"github.com/aquasecurity/trivy/pkg/log"
"github.com/spf13/cobra/doc"
)
const (
@@ -35,50 +35,43 @@ func main() {
os.Setenv("XDG_DATA_HOME", os.TempDir())
cmd := commands.NewApp()
allFlagGroups := getAllFlags()
cmd.DisableAutoGenTag = true
if err := doc.GenMarkdownTree(cmd, "./docs/docs/references/configuration/cli"); err != nil {
log.Fatal("Fatal error", log.Err(err))
}
if err := generateConfigDocs("./docs/docs/references/configuration/config-file.md"); err != nil {
if err := generateConfigDocs("./docs/docs/references/configuration/config-file.md", allFlagGroups); err != nil {
log.Fatal("Fatal error in config file generation", log.Err(err))
}
if err := generateTelemetryFlagDocs("./docs/docs/advanced/telemetry-flags.md", allFlagGroups); err != nil {
log.Fatal("Fatal error in telemetry docs generation", log.Err(err))
}
}
// generateTelemetryFlagDocs updates the telemetry section in the documentation file
// with the flags that are safe to be included in telemetry.
func generateTelemetryFlagDocs(filename string, allFlagGroups []flag.FlagGroup) error {
var telemetryFlags []string
for _, group := range allFlagGroups {
flags := group.Flags()
for _, f := range flags {
if f.IsTelemetrySafe() && f.GetConfigName() != "" {
telemetryFlags = append(telemetryFlags, fmt.Sprintf("--%s", f.GetName()))
}
}
}
sort.Strings(telemetryFlags)
flagContent := fmt.Sprintf("```\n%s\n```\n", strings.Join(telemetryFlags, "\n"))
if err := os.WriteFile(filename, []byte(flagContent), 0644); err != nil {
return fmt.Errorf("failed to write to %s: %w", filename, err)
}
return nil
}
// generateConfigDocs creates markdown file for Trivy config.
func generateConfigDocs(filename string) error {
// remoteFlags should contain Client and Server flags.
// NewClientFlags doesn't initialize `Listen` field
remoteFlags := flag.NewClientFlags()
remoteFlags.Listen = flag.ServerListenFlag.Clone()
// These flags don't work from config file.
// Clear configName to skip them later.
globalFlags := flag.NewGlobalFlagGroup()
globalFlags.ConfigFile.ConfigName = ""
globalFlags.ShowVersion.ConfigName = ""
globalFlags.GenerateDefaultConfig.ConfigName = ""
var allFlagGroups = []flag.FlagGroup{
globalFlags,
flag.NewCacheFlagGroup(),
flag.NewCleanFlagGroup(),
remoteFlags,
flag.NewDBFlagGroup(),
flag.NewImageFlagGroup(),
flag.NewK8sFlagGroup(),
flag.NewLicenseFlagGroup(),
flag.NewMisconfFlagGroup(),
flag.NewModuleFlagGroup(),
flag.NewPackageFlagGroup(),
flag.NewRegistryFlagGroup(),
flag.NewRegoFlagGroup(),
flag.NewReportFlagGroup(),
flag.NewRepoFlagGroup(),
flag.NewScanFlagGroup(),
flag.NewSecretFlagGroup(),
flag.NewVulnerabilityFlagGroup(),
}
func generateConfigDocs(filename string, allFlagGroups []flag.FlagGroup) error {
f, err := os.Create(filename)
if err != nil {
return err
@@ -87,6 +80,10 @@ func generateConfigDocs(filename string) error {
f.WriteString("# " + title + "\n\n")
f.WriteString(description + "\n")
if len(allFlagGroups) == 0 {
return fmt.Errorf("no flag groups found")
}
for _, group := range allFlagGroups {
f.WriteString("## " + group.Name() + " options\n")
writeFlags(group, f)
@@ -161,3 +158,39 @@ func writeFlagValue(val any, ind string, w *os.File) {
fmt.Fprintf(w, " %v\n", v)
}
}
func getAllFlags() []flag.FlagGroup {
// remoteFlags should contain Client and Server flags.
// NewClientFlags doesn't initialize `Listen` field
remoteFlags := flag.NewClientFlags()
remoteFlags.Listen = flag.ServerListenFlag.Clone()
// These flags don't work from config file.
// Clear configName to skip them later.
globalFlags := flag.NewGlobalFlagGroup()
globalFlags.ConfigFile.ConfigName = ""
globalFlags.ShowVersion.ConfigName = ""
globalFlags.GenerateDefaultConfig.ConfigName = ""
return []flag.FlagGroup{
globalFlags,
flag.NewCacheFlagGroup(),
flag.NewCleanFlagGroup(),
remoteFlags,
flag.NewDBFlagGroup(),
flag.NewImageFlagGroup(),
flag.NewK8sFlagGroup(),
flag.NewLicenseFlagGroup(),
flag.NewMisconfFlagGroup(),
flag.NewModuleFlagGroup(),
flag.NewPackageFlagGroup(),
flag.NewRegistryFlagGroup(),
flag.NewRegoFlagGroup(),
flag.NewReportFlagGroup(),
flag.NewRepoFlagGroup(),
flag.NewScanFlagGroup(),
flag.NewSecretFlagGroup(),
flag.NewVulnerabilityFlagGroup(),
}
}

View File

@@ -262,6 +262,7 @@ markdown_extensions:
- pymdownx.highlight
- pymdownx.details
- pymdownx.magiclink
- pymdownx.snippets
- pymdownx.superfences:
custom_fences:
- name: mermaid

View File

@@ -124,13 +124,9 @@ func NewRunner(ctx context.Context, cliOptions flag.Options, opts ...RunnerOptio
Insecure: cliOptions.Insecure,
Timeout: cliOptions.Timeout,
}))
// 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),
)
// get the sub command that is being used or fallback to "trivy"
commandName := lo.Ternary(len(os.Args) > 1, os.Args[1], "trivy")
r.versionChecker = notification.NewVersionChecker(commandName, &cliOptions)
// Update the vulnerability database if needed.
if err := r.initDB(ctx, cliOptions); err != nil {
@@ -157,7 +153,7 @@ func NewRunner(ctx context.Context, cliOptions flag.Options, opts ...RunnerOptio
// 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:])
r.versionChecker.RunUpdateCheck(ctx)
}
return r, nil

View File

@@ -15,11 +15,13 @@ var (
Name: "service",
ConfigName: "cloud.aws.service",
Usage: "Only scan AWS Service(s) specified with this flag. Can specify multiple services using --service A --service B etc.",
TelemetrySafe: true,
}
awsSkipServicesFlag = Flag[[]string]{
Name: "skip-service",
ConfigName: "cloud.aws.skip-service",
Usage: "Skip selected AWS Service(s) specified with this flag. Can specify multiple services using --skip-service A --skip-service B etc.",
TelemetrySafe: true,
}
awsAccountFlag = Flag[string]{
Name: "account",

View File

@@ -32,6 +32,7 @@ var (
Shorthand: "q",
Usage: "suppress progress bar and log output",
Persistent: true,
TelemetrySafe: true,
}
DebugFlag = Flag[bool]{
Name: "debug",
@@ -39,12 +40,14 @@ var (
Shorthand: "d",
Usage: "debug mode",
Persistent: true,
TelemetrySafe: true,
}
InsecureFlag = Flag[bool]{
Name: "insecure",
ConfigName: "insecure",
Usage: "allow insecure server connections",
Persistent: true,
TelemetrySafe: true,
}
TimeoutFlag = Flag[time.Duration]{
Name: "timeout",
@@ -52,6 +55,7 @@ var (
Default: time.Second * 300, // 5 mins
Usage: "timeout",
Persistent: true,
TelemetrySafe: true,
}
CacheDirFlag = Flag[string]{
Name: "cache-dir",

View File

@@ -103,6 +103,7 @@ var (
lo.Without(analyzer.TypeConfigFiles, analyzer.TypeYAML, analyzer.TypeJSON),
),
Usage: "comma-separated list of misconfig scanners to use for misconfiguration scanning",
TelemetrySafe: true,
}
ConfigFileSchemasFlag = Flag[[]string]{
Name: "config-file-schemas",

View File

@@ -76,6 +76,9 @@ type Flag[T FlagType] struct {
// Aliases represents aliases
Aliases []Alias
// TelemetrySafe indicates if the flag value is safe to be included in telemetry.
TelemetrySafe bool
// value is the value passed through CLI flag, env, or config file.
// It is populated after flag.Parse() is called.
value T
@@ -218,6 +221,17 @@ func (f *Flag[T]) GetAliases() []Alias {
return f.Aliases
}
func (f *Flag[T]) IsTelemetrySafe() bool {
return f.TelemetrySafe
}
func (f *Flag[T]) IsSet() bool {
if f == nil {
return false
}
return f.isSet()
}
func (f *Flag[T]) Hidden() bool {
return f.Deprecated != "" || f.Removed != "" || f.Internal
}
@@ -349,6 +363,8 @@ type Flagger interface {
GetDefaultValue() any
GetAliases() []Alias
Hidden() bool
IsTelemetrySafe() bool
IsSet() bool
Parse() error
Add(cmd *cobra.Command)
@@ -391,6 +407,9 @@ type Options struct {
// args is the arguments passed to the command.
args []string
// usedFlags allows us to get the underlying flags for the options
usedFlags []Flagger
}
// Align takes consistency of options
@@ -555,6 +574,11 @@ func (o *Options) OutputWriter(ctx context.Context) (io.Writer, func() error, er
return f, f.Close, nil
}
// GetUsedFlags returns the explicitly set flags for the options.
func (o *Options) GetUsedFlags() []Flagger {
return o.usedFlags
}
func (o *Options) outputPluginWriter(ctx context.Context) (io.Writer, func() error, error) {
pluginName := strings.TrimPrefix(o.Output, "plugin=")
@@ -651,6 +675,8 @@ func (f *Flags) ToOptions(args []string) (Options, error) {
return Options{}, xerrors.Errorf("unable to parse flags: %w", err)
}
opts.usedFlags = append(opts.usedFlags, usedFlags(group)...)
if err := group.ToOptions(&opts); err != nil {
return Options{}, xerrors.Errorf("unable to convert flags to options: %w", err)
}
@@ -751,3 +777,21 @@ func findFlagGroup[T FlagGroup](f *Flags) (T, bool) {
var zero T
return zero, false
}
// usedFlags returns a slice of flags that are set in the given FlagGroup.
func usedFlags(fg FlagGroup) []Flagger {
if fg == nil || fg.Flags() == nil {
return nil
}
var flags []Flagger
for _, flag := range fg.Flags() {
if flag == nil {
continue
}
if flag.IsSet() {
flags = append(flags, flag)
}
}
return flags
}

View File

@@ -11,6 +11,7 @@ var (
Name: "include-dev-deps",
ConfigName: "pkg.include-dev-deps",
Usage: "include development dependencies in the report (supported: npm, yarn, gradle)",
TelemetrySafe: true,
}
PkgTypesFlag = Flag[[]string]{
Name: "pkg-types",
@@ -25,6 +26,7 @@ var (
Deprecated: true, // --vuln-type was renamed to --pkg-types
},
},
TelemetrySafe: true,
}
PkgRelationshipsFlag = Flag[[]string]{
Name: "pkg-relationships",
@@ -32,6 +34,7 @@ var (
Default: xstrings.ToStringSlice(ftypes.Relationships),
Values: xstrings.ToStringSlice(ftypes.Relationships),
Usage: "list of package relationships",
TelemetrySafe: true,
}
)

View File

@@ -32,6 +32,7 @@ var (
Default: string(types.FormatTable),
Values: xstrings.ToStringSlice(types.SupportedFormats),
Usage: "format",
TelemetrySafe: true,
}
ReportFormatFlag = Flag[string]{
Name: "report",
@@ -42,6 +43,7 @@ var (
"summary",
},
Usage: "specify a report format for the output",
TelemetrySafe: true,
}
TemplateFlag = Flag[string]{
Name: "template",
@@ -58,6 +60,7 @@ var (
Name: "list-all-pkgs",
ConfigName: "list-all-pkgs",
Usage: "output all packages in the JSON report regardless of vulnerability",
TelemetrySafe: true,
}
IgnoreFileFlag = Flag[string]{
Name: "ignorefile",
@@ -98,6 +101,7 @@ var (
Default: dbTypes.SeverityNames,
Values: dbTypes.SeverityNames,
Usage: "severities of security issues to be displayed",
TelemetrySafe: true,
}
ComplianceFlag = Flag[string]{
Name: "compliance",
@@ -108,6 +112,7 @@ var (
Name: "show-suppressed",
ConfigName: "scan.show-suppressed",
Usage: "[EXPERIMENTAL] show suppressed vulnerabilities",
TelemetrySafe: true,
}
TableModeFlag = Flag[[]string]{
Name: "table-mode",

View File

@@ -66,6 +66,7 @@ var (
},
},
Usage: "comma-separated list of what security issues to detect",
TelemetrySafe: true,
}
FilePatternsFlag = Flag[[]string]{
Name: "file-patterns",
@@ -112,6 +113,7 @@ var (
- "precise": Prioritizes precise by minimizing false positives.
- "comprehensive": Aims to detect more security findings at the cost of potential false positives.
`,
TelemetrySafe: true,
}
DistroFlag = Flag[string]{
Name: "distro",

View File

@@ -21,6 +21,7 @@ var (
ConfigName: "vulnerability.ignore-status",
Values: dbTypes.Statuses,
Usage: "comma-separated list of vulnerability status to ignore",
TelemetrySafe: true,
}
VEXFlag = Flag[[]string]{
Name: "vex",
@@ -40,6 +41,7 @@ var (
},
Values: append(xstrings.ToStringSlice(vulnerability.AllSourceIDs), "auto"),
Usage: "order of data sources for selecting vulnerability severity level",
TelemetrySafe: true,
}
)

View File

@@ -8,10 +8,14 @@ import (
"io"
"net/http"
"runtime"
"strconv"
"strings"
"time"
"github.com/samber/lo"
"github.com/aquasecurity/go-version/pkg/semver"
"github.com/aquasecurity/trivy/pkg/flag"
"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/version/app"
xhttp "github.com/aquasecurity/trivy/pkg/x/http"
@@ -19,9 +23,8 @@ import (
type VersionChecker struct {
updatesApi string
skipUpdateCheck bool
quiet bool
telemetryDisabled bool
commandName string
cliOptions *flag.Options
done bool
responseReceived bool
@@ -30,17 +33,15 @@ type VersionChecker struct {
}
// 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 {
// updates API URL.
func NewVersionChecker(commandName string, cliOptions *flag.Options) *VersionChecker {
v := &VersionChecker{
updatesApi: "https://check.trivy.dev/updates",
currentVersion: app.Version(),
commandName: commandName,
cliOptions: cliOptions,
}
for _, opt := range opts {
opt(v)
}
return v
}
@@ -50,17 +51,17 @@ func NewVersionChecker(opts ...Option) *VersionChecker {
// 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) {
func (v *VersionChecker) RunUpdateCheck(ctx context.Context) {
logger := log.WithPrefix("notification")
if v.skipUpdateCheck && v.telemetryDisabled {
if v.cliOptions.SkipVersionCheck && v.cliOptions.DisableTelemetry {
logger.Debug("Skipping update check and metric ping")
return
}
go func() {
logger.Debug("Running version check")
args = getFlags(args)
commandParts := v.getFlags()
client := xhttp.ClientWithContext(ctx, xhttp.WithTimeout(3*time.Second))
req, err := http.NewRequestWithContext(ctx, http.MethodGet, v.updatesApi, http.NoBody)
@@ -70,9 +71,10 @@ func (v *VersionChecker) RunUpdateCheck(ctx context.Context, args []string) {
}
// if the user hasn't disabled metrics, send the anonymous information as headers
if !v.telemetryDisabled {
if !v.cliOptions.DisableTelemetry {
req.Header.Set("Trivy-Identifier", uniqueIdentifier())
req.Header.Set("Trivy-Command", strings.Join(args, " "))
req.Header.Set("Trivy-Command", v.commandName)
req.Header.Set("Trivy-Flags", commandParts)
req.Header.Set("Trivy-OS", runtime.GOOS)
req.Header.Set("Trivy-Arch", runtime.GOARCH)
}
@@ -91,7 +93,7 @@ func (v *VersionChecker) RunUpdateCheck(ctx context.Context, args []string) {
}
// enable priting if update allowed and quiet mode is not set
if !v.skipUpdateCheck && !v.quiet {
if !v.cliOptions.SkipVersionCheck && !v.cliOptions.Quiet {
v.responseReceived = true
}
logger.Debug("Version check completed", log.String("latest_version", v.latestVersion.Trivy.LatestVersion))
@@ -175,17 +177,6 @@ func (v *VersionChecker) Warnings() []string {
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 == "" {
@@ -211,3 +202,39 @@ func (fd *flexibleTime) UnmarshalJSON(b []byte) error {
return fmt.Errorf("unable to parse date: %s", s)
}
func (v *VersionChecker) getFlags() string {
var flags []string
for _, f := range v.cliOptions.GetUsedFlags() {
name := f.GetName()
if name == "" {
continue // Skip flags without a name
}
value := lo.Ternary(!f.IsTelemetrySafe(), "***", getFlagValue(f))
flags = append(flags, fmt.Sprintf("--%s=%s", name, value))
}
return strings.Join(flags, " ")
}
func getFlagValue(f flag.Flagger) string {
type flagger[T flag.FlagType] interface {
Value() T
}
switch ff := f.(type) {
case flagger[string]:
return ff.Value()
case flagger[int]:
return strconv.Itoa(ff.Value())
case flagger[float64]:
return fmt.Sprintf("%f", ff.Value())
case flagger[bool]:
return strconv.FormatBool(ff.Value())
case flagger[time.Duration]:
return ff.Value().String()
case flagger[[]string]:
return strings.Join(ff.Value(), ",")
default:
return "***" // Default case for unsupported types
}
}

View File

@@ -9,14 +9,21 @@ import (
"testing"
"time"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/aquasecurity/trivy/pkg/flag"
)
func TestPrintNotices(t *testing.T) {
tests := []struct {
name string
options []Option
skipVersionCheck bool
quiet bool
disableTelemetry bool
currentVersion string
latestVersion string
announcements []announcement
responseExpected bool
@@ -24,41 +31,37 @@ func TestPrintNotices(t *testing.T) {
}{
{
name: "New version with no announcements",
options: []Option{WithCurrentVersion("0.58.0")},
currentVersion: "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 includes a prefixed version number",
options: []Option{WithCurrentVersion("0.58.0")},
currentVersion: "0.58.0",
latestVersion: "v0.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),
},
quiet: true,
currentVersion: "0.58.0",
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),
},
name: "new version available but --skip-version-check mode enabled",
skipVersionCheck: true,
currentVersion: "0.58.0",
latestVersion: "0.60.0",
responseExpected: false,
expectedOutput: "",
},
{
name: "New version with announcements",
options: []Option{WithCurrentVersion("0.58.0")},
currentVersion: "0.58.0",
latestVersion: "0.60.0",
announcements: []announcement{
{
@@ -72,7 +75,7 @@ func TestPrintNotices(t *testing.T) {
},
{
name: "No new version with announcements",
options: []Option{WithCurrentVersion("0.60.0")},
currentVersion: "0.60.0",
latestVersion: "0.60.0",
announcements: []announcement{
{
@@ -86,7 +89,7 @@ func TestPrintNotices(t *testing.T) {
},
{
name: "No new version with announcements and zero time",
options: []Option{WithCurrentVersion("0.60.0")},
currentVersion: "0.60.0",
latestVersion: "0.60.0",
announcements: []announcement{
{
@@ -100,7 +103,7 @@ func TestPrintNotices(t *testing.T) {
},
{
name: "No new version with announcement that fails announcement version constraints",
options: []Option{WithCurrentVersion("0.60.0")},
currentVersion: "0.60.0",
latestVersion: "0.60.0",
announcements: []announcement{
{
@@ -115,7 +118,7 @@ func TestPrintNotices(t *testing.T) {
},
{
name: "No new version with announcement where current version is greater than to_version",
options: []Option{WithCurrentVersion("0.60.0")},
currentVersion: "0.60.0",
latestVersion: "0.60.0",
announcements: []announcement{
{
@@ -130,7 +133,7 @@ func TestPrintNotices(t *testing.T) {
},
{
name: "No new version with announcement that satisfies version constraint but outside date range",
options: []Option{WithCurrentVersion("0.60.0")},
currentVersion: "0.60.0",
latestVersion: "0.60.0",
announcements: []announcement{
{
@@ -145,7 +148,7 @@ func TestPrintNotices(t *testing.T) {
},
{
name: "No new version with multiple announcements, one of which is valid",
options: []Option{WithCurrentVersion("0.60.0")},
currentVersion: "0.60.0",
latestVersion: "0.60.0",
announcements: []announcement{
{
@@ -165,7 +168,8 @@ func TestPrintNotices(t *testing.T) {
},
{
name: "No new version with no announcements and quiet mode",
options: []Option{WithCurrentVersion("0.60.0"), WithQuietMode(true)},
quiet: true,
currentVersion: "0.60.0",
latestVersion: "0.60.0",
announcements: []announcement{},
responseExpected: false,
@@ -173,7 +177,7 @@ func TestPrintNotices(t *testing.T) {
},
{
name: "No new version with no announcements",
options: []Option{WithCurrentVersion("0.60.0")},
currentVersion: "0.60.0",
latestVersion: "0.60.0",
announcements: []announcement{},
responseExpected: true,
@@ -185,11 +189,22 @@ func TestPrintNotices(t *testing.T) {
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)
cliOpts := &flag.Options{
GlobalOptions: flag.GlobalOptions{
Quiet: tt.quiet,
},
ScanOptions: flag.ScanOptions{
SkipVersionCheck: tt.skipVersionCheck,
DisableTelemetry: tt.disableTelemetry,
},
}
v := NewVersionChecker("testCommand", cliOpts)
v.updatesApi = server.URL
v.currentVersion = tt.currentVersion
v.RunUpdateCheck(t.Context())
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)
@@ -207,32 +222,29 @@ func TestPrintNotices(t *testing.T) {
func TestCheckForNotices(t *testing.T) {
tests := []struct {
name string
options []Option
skipVersionCheck bool
disableTelemetry bool
quiet bool
currentVersion string
expectedVersion string
expectedAnnouncements []announcement
expectNoMetrics bool
}{
{
name: "new version with no announcements",
options: []Option{
WithCurrentVersion("0.58.0"),
},
currentVersion: "0.58.0",
expectedVersion: "0.60.0",
},
{
name: "new version with disabled metrics",
options: []Option{
WithCurrentVersion("0.58.0"),
WithTelemetryDisabled(true),
},
disableTelemetry: true,
currentVersion: "0.58.0",
expectedVersion: "0.60.0",
expectNoMetrics: true,
},
{
name: "new version and a new announcement",
options: []Option{
WithCurrentVersion("0.58.0"),
},
currentVersion: "0.58.0",
expectedVersion: "0.60.0",
expectedAnnouncements: []announcement{
{
@@ -250,10 +262,20 @@ func TestCheckForNotices(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(updates.handler))
defer server.Close()
tt.options = append(tt.options, WithUpdatesApi(server.URL))
v := NewVersionChecker(tt.options...)
cliOpts := &flag.Options{
GlobalOptions: flag.GlobalOptions{
Quiet: tt.quiet,
},
ScanOptions: flag.ScanOptions{
SkipVersionCheck: tt.skipVersionCheck,
DisableTelemetry: tt.disableTelemetry,
},
}
v.RunUpdateCheck(t.Context(), nil)
v := NewVersionChecker("testCommand", cliOpts)
v.updatesApi = server.URL
v.RunUpdateCheck(t.Context())
require.Eventually(t, func() bool { return v.done }, time.Second*5, 500)
require.Eventually(t, func() bool { return v.responseReceived }, time.Second*5, 500)
latestVersion, err := v.LatestVersion()
@@ -262,11 +284,9 @@ func TestCheckForNotices(t *testing.T) {
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"))
}
@@ -344,3 +364,116 @@ func TestFlexibleDate(t *testing.T) {
})
}
}
func TestCheckCommandHeaders(t *testing.T) {
tests := []struct {
name string
command string
commandArgs []string
env map[string]string
ignoreParseError bool
expectedCommandHeader string
expectedCommandArgsHeader string
}{
{
name: "image command with no flags",
command: "image",
commandArgs: []string{"nginx"},
expectedCommandHeader: "image",
},
{
name: "image command with flags",
command: "image",
commandArgs: []string{"--severity", "CRITICAL", "--scanners", "vuln,misconfig", "--pkg-types", "library", "nginx", "--include-dev-deps"},
expectedCommandHeader: "image",
expectedCommandArgsHeader: "--include-dev-deps=true --pkg-types=library --severity=CRITICAL --scanners=vuln,misconfig",
},
{
name: "image command with multiple flags",
command: "image",
commandArgs: []string{"--severity", "MEDIUM", "-s", "CRITICAL", "--scanners", "misconfig", "nginx"},
expectedCommandHeader: "image",
expectedCommandArgsHeader: "--severity=MEDIUM,CRITICAL --scanners=misconfig",
},
{
name: "filesystem command with flags",
command: "fs",
commandArgs: []string{"--severity=HIGH", "--vex", "repo", "--vuln-severity-source", "nvd,debian", "../trivy-ci-test"},
expectedCommandHeader: "fs",
expectedCommandArgsHeader: "--severity=HIGH --vex=*** --vuln-severity-source=nvd,debian",
},
{
name: "filesystem command with flags including an invalid flag",
command: "fs",
commandArgs: []string{"--severity=HIGH", "--vex", "repo", "--vuln-severity-source", "nvd,debian", "--invalid-flag", "../trivy-ci-test"},
ignoreParseError: true,
expectedCommandHeader: "fs",
expectedCommandArgsHeader: "--severity=HIGH --vex=*** --vuln-severity-source=nvd,debian",
},
{
name: "filesystem with environment variables",
command: "fs",
commandArgs: []string{"--severity", "HIGH", "--vex", "repo", "/home/user/code"},
env: map[string]string{
"TRIVY_SCANNERS": "secret,misconfig",
},
expectedCommandHeader: "fs",
expectedCommandArgsHeader: "--severity=HIGH --scanners=secret,misconfig --vex=***",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
updates := newUpdatesServer(t, "0.60.0", nil)
server := httptest.NewServer(http.HandlerFunc(updates.handler))
defer server.Close()
for key, value := range tt.env {
t.Setenv(key, value)
}
// clean up the env
defer func() {
server.Close()
}()
opts := getOptionsForArgs(t, tt.commandArgs, tt.ignoreParseError)
v := NewVersionChecker(tt.command, opts)
v.updatesApi = server.URL
v.RunUpdateCheck(t.Context())
require.Eventually(t, func() bool { return v.done }, time.Second*5, 500)
require.NotNil(t, updates.lastRequest)
assert.Equal(t, tt.expectedCommandHeader, updates.lastRequest.Header.Get("Trivy-Command"))
assert.Equal(t, tt.expectedCommandArgsHeader, updates.lastRequest.Header.Get("Trivy-Flags"))
})
}
}
// getOptionsForArgs uses a basic command to parse the flags so we can generate
// an options object from it
func getOptionsForArgs(t *testing.T, commandArgs []string, ignoreParseError bool) *flag.Options {
flags := flag.Flags{
flag.NewGlobalFlagGroup(),
flag.NewImageFlagGroup(),
flag.NewMisconfFlagGroup(),
flag.NewPackageFlagGroup(),
flag.NewReportFlagGroup(),
flag.NewScanFlagGroup(),
flag.NewVulnerabilityFlagGroup(),
}
// simple command to facilitate flag parsing
cmd := &cobra.Command{}
flags.AddFlags(cmd)
err := cmd.ParseFlags(commandArgs)
if !ignoreParseError {
require.NoError(t, err)
}
require.NoError(t, flags.Bind(cmd))
opts, err := flags.ToOptions(commandArgs)
require.NoError(t, err)
return &opts
}

View File

@@ -1,37 +0,0 @@
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
}
}