Skip to content

Commit

Permalink
update policy eval UI
Browse files Browse the repository at this point in the history
Signed-off-by: Benji Visser <[email protected]>
  • Loading branch information
noqcks committed Sep 19, 2023
1 parent 5170793 commit f19a439
Show file tree
Hide file tree
Showing 6 changed files with 246 additions and 9 deletions.
93 changes: 92 additions & 1 deletion cmd/xeol/cli/commands/root.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
package commands

import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"strings"
"sync"

"github.com/CycloneDX/cyclonedx-go"
"github.com/anchore/clio"
"github.com/anchore/syft/syft/formats/common/cyclonedxhelpers"
"github.com/anchore/syft/syft/linux"
syftPkg "github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
"github.com/hashicorp/go-multierror"
"github.com/spf13/cobra"
"github.com/wagoodman/go-partybus"
Expand All @@ -20,6 +25,7 @@ import (
"github.com/xeol-io/xeol/internal/format"
"github.com/xeol-io/xeol/internal/log"
"github.com/xeol-io/xeol/internal/stringutil"
"github.com/xeol-io/xeol/internal/xeolio"
"github.com/xeol-io/xeol/xeol"
"github.com/xeol-io/xeol/xeol/db"
"github.com/xeol-io/xeol/xeol/event"
Expand All @@ -28,7 +34,10 @@ 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/policy/types"
"github.com/xeol-io/xeol/xeol/presenter/models"
"github.com/xeol-io/xeol/xeol/report"
"github.com/xeol-io/xeol/xeol/store"
"github.com/xeol-io/xeol/xeol/xeolerr"
)
Expand Down Expand Up @@ -100,8 +109,27 @@ func runXeol(app clio.Application, opts *options.Xeol, userInput string) error {
var pkgContext pkg.Context
var wg = &sync.WaitGroup{}
var loadedDB, gatheredPackages bool
var policies []policy.Policy
var certificates string
x := xeolio.NewXeolClient(opts.APIKey)

wg.Add(2)
wg.Add(3)
go func() {
defer wg.Done()
log.Debug("Fetching organization policies")
if opts.APIKey != "" {
policies, err = x.FetchPolicies()
if err != nil {
errs <- fmt.Errorf("failed to fetch policy: %w", err)
return
}
certificates, err = x.FetchCertificates()
if err != nil {
errs <- fmt.Errorf("failed to fetch certificate: %w", err)
return
}
}
}()

go func() {
defer wg.Done()
Expand Down Expand Up @@ -156,6 +184,64 @@ func runXeol(app clio.Application, opts *options.Xeol, userInput string) error {
}
}

var failScan bool
var imageVerified bool
var sourceIsImageType bool
switch s.Source.Metadata.(type) {
case source.StereoscopeImageSourceMetadata:
sourceIsImageType = true
}

for _, p := range policies {
switch p.GetPolicyType() {
case types.PolicyTypeNotary:
// Notary policy is only applicable to images
if !sourceIsImageType {
continue
}
shouldFailScan, res := p.Evaluate(allMatches, opts.ProjectName, userInput, certificates)
imageVerified = res.GetVerified()
if shouldFailScan {
failScan = true
}

case types.PolicyTypeEol:
shouldFailScan, _ := p.Evaluate(allMatches, opts.ProjectName, "", "")
if shouldFailScan {
failScan = true
}
}
}

if opts.APIKey != "" {
buf := new(bytes.Buffer)
bom := cyclonedxhelpers.ToFormatModel(*s)
enc := cyclonedx.NewBOMEncoder(buf, cyclonedx.BOMFileFormatJSON)
if err := enc.Encode(bom); err != nil {
errs <- fmt.Errorf("failed to encode sbom: %w", err)
return
}

eventSource, err := xeolio.NewEventSource(s.Source)
if err != nil {
errs <- fmt.Errorf("failed to create event source: %w", err)
return
}

if err := x.SendEvent(report.XeolEventPayload{
Matches: allMatches.Sorted(),
Packages: packages,
Context: pkgContext,
AppConfig: opts,
EventSource: eventSource,
ImageVerified: imageVerified,
Sbom: base64.StdEncoding.EncodeToString(buf.Bytes()),
}); err != nil {
errs <- fmt.Errorf("failed to send eol event: %w", err)
return
}
}

if err := writer.Write(models.PresenterConfig{
Matches: allMatches,
Packages: packages,
Expand All @@ -166,6 +252,11 @@ func runXeol(app clio.Application, opts *options.Xeol, userInput string) error {
}); err != nil {
errs <- err
}

if failScan {
errs <- xeolerr.ErrPolicyViolation
return
}
}()

return readAllErrors(errs)
Expand Down
2 changes: 1 addition & 1 deletion cmd/xeol/internal/ui/no_ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func (n *NoUI) Setup(subscription partybus.Unsubscribable) error {

func (n *NoUI) Handle(e partybus.Event) error {
switch e.Type {
case event.CLIReport, event.CLINotification:
case event.CLIReport, event.CLINotification, event.EolPolicyEvaluationMessage, event.NotaryPolicyEvaluationMessage:
// keep these for when the UI is terminated to show to the screen (or perform other events)
n.finalizeEvents = append(n.finalizeEvents, e)
case event.CLIExit:
Expand Down
75 changes: 75 additions & 0 deletions cmd/xeol/internal/ui/post_ui_event_writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ import (
"github.com/xeol-io/xeol/internal/log"
"github.com/xeol-io/xeol/xeol/event"
"github.com/xeol-io/xeol/xeol/event/parsers"

xeolEventParsers "github.com/xeol-io/xeol/xeol/event/parsers"
policyTypes "github.com/xeol-io/xeol/xeol/policy/types"
)

var (
terminalRed = lipgloss.Color("196")
terminalYellow = lipgloss.Color("214")
)

type postUIEventWriter struct {
Expand All @@ -30,6 +38,18 @@ type eventWriter func(io.Writer, ...partybus.Event) error
func newPostUIEventWriter(stdout, stderr io.Writer) *postUIEventWriter {
return &postUIEventWriter{
handles: []postUIHandle{
{
event: event.EolPolicyEvaluationMessage,
respectQuiet: false,
writer: stdout,
dispatch: writeEolPolicyEvaluationMessage,
},
{
event: event.NotaryPolicyEvaluationMessage,
respectQuiet: false,
writer: stdout,
dispatch: writeNotaryPolicyEvaluationMessage,
},
{
event: event.CLIReport,
respectQuiet: false,
Expand Down Expand Up @@ -113,6 +133,61 @@ func writeNotifications(writer io.Writer, events ...partybus.Event) error {
return nil
}

func writeNotaryPolicyEvaluationMessage(writer io.Writer, events ...partybus.Event) error {
for _, e := range events {
// show the report to stdout
nt, err := xeolEventParsers.ParseNotaryPolicyEvaluationMessage(e)
if err != nil {
return fmt.Errorf("bad %s event: %w", e.Type, err)
}

var notice string
if nt.Action == policyTypes.PolicyActionDeny {
notice = lipgloss.NewStyle().Foreground(terminalRed).Italic(true).Render(fmt.Sprintf("[%s][%s] Policy Violation: image '%s' is not signed by a trusted party.\n",
nt.Action, nt.Type, nt.ImageReference))
} else {
if nt.FailDate != "" {
notice = lipgloss.NewStyle().Foreground(terminalYellow).Italic(true).Render(fmt.Sprintf("[%s][%s] Policy Violation: image '%s' is not signed by a trusted party. This policy will fail builds starting on %s.\n",
nt.Action, nt.Type, nt.ImageReference, nt.FailDate))
} else {
notice = lipgloss.NewStyle().Foreground(terminalYellow).Italic(true).Render(fmt.Sprintf("[%s][%s] Policy Violation: image '%s' is not signed by a trusted party.\n",
nt.Action, nt.Type, nt.ImageReference))
}
}
if _, err := fmt.Fprintln(writer, notice); err != nil {
// don't let this be fatal
log.WithFields("error", err).Warn("failed to write app update notification")
}
}
return nil
}

func writeEolPolicyEvaluationMessage(writer io.Writer, events ...partybus.Event) error {
for _, e := range events {
// show the report to stdout
pt, err := xeolEventParsers.ParseEolPolicyEvaluationMessage(e)
if err != nil {
return fmt.Errorf("bad %s event: %w", e.Type, err)
}

var notice string
if pt.Action == policyTypes.PolicyActionDeny {
notice = lipgloss.NewStyle().Foreground(terminalRed).Italic(true).Render(fmt.Sprintf("[%s][%s] Policy Violation: %s (v%s) needs to be upgraded to a newer version.\n", pt.Action, pt.Type, pt.ProductName, pt.Cycle))
} else {
if pt.FailDate != "" {
notice = lipgloss.NewStyle().Foreground(terminalYellow).Italic(true).Render(fmt.Sprintf("[%s][%s] Policy Violation: %s (v%s) needs to be upgraded to a newer version. This policy will fail builds starting on %s.\n", pt.Action, pt.Type, pt.ProductName, pt.Cycle, pt.FailDate))
} else {
notice = lipgloss.NewStyle().Foreground(terminalYellow).Italic(true).Render(fmt.Sprintf("[%s][%s] Policy Violation: %s (v%s) needs to be upgraded to a newer version.\n", pt.Action, pt.Type, pt.ProductName, pt.Cycle))
}
}
if _, err := fmt.Fprintln(writer, notice); err != nil {
// don't let this be fatal
log.WithFields("error", err).Warn("failed to write app update notification")
}
}
return nil
}

func writeAppUpdate(writer io.Writer, events ...partybus.Event) error {
// 13 = high intensity magenta (ANSI 16 bit code) + italics
style := lipgloss.NewStyle().Foreground(lipgloss.Color("13")).Italic(true)
Expand Down
2 changes: 1 addition & 1 deletion cmd/xeol/internal/ui/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
log.WithFields("component", "ui").Tracef("event: %q", msg.Type)

switch msg.Type {
case event.CLIReport, event.CLINotification, event.CLIExit, event.CLIAppUpdateAvailable:
case event.CLIReport, event.CLINotification, event.CLIExit, event.CLIAppUpdateAvailable, event.EolPolicyEvaluationMessage, event.NotaryPolicyEvaluationMessage:
// keep these for when the UI is terminated to show to the screen (or perform other events)
m.finalizeEvents = append(m.finalizeEvents, msg)

Expand Down
20 changes: 14 additions & 6 deletions internal/xeolio/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,17 +64,19 @@ func (x *XeolClient) FetchCertificates() (string, error) {
var raw json.RawMessage
statusCode, err := x.makeRequest("GET", XeolAPIURL, "certificate", nil, &raw)
if err != nil {
return "", err
log.Warnf("failed to fetch certificates, continuing without notary policy evaluation")
return "", nil
}

if statusCode == http.StatusNotFound {
log.Debugf("no certificates found in xeol.io API response")
log.Warnf("no certificates found in xeol.io API response")
return "", nil
}

var resp CertificateResponse
if err := json.Unmarshal(raw, &resp); err != nil {
return "", err
log.Warnf("failed to unmarshal certificates, continuing without notary policy evaluation")
return "", nil
}

return resp.Certificate, nil
Expand All @@ -84,15 +86,21 @@ func (x *XeolClient) FetchPolicies() ([]policy.Policy, error) {
var raw json.RawMessage
statusCode, err := x.makeRequest("GET", XeolAPIURL, "v2/policy", nil, &raw)
if err != nil {
return nil, err
log.Warnf("failed to fetch policies, continuing without policy evaluation")
return nil, nil
}

if statusCode == http.StatusNotFound {
log.Debugf("no policies found in xeol.io API response")
log.Warnf("no policies found in xeol.io API response")
return nil, nil
}

return policy.UnmarshalPolicies(raw)
policies, err := policy.UnmarshalPolicies(raw)
if err != nil {
log.Warnf("failed to unmarshal policies, continuing without policy evaluation")
return nil, nil
}
return policies, nil
}

func (x *XeolClient) SendEvent(payload report.XeolEventPayload) error {
Expand Down
63 changes: 63 additions & 0 deletions internal/xeolio/source.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package xeolio

import (
"fmt"

"github.com/anchore/syft/syft/source"
)

type EventSource interface {
Serialize() map[string]interface{}
}

type DirectorySource struct {
Type string
Target string
}

func (s *DirectorySource) Serialize() map[string]interface{} {
return map[string]interface{}{
"Type": s.Type,
"Target": s.Target,
}
}

func NewDirectorySource(dirSource source.DirectorySourceMetadata) *DirectorySource {
return &DirectorySource{
Type: "DirectoryScheme",
Target: dirSource.Path,
}
}

type ImageSource struct {
Type string
ImageName string
ImageDigest string
}

func NewImageSource(imageSource source.StereoscopeImageSourceMetadata) *ImageSource {
return &ImageSource{
Type: "ImageScheme",
ImageName: imageSource.UserInput,
ImageDigest: imageSource.ManifestDigest,
}
}

func (s *ImageSource) Serialize() map[string]interface{} {
return map[string]interface{}{
"Type": s.Type,
"ImageName": s.ImageName,
"ImageDigest": s.ImageDigest,
}
}

func NewEventSource(sbomSource source.Description) (map[string]interface{}, error) {
switch v := sbomSource.Metadata.(type) {
case source.DirectorySourceMetadata:
return NewDirectorySource(v).Serialize(), nil
case source.StereoscopeImageSourceMetadata:
return NewImageSource(v).Serialize(), nil
default:
return nil, fmt.Errorf("unsupported source type: %s", v)
}
}

0 comments on commit f19a439

Please sign in to comment.