diff --git a/pkg/config/config.go b/pkg/config/config.go index 3fed1a7..5e35ee4 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -30,7 +30,6 @@ var defaultConfig *Config var configPath string func init() { - homeDir, err := os.UserHomeDir() if err != nil { homeDir = os.TempDir() @@ -58,24 +57,23 @@ func init() { _ = os.MkdirAll(lazyTrivyConfigDir, os.ModePerm) configPath = filepath.Join(lazyTrivyConfigDir, "config.yaml") - } func Load() (*Config, error) { - logger.Debug("Attempting to load config from %s", configPath) + logger.Debugf("Attempting to load config from %s", configPath) if _, err := os.Stat(configPath); err != nil { - logger.Debug("No config file found, using defaults") + logger.Debugf("No config file found, using defaults") return defaultConfig, nil } content, err := os.ReadFile(configPath) if err != nil { - logger.Error("Error reading config file: %s", err) + logger.Errorf("Error reading config file: %s", err) return defaultConfig, nil } if err := yaml.Unmarshal(content, &defaultConfig); err != nil { - logger.Error("Error parsing config file: %s", err) + logger.Errorf("Error parsing config file: %s", err) return defaultConfig, err } @@ -83,15 +81,15 @@ func Load() (*Config, error) { } func Save(config *Config) error { - logger.Debug("Saving the config to %s", configPath) + logger.Debugf("Saving the config to %s", configPath) content, err := yaml.Marshal(config) if err != nil { - logger.Error("Error marshalling config: %s", err) + logger.Errorf("Error marshalling config: %s", err) return err } - if err := os.WriteFile(configPath, content, 0644); err != nil { - logger.Error("Error writing config file: %s", err) + if err := os.WriteFile(configPath, content, 0600); err != nil { + logger.Errorf("Error writing config file: %s", err) return err } diff --git a/pkg/controllers/aws/aws.go b/pkg/controllers/aws/aws.go index 846949a..c26a245 100644 --- a/pkg/controllers/aws/aws.go +++ b/pkg/controllers/aws/aws.go @@ -41,7 +41,7 @@ func NewAWSController(cui *gocui.Gui, dockerClient *docker.Client, cfg *config.C } func (c *Controller) Initialise() error { - logger.Debug("Initialising AWS controller") + logger.Debugf("Initialising AWS controller") var outerErr error c.Cui.Update(func(gui *gocui.Gui) error { @@ -51,13 +51,13 @@ func (c *Controller) Initialise() error { return err } - logger.Debug("Configuring keyboard shortcuts") + logger.Debugf("Configuring keyboard shortcuts") if err := c.configureKeyBindings(); err != nil { return fmt.Errorf("failed to configure global keys: %w", err) } for _, v := range c.Views { - if err := v.ConfigureKeys(); err != nil { + if err := v.ConfigureKeys(gui); err != nil { return fmt.Errorf("failed to configure view keys: %w", err) } } @@ -78,13 +78,13 @@ func (c *Controller) Initialise() error { } func (c *Controller) refreshServices() error { - logger.Debug("getting caches services") + logger.Debugf("getting caches services") services, err := c.accountRegionCacheServices(c.Config.AWS.AccountNo, c.Config.AWS.Region) if err != nil { return err } - logger.Debug("Updating the services view with the identified services") + logger.Debugf("Updating the services view with the identified services") if v, ok := c.Views[widgets.Services].(*widgets.ServicesWidget); ok { if err := v.RefreshServices(services, 20); err != nil { return err @@ -94,7 +94,7 @@ func (c *Controller) refreshServices() error { } func (c *Controller) CreateWidgets(manager base.Manager) error { - logger.Debug("Creating AWS view widgets") + logger.Debugf("Creating AWS view widgets") menuItems := []string{ "[?] help", "s[w]itch mode", "[t]erminate scan", "[q]uit", "\n\nNavigation: Use arrow keys to navigate and ESC to exit screens", @@ -118,7 +118,7 @@ func (c *Controller) CreateWidgets(manager base.Manager) error { } func (c *Controller) UpdateAccount(account string) error { - logger.Debug("Updating the AWS account details in the config") + logger.Debugf("Updating the AWS account details in the config") c.Config.AWS.AccountNo = account c.Config.AWS.Region = "us-east-1" if err := c.Config.Save(); err != nil { @@ -129,7 +129,7 @@ func (c *Controller) UpdateAccount(account string) error { } func (c *Controller) UpdateRegion(region string) error { - logger.Debug("Updating the AWS region details in the config") + logger.Debugf("Updating the AWS region details in the config") c.Config.AWS.Region = region if err := c.Config.Save(); err != nil { return err @@ -140,7 +140,7 @@ func (c *Controller) UpdateRegion(region string) error { func (c *Controller) update() error { if v, ok := c.Views[widgets.Account]; ok { if a, ok := v.(*widgets.AccountWidget); ok { - logger.Debug("Updating the AWS account details in the UI") + logger.Debugf("Updating the AWS account details in the UI") a.UpdateAccount(c.Config.AWS.AccountNo, c.Config.AWS.Region) if err := c.refreshServices(); err != nil { return err @@ -180,10 +180,10 @@ func (c *Controller) moveViewRight(*gocui.Gui, *gocui.View) error { func (c *Controller) switchAccount(gui *gocui.Gui, _ *gocui.View) error { - logger.Debug("Switching AWS account") + logger.Debugf("Switching AWS account") accounts, err := c.listAccountNumbers() if err != nil { - logger.Error("Failed to list AWS accounts. %s", err) + logger.Errorf("Failed to list AWS accounts. %s", err) return err } @@ -191,7 +191,7 @@ func (c *Controller) switchAccount(gui *gocui.Gui, _ *gocui.View) error { accountChoices := widgets.NewChoiceWidget("choice", x/2-10, y/2-2, x/2+10, y/2+2, " Choose or ESC ", accounts, c.UpdateAccount, c) if err := accountChoices.Layout(gui); err != nil { - logger.Error("Failed to create account choice widget. %s", err) + logger.Errorf("Failed to create account choice widget. %s", err) return fmt.Errorf("error when rendering account choices: %w", err) } gui.Update(func(gui *gocui.Gui) error { @@ -203,10 +203,10 @@ func (c *Controller) switchAccount(gui *gocui.Gui, _ *gocui.View) error { } func (c *Controller) switchRegion(gui *gocui.Gui, _ *gocui.View) error { - logger.Debug("Switching AWS region") + logger.Debugf("Switching AWS region") regions, err := c.listRegions(c.Config.AWS.AccountNo) if err != nil { - logger.Error("Failed to list AWS regions. %s", err) + logger.Errorf("Failed to list AWS regions. %s", err) return err } @@ -214,7 +214,7 @@ func (c *Controller) switchRegion(gui *gocui.Gui, _ *gocui.View) error { regionChoices := widgets.NewChoiceWidget("choice", x/2-10, y/2-2, x/2+10, y/2+len(regions), " Choose or ESC ", regions, c.UpdateRegion, c) if err := regionChoices.Layout(gui); err != nil { - logger.Error("Failed to create region choice widget. %s", err) + logger.Errorf("Failed to create region choice widget. %s", err) return fmt.Errorf("error when rendering region choices: %w", err) } gui.Update(func(gui *gocui.Gui) error { @@ -227,7 +227,7 @@ func (c *Controller) switchRegion(gui *gocui.Gui, _ *gocui.View) error { func (c *Controller) discoverAccount(region string) (string, string, error) { ctx := context.Background() - logger.Debug("Loading credentials from default config") + logger.Debugf("Loading credentials from default config") cfg, err := awsConfig.LoadDefaultConfig(ctx) if err != nil { return "", "", err @@ -239,21 +239,21 @@ func (c *Controller) discoverAccount(region string) (string, string, error) { } if regionEnv, ok := os.LookupEnv("AWS_REGION"); ok { - logger.Debug("Using AWS_REGION environment variable") + logger.Debugf("Using AWS_REGION environment variable") cfg.Region = regionEnv } svc := sts.NewFromConfig(cfg) result, err := svc.GetCallerIdentity(ctx, &sts.GetCallerIdentityInput{}) if err != nil { - logger.Error("Error getting caller identity") + logger.Errorf("Error getting caller identity") return "", "", fmt.Errorf("failed to discover AWS caller identity: %w", err) } if result.Account == nil { return "", "", fmt.Errorf("missing account id for aws account") } - logger.Debug("Discovered AWS account %s", *result.Account) + logger.Debugf("Discovered AWS account %s", *result.Account) return *result.Account, cfg.Region, nil } @@ -263,7 +263,7 @@ func (c *Controller) scanAccount(gui *gocui.Gui, _ *gocui.View) error { if err != nil { if strings.HasPrefix(err.Error(), "failed to discover AWS caller identity") { c.UpdateStatus("Failed to discover AWS credentials.") - logger.Error("failed to discover AWS credentials: %v", err) + logger.Errorf("failed to discover AWS credentials: %v", err) return NewErrNoValidCredentials() } return err @@ -272,7 +272,7 @@ func (c *Controller) scanAccount(gui *gocui.Gui, _ *gocui.View) error { c.UpdateStatus("Checking credentials for account...") if account != c.Config.AWS.AccountNo && c.Config.AWS.AccountNo != "" { c.UpdateStatus("Account number does not match credentials.") - logger.Error("Account number does not match credentials.") + logger.Errorf("Account number does not match credentials.") return fmt.Errorf("account number mismatch: %s != %s", account, c.Config.AWS.AccountNo) } @@ -293,7 +293,7 @@ func (c *Controller) scanAccount(gui *gocui.Gui, _ *gocui.View) error { _, _ = gui.SetCurrentView(widgets.Results) if err := c.refreshServices(); err != nil { - logger.Error("Error refreshing services: %v", err) + logger.Errorf("Error refreshing services: %v", err) } c.UpdateStatus("Account scan complete.") }() @@ -313,8 +313,8 @@ func (c *Controller) RenderAWSResultsReport(report *output.Report) error { func (c *Controller) RenderAWSResultsReportSummary(report *output.Report) error { if v, ok := c.Views[widgets.Results].(*widgets.AWSResultWidget); ok { - v.UpdateResultsTable([]*output.Report{report}) - _, _ = c.Cui.SetCurrentView(widgets.Results) + v.UpdateResultsTable([]*output.Report{report}, c.Cui) + } return fmt.Errorf("failed to render results report summary") //nolint:goerr113 } diff --git a/pkg/controllers/aws/help.go b/pkg/controllers/aws/help.go index 88a2bdd..af8ba9d 100644 --- a/pkg/controllers/aws/help.go +++ b/pkg/controllers/aws/help.go @@ -20,7 +20,7 @@ func help(gui *gocui.Gui, _ *gocui.View) error { w, h := gui.Size() - v := widgets.NewHelpWidget("help", w/2-22, h/2-4, w/2+23, h/2+3, helpCommands) + v := widgets.NewAnnouncementWidget("help", "Help", w, h, helpCommands, gui) if err := gui.SetKeybinding("help", gocui.KeyEsc, gocui.ModNone, func(gui *gocui.Gui, _ *gocui.View) error { if _, err := gui.SetCurrentView("services"); err != nil { diff --git a/pkg/controllers/aws/key_bindings.go b/pkg/controllers/aws/key_bindings.go index 0f7fc09..5ba274d 100644 --- a/pkg/controllers/aws/key_bindings.go +++ b/pkg/controllers/aws/key_bindings.go @@ -9,7 +9,7 @@ import ( ) func (c *Controller) configureKeyBindings() error { - logger.Debug("Configuring global AWS Controller keyboard shortcuts") + logger.Debugf("Configuring global AWS Controller keyboard shortcuts") if err := c.ConfigureGlobalKeyBindings(); err != nil { return fmt.Errorf("error configuring global keybindings: %w", err) } diff --git a/pkg/controllers/aws/service.go b/pkg/controllers/aws/service.go index 21e3a63..df19e42 100644 --- a/pkg/controllers/aws/service.go +++ b/pkg/controllers/aws/service.go @@ -50,7 +50,7 @@ func (c *Controller) CancelCurrentScan(_ *gocui.Gui, _ *gocui.View) error { c.Lock() defer c.Unlock() if c.ActiveCancel != nil { - logger.Debug("Cancelling current scan") + logger.Debugf("Cancelling current scan") c.UpdateStatus("Current scan cancelled.") c.ActiveCancel() c.ActiveCancel = nil diff --git a/pkg/controllers/aws/state.go b/pkg/controllers/aws/state.go index 1f43983..f2b1eff 100644 --- a/pkg/controllers/aws/state.go +++ b/pkg/controllers/aws/state.go @@ -5,14 +5,12 @@ import ( "fmt" "os" "path/filepath" - "sync" "github.com/owenrumney/lazytrivy/pkg/logger" "github.com/owenrumney/lazytrivy/pkg/output" ) type state struct { - stateLock sync.Mutex services []string selectedService string serviceWidth int @@ -25,7 +23,7 @@ func (s *state) accountRegionCache(accountID, region string) string { } func (s *state) listAccountNumbers() ([]string, error) { - logger.Debug("listing account numbers") + logger.Debugf("listing account numbers") var accountNumbers []string fileInfos, err := os.ReadDir(s.cacheDirectory) if err != nil { @@ -40,7 +38,7 @@ func (s *state) listAccountNumbers() ([]string, error) { } func (s *state) listRegions(accountNumber string) ([]string, error) { - logger.Debug("listing regions") + logger.Debugf("listing regions") var regions []string accountPath := filepath.Join(s.cacheDirectory, accountNumber) fileInfos, err := os.ReadDir(accountPath) @@ -59,7 +57,7 @@ func (s *state) accountRegionCacheExists(accountID, region string) bool { if _, err := os.Stat(s.accountRegionCache(accountID, region)); err == nil { return true } - logger.Debug("cache does not exist for %s (%s)", accountID, region) + logger.Debugf("cache does not exist for %s (%s)", accountID, region) return false } @@ -93,21 +91,6 @@ func (s *state) accountRegionCacheServices(accountID, region string) ([]string, return services, nil } -func (s *state) updateServices(services []string) { - s.stateLock.Lock() - defer s.stateLock.Unlock() - s.services = services - - s.serviceWidth = getLongestName(services) - s.selectedService = "" -} - -func (s *state) setSelected(selectedImage string) { - s.stateLock.Lock() - defer s.stateLock.Unlock() - s.selectedService = selectedImage -} - func (s *state) getServiceReport(accountID, region, serviceName string) (*output.Report, error) { cachePath := s.accountRegionCache(accountID, region) @@ -120,7 +103,7 @@ func (s *state) getServiceReport(accountID, region, serviceName string) (*output var report output.Report if err := json.Unmarshal(content, &report); err != nil { - logger.Error("failed to unmarshal report: %s", err) + logger.Errorf("failed to unmarshal report: %s", err) return nil, err } report.Process() diff --git a/pkg/controllers/base/global_keybindings.go b/pkg/controllers/base/global_keybindings.go index bdbcfbd..e78f94e 100644 --- a/pkg/controllers/base/global_keybindings.go +++ b/pkg/controllers/base/global_keybindings.go @@ -8,7 +8,7 @@ import ( ) func (c *Controller) ConfigureGlobalKeyBindings() error { - logger.Debug("Configuring global keyboard shortcuts") + logger.Debugf("Configuring global keyboard shortcuts") if err := c.Cui.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, Quit); err != nil { return fmt.Errorf("error setting keybinding for quitting with Ctrl+C: %w", err) diff --git a/pkg/controllers/gui/gui.go b/pkg/controllers/gui/gui.go index f6b2717..cc531cc 100644 --- a/pkg/controllers/gui/gui.go +++ b/pkg/controllers/gui/gui.go @@ -26,7 +26,7 @@ type Controller struct { } func New() (*Controller, error) { - logger.Debug("Creating GUI") + logger.Debugf("Creating GUI") cui, err := gocui.NewGui(gocui.OutputNormal, true) if err != nil { return nil, fmt.Errorf("failed to create gui: %w", err) diff --git a/pkg/controllers/vulnerabilities/help.go b/pkg/controllers/vulnerabilities/help.go index 1ee351e..47b29e8 100644 --- a/pkg/controllers/vulnerabilities/help.go +++ b/pkg/controllers/vulnerabilities/help.go @@ -19,16 +19,7 @@ func help(gui *gocui.Gui, _ *gocui.View) error { w, h := gui.Size() - v := widgets.NewHelpWidget("help", w/2-22, h/2-4, w/2+22, h/2+4, helpCommands) - - if err := gui.SetKeybinding("help", gocui.KeyEsc, gocui.ModNone, func(gui *gocui.Gui, _ *gocui.View) error { - if _, err := gui.SetCurrentView("images"); err != nil { - return err - } - return gui.DeleteView("help") - }); err != nil { - return err - } + v := widgets.NewAnnouncementWidget("help", "Help", w, h, helpCommands, gui) gui.Update(func(g *gocui.Gui) error { if err := v.Layout(g); err != nil { diff --git a/pkg/controllers/vulnerabilities/image.go b/pkg/controllers/vulnerabilities/image.go index 5e8fca8..08ce41d 100644 --- a/pkg/controllers/vulnerabilities/image.go +++ b/pkg/controllers/vulnerabilities/image.go @@ -12,7 +12,7 @@ import ( ) func (c *Controller) SetSelected(selected string) { - logger.Debug("Setting selected image to %s", selected) + logger.Debugf("Setting selected image to %s", selected) c.setSelected(strings.TrimSpace(selected)) } @@ -81,12 +81,12 @@ func (c *Controller) ScanAllImages(gui *gocui.Gui, _ *gocui.View) error { } func (c *Controller) RefreshImages() error { - logger.Debug("refreshing images") + logger.Debugf("refreshing images") c.UpdateStatus("Refreshing images") defer c.ClearStatus() images := c.DockerClient.ListImages() - logger.Debug("found %d images", len(images)) + logger.Debugf("found %d images", len(images)) c.updateImages(images) if v, ok := c.Views[widgets.Images].(*widgets.ImagesWidget); ok { diff --git a/pkg/controllers/vulnerabilities/vulnerability.go b/pkg/controllers/vulnerabilities/vulnerability.go index db9f7e7..0a77058 100644 --- a/pkg/controllers/vulnerabilities/vulnerability.go +++ b/pkg/controllers/vulnerabilities/vulnerability.go @@ -32,7 +32,7 @@ func NewVulnerabilityController(cui *gocui.Gui, dockerClient *docker.Client, cfg } func (c *Controller) Initialise() error { - logger.Debug("initialising vulnerability controller") + logger.Debugf("initialising vulnerability controller") var outerErr error c.Cui.Update(func(gui *gocui.Gui) error { @@ -45,7 +45,7 @@ func (c *Controller) Initialise() error { } for _, v := range c.Views { - if err := v.ConfigureKeys(); err != nil { + if err := v.ConfigureKeys(nil); err != nil { return fmt.Errorf("failed to configure view keys: %w", err) } } @@ -119,18 +119,15 @@ func (c *Controller) moveViewRight(*gocui.Gui, *gocui.View) error { func (c *Controller) RenderResultsReport(report *output.Report) error { if v, ok := c.Views[widgets.Results].(*widgets.ImageResultWidget); ok { - v.RenderReport(report, "ALL") - _, err := c.Cui.SetCurrentView(widgets.Results) - if err != nil { - return fmt.Errorf("failed to set current view: %w", err) - } + v.RenderReport(report, "ALL", c.Cui) + } return nil } func (c *Controller) RenderResultsReportSummary(reports []*output.Report) error { if v, ok := c.Views[widgets.Results].(*widgets.ImageResultWidget); ok { - v.UpdateResultsTable(reports) + v.UpdateResultsTable(reports, c.Cui) _, err := c.Cui.SetCurrentView(widgets.Results) if err != nil { return fmt.Errorf("error setting current view: %w", err) diff --git a/pkg/docker/client.go b/pkg/docker/client.go index f28bfdd..d518d3f 100644 --- a/pkg/docker/client.go +++ b/pkg/docker/client.go @@ -32,7 +32,7 @@ type Client struct { } func NewClient() *Client { - logger.Debug("Creating docker client") + logger.Debugf("Creating docker client") cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { panic(err) @@ -58,7 +58,7 @@ func (c *Client) ListImages() []string { if image.RepoTags != nil { imageName := image.RepoTags[0] if strings.HasPrefix(imageName, "lazytrivy:") { - logger.Debug("Found trivy image %s", imageName) + logger.Debugf("Found trivy image %s", imageName) c.trivyImagePresent = true continue @@ -70,7 +70,7 @@ func (c *Client) ListImages() []string { sort.Strings(imageNames) c.imageNames = imageNames - logger.Debug("Found %d images", len(imageNames)) + logger.Debugf("Found %d images", len(imageNames)) return c.imageNames } @@ -102,18 +102,18 @@ func (c *Client) ScanService(ctx context.Context, serviceName string, accountNo, } if serviceName != "" { - logger.Debug("Scan will target service %s", serviceName) + logger.Debugf("Scan will target service %s", serviceName) command = append(command, "--services", serviceName) } if updateCache { - logger.Debug("Cache will be updated for %s", serviceName) + logger.Debugf("Cache will be updated for %s", serviceName) command = append(command, "--update-cache") } return c.scan(ctx, command, target, env, progress) } func (c *Client) ScanImage(ctx context.Context, imageName string, progress Progress) (*output.Report, error) { - logger.Debug("Scanning image %s", imageName) + logger.Debugf("Scanning image %s", imageName) progress.UpdateStatus(fmt.Sprintf("Scanning image %s...", imageName)) command := []string{"image", "-f=json", imageName} @@ -122,44 +122,18 @@ func (c *Client) ScanImage(ctx context.Context, imageName string, progress Progr func (c *Client) scan(ctx context.Context, command []string, scanTarget string, env []string, progress Progress) (*output.Report, error) { if !c.trivyImagePresent { - logger.Debug("Creating the docker image, it isn't present") - - dockerfile := createDockerFile() - tempDir, err := os.MkdirTemp("", "lazytrivy") - dockerFilePath := filepath.Join(tempDir, "Dockerfile") - - defer func() { _ = os.RemoveAll(tempDir) }() - - if err := os.WriteFile(dockerFilePath, []byte(dockerfile), 0644); err != nil { - return nil, err - } - - tar, err := archive.TarWithOptions(tempDir, &archive.TarOptions{}) - if err != nil { - return nil, err - } - - resp, err := c.client.ImageBuild(ctx, tar, types.ImageBuildOptions{ - PullParent: true, - Dockerfile: "Dockerfile", - Tags: []string{"lazytrivy:latest"}, - }) - if err != nil { - return nil, err - } - - _, _ = io.Copy(io.Discard, resp.Body) - if err := resp.Body.Close(); err != nil { - return nil, err + report, err2 := c.buildScannerImage(ctx) + if err2 != nil { + return report, err2 } } - logger.Debug("Running trivy scan with command %s", command) + logger.Debugf("Running trivy scan with command %s", command) userHomeDir, err := os.UserHomeDir() if err != nil { - logger.Debug("Error getting user home dir: %s", err) + logger.Debugf("Error getting user home dir: %s", err) userHomeDir = os.TempDir() } @@ -186,7 +160,7 @@ func (c *Client) scan(ctx context.Context, command []string, scanTarget string, // make sure we kill the container defer func() { - logger.Debug("Removing container %s", cont.ID) + logger.Debugf("Removing container %s", cont.ID) _ = c.client.ContainerRemove(ctx, cont.ID, types.ContainerRemoveOptions{}) }() @@ -230,23 +204,57 @@ func (c *Client) scan(ctx context.Context, command []string, scanTarget string, } } +func (c *Client) buildScannerImage(ctx context.Context) (*output.Report, error) { + logger.Debugf("Creating the docker image, it isn't present") + + dockerfile := createDockerFile() + tempDir, err := os.MkdirTemp("", "lazytrivy") + dockerFilePath := filepath.Join(tempDir, "Dockerfile") + + defer func() { _ = os.RemoveAll(tempDir) }() + + if err := os.WriteFile(dockerFilePath, []byte(dockerfile), 0644); err != nil { + return nil, err + } + + tar, err := archive.TarWithOptions(tempDir, &archive.TarOptions{}) + if err != nil { + return nil, err + } + + resp, err := c.client.ImageBuild(ctx, tar, types.ImageBuildOptions{ + PullParent: true, + Dockerfile: "Dockerfile", + Tags: []string{"lazytrivy:latest"}, + }) + if err != nil { + return nil, err + } + + _, _ = io.Copy(io.Discard, resp.Body) + if err := resp.Body.Close(); err != nil { + return nil, err + } + return nil, nil +} + func (c *Client) ScanAllImages(ctx context.Context, progress Progress) ([]*output.Report, error) { var reports []*output.Report // nolint for _, imageName := range c.imageNames { progress.UpdateStatus(fmt.Sprintf("Scanning image %s...", imageName)) - logger.Debug("Scanning image %s", imageName) + logger.Debugf("Scanning image %s", imageName) report, err := c.ScanImage(ctx, imageName, progress) if err != nil { return nil, err } progress.UpdateStatus(fmt.Sprintf("Scanning image %s...done", imageName)) - logger.Debug("Scanning image %s...done", imageName) + logger.Debugf("Scanning image %s...done", imageName) reports = append(reports, report) select { case <-ctx.Done(): - logger.Debug("Context cancelled") + logger.Debugf("Context cancelled") return nil, ctx.Err() // nolint default: } diff --git a/pkg/logger/debug.go b/pkg/logger/debug.go index 750d33f..40500c3 100644 --- a/pkg/logger/debug.go +++ b/pkg/logger/debug.go @@ -14,19 +14,19 @@ var ( func EnableDebugging() { debugEnabled = true - logFile := filepath.Join(os.TempDir(), "lazytrivy-logger.log") - debugFile, _ = os.OpenFile(logFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) + logFile := filepath.Join(os.TempDir(), "lazytrivy-logger.logf") + debugFile, _ = os.OpenFile(logFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) // nolint: nosnakecase } -func Debug(format string, args ...interface{}) { - log("DEBUG", format, args...) +func Debugf(format string, args ...interface{}) { + logf("DEBUG", format, args...) } -func Error(format string, args ...interface{}) { - log("ERROR", format, args...) +func Errorf(format string, args ...interface{}) { + logf("ERROR", format, args...) } -func log(level string, format string, args ...interface{}) { +func logf(level string, format string, args ...interface{}) { if debugEnabled { _, _ = fmt.Fprintf(debugFile, fmt.Sprintf("%s [%s] ", time.RFC3339, level)+fmt.Sprintf(format, args...)) _, _ = fmt.Fprintln(debugFile) diff --git a/pkg/output/report.go b/pkg/output/report.go index c66d8df..fe84bd1 100644 --- a/pkg/output/report.go +++ b/pkg/output/report.go @@ -68,10 +68,10 @@ type Misconfiguration struct { } func FromJSON(imageName string, content string) (*Report, error) { - logger.Debug("Parsing JSON report") + logger.Debugf("Parsing JSON report") var report Report if err := json.Unmarshal([]byte(content), &report); err != nil { - logger.Error("Failed to parse JSON report. %s", err) + logger.Errorf("Failed to parse JSON report. %s", err) return nil, err } report.Process() @@ -102,7 +102,7 @@ func (r *Report) Process() { } } if found { - r.vulnerabilities++ + foundResult.Vulnerabilities = append(foundResult.Vulnerabilities, v) } else { foundResult = &Result{ @@ -111,6 +111,7 @@ func (r *Report) Process() { } sevMap = append(sevMap, foundResult) } + r.vulnerabilities++ r.SeverityMap[v.Severity] = sevMap r.SeverityCount[v.Severity]++ @@ -132,7 +133,7 @@ func (r *Report) Process() { } } if found { - r.misconfigurations++ + foundResult.Misconfigurations = append(foundResult.Misconfigurations, m) } else { foundResult = &Result{ @@ -141,6 +142,7 @@ func (r *Report) Process() { } sevMap = append(sevMap, foundResult) } + r.misconfigurations++ r.SeverityMap[m.Severity] = sevMap r.SeverityCount[m.Severity]++ @@ -175,3 +177,7 @@ func (r *Report) GetTotalVulnerabilities() int { func (r *Report) GetTotalMisconfigurations() int { return r.misconfigurations } + +func (r *Report) HasIssues() bool { + return r.GetTotalVulnerabilities() > 0 || r.GetTotalMisconfigurations() > 0 +} diff --git a/pkg/widgets/account.go b/pkg/widgets/account.go index df2d563..2a313a8 100644 --- a/pkg/widgets/account.go +++ b/pkg/widgets/account.go @@ -32,7 +32,7 @@ func NewAccountWidget(name, accountNumber, region string) *AccountWidget { } } -func (w *AccountWidget) ConfigureKeys() error { +func (w *AccountWidget) ConfigureKeys(*gocui.Gui) error { // nothing to configure here return nil } diff --git a/pkg/widgets/announce.go b/pkg/widgets/announce.go new file mode 100644 index 0000000..5aed49f --- /dev/null +++ b/pkg/widgets/announce.go @@ -0,0 +1,92 @@ +package widgets + +import ( + "errors" + "fmt" + "strings" + + "github.com/awesome-gocui/gocui" +) + +type AnnouncementWidget struct { + name string + x, y int + w, h int + body []string + v *gocui.View + title string + ctx *gocui.Gui +} + +func (w *AnnouncementWidget) RefreshView() { + panic("unimplemented") +} + +func NewAnnouncementWidget(name, title string, width, height int, lines []string, ctx *gocui.Gui) *AnnouncementWidget { + maxLength := 0 + + for _, item := range lines { + if len(item) > maxLength { + maxLength = len(item) + } + } + + maxLength += 2 + maxHeight := len(lines) + 2 + + x := width/2 - maxLength/2 + w := width/2 + maxLength/2 + + y := height/2 - maxHeight/2 + h := height/2 + maxHeight/2 + + return &AnnouncementWidget{ + name: name, + title: title, + x: x, + y: y, + w: w, + h: h, + body: lines, + v: nil, + ctx: ctx, + } +} + +func (w *AnnouncementWidget) ConfigureKeys(*gocui.Gui) error { + if err := w.ctx.SetKeybinding(w.name, gocui.KeyEsc, gocui.ModNone, func(gui *gocui.Gui, _ *gocui.View) error { + if _, err := gui.SetCurrentView(Results); err != nil { + return err + } + return gui.DeleteView(w.name) + }); err != nil { + return err + } + + if err := w.ctx.SetKeybinding(w.name, 'q', gocui.ModNone, func(gui *gocui.Gui, _ *gocui.View) error { + if _, err := gui.SetCurrentView(Results); err != nil { + return err + } + return gui.DeleteView(w.name) + }); err != nil { + return err + } + return nil +} + +func (w *AnnouncementWidget) Layout(g *gocui.Gui) error { + v, err := g.SetView(w.name, w.x, w.y, w.w, w.h, 0) + if err != nil { + if !errors.Is(err, gocui.ErrUnknownView) { + return fmt.Errorf("%w", err) + } + } + v.Clear() + _, _ = fmt.Fprint(v, strings.Join(w.body, "\n")) + v.Title = fmt.Sprintf(" %s ", w.title) + v.Wrap = true + v.FrameColor = gocui.ColorGreen + w.v = v + + return w.ConfigureKeys(nil) +} diff --git a/pkg/widgets/aws_results.go b/pkg/widgets/aws_results.go index a43ee4d..0293e7b 100644 --- a/pkg/widgets/aws_results.go +++ b/pkg/widgets/aws_results.go @@ -38,7 +38,7 @@ func NewAWSResultWidget(name string, g awsContext) *AWSResultWidget { return widget } -func (w *AWSResultWidget) ConfigureKeys() error { +func (w *AWSResultWidget) ConfigureKeys(gui *gocui.Gui) error { if err := w.configureListWidgetKeys(w.name); err != nil { return err } @@ -49,14 +49,14 @@ func (w *AWSResultWidget) ConfigureKeys() error { if err := w.ctx.SetKeyBinding(w.name, 'b', gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error { if w.results != nil && len(w.results) > 0 { - w.UpdateResultsTable([]*output.Report{w.currentReport}) + w.UpdateResultsTable([]*output.Report{w.currentReport}, g) } return nil }); err != nil { return fmt.Errorf("failed to set keybinding: %w", err) } - if err := w.addFilteringKeyBindings(); err != nil { + if err := w.addFilteringKeyBindings(gui); err != nil { return err } @@ -71,8 +71,8 @@ func (w *AWSResultWidget) diveDeeper(g *gocui.Gui, _ *gocui.View) error { return nil } w.currentResult = w.results[id] - logger.Debug("Diving deeper into result: %s", w.currentResult.Target) - w.GenerateFilteredReport("ALL") + logger.Debugf("Diving deeper into result: %s", w.currentResult.Target) + w.GenerateFilteredReport("ALL", g) case DetailsResultMode: x, y, wi, h := w.v.Dimensions() @@ -137,12 +137,32 @@ func (w *AWSResultWidget) Reset() { } } -func (w *AWSResultWidget) UpdateResultsTable(reports []*output.Report) { +func (w *AWSResultWidget) UpdateResultsTable(reports []*output.Report, g *gocui.Gui) { if len(reports) == 0 { return } + w.mode = SummaryResultMode w.currentReport = reports[0] + w.currentReport.Process() + w.v.Clear() + w.body = []string{} + + if w.currentReport == nil || !w.currentReport.HasIssues() { + width, height := w.v.Size() + + lines := []string{ + "Great News!", + "", + "No misconfigurations found!", + } + + announcement := NewAnnouncementWidget(Announcement, "No Results", width, height, lines, g) + _ = announcement.Layout(g) + _, _ = g.SetCurrentView(Announcement) + + return + } width, _ := w.v.Size() @@ -180,6 +200,7 @@ func (w *AWSResultWidget) UpdateResultsTable(reports []*output.Report) { w.body = bodyContent + _, _ = g.SetCurrentView(Results) w.ctx.RefreshView(w.name) w.SetStartPosition(3) @@ -190,11 +211,23 @@ func (w *AWSResultWidget) UpdateResultsTable(reports []*output.Report) { func (w *AWSResultWidget) RenderReport(report *output.Report, severity string) { w.currentReport = report - w.GenerateFilteredReport(severity) + w.GenerateFilteredReport(severity, nil) } -func (w *AWSResultWidget) GenerateFilteredReport(severity string) { +func (w *AWSResultWidget) GenerateFilteredReport(severity string, g *gocui.Gui) { if w.currentResult == nil || len(w.currentResult.Misconfigurations) == 0 { + width, height := w.v.Size() + + lines := []string{ + "Great News!", + "", + "No misconfigurations found!", + } + + announcement := NewAnnouncementWidget(Announcement, "No Results", width, height, lines, g) + _ = announcement.Layout(g) + _, _ = g.SetCurrentView(Announcement) + return } diff --git a/pkg/widgets/aws_summary.go b/pkg/widgets/aws_summary.go index 176c743..cc44be2 100644 --- a/pkg/widgets/aws_summary.go +++ b/pkg/widgets/aws_summary.go @@ -19,7 +19,6 @@ type AWSSummaryWidget struct { func NewAWSSummaryWidget(name string, x, y, w, h int, ctx awsContext, vulnerability output.Misconfiguration) (*AWSSummaryWidget, error) { if err := ctx.SetKeyBinding(Remote, gocui.KeyEnter, gocui.ModNone, func(gui *gocui.Gui, view *gocui.View) error { - gui.Mouse = true gui.Cursor = false diff --git a/pkg/widgets/choices.go b/pkg/widgets/choices.go index 8627f17..c7fd95a 100644 --- a/pkg/widgets/choices.go +++ b/pkg/widgets/choices.go @@ -43,7 +43,7 @@ func NewChoiceWidget(name string, x, y, w, h int, title string, choices []string } } -func (w *ChoiceWidget) ConfigureKeys() error { +func (w *ChoiceWidget) ConfigureKeys(*gocui.Gui) error { if err := w.configureListWidgetKeys(w.name); err != nil { return err } @@ -84,7 +84,7 @@ func (w *ChoiceWidget) Layout(g *gocui.Gui) error { w.SetStartPosition(0) w.v = v - if err := w.ConfigureKeys(); err != nil { + if err := w.ConfigureKeys(nil); err != nil { return err } diff --git a/pkg/widgets/help.go b/pkg/widgets/help.go deleted file mode 100644 index ebfc119..0000000 --- a/pkg/widgets/help.go +++ /dev/null @@ -1,57 +0,0 @@ -package widgets - -import ( - "errors" - "fmt" - "strings" - - "github.com/awesome-gocui/gocui" -) - -type HelpWidget struct { - name string - x, y int - w, h int - body []string - v *gocui.View -} - -func (w *HelpWidget) RefreshView() { - panic("unimplemented") -} - -func NewHelpWidget(name string, x, y, w, h int, helpItems []string) *HelpWidget { - // TODO update to accept parent size and calculate own size based on helpItems - - return &HelpWidget{ - name: name, - x: x, - y: y, - w: w, - h: h, - body: helpItems, - v: nil, - } -} - -func (w *HelpWidget) ConfigureKeys() error { - // nothing to configure here - return nil -} - -func (w *HelpWidget) Layout(g *gocui.Gui) error { - v, err := g.SetView(w.name, w.x, w.y, w.w, w.h, 0) - if err != nil { - if !errors.Is(err, gocui.ErrUnknownView) { - return fmt.Errorf("%w", err) - } - } - v.Clear() - _, _ = fmt.Fprint(v, strings.Join(w.body, "\n")) - v.Title = " Help " - v.Subtitle = " ESC to exit" - v.Wrap = true - v.FrameColor = gocui.ColorGreen - w.v = v - return nil -} diff --git a/pkg/widgets/host.go b/pkg/widgets/host.go index de50f63..837b057 100644 --- a/pkg/widgets/host.go +++ b/pkg/widgets/host.go @@ -34,7 +34,7 @@ func NewHostWidget(name string, ctx vulnerabilityContext) *HostWidget { } } -func (w *HostWidget) ConfigureKeys() error { +func (w *HostWidget) ConfigureKeys(*gocui.Gui) error { // nothing to configure here return nil } diff --git a/pkg/widgets/image_results.go b/pkg/widgets/image_results.go index e0d3b50..121aef1 100644 --- a/pkg/widgets/image_results.go +++ b/pkg/widgets/image_results.go @@ -35,7 +35,7 @@ func NewImageResultWidget(name string, g vulnerabilityContext) *ImageResultWidge return widget } -func (w *ImageResultWidget) ConfigureKeys() error { +func (w *ImageResultWidget) ConfigureKeys(g *gocui.Gui) error { if err := w.configureListWidgetKeys(w.name); err != nil { return err } @@ -50,14 +50,14 @@ func (w *ImageResultWidget) ConfigureKeys() error { if err := w.ctx.SetKeyBinding(w.name, 'b', gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error { if w.reports != nil && len(w.reports) > 0 { - w.UpdateResultsTable(w.reports) + w.UpdateResultsTable(w.reports, g) } return nil }); err != nil { return fmt.Errorf("failed to set keybinding: %w", err) } - if err := w.addFilteringKeyBindings(); err != nil { + if err := w.addFilteringKeyBindings(nil); err != nil { return err } @@ -74,7 +74,7 @@ func (w *ImageResultWidget) diveDeeper(g *gocui.Gui, v *gocui.View) error { return nil } - w.GenerateFilteredReport("ALL") + w.GenerateFilteredReport("ALL", g) case DetailsResultMode: x, y, wi, h := v.Dimensions() @@ -105,7 +105,7 @@ func (w *ImageResultWidget) diveDeeper(g *gocui.Gui, v *gocui.View) error { return nil } -func (w *ImageResultWidget) UpdateResultsTable(reports []*output.Report) { +func (w *ImageResultWidget) UpdateResultsTable(reports []*output.Report, g *gocui.Gui) { w.mode = SummaryResultMode w.reports = reports @@ -148,13 +148,33 @@ func (w *ImageResultWidget) UpdateResultsTable(reports []*output.Report) { w.v.Subtitle = "" } -func (w *ImageResultWidget) RenderReport(report *output.Report, severity string) { +func (w *ImageResultWidget) RenderReport(report *output.Report, severity string, cui *gocui.Gui) { w.currentReport = report + w.v.Clear() + w.body = []string{} - w.GenerateFilteredReport(severity) + if w.currentReport == nil || !w.currentReport.HasIssues() { + width, height := w.v.Size() + + lines := []string{ + "Great News!", + "", + "No vulnerabilities found!", + } + + announcement := NewAnnouncementWidget(Announcement, "No Results", width, height, lines, cui) + _ = announcement.Layout(cui) + _, _ = cui.SetCurrentView(Announcement) + + return + } + + w.GenerateFilteredReport(severity, cui) + + _, _ = cui.SetCurrentView(Results) } -func (w *ImageResultWidget) GenerateFilteredReport(severity string) { +func (w *ImageResultWidget) GenerateFilteredReport(severity string, g *gocui.Gui) { if w.currentReport == nil { return } diff --git a/pkg/widgets/images.go b/pkg/widgets/images.go index 1c91433..1ee6fa6 100644 --- a/pkg/widgets/images.go +++ b/pkg/widgets/images.go @@ -15,9 +15,8 @@ type ImagesWidget struct { x, y int w, h int - imageCount int - ctx vulnerabilityContext - v *gocui.View + ctx vulnerabilityContext + v *gocui.View } func NewImagesWidget(name string, g vulnerabilityContext) *ImagesWidget { @@ -39,7 +38,7 @@ func NewImagesWidget(name string, g vulnerabilityContext) *ImagesWidget { return widget } -func (w *ImagesWidget) ConfigureKeys() error { +func (w *ImagesWidget) ConfigureKeys(*gocui.Gui) error { if err := w.ctx.SetKeyBinding(w.name, gocui.KeyArrowUp, gocui.ModNone, w.previousItem); err != nil { return fmt.Errorf("failed to set the previous image %w", err) } diff --git a/pkg/widgets/list.go b/pkg/widgets/list.go index dcb3743..835c9bb 100644 --- a/pkg/widgets/list.go +++ b/pkg/widgets/list.go @@ -51,10 +51,9 @@ func (w *ListWidget) nextItem(_ *gocui.Gui, v *gocui.View) error { } v.MoveCursor(0, 1) - _, h := v.Size() _, oy := v.Origin() _, y := v.Cursor() - if y == h { + if _, h := v.Size(); y == h { if err := v.SetOrigin(0, oy+1); err != nil { return err } @@ -74,6 +73,10 @@ func (w *ListWidget) nextItem(_ *gocui.Gui, v *gocui.View) error { } func (w *ListWidget) CurrentItemPosition() int { + if len(w.body) == 0 { + return -1 + } + currentLine := w.body[w.currentPos] if strings.HasPrefix(currentLine, "**") { idString := strings.TrimPrefix(strings.Split(currentLine, "***")[0], "**") diff --git a/pkg/widgets/menu.go b/pkg/widgets/menu.go index 8bf8c92..948c61e 100644 --- a/pkg/widgets/menu.go +++ b/pkg/widgets/menu.go @@ -34,7 +34,7 @@ func NewMenuWidget(name string, x, y, w, h int, menuItems []string) *MenuWidget } } -func (w *MenuWidget) ConfigureKeys() error { +func (w *MenuWidget) ConfigureKeys(*gocui.Gui) error { // nothing to configure here return nil } diff --git a/pkg/widgets/results.go b/pkg/widgets/results.go index 7adc450..f17c8aa 100644 --- a/pkg/widgets/results.go +++ b/pkg/widgets/results.go @@ -21,16 +21,16 @@ type ResultsWidget struct { ListWidget name string - generateReportFunc func(severity string) - updateResultsTableFunc func(reports []*output.Report) + generateReportFunc func(severity string, gui *gocui.Gui) + updateResultsTableFunc func(reports []*output.Report, g *gocui.Gui) ctx baseContext currentReport *output.Report mode ResultsMode v *gocui.View } -func NewResultsWidget(name string, generateReportFunc func(severity string), - updateResultsTableFunc func(reports []*output.Report), g baseContext) ResultsWidget { +func NewResultsWidget(name string, generateReportFunc func(severity string, gui *gocui.Gui), + updateResultsTableFunc func(reports []*output.Report, g *gocui.Gui), g baseContext) ResultsWidget { widget := ResultsWidget{ ListWidget: ListWidget{ ctx: g, @@ -45,17 +45,17 @@ func NewResultsWidget(name string, generateReportFunc func(severity string), return widget } -func (w *ResultsWidget) addFilteringKeyBinding(key rune, severity string) error { +func (w *ResultsWidget) addFilteringKeyBinding(key rune, severity string, gui *gocui.Gui) error { if err := w.ctx.SetKeyBinding(w.name, key, gocui.ModNone, func(gui *gocui.Gui, view *gocui.View) error { if w.currentReport == nil { return nil } switch severity { case "ALL": - w.generateReportFunc(severity) + w.generateReportFunc(severity, gui) default: if w.currentReport.SeverityCount[severity] > 0 { - w.generateReportFunc(severity) + w.generateReportFunc(severity, gui) } } @@ -66,40 +66,30 @@ func (w *ResultsWidget) addFilteringKeyBinding(key rune, severity string) error return nil } -func (w *ResultsWidget) addFilteringKeyBindings() error { - logger.Debug("adding filtering keybindings") - if err := w.addFilteringKeyBinding('e', "ALL"); err != nil { +func (w *ResultsWidget) addFilteringKeyBindings(gui *gocui.Gui) error { + logger.Debugf("adding filtering keybindings") + if err := w.addFilteringKeyBinding('e', "ALL", gui); err != nil { return err } - if err := w.addFilteringKeyBinding('c', "CRITICAL"); err != nil { + if err := w.addFilteringKeyBinding('c', "CRITICAL", nil); err != nil { return err } - if err := w.addFilteringKeyBinding('h', "HIGH"); err != nil { + if err := w.addFilteringKeyBinding('h', "HIGH", nil); err != nil { return err } - if err := w.addFilteringKeyBinding('m', "MEDIUM"); err != nil { + if err := w.addFilteringKeyBinding('m', "MEDIUM", nil); err != nil { return err } - if err := w.addFilteringKeyBinding('l', "LOW"); err != nil { + if err := w.addFilteringKeyBinding('l', "LOW", nil); err != nil { return err } - if err := w.addFilteringKeyBinding('u', "UNKNOWN"); err != nil { + if err := w.addFilteringKeyBinding('u', "UNKNOWN", nil); err != nil { return err } return nil } -func (w *ResultsWidget) RenderReport(report *output.Report, severity string) { - w.currentReport = report - - w.generateReportFunc(severity) -} - -func (w *ResultsWidget) UpdateResultsTable(reports []*output.Report) { - w.updateResultsTableFunc(reports) -} - func (w *ResultsWidget) layout(g *gocui.Gui, x int, y int, wi int, h int) error { v, err := g.View(w.name) @@ -155,6 +145,6 @@ func (w *ResultsWidget) Layout(*gocui.Gui) error { return nil } -func (w *ResultsWidget) ConfigureKeys() error { +func (w *ResultsWidget) ConfigureKeys(*gocui.Gui) error { return nil } diff --git a/pkg/widgets/services.go b/pkg/widgets/services.go index 300608d..dae0904 100644 --- a/pkg/widgets/services.go +++ b/pkg/widgets/services.go @@ -38,7 +38,7 @@ func NewServicesWidget(name string, g awsContext) *ServicesWidget { return widget } -func (w *ServicesWidget) ConfigureKeys() error { +func (w *ServicesWidget) ConfigureKeys(*gocui.Gui) error { if err := w.ctx.SetKeyBinding(w.name, gocui.KeyArrowUp, gocui.ModNone, w.previousItem); err != nil { return fmt.Errorf("failed to set the previous image %w", err) } @@ -81,8 +81,6 @@ func (w *ServicesWidget) Layout(g *gocui.Gui) error { } func (w *ServicesWidget) RefreshServices(services []string, serviceWidth int) error { - // w.w = serviceWidth + 4 - serviceList := make([]string, len(services)) for i, service := range services { serviceList[i] = fmt.Sprintf(" % -*s", serviceWidth+1, service) diff --git a/pkg/widgets/status.go b/pkg/widgets/status.go index 99d9158..6b39819 100644 --- a/pkg/widgets/status.go +++ b/pkg/widgets/status.go @@ -28,7 +28,7 @@ func NewStatusWidget(name string) *StatusWidget { } } -func (w *StatusWidget) ConfigureKeys() error { +func (w *StatusWidget) ConfigureKeys(*gocui.Gui) error { return nil } diff --git a/pkg/widgets/widget.go b/pkg/widgets/widget.go index c05f758..62dd87d 100644 --- a/pkg/widgets/widget.go +++ b/pkg/widgets/widget.go @@ -3,20 +3,21 @@ package widgets import "github.com/awesome-gocui/gocui" const ( - Filter = "filter" - Host = "host" - Images = "images" - Menu = "menu" - Remote = "remote" - Results = "results" - Status = "status" - Summary = "summary" - Services = "services" - Account = "account" + Account = "account" + Announcement = "announcement" + Filter = "filter" + Host = "host" + Images = "images" + Menu = "menu" + Remote = "remote" + Results = "results" + Services = "services" + Status = "status" + Summary = "summary" ) type Widget interface { - ConfigureKeys() error + ConfigureKeys(*gocui.Gui) error Layout(*gocui.Gui) error RefreshView() }