Skip to content

Commit

Permalink
[pro] Add policy enforcement (#73)
Browse files Browse the repository at this point in the history
Signed-off-by: Benji Visser <benji@093b.org>
  • Loading branch information
noqcks authored Jul 16, 2023
1 parent cd484c2 commit 2c343c9
Show file tree
Hide file tree
Showing 12 changed files with 643 additions and 26 deletions.
31 changes: 25 additions & 6 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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() {
Expand All @@ -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()
Expand Down Expand Up @@ -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),
Expand Down
16 changes: 12 additions & 4 deletions internal/config/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"`
Expand Down Expand Up @@ -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", "")

Expand Down Expand Up @@ -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)
Expand Down
21 changes: 21 additions & 0 deletions internal/ui/common_event_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions internal/ui/ephemeral_terminal_ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 4 additions & 0 deletions internal/ui/logger_ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
95 changes: 79 additions & 16 deletions internal/xeolio/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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)
}
1 change: 1 addition & 0 deletions xeol/event/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
13 changes: 13 additions & 0 deletions xeol/event/parsers/parsers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit 2c343c9

Please sign in to comment.