Skip to content

Commit

Permalink
Merge pull request #88 from adevinta/expiration-date
Browse files Browse the repository at this point in the history
internal/config: add ReportConfig.Exclusions.ExpirationDate field
  • Loading branch information
seilagamo authored Jul 24, 2024
2 parents 7a9bd78 + a9a3b59 commit e259311
Show file tree
Hide file tree
Showing 7 changed files with 222 additions and 1 deletion.
2 changes: 2 additions & 0 deletions cmd/lava/internal/help/helpdoc.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ The exclusion rules support the following filters:
and the checktype options.
- summary: regular expression that matches the summary of the
vulnerability.
- expiration: is the date on which the exclusion becomes inactive.
The format is YYYY/MM/DD.
A finding is excluded if it matches all the filters of an exclusion
rule.
Expand Down
48 changes: 48 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"os"
"regexp"
"strings"
"time"

agentconfig "github.com/adevinta/vulcan-agent/config"
types "github.com/adevinta/vulcan-types"
Expand Down Expand Up @@ -50,6 +51,10 @@ var (
// ErrInvalidOutputFormat means that the output format is
// invalid.
ErrInvalidOutputFormat = errors.New("invalid output format")

// ErrInvalidExpirationDate means that the expiration date is
// invalid.
ErrInvalidExpirationDate = errors.New("invalid expiration date")
)

// Config represents a Lava configuration.
Expand Down Expand Up @@ -391,6 +396,49 @@ type Exclusion struct {
// the vulnerability.
Summary string `yaml:"summary"`

// ExpirationDate is the date on which the exclusion becomes inactive.
// The format is YYYY/MM/DD.
ExpirationDate ExpirationDate `yaml:"expiration"`

// Description describes the exclusion.
Description string `yaml:"description"`
}

// ExpirationDateLayout is the input format for the [ExpirationDate].
const ExpirationDateLayout = "2006/01/02"

// ExpirationDate represents when an exclusion is not valid any more.
type ExpirationDate struct {
time.Time
}

// parseExpirationDate converts a string into an [ExpirationDate] value.
func parseExpirationDate(date string) (ExpirationDate, error) {
t, err := time.Parse(ExpirationDateLayout, date)
if err != nil {
return ExpirationDate{}, fmt.Errorf("%w: %w", ErrInvalidExpirationDate, err)
}
return ExpirationDate{Time: t}, nil
}

// UnmarshalText decodes an [ExpirationDate] text into an [ExpirationDate]
// value. It returns error if the provided string does not match the
// date format.
func (ed *ExpirationDate) UnmarshalText(text []byte) error {
expirationDate, err := parseExpirationDate(string(text))
if err != nil {
return err
}
*ed = expirationDate
return nil
}

// MarshalText encodes an [ExpirationDate] value as text.
func (ed ExpirationDate) MarshalText() (text []byte, err error) {
return []byte(ed.String()), nil
}

// String returns the string representation of the expiration date.
func (ed ExpirationDate) String() string {
return ed.Format(ExpirationDateLayout)
}
90 changes: 89 additions & 1 deletion internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
package config

import (
"bytes"
"errors"
"io"
"log/slog"
"regexp"
"testing"
"time"

agentconfig "github.com/adevinta/vulcan-agent/config"
types "github.com/adevinta/vulcan-types"
Expand Down Expand Up @@ -234,6 +236,39 @@ func TestParse(t *testing.T) {
want: Config{},
wantErrRegexp: regexp.MustCompile(`level string ".*": unknown name`),
},
{
name: "valid expiration date",
file: "testdata/valid_expiration_date.yaml",
want: Config{
LavaVersion: "v1.0.0",
ChecktypeURLs: []string{
"checktypes.json",
},
Targets: []Target{
{
Identifier: "example.com",
AssetType: types.DomainName,
},
},
ReportConfig: ReportConfig{
Format: OutputFormatHuman,
OutputFile: "",
Exclusions: []Exclusion{
{
Summary: "Secret Leaked in Git Repository",
Description: "Ignore test certificates.",
ExpirationDate: mustParseExpDate("2024/07/05"),
},
},
},
},
},
{
name: "invalid expiration date",
file: "testdata/invalid_expiration_date.yaml",
want: Config{},
wantErr: ErrInvalidExpirationDate,
},
}

for _, tt := range tests {
Expand All @@ -259,7 +294,6 @@ func TestParse(t *testing.T) {
t.Errorf("unexpected error: %v", err)
}
}

if diff := cmp.Diff(tt.want, got); diff != "" {
t.Errorf("configs mismatch (-want +got):\n%v", diff)
}
Expand Down Expand Up @@ -377,3 +411,57 @@ func TestSeverity_MarshalText(t *testing.T) {
func ptr[V any](v V) *V {
return &v
}

func TestParseExpirationDate(t *testing.T) {
tests := []struct {
name string
date string
want ExpirationDate
wantErr error
}{
{
name: "valid date",
date: "2024/07/05",
want: mustParseExpDate("2024/07/05"),
wantErr: nil,
},
{
name: "invalid date",
date: "2024-07-05",
want: ExpirationDate{},
wantErr: ErrInvalidExpirationDate,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseExpirationDate(tt.date)
if !errors.Is(err, tt.wantErr) {
t.Errorf("unexpected error: want: %v, got: %v", tt.wantErr, err)
}
if !got.Equal(tt.want.Time) {
t.Errorf("unexpected date: want: %v, got: %v", tt.want, got)
}
})
}
}

func TestExpirationDate_MarshalText(t *testing.T) {
date := mustParseExpDate("2024/07/05")
want := []byte("2024/07/05")

got, err := date.MarshalText()
if err != nil {
t.Errorf("unexpected error: want: %v, got: %v", nil, err)
}
if !bytes.Equal(got, want) {
t.Errorf("unexpected expiration date string: want: %s, got: %s", want, got)
}
}

func mustParseExpDate(date string) ExpirationDate {
t, err := time.Parse(ExpirationDateLayout, date)
if err != nil {
panic(err)
}
return ExpirationDate{Time: t}
}
11 changes: 11 additions & 0 deletions internal/config/testdata/invalid_expiration_date.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
lava: v1.0.0
checktypes:
- checktypes.json
targets:
- identifier: example.com
type: DomainName
report:
exclusions:
- description: Ignore test certificates.
summary: 'Secret Leaked in Git Repository'
expiration: 2024-07-05
11 changes: 11 additions & 0 deletions internal/config/testdata/valid_expiration_date.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
lava: v1.0.0
checktypes:
- checktypes.json
targets:
- identifier: example.com
type: DomainName
report:
exclusions:
- description: Ignore test certificates.
summary: 'Secret Leaked in Git Repository'
expiration: 2024/07/05
8 changes: 8 additions & 0 deletions internal/report/report.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"os"
"regexp"
"slices"
"time"

report "github.com/adevinta/vulcan-report"

Expand All @@ -30,6 +31,9 @@ type Writer struct {
exclusions []config.Exclusion
}

// timeNow is set by tests to mock the current time.
var timeNow = time.Now

// NewWriter creates a new instance of a report writer.
func NewWriter(cfg config.ReportConfig) (Writer, error) {
var prn printer
Expand Down Expand Up @@ -138,6 +142,10 @@ func (writer Writer) parseReport(er engine.Report) ([]vulnerability, error) {
// excluded based on the [Writer] configuration and the affected target.
func (writer Writer) isExcluded(v report.Vulnerability, target string) (bool, error) {
for _, excl := range writer.exclusions {
if !excl.ExpirationDate.IsZero() && excl.ExpirationDate.Before(timeNow()) {
continue
}

if excl.Fingerprint != "" && v.Fingerprint != excl.Fingerprint {
continue
}
Expand Down
53 changes: 53 additions & 0 deletions internal/report/report_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"os"
"path"
"testing"
"time"

vreport "github.com/adevinta/vulcan-report"
"github.com/google/go-cmp/cmp"
Expand Down Expand Up @@ -631,9 +632,53 @@ func TestWriter_isExcluded(t *testing.T) {
want: false,
wantNilErr: true,
},
{
name: "active exclusion",
vulnerability: vreport.Vulnerability{
Summary: "Vulnerability Summary 1",
Score: 6.7,
},
target: ".",
rConfig: config.ReportConfig{
Exclusions: []config.Exclusion{
{
Summary: "Summary 1",
Description: "Excluded vulnerabilities Summary 1",
ExpirationDate: mustParseExpDate("2024/05/06"),
},
},
},
want: true,
wantNilErr: true,
},
{
name: "expired exclusion",
vulnerability: vreport.Vulnerability{
Summary: "Vulnerability Summary 1",
Score: 6.7,
},
target: ".",
rConfig: config.ReportConfig{
Exclusions: []config.Exclusion{
{
Summary: "Summary 1",
Description: "Excluded vulnerabilities Summary 1",
ExpirationDate: mustParseExpDate("2023/05/06"),
},
},
},
want: false,
wantNilErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
oldTimeNow := timeNow
defer func() { timeNow = oldTimeNow }()
timeNow = func() time.Time {
tn, _ := time.Parse(time.RFC3339, "2024-01-02T15:04:05Z")
return tn
}
w, err := NewWriter(tt.rConfig)
if err != nil {
t.Fatalf("unable to create a report writer: %v", err)
Expand Down Expand Up @@ -1458,3 +1503,11 @@ func statusLess(a, b checkStatus) bool {
func ptr[V any](v V) *V {
return &v
}

func mustParseExpDate(date string) config.ExpirationDate {
t, err := time.Parse(config.ExpirationDateLayout, date)
if err != nil {
panic(err)
}
return config.ExpirationDate{Time: t}
}

0 comments on commit e259311

Please sign in to comment.