Files
trivy/pkg/notification/notice_test.go

480 lines
15 KiB
Go

package notification
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"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
skipVersionCheck bool
quiet bool
disableTelemetry bool
currentVersion string
latestVersion string
announcements []announcement
responseExpected bool
expectedOutput string
}{
{
name: "New version with no announcements",
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",
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",
quiet: true,
currentVersion: "0.58.0",
latestVersion: "0.60.0",
responseExpected: false,
expectedOutput: "",
},
{
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",
currentVersion: "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",
currentVersion: "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",
},
{
name: "No new version with announcements and zero time",
currentVersion: "0.60.0",
latestVersion: "0.60.0",
announcements: []announcement{
{
FromDate: time.Time{},
ToDate: time.Time{},
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",
},
{
name: "No new version with announcement that fails announcement version constraints",
currentVersion: "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),
FromVersion: "0.61.0",
Announcement: "There are some amazing things happening right now!",
},
},
responseExpected: true,
expectedOutput: "",
},
{
name: "No new version with announcement where current version is greater than to_version",
currentVersion: "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),
ToVersion: "0.59.0",
Announcement: "There are some amazing things happening right now!",
},
},
responseExpected: true,
expectedOutput: "",
},
{
name: "No new version with announcement that satisfies version constraint but outside date range",
currentVersion: "0.60.0",
latestVersion: "0.60.0",
announcements: []announcement{
{
FromDate: time.Date(2024, 2, 2, 12, 0, 0, 0, time.UTC),
ToDate: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
FromVersion: "0.60.0",
Announcement: "There are some amazing things happening right now!",
},
},
responseExpected: true,
expectedOutput: "",
},
{
name: "No new version with multiple announcements, one of which is valid",
currentVersion: "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!",
},
{
FromDate: time.Date(2025, 2, 2, 12, 0, 0, 0, time.UTC),
ToDate: time.Date(2999, 1, 1, 0, 0, 0, 0, time.UTC),
FromVersion: "0.61.0",
Announcement: "This announcement should not be displayed",
},
},
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",
},
{
name: "No new version with no announcements and quiet mode",
quiet: true,
currentVersion: "0.60.0",
latestVersion: "0.60.0",
announcements: []announcement{},
responseExpected: false,
expectedOutput: "",
},
{
name: "No new version with no announcements",
currentVersion: "0.60.0",
latestVersion: "0.60.0",
announcements: []announcement{},
responseExpected: true,
expectedOutput: "",
},
}
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))
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)
sb := bytes.NewBufferString("")
v.PrintNotices(t.Context(), 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
skipVersionCheck bool
disableTelemetry bool
quiet bool
currentVersion string
expectedVersion string
expectedAnnouncements []announcement
expectNoMetrics bool
}{
{
name: "new version with no announcements",
currentVersion: "0.58.0",
expectedVersion: "0.60.0",
},
{
name: "new version with disabled metrics",
disableTelemetry: true,
currentVersion: "0.58.0",
expectedVersion: "0.60.0",
expectNoMetrics: true,
},
{
name: "new version and a new announcement",
currentVersion: "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()
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.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()
require.NoError(t, err)
assert.Equal(t, tt.expectedVersion, latestVersion.String())
assert.ElementsMatch(t, tt.expectedAnnouncements, v.Announcements())
if tt.expectNoMetrics {
require.NotNil(t, updates.lastRequest)
assert.Empty(t, updates.lastRequest.Header.Get("Trivy-Identifier"))
} else {
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())
})
}
}
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
}