mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-12 07:40:48 -08:00
480 lines
15 KiB
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
|
|
}
|