diff --git a/cmd/root.go b/cmd/root.go index f07ce5ce..3381ad20 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -30,6 +30,7 @@ import ( distroMatcher "github.com/xeol-io/xeol/xeol/matcher/distro" pkgMatcher "github.com/xeol-io/xeol/xeol/matcher/packages" "github.com/xeol-io/xeol/xeol/pkg" + "github.com/xeol-io/xeol/xeol/policy" "github.com/xeol-io/xeol/xeol/presenter" "github.com/xeol-io/xeol/xeol/presenter/models" "github.com/xeol-io/xeol/xeol/report" @@ -230,7 +231,7 @@ func isVerbose() (result bool) { return appConfig.CliOptions.Verbosity > 0 || isPipedInput } -//nolint:funlen +//nolint:funlen,gocognit func startWorker(userInput string, failOnEolFound bool, eolMatchDate time.Time) <-chan error { errs := make(chan error) go func() { @@ -252,8 +253,21 @@ func startWorker(userInput string, failOnEolFound bool, eolMatchDate time.Time) var pkgContext pkg.Context var wg = &sync.WaitGroup{} var loadedDB, gatheredPackages bool + var policies []xeolio.Policy + x := xeolio.NewXeolClient(appConfig.APIKey) - wg.Add(2) + wg.Add(3) + go func() { + defer wg.Done() + log.Debug("Fetching organization policies") + if appConfig.APIKey != "" { + policies, err = x.FetchPolicies() + if err != nil { + errs <- fmt.Errorf("failed to fetch policy: %w", err) + return + } + } + }() go func() { defer wg.Done() @@ -307,20 +321,25 @@ func startWorker(userInput string, failOnEolFound bool, eolMatchDate time.Time) DBStatus: status, } - if appConfig.APIKey != "" && appConfig.APIURL != "" { - x := xeolio.NewXeolEvent(appConfig.APIURL, appConfig.APIKey, report.XeolEventPayload{ + if appConfig.APIKey != "" { + if err := x.SendEvent(report.XeolEventPayload{ Matches: allMatches.Sorted(), Packages: packages, Context: pkgContext, AppConfig: appConfig, ImageName: sbom.Source.ImageMetadata.UserInput, - }) - if err := x.Send(); err != nil { + }); err != nil { errs <- fmt.Errorf("failed to send eol event: %w", err) return } } + failScan := policy.Evaluate(policies, allMatches) + if failScan { + errs <- xeolerr.ErrPolicyViolation + return + } + bus.Publish(partybus.Event{ Type: event.EolScanningFinished, Value: presenter.GetPresenter(presenterConfig, pb), diff --git a/internal/config/application.go b/internal/config/application.go index 8b3e3a51..3f71c48f 100644 --- a/internal/config/application.go +++ b/internal/config/application.go @@ -25,7 +25,7 @@ type defaultValueLoader interface { loadDefaultValues(*viper.Viper) } -const XeolAPIUrl = "https://engine.xeol.io/v1/scan" +const DefaultProLookahead = "now+3y" type parser interface { parseConfigValues() error @@ -46,7 +46,6 @@ type Application struct { EolMatchDate time.Time `yaml:"-" json:"-"` FailOnEolFound bool `yaml:"fail-on-eol-found" json:"fail-on-eol-found" mapstructure:"fail-on-eol-found"` // whether to exit with a non-zero exit code if any EOLs are found APIKey string `yaml:"api-key" json:"api-key" mapstructure:"api-key"` - APIURL string `yaml:"api-url" json:"api-url" mapstructure:"api-url"` ProjectName string `yaml:"project-name" json:"project-name" mapstructure:"project-name"` ImagePath string `yaml:"image-path" json:"image-path" mapstructure:"image-path"` Registry registry `yaml:"registry" json:"registry" mapstructure:"registry"` @@ -91,7 +90,6 @@ func (cfg Application) loadDefaultValues(v *viper.Viper) { v.SetDefault("check-for-app-update", true) v.SetDefault("fail-on-eol-found", false) v.SetDefault("project-name", getDefaultProjectName()) - v.SetDefault("api-url", XeolAPIUrl) v.SetDefault("image-path", "Dockerfile") v.SetDefault("default-image-pull-source", "") @@ -210,12 +208,22 @@ func (cfg *Application) parseConfigValues() error { } func (cfg *Application) parseLookaheadOption() error { + var err error + // if the user has specified an API key and is posting results to xeol.io, then we + // set a default lookahead value to 3 years from now + if cfg.APIKey != "" { + cfg.EolMatchDate, err = tparse.ParseNow(time.RFC3339, DefaultProLookahead) + if err != nil { + return fmt.Errorf("bad --lookahead value: '%s'", cfg.Lookahead) + } + return nil + } + if cfg.Lookahead == "none" { cfg.EolMatchDate = time.Now() return nil } - var err error cfg.EolMatchDate, err = tparse.ParseNow(time.RFC3339, fmt.Sprintf("now+%s", cfg.Lookahead)) if err != nil { return fmt.Errorf("bad --lookahead value: '%s'", cfg.Lookahead) diff --git a/internal/ui/common_event_handlers.go b/internal/ui/common_event_handlers.go index 23df7a7d..d7d53cc1 100644 --- a/internal/ui/common_event_handlers.go +++ b/internal/ui/common_event_handlers.go @@ -4,11 +4,32 @@ import ( "fmt" "io" + "github.com/gookit/color" "github.com/wagoodman/go-partybus" xeolEventParsers "github.com/xeol-io/xeol/xeol/event/parsers" + "github.com/xeol-io/xeol/xeol/policy" ) +func handlePolicyEvaluationMessage(event partybus.Event, reportOutput io.Writer) error { + // show the report to stdout + pt, err := xeolEventParsers.ParsePolicyEvaluationMessage(event) + if err != nil { + return fmt.Errorf("bad %s event: %w", event.Type, err) + } + + var message string + if pt.Type == policy.PolicyTypeDeny { + message = color.Red.Sprintf("[%s] Policy Violation: %s (v%s) needs to upgraded to a newer version. This scan will now exit non-zero.\n\n", pt.Type, pt.ProductName, pt.Cycle) + } else { + message = color.Yellow.Sprintf("[%s] Policy Violation: %s (v%s) needs to be upgraded to a newer version. This policy will fail builds starting on %s.\n\n", pt.Type, pt.ProductName, pt.Cycle, pt.FailDate) + } + if _, err := reportOutput.Write([]byte(message)); err != nil { + return fmt.Errorf("unable to show policy evaluation message: %w", err) + } + return nil +} + func handleEolScanningFinished(event partybus.Event, reportOutput io.Writer) error { // show the report to stdout pres, err := xeolEventParsers.ParseEolScanningFinished(event) diff --git a/internal/ui/ephemeral_terminal_ui.go b/internal/ui/ephemeral_terminal_ui.go index b955c41a..43b6860b 100644 --- a/internal/ui/ephemeral_terminal_ui.go +++ b/internal/ui/ephemeral_terminal_ui.go @@ -77,6 +77,14 @@ func (h *ephemeralTerminalUI) Handle(event partybus.Event) error { if err := h.handler.Handle(ctx, h.frame, event, h.waitGroup); err != nil { log.Errorf("unable to show %s event: %+v", event.Type, err) } + case event.Type == xeolEvent.PolicyEvaluationMessage: + // we need to close the screen now since signaling the the presenter is ready means that we + // are about to write bytes to stdout, so we should reset the terminal state first + h.closeScreen(false) + + if err := handlePolicyEvaluationMessage(event, h.reportOutput); err != nil { + log.Errorf("unable to show %s event: %+v", event.Type, err) + } case event.Type == xeolEvent.AppUpdateAvailable: if err := handleAppUpdateAvailable(ctx, h.frame, event, h.waitGroup); err != nil { diff --git a/internal/ui/logger_ui.go b/internal/ui/logger_ui.go index 95fb25ea..4b7f72b8 100644 --- a/internal/ui/logger_ui.go +++ b/internal/ui/logger_ui.go @@ -28,6 +28,10 @@ func (l *loggerUI) Setup(unsubscribe func() error) error { func (l loggerUI) Handle(event partybus.Event) error { switch event.Type { + case xeolEvent.PolicyEvaluationMessage: + if err := handlePolicyEvaluationMessage(event, l.reportOutput); err != nil { + log.Warnf("unable to show policy evaluation message event: %+v", err) + } case xeolEvent.EolScanningFinished: if err := handleEolScanningFinished(event, l.reportOutput); err != nil { log.Warnf("unable to show catalog image finished event: %+v", err) diff --git a/internal/xeolio/request.go b/internal/xeolio/request.go index ff2a2085..af9a5692 100644 --- a/internal/xeolio/request.go +++ b/internal/xeolio/request.go @@ -4,26 +4,71 @@ import ( "bytes" "encoding/json" "fmt" + "io" "net/http" + "strings" "time" "github.com/xeol-io/xeol/internal/log" "github.com/xeol-io/xeol/xeol/report" ) -type XeolEvent struct { - URL string - APIKey string - Payload report.XeolEventPayload +type PolicyType string +type CycleOperator string + +const ( + XeolAPIURL = "https://api.xeol.io" + XeolEngineURL = "https://engine.xeol.io" + + PolicyTypeEol PolicyType = "EOL" + + CycleOperatorLessThan CycleOperator = "LT" + CycleOperatorLessThanOrEqual CycleOperator = "LTE" + CycleOperatorEqual CycleOperator = "EQ" +) + +type Policy struct { + ID string `json:"id"` + PolicyType PolicyType `json:"policy_type"` + WarnDate string `json:"warn_date"` + DenyDate string `json:"deny_date"` + ProductName string `json:"product_name"` + Cycle string `json:"cycle"` + CycleOperator CycleOperator `json:"cycle_operator"` } -func (x *XeolEvent) Send() error { - payload, err := json.Marshal(x.Payload) - if err != nil { - return fmt.Errorf("error marshalling xeol.io API request: %v", err) +func (pt *PolicyType) UnmarshalJSON(b []byte) error { + str := strings.Trim(string(b), "\"") + if str != string(PolicyTypeEol) { + return fmt.Errorf("invalid PolicyType %s", str) + } + *pt = PolicyType(str) + return nil +} + +func (co *CycleOperator) UnmarshalJSON(b []byte) error { + str := strings.Trim(string(b), "\"") + switch str { + case string(CycleOperatorLessThan), string(CycleOperatorLessThanOrEqual), string(CycleOperatorEqual): + *co = CycleOperator(str) + default: + return fmt.Errorf("invalid CycleOperator %s", str) + } + return nil +} + +type XeolClient struct { + APIKey string +} + +func NewXeolClient(apiKey string) *XeolClient { + return &XeolClient{ + APIKey: apiKey, } +} - req, err := http.NewRequest("PUT", x.URL, bytes.NewBuffer(payload)) +func (x *XeolClient) makeRequest(method, url, path string, body io.Reader, out interface{}) error { + req, err := http.NewRequest(method, fmt.Sprintf("%s/%s", url, path), body) if err != nil { return err } @@ -38,20 +83,38 @@ func (x *XeolEvent) Send() error { if err != nil { return fmt.Errorf("xeol.io API request failed: %v", err) } - defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { return fmt.Errorf("xeol.io API unexpected status code %d", resp.StatusCode) } - log.Debug("sent event to xeol.io API at %s", x.URL) + if out != nil { + if err := json.NewDecoder(resp.Body).Decode(out); err != nil { + return fmt.Errorf("xeol.io API response decode failed: %v", err) + } + } else { + log.Debug("sent event to xeol.io API at %s", req.URL.String()) + } + return nil } -func NewXeolEvent(url string, apiKey string, payload report.XeolEventPayload) *XeolEvent { - return &XeolEvent{ - URL: url, - APIKey: apiKey, - Payload: payload, +func (x *XeolClient) FetchPolicies() ([]Policy, error) { + var policies []Policy + err := x.makeRequest("GET", XeolAPIURL, "v1/policy", nil, &policies) + if err != nil { + return nil, err } + + return policies, nil +} + +func (x *XeolClient) SendEvent(payload report.XeolEventPayload) error { + p, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("error marshalling xeol.io API request: %v", err) + } + + return x.makeRequest("PUT", XeolEngineURL, "v1/scan", bytes.NewBuffer(p), nil) } diff --git a/xeol/event/event.go b/xeol/event/event.go index 4c65a6ec..e8f7a242 100644 --- a/xeol/event/event.go +++ b/xeol/event/event.go @@ -7,6 +7,7 @@ const ( UpdateEolDatabase partybus.EventType = "xeol-update-eol-database" EolScanningStarted partybus.EventType = "xeol-eol-scanning-started" EolScanningFinished partybus.EventType = "xeol-eol-scanning-finished" + PolicyEvaluationMessage partybus.EventType = "xeol-policy-evaluation-message" AttestationVerified partybus.EventType = "xeol-attestation-signature-passed" AttestationVerificationSkipped partybus.EventType = "xeol-attestation-verification-skipped" NonRootCommandFinished partybus.EventType = "xeol-non-root-command-finished" diff --git a/xeol/event/parsers/parsers.go b/xeol/event/parsers/parsers.go index 24a2d1e5..915a740b 100644 --- a/xeol/event/parsers/parsers.go +++ b/xeol/event/parsers/parsers.go @@ -8,6 +8,7 @@ import ( "github.com/xeol-io/xeol/xeol/event" "github.com/xeol-io/xeol/xeol/matcher" + "github.com/xeol-io/xeol/xeol/policy" "github.com/xeol-io/xeol/xeol/presenter" ) @@ -36,6 +37,18 @@ func checkEventType(actual, expected partybus.EventType) error { return nil } +func ParsePolicyEvaluationMessage(e partybus.Event) (*policy.EvaluationResult, error) { + if err := checkEventType(e.Type, event.PolicyEvaluationMessage); err != nil { + return nil, err + } + + pt, ok := e.Value.(policy.EvaluationResult) + if !ok { + return nil, newPayloadErr(e.Type, "Value", e.Value) + } + return &pt, nil +} + func ParseAppUpdateAvailable(e partybus.Event) (string, error) { if err := checkEventType(e.Type, event.AppUpdateAvailable); err != nil { return "", err diff --git a/xeol/policy/policy.go b/xeol/policy/policy.go new file mode 100644 index 00000000..04a40b05 --- /dev/null +++ b/xeol/policy/policy.go @@ -0,0 +1,133 @@ +package policy + +import ( + "time" + + "github.com/Masterminds/semver" + "github.com/wagoodman/go-partybus" + + "github.com/xeol-io/xeol/internal/bus" + "github.com/xeol-io/xeol/internal/log" + "github.com/xeol-io/xeol/internal/xeolio" + "github.com/xeol-io/xeol/xeol/event" + "github.com/xeol-io/xeol/xeol/match" +) + +const ( + DateLayout = "2006-01-02" + PolicyTypeWarn EvaluationType = "WARN" + PolicyTypeDeny EvaluationType = "DENY" +) + +var timeNow = time.Now + +type EvaluationType string + +type EvaluationResult struct { + Type EvaluationType + ProductName string + Cycle string + FailDate string +} + +func cycleOperatorMatch(m match.Match, policy xeolio.Policy) bool { + if m.Cycle.ProductName != policy.ProductName { + return false + } + + pv, err := semver.NewVersion(policy.Cycle) + if err != nil { + log.Debugf("Invalid policy cycle version: %s", policy.Cycle) + return false + } + + mv, err := semver.NewVersion(m.Cycle.ReleaseCycle) + if err != nil { + log.Debugf("Invalid match cycle version: %s", m.Cycle.ReleaseCycle) + return false + } + + switch policy.CycleOperator { + case xeolio.CycleOperatorLessThan: + return mv.LessThan(pv) + case xeolio.CycleOperatorLessThanOrEqual: + return !mv.GreaterThan(pv) // equivalent to mv <= pv + case xeolio.CycleOperatorEqual: + return mv.Equal(pv) + default: + log.Debugf("Invalid policy cycle operator: %s", policy.CycleOperator) + return false + } +} + +func warnMatch(policy xeolio.Policy) bool { + warnDate, err := time.Parse(DateLayout, policy.WarnDate) + if err != nil { + log.Debugf("Invalid policy warn date: %s", policy.WarnDate) + return false + } + if timeNow().After(warnDate) { + return true + } + return false +} + +func denyMatch(policy xeolio.Policy) bool { + denyDate, err := time.Parse(DateLayout, policy.DenyDate) + if err != nil { + log.Debugf("Invalid policy deny date: %s", policy.DenyDate) + return false + } + if timeNow().After(denyDate) { + return true + } + return false +} + +func evaluateMatches(policies []xeolio.Policy, matches match.Matches) []EvaluationResult { + var results []EvaluationResult + for _, policy := range policies { + for _, match := range matches.Sorted() { + if cycleOperatorMatch(match, policy) { + if denyMatch(policy) { + results = append(results, EvaluationResult{ + Type: PolicyTypeDeny, + ProductName: match.Cycle.ProductName, + Cycle: match.Cycle.ReleaseCycle, + }, + ) + continue + } + if warnMatch(policy) { + results = append(results, EvaluationResult{ + Type: PolicyTypeWarn, + ProductName: match.Cycle.ProductName, + Cycle: match.Cycle.ReleaseCycle, + FailDate: policy.DenyDate, + }, + ) + continue + } + } + } + } + return results +} + +// Evaluate evaluates a set of policies against a set of matches. +func Evaluate(policies []xeolio.Policy, matches match.Matches) bool { + policyMatches := evaluateMatches(policies, matches) + // whether we should fail the scan or not + failScan := false + + for _, policyMatch := range policyMatches { + if policyMatch.Type == PolicyTypeDeny { + failScan = true + } + bus.Publish(partybus.Event{ + Type: event.PolicyEvaluationMessage, + Value: policyMatch, + }) + } + return failScan +} diff --git a/xeol/policy/policy_test.go b/xeol/policy/policy_test.go new file mode 100644 index 00000000..0c209610 --- /dev/null +++ b/xeol/policy/policy_test.go @@ -0,0 +1,343 @@ +package policy + +import ( + "reflect" + "testing" + "time" + + syftPkg "github.com/anchore/syft/syft/pkg" + "github.com/google/uuid" + + "github.com/xeol-io/xeol/internal/xeolio" + "github.com/xeol-io/xeol/xeol/eol" + "github.com/xeol-io/xeol/xeol/match" + "github.com/xeol-io/xeol/xeol/pkg" +) + +func TestEvaluate(t *testing.T) { + tests := []struct { + name string + policy []xeolio.Policy + matches []match.Match + want []EvaluationResult + }{ + { + name: "policy with no matches", + policy: []xeolio.Policy{ + { + ProductName: "foo", + Cycle: "1.0.0", + CycleOperator: xeolio.CycleOperatorLessThan, + WarnDate: "2021-01-01", + DenyDate: "2021-01-01", + }, + }, + matches: []match.Match{ + { + Cycle: eol.Cycle{ + ProductName: "product-e", + ReleaseCycle: "1.0.0", + }, + Package: pkg.Package{ + ID: pkg.ID(uuid.NewString()), + Name: "package-e", + Version: "2.0.0", + Type: syftPkg.RpmPkg, + }, + }, + }, + want: nil, + }, + { + name: "policy with deny match", + policy: []xeolio.Policy{ + { + ProductName: "foo", + Cycle: "1.0", + CycleOperator: xeolio.CycleOperatorLessThanOrEqual, + WarnDate: "2021-01-01", + DenyDate: "2021-01-29", + }, + }, + matches: []match.Match{ + { + Cycle: eol.Cycle{ + ProductName: "foo", + ReleaseCycle: "1.0", + }, + Package: pkg.Package{ + ID: pkg.ID(uuid.NewString()), + Name: "foo", + Version: "1.2.1", + Type: syftPkg.RpmPkg, + }, + }, + }, + want: []EvaluationResult{ + { + Type: PolicyTypeDeny, + ProductName: "foo", + Cycle: "1.0", + }, + }, + }, + { + name: "policy with warn match, version less than equal", + policy: []xeolio.Policy{ + { + ProductName: "foo", + Cycle: "1.0", + CycleOperator: xeolio.CycleOperatorLessThanOrEqual, + WarnDate: "2021-01-01", + DenyDate: "2021-03-01", + }, + }, + matches: []match.Match{ + { + Cycle: eol.Cycle{ + ProductName: "foo", + ReleaseCycle: "1.0", + }, + Package: pkg.Package{ + ID: pkg.ID(uuid.NewString()), + Name: "foo", + Version: "1.2.1", + Type: syftPkg.RpmPkg, + }, + }, + }, + want: []EvaluationResult{ + { + Type: PolicyTypeWarn, + ProductName: "foo", + Cycle: "1.0", + FailDate: "2021-03-01", + }, + }, + }, + { + name: "policy with warn match, version less than", + policy: []xeolio.Policy{ + { + ProductName: "foo", + Cycle: "1.0", + CycleOperator: xeolio.CycleOperatorLessThan, + WarnDate: "2021-01-01", + DenyDate: "2021-03-01", + }, + }, + matches: []match.Match{ + { + Cycle: eol.Cycle{ + ProductName: "foo", + ReleaseCycle: "0.9", + }, + Package: pkg.Package{ + ID: pkg.ID(uuid.NewString()), + Name: "foo", + Version: "0.9.0", + Type: syftPkg.RpmPkg, + }, + }, + }, + want: []EvaluationResult{ + { + Type: PolicyTypeWarn, + ProductName: "foo", + Cycle: "0.9", + FailDate: "2021-03-01", + }, + }, + }, + { + name: "policy with warn match, version equal", + policy: []xeolio.Policy{ + { + ProductName: "foo", + Cycle: "1.0", + CycleOperator: xeolio.CycleOperatorEqual, + WarnDate: "2021-01-01", + DenyDate: "2021-03-01", + }, + }, + matches: []match.Match{ + { + Cycle: eol.Cycle{ + ProductName: "foo", + ReleaseCycle: "1.0", + }, + Package: pkg.Package{ + ID: pkg.ID(uuid.NewString()), + Name: "foo", + Version: "1.0.1", + Type: syftPkg.RpmPkg, + }, + }, + }, + want: []EvaluationResult{ + { + Type: PolicyTypeWarn, + ProductName: "foo", + Cycle: "1.0", + FailDate: "2021-03-01", + }, + }, + }, + { + name: "test multiple policy matches", + policy: []xeolio.Policy{ + { + ProductName: "foo", + Cycle: "1.3", + CycleOperator: xeolio.CycleOperatorEqual, + WarnDate: "2021-01-01", + DenyDate: "2021-03-01", + }, + { + ProductName: "bar", + Cycle: "2.1", + CycleOperator: xeolio.CycleOperatorLessThanOrEqual, + WarnDate: "2021-01-01", + DenyDate: "2021-01-29", + }, + }, + matches: []match.Match{ + { + Cycle: eol.Cycle{ + ProductName: "foo", + ReleaseCycle: "1.3", + }, + Package: pkg.Package{ + ID: pkg.ID(uuid.NewString()), + Name: "foo", + Version: "1.3.0", + Type: syftPkg.RpmPkg, + }, + }, + { + Cycle: eol.Cycle{ + ProductName: "bar", + ReleaseCycle: "2.0", + }, + Package: pkg.Package{ + ID: pkg.ID(uuid.NewString()), + Name: "bar", + Version: "2.0.1", + Type: syftPkg.RpmPkg, + }, + }, + }, + want: []EvaluationResult{ + { + Type: PolicyTypeWarn, + ProductName: "foo", + Cycle: "1.3", + FailDate: "2021-03-01", + }, + { + Type: PolicyTypeDeny, + ProductName: "bar", + Cycle: "2.0", + }, + }, + }, + { + name: "test bad dates", + policy: []xeolio.Policy{ + { + ProductName: "foo", + Cycle: "1.3", + CycleOperator: xeolio.CycleOperatorEqual, + WarnDate: "2021/01/01", + DenyDate: "2021/03/01", + }, + }, + matches: []match.Match{ + { + Cycle: eol.Cycle{ + ProductName: "foo", + ReleaseCycle: "1.3", + }, + Package: pkg.Package{ + ID: pkg.ID(uuid.NewString()), + Name: "foo", + Version: "1.3.0", + Type: syftPkg.RpmPkg, + }, + }, + }, + want: nil, + }, + { + name: "test bad cycles", + policy: []xeolio.Policy{ + { + ProductName: "foo", + Cycle: "1.4", + CycleOperator: xeolio.CycleOperatorEqual, + WarnDate: "2021/01/01", + DenyDate: "2021/03/01", + }, + }, + matches: []match.Match{ + { + Cycle: eol.Cycle{ + ProductName: "foo", + ReleaseCycle: "1.3", + }, + Package: pkg.Package{ + ID: pkg.ID(uuid.NewString()), + Name: "foo", + Version: "1.3.0", + Type: syftPkg.RpmPkg, + }, + }, + }, + want: nil, + }, + { + name: "test future dates", + policy: []xeolio.Policy{ + { + ProductName: "foo", + Cycle: "1.3", + CycleOperator: xeolio.CycleOperatorEqual, + WarnDate: "2021-04-01", + DenyDate: "2021-05-01", + }, + }, + matches: []match.Match{ + { + Cycle: eol.Cycle{ + ProductName: "foo", + ReleaseCycle: "1.3", + }, + Package: pkg.Package{ + ID: pkg.ID(uuid.NewString()), + Name: "foo", + Version: "1.3.0", + Type: syftPkg.RpmPkg, + }, + }, + }, + want: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + timeNow = func() time.Time { + return time.Date(2021, 2, 1, 0, 0, 0, 0, time.UTC) + } + + matches := match.NewMatches(tt.matches...) + policyMatches := evaluateMatches(tt.policy, matches) + if len(policyMatches) != len(tt.want) { + t.Errorf("expected %d policy matches, got %d", len(tt.want), len(policyMatches)) + } + if !reflect.DeepEqual(policyMatches, tt.want) { + t.Errorf("expected policy matches to be %v, got %v", tt.want, policyMatches) + } + }) + } +} diff --git a/xeol/search/purl.go b/xeol/search/purl.go index b1dc87f2..2e34c61c 100644 --- a/xeol/search/purl.go +++ b/xeol/search/purl.go @@ -68,6 +68,8 @@ func ByDistroCpe(store eol.Provider, distro *linux.Release, eolMatchDate time.Ti return match.Match{}, nil } +// returnMatchingCycle returns the first cycle that matches the version string. +// If no cycle matches, an empty cycle is returned. func returnMatchingCycle(version string, cycles []eol.Cycle) (eol.Cycle, error) { v, err := semver.NewVersion(version) if err != nil { diff --git a/xeol/xeolerr/errors.go b/xeol/xeolerr/errors.go index 63bcccc7..521649d5 100644 --- a/xeol/xeolerr/errors.go +++ b/xeol/xeolerr/errors.go @@ -3,4 +3,6 @@ package xeolerr var ( // ErrEolFound indicates when an EOL package is found and --fail-on-eol-found is set ErrEolFound = NewExpectedErr("discovered EOL packages") + // ErrPolicyViolation indicates when a policy violation has occurred + ErrPolicyViolation = NewExpectedErr("policy violation") )