Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pkg/commands/artifact/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ func (r *runner) Close(ctx context.Context) error {

// silently check if there is notifications
if r.versionChecker != nil {
r.versionChecker.PrintNotices(os.Stderr)
r.versionChecker.PrintNotices(ctx, os.Stderr)
}

return errs
Expand Down
65 changes: 27 additions & 38 deletions pkg/notification/notice.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package notification
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
Expand All @@ -15,30 +16,6 @@ import (
"github.com/aquasecurity/trivy/pkg/version/app"
)

// flexibleTime is a custom time type that can handle
// different date formats in JSON. It implements the
// UnmarshalJSON method to parse the date string into a time.Time object.
type flexibleTime struct {
time.Time
}

type versionInfo struct {
LatestVersion string `json:"latest_version"`
LatestDate flexibleTime `json:"latest_date"`
}

type announcement struct {
FromDate time.Time `json:"from_date"`
ToDate time.Time `json:"to_date"`
Announcement string `json:"announcement"`
}

type updateResponse struct {
Trivy versionInfo `json:"trivy"`
Announcements []announcement `json:"announcements"`
Warnings []string `json:"warnings"`
}

type VersionChecker struct {
updatesApi string
skipUpdateCheck bool
Expand Down Expand Up @@ -125,37 +102,37 @@ func (v *VersionChecker) RunUpdateCheck(ctx context.Context, args []string) {

// PrintNotices prints any announcements or warnings
// to the output writer, most likely stderr
func (v *VersionChecker) PrintNotices(output io.Writer) {
func (v *VersionChecker) PrintNotices(ctx context.Context, output io.Writer) {
if !v.responseReceived {
return
}

logger := log.WithPrefix("notification")
logger.Debug("Printing notices")
var notices []string

notices = append(notices, v.Warnings()...)
for _, announcement := range v.Announcements() {
if time.Now().Before(announcement.ToDate) && time.Now().After(announcement.FromDate) {
notices = append(notices, announcement.Announcement)
}
}

cv, err := semver.Parse(strings.TrimPrefix(v.currentVersion, "v"))
cv, err := v.CurrentVersion()
if err != nil {
return
}

lv, err := semver.Parse(strings.TrimPrefix(v.LatestVersion(), "v"))
lv, err := v.LatestVersion()
if err != nil {
return
}

notices = append(notices, v.Warnings()...)
for _, announcement := range v.Announcements() {
if announcement.shouldDisplay(ctx, cv) {
notices = append(notices, announcement.Announcement)
}
}

if cv.LessThan(lv) {
notices = append(notices, fmt.Sprintf("Version %s of Trivy is now available, current version is %s", lv, cv))
}

if len(notices) > 0 {
logger.Debug("Printing notices")
fmt.Fprintf(output, "\n📣 \x1b[34mNotices:\x1b[0m\n")
for _, notice := range notices {
fmt.Fprintf(output, " - %s\n", notice)
Expand All @@ -166,11 +143,23 @@ func (v *VersionChecker) PrintNotices(output io.Writer) {
}
}

func (v *VersionChecker) LatestVersion() string {
func (v *VersionChecker) CurrentVersion() (semver.Version, error) {
current, err := semver.Parse(strings.TrimPrefix(v.currentVersion, "v"))
if err != nil {
return semver.Version{}, fmt.Errorf("failed to parse current version: %w", err)
}
return current, nil
}

func (v *VersionChecker) LatestVersion() (semver.Version, error) {
if v.responseReceived {
return v.latestVersion.Trivy.LatestVersion
latest, err := semver.Parse(strings.TrimPrefix(v.latestVersion.Trivy.LatestVersion, "v"))
if err != nil {
return semver.Version{}, fmt.Errorf("failed to parse latest version: %w", err)
}
return latest, nil
}
return ""
return semver.Version{}, errors.New("no response received from version check")
}

func (v *VersionChecker) Announcements() []announcement {
Expand Down
101 changes: 99 additions & 2 deletions pkg/notification/notice_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,101 @@ func TestPrintNotices(t *testing.T) {
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",
options: []Option{WithCurrentVersion("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",
options: []Option{WithCurrentVersion("0.60.0")},
latestVersion: "0.60.0",
announcements: []announcement{
{
FromDate: time.Date(2025, 2, 2, 12, 0, 0, 0, time.UTC),
ToDate: time.Date(2999, 1, 1, 0, 0, 0, 0, time.UTC),
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",
options: []Option{WithCurrentVersion("0.60.0")},
latestVersion: "0.60.0",
announcements: []announcement{
{
FromDate: time.Date(2025, 2, 2, 12, 0, 0, 0, time.UTC),
ToDate: time.Date(2999, 1, 1, 0, 0, 0, 0, time.UTC),
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",
options: []Option{WithCurrentVersion("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",
options: []Option{WithCurrentVersion("0.60.0")},
latestVersion: "0.60.0",
announcements: []announcement{
{
FromDate: time.Date(2025, 2, 2, 12, 0, 0, 0, time.UTC),
ToDate: time.Date(2999, 1, 1, 0, 0, 0, 0, time.UTC),
Announcement: "There are some amazing things happening right now!",
},
{
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",
options: []Option{WithCurrentVersion("0.60.0"), WithQuietMode(true)},
latestVersion: "0.60.0",
announcements: []announcement{},
responseExpected: false,
expectedOutput: "",
},
{
name: "No new version with no announcements",
options: []Option{WithCurrentVersion("0.60.0")},
latestVersion: "0.60.0",
announcements: []announcement{},
responseExpected: true,
expectedOutput: "",
},
}

for _, tt := range tests {
Expand All @@ -99,7 +194,7 @@ func TestPrintNotices(t *testing.T) {
require.Eventually(t, func() bool { return v.responseReceived == tt.responseExpected }, time.Second*5, 500)

sb := bytes.NewBufferString("")
v.PrintNotices(sb)
v.PrintNotices(t.Context(), sb)
assert.Equal(t, tt.expectedOutput, sb.String())

// check metrics are sent
Expand Down Expand Up @@ -161,7 +256,9 @@ func TestCheckForNotices(t *testing.T) {
v.RunUpdateCheck(t.Context(), nil)
require.Eventually(t, func() bool { return v.done }, time.Second*5, 500)
require.Eventually(t, func() bool { return v.responseReceived }, time.Second*5, 500)
assert.Equal(t, tt.expectedVersion, v.LatestVersion())
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 {
Expand Down
58 changes: 58 additions & 0 deletions pkg/notification/response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package notification

import (
"context"
"time"

"github.com/aquasecurity/go-version/pkg/semver"
"github.com/aquasecurity/trivy/pkg/clock"
)

// flexibleTime is a custom time type that can handle
// different date formats in JSON. It implements the
// UnmarshalJSON method to parse the date string into a time.Time object.
type flexibleTime struct {
time.Time
}

type versionInfo struct {
LatestVersion string `json:"latest_version"`
LatestDate flexibleTime `json:"latest_date"`
}

type announcement struct {
FromDate time.Time `json:"from_date"`
ToDate time.Time `json:"to_date"`
FromVersion string `json:"from_version"`
ToVersion string `json:"to_version"`
Announcement string `json:"announcement"`
}

type updateResponse struct {
Trivy versionInfo `json:"trivy"`
Announcements []announcement `json:"announcements"`
Warnings []string `json:"warnings"`
}

// shoudDisplay checks if the announcement should be displayed
// based on the current time and version. If version and date constraints are provided
// they are checked against the current time and version.
func (a *announcement) shouldDisplay(ctx context.Context, currentVersion semver.Version) bool {
if !a.FromDate.IsZero() && clock.Now(ctx).Before(a.FromDate) {
return false
}
if !a.ToDate.IsZero() && clock.Now(ctx).After(a.ToDate) {
return false
}
if a.FromVersion != "" {
if fromVersion, err := semver.Parse(a.FromVersion); err == nil && currentVersion.LessThan(fromVersion) {
return false
}
}
if a.ToVersion != "" {
if toVersion, err := semver.Parse(a.ToVersion); err == nil && currentVersion.GreaterThanOrEqual(toVersion) {
return false
}
}
return true
}
Loading