mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-07 21:30:46 -08:00
fix(cli): add some values to the telemetry call (#9056)
This commit is contained in:
19
docs/docs/advanced/telemetry-flags.md
Normal file
19
docs/docs/advanced/telemetry-flags.md
Normal 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
|
||||
```
|
||||
@@ -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>.
|
||||
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -262,6 +262,7 @@ markdown_extensions:
|
||||
- pymdownx.highlight
|
||||
- pymdownx.details
|
||||
- pymdownx.magiclink
|
||||
- pymdownx.snippets
|
||||
- pymdownx.superfences:
|
||||
custom_fences:
|
||||
- name: mermaid
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user