* feat(cve/mitre): support go-cve-dictionary:mitre * chore: adopt reviewer comment * refactor(models): refactor CveContents method
1081 lines
30 KiB
Go
1081 lines
30 KiB
Go
package tui
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
|
|
"golang.org/x/exp/slices"
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/future-architect/vuls/config"
|
|
"github.com/future-architect/vuls/cti"
|
|
"github.com/future-architect/vuls/logging"
|
|
"github.com/future-architect/vuls/models"
|
|
"github.com/future-architect/vuls/util"
|
|
"github.com/google/subcommands"
|
|
"github.com/gosuri/uitable"
|
|
"github.com/jesseduffield/gocui"
|
|
)
|
|
|
|
var scanResults models.ScanResults
|
|
var currentScanResult models.ScanResult
|
|
var vinfos []models.VulnInfo
|
|
var currentVinfo int
|
|
var currentDetailLimitY int
|
|
var currentChangelogLimitY int
|
|
|
|
// RunTui execute main logic
|
|
func RunTui(results models.ScanResults) subcommands.ExitStatus {
|
|
scanResults = results
|
|
sort.Slice(scanResults, func(i, j int) bool {
|
|
if scanResults[i].ServerName == scanResults[j].ServerName {
|
|
return scanResults[i].Container.Name < scanResults[j].Container.Name
|
|
}
|
|
return scanResults[i].ServerName < scanResults[j].ServerName
|
|
})
|
|
|
|
g := gocui.NewGui()
|
|
err := g.Init()
|
|
if err != nil {
|
|
logging.Log.Errorf("%+v", err)
|
|
return subcommands.ExitFailure
|
|
}
|
|
defer g.Close()
|
|
|
|
g.SetLayout(layout)
|
|
if err := keybindings(g); err != nil {
|
|
logging.Log.Errorf("%+v", err)
|
|
return subcommands.ExitFailure
|
|
}
|
|
g.SelBgColor = gocui.ColorGreen
|
|
g.SelFgColor = gocui.ColorBlack
|
|
g.Cursor = true
|
|
|
|
if err := g.MainLoop(); err != nil {
|
|
g.Close()
|
|
logging.Log.Errorf("%+v", err)
|
|
os.Exit(1)
|
|
}
|
|
return subcommands.ExitSuccess
|
|
}
|
|
|
|
func keybindings(g *gocui.Gui) (err error) {
|
|
errs := []error{}
|
|
|
|
// Move between views
|
|
errs = append(errs, g.SetKeybinding("side", gocui.KeyTab, gocui.ModNone, nextView))
|
|
// errs = append(errs, g.SetKeybinding("side", gocui.KeyCtrlH, gocui.ModNone, previousView))
|
|
// errs = append(errs, g.SetKeybinding("side", gocui.KeyCtrlL, gocui.ModNone, nextView))
|
|
// errs = append(errs, g.SetKeybinding("side", gocui.KeyArrowRight, gocui.ModAlt, nextView))
|
|
errs = append(errs, g.SetKeybinding("side", gocui.KeyArrowDown, gocui.ModNone, cursorDown))
|
|
errs = append(errs, g.SetKeybinding("side", gocui.KeyCtrlJ, gocui.ModNone, cursorDown))
|
|
errs = append(errs, g.SetKeybinding("side", gocui.KeyArrowUp, gocui.ModNone, cursorUp))
|
|
errs = append(errs, g.SetKeybinding("side", gocui.KeyCtrlK, gocui.ModNone, cursorUp))
|
|
errs = append(errs, g.SetKeybinding("side", gocui.KeyCtrlD, gocui.ModNone, cursorPageDown))
|
|
errs = append(errs, g.SetKeybinding("side", gocui.KeyCtrlU, gocui.ModNone, cursorPageUp))
|
|
errs = append(errs, g.SetKeybinding("side", gocui.KeySpace, gocui.ModNone, cursorPageDown))
|
|
errs = append(errs, g.SetKeybinding("side", gocui.KeyBackspace, gocui.ModNone, cursorPageUp))
|
|
errs = append(errs, g.SetKeybinding("side", gocui.KeyBackspace2, gocui.ModNone, cursorPageUp))
|
|
errs = append(errs, g.SetKeybinding("side", gocui.KeyCtrlN, gocui.ModNone, cursorDown))
|
|
errs = append(errs, g.SetKeybinding("side", gocui.KeyCtrlP, gocui.ModNone, cursorUp))
|
|
errs = append(errs, g.SetKeybinding("side", gocui.KeyEnter, gocui.ModNone, nextView))
|
|
|
|
// errs = append(errs, g.SetKeybinding("msg", gocui.KeyEnter, gocui.ModNone, delMsg))
|
|
// errs = append(errs, g.SetKeybinding("side", gocui.KeyEnter, gocui.ModNone, showMsg))
|
|
|
|
// summary
|
|
errs = append(errs, g.SetKeybinding("summary", gocui.KeyTab, gocui.ModNone, nextView))
|
|
errs = append(errs, g.SetKeybinding("summary", gocui.KeyCtrlQ, gocui.ModNone, previousView))
|
|
errs = append(errs, g.SetKeybinding("summary", gocui.KeyCtrlH, gocui.ModNone, previousView))
|
|
// errs = append(errs, g.SetKeybinding("summary", gocui.KeyCtrlL, gocui.ModNone, nextView))
|
|
// errs = append(errs, g.SetKeybinding("summary", gocui.KeyArrowLeft, gocui.ModAlt, previousView))
|
|
// errs = append(errs, g.SetKeybinding("summary", gocui.KeyArrowDown, gocui.ModAlt, nextView))
|
|
errs = append(errs, g.SetKeybinding("summary", gocui.KeyArrowDown, gocui.ModNone, cursorDown))
|
|
errs = append(errs, g.SetKeybinding("summary", gocui.KeyArrowUp, gocui.ModNone, cursorUp))
|
|
errs = append(errs, g.SetKeybinding("summary", gocui.KeyCtrlJ, gocui.ModNone, cursorDown))
|
|
errs = append(errs, g.SetKeybinding("summary", gocui.KeyCtrlK, gocui.ModNone, cursorUp))
|
|
errs = append(errs, g.SetKeybinding("summary", gocui.KeyCtrlD, gocui.ModNone, cursorPageDown))
|
|
errs = append(errs, g.SetKeybinding("summary", gocui.KeyCtrlU, gocui.ModNone, cursorPageUp))
|
|
errs = append(errs, g.SetKeybinding("summary", gocui.KeySpace, gocui.ModNone, cursorPageDown))
|
|
errs = append(errs, g.SetKeybinding("summary", gocui.KeyBackspace, gocui.ModNone, cursorPageUp))
|
|
errs = append(errs, g.SetKeybinding("summary", gocui.KeyBackspace2, gocui.ModNone, cursorPageUp))
|
|
errs = append(errs, g.SetKeybinding("summary", gocui.KeyEnter, gocui.ModNone, nextView))
|
|
errs = append(errs, g.SetKeybinding("summary", gocui.KeyCtrlN, gocui.ModNone, nextSummary))
|
|
errs = append(errs, g.SetKeybinding("summary", gocui.KeyCtrlP, gocui.ModNone, previousSummary))
|
|
|
|
// detail
|
|
errs = append(errs, g.SetKeybinding("detail", gocui.KeyTab, gocui.ModNone, nextView))
|
|
errs = append(errs, g.SetKeybinding("detail", gocui.KeyCtrlQ, gocui.ModNone, previousView))
|
|
errs = append(errs, g.SetKeybinding("detail", gocui.KeyCtrlH, gocui.ModNone, nextView))
|
|
// errs = append(errs, g.SetKeybinding("detail", gocui.KeyCtrlL, gocui.ModNone, nextView))
|
|
// errs = append(errs, g.SetKeybinding("detail", gocui.KeyArrowUp, gocui.ModAlt, previousView))
|
|
// errs = append(errs, g.SetKeybinding("detail", gocui.KeyArrowLeft, gocui.ModAlt, nextView))
|
|
errs = append(errs, g.SetKeybinding("detail", gocui.KeyArrowDown, gocui.ModNone, cursorDown))
|
|
errs = append(errs, g.SetKeybinding("detail", gocui.KeyArrowUp, gocui.ModNone, cursorUp))
|
|
errs = append(errs, g.SetKeybinding("detail", gocui.KeyCtrlJ, gocui.ModNone, cursorDown))
|
|
errs = append(errs, g.SetKeybinding("detail", gocui.KeyCtrlK, gocui.ModNone, cursorUp))
|
|
errs = append(errs, g.SetKeybinding("detail", gocui.KeyCtrlD, gocui.ModNone, cursorPageDown))
|
|
errs = append(errs, g.SetKeybinding("detail", gocui.KeyCtrlU, gocui.ModNone, cursorPageUp))
|
|
errs = append(errs, g.SetKeybinding("detail", gocui.KeySpace, gocui.ModNone, cursorPageDown))
|
|
errs = append(errs, g.SetKeybinding("detail", gocui.KeyBackspace, gocui.ModNone, cursorPageUp))
|
|
errs = append(errs, g.SetKeybinding("detail", gocui.KeyBackspace2, gocui.ModNone, cursorPageUp))
|
|
// errs = append(errs, g.SetKeybinding("detail", gocui.KeyCtrlM, gocui.ModNone, cursorMoveMiddle))
|
|
errs = append(errs, g.SetKeybinding("detail", gocui.KeyCtrlN, gocui.ModNone, nextSummary))
|
|
errs = append(errs, g.SetKeybinding("detail", gocui.KeyCtrlP, gocui.ModNone, previousSummary))
|
|
errs = append(errs, g.SetKeybinding("detail", gocui.KeyEnter, gocui.ModNone, nextView))
|
|
|
|
// changelog
|
|
errs = append(errs, g.SetKeybinding("changelog", gocui.KeyTab, gocui.ModNone, nextView))
|
|
errs = append(errs, g.SetKeybinding("changelog", gocui.KeyCtrlQ, gocui.ModNone, previousView))
|
|
errs = append(errs, g.SetKeybinding("changelog", gocui.KeyCtrlH, gocui.ModNone, nextView))
|
|
// errs = append(errs, g.SetKeybinding("changelog", gocui.KeyCtrlL, gocui.ModNone, nextView))
|
|
// errs = append(errs, g.SetKeybinding("changelog", gocui.KeyArrowUp, gocui.ModAlt, previousView))
|
|
// errs = append(errs, g.SetKeybinding("changelog", gocui.KeyArrowLeft, gocui.ModAlt, nextView))
|
|
errs = append(errs, g.SetKeybinding("changelog", gocui.KeyArrowDown, gocui.ModNone, cursorDown))
|
|
errs = append(errs, g.SetKeybinding("changelog", gocui.KeyArrowUp, gocui.ModNone, cursorUp))
|
|
errs = append(errs, g.SetKeybinding("changelog", gocui.KeyCtrlJ, gocui.ModNone, cursorDown))
|
|
errs = append(errs, g.SetKeybinding("changelog", gocui.KeyCtrlK, gocui.ModNone, cursorUp))
|
|
errs = append(errs, g.SetKeybinding("changelog", gocui.KeyCtrlD, gocui.ModNone, cursorPageDown))
|
|
errs = append(errs, g.SetKeybinding("changelog", gocui.KeyCtrlU, gocui.ModNone, cursorPageUp))
|
|
errs = append(errs, g.SetKeybinding("changelog", gocui.KeySpace, gocui.ModNone, cursorPageDown))
|
|
errs = append(errs, g.SetKeybinding("changelog", gocui.KeyBackspace, gocui.ModNone, cursorPageUp))
|
|
errs = append(errs, g.SetKeybinding("changelog", gocui.KeyBackspace2, gocui.ModNone, cursorPageUp))
|
|
// errs = append(errs, g.SetKeybinding("changelog", gocui.KeyCtrlM, gocui.ModNone, cursorMoveMiddle))
|
|
errs = append(errs, g.SetKeybinding("changelog", gocui.KeyCtrlN, gocui.ModNone, nextSummary))
|
|
errs = append(errs, g.SetKeybinding("changelog", gocui.KeyCtrlP, gocui.ModNone, previousSummary))
|
|
errs = append(errs, g.SetKeybinding("changelog", gocui.KeyEnter, gocui.ModNone, nextView))
|
|
|
|
// errs = append(errs, g.SetKeybinding("msg", gocui.KeyEnter, gocui.ModNone, delMsg))
|
|
// errs = append(errs, g.SetKeybinding("detail", gocui.KeyEnter, gocui.ModNone, showMsg))
|
|
|
|
errs = append(errs, g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit))
|
|
// errs = append(errs, g.SetKeybinding("side", gocui.KeyEnter, gocui.ModNone, getLine))
|
|
// errs = append(errs, g.SetKeybinding("msg", gocui.KeyEnter, gocui.ModNone, delMsg))
|
|
|
|
for _, e := range errs {
|
|
if e != nil {
|
|
return e
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func nextView(g *gocui.Gui, v *gocui.View) error {
|
|
var err error
|
|
|
|
if v == nil {
|
|
return g.SetCurrentView("side")
|
|
}
|
|
switch v.Name() {
|
|
case "side":
|
|
err = g.SetCurrentView("summary")
|
|
case "summary":
|
|
err = g.SetCurrentView("detail")
|
|
case "detail":
|
|
err = g.SetCurrentView("changelog")
|
|
case "changelog":
|
|
err = g.SetCurrentView("side")
|
|
default:
|
|
err = g.SetCurrentView("summary")
|
|
}
|
|
return err
|
|
}
|
|
|
|
func previousView(g *gocui.Gui, v *gocui.View) error {
|
|
var err error
|
|
|
|
if v == nil {
|
|
return g.SetCurrentView("side")
|
|
}
|
|
switch v.Name() {
|
|
case "side":
|
|
err = g.SetCurrentView("side")
|
|
case "summary":
|
|
err = g.SetCurrentView("side")
|
|
case "detail":
|
|
err = g.SetCurrentView("summary")
|
|
case "changelog":
|
|
err = g.SetCurrentView("detail")
|
|
default:
|
|
err = g.SetCurrentView("side")
|
|
}
|
|
return err
|
|
}
|
|
|
|
func movable(v *gocui.View, nextY int) (ok bool, yLimit int) {
|
|
switch v.Name() {
|
|
case "side":
|
|
yLimit = len(scanResults) - 1
|
|
if yLimit < nextY {
|
|
return false, yLimit
|
|
}
|
|
return true, yLimit
|
|
case "summary":
|
|
yLimit = len(currentScanResult.ScannedCves) - 1
|
|
if yLimit < nextY {
|
|
return false, yLimit
|
|
}
|
|
return true, yLimit
|
|
case "detail":
|
|
// if currentDetailLimitY < nextY {
|
|
// return false, currentDetailLimitY
|
|
// }
|
|
return true, currentDetailLimitY
|
|
case "changelog":
|
|
// if currentChangelogLimitY < nextY {
|
|
// return false, currentChangelogLimitY
|
|
// }
|
|
return true, currentChangelogLimitY
|
|
default:
|
|
return true, 0
|
|
}
|
|
}
|
|
|
|
func pageUpDownJumpCount(v *gocui.View) int {
|
|
var jump int
|
|
switch v.Name() {
|
|
case "side", "summary":
|
|
jump = 8
|
|
case "detail", "changelog":
|
|
jump = 30
|
|
default:
|
|
jump = 8
|
|
}
|
|
return jump
|
|
}
|
|
|
|
// redraw views
|
|
func onMovingCursorRedrawView(g *gocui.Gui, v *gocui.View) error {
|
|
switch v.Name() {
|
|
case "summary":
|
|
if err := redrawDetail(g); err != nil {
|
|
return err
|
|
}
|
|
if err := redrawChangelog(g); err != nil {
|
|
return err
|
|
}
|
|
case "side":
|
|
if err := changeHost(g, v); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func cursorDown(g *gocui.Gui, v *gocui.View) error {
|
|
if v != nil {
|
|
cx, cy := v.Cursor()
|
|
ox, oy := v.Origin()
|
|
// ok, := movable(v, oy+cy+1)
|
|
// _, maxY := v.Size()
|
|
ok, _ := movable(v, oy+cy+1)
|
|
// log.Info(cy, oy)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
if err := v.SetCursor(cx, cy+1); err != nil {
|
|
if err := v.SetOrigin(ox, oy+1); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
err := onMovingCursorRedrawView(g, v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
cx, cy := v.Cursor()
|
|
ox, oy := v.Origin()
|
|
_ = debug(g, fmt.Sprintf("%v, %v, %v, %v", cx, cy, ox, oy))
|
|
return nil
|
|
}
|
|
|
|
func cursorMoveMiddle(g *gocui.Gui, v *gocui.View) error {
|
|
if v != nil {
|
|
_, maxY := v.Size()
|
|
cx, _ := v.Cursor()
|
|
if err := v.SetCursor(cx, maxY/2); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return onMovingCursorRedrawView(g, v)
|
|
}
|
|
|
|
func cursorPageDown(g *gocui.Gui, v *gocui.View) error {
|
|
jump := pageUpDownJumpCount(v)
|
|
|
|
if v != nil {
|
|
cx, cy := v.Cursor()
|
|
ox, oy := v.Origin()
|
|
ok, yLimit := movable(v, oy+cy+jump)
|
|
_, maxY := v.Size()
|
|
|
|
if !ok {
|
|
if yLimit < maxY {
|
|
_ = v.SetCursor(cx, yLimit)
|
|
} else {
|
|
_ = v.SetCursor(cx, maxY-1)
|
|
_ = v.SetOrigin(ox, yLimit-maxY+1)
|
|
}
|
|
} else if yLimit < oy+jump+maxY {
|
|
if yLimit < maxY {
|
|
_ = v.SetCursor(cx, yLimit)
|
|
} else {
|
|
_ = v.SetOrigin(ox, yLimit-maxY+1)
|
|
_ = v.SetCursor(cx, maxY-1)
|
|
}
|
|
} else {
|
|
_ = v.SetCursor(cx, cy)
|
|
if err := v.SetOrigin(ox, oy+jump); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
_ = onMovingCursorRedrawView(g, v)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func cursorUp(g *gocui.Gui, v *gocui.View) error {
|
|
if v != nil {
|
|
ox, oy := v.Origin()
|
|
cx, cy := v.Cursor()
|
|
if err := v.SetCursor(cx, cy-1); err != nil && 0 < oy {
|
|
if err := v.SetOrigin(ox, oy-1); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
_ = onMovingCursorRedrawView(g, v)
|
|
return nil
|
|
}
|
|
|
|
func cursorPageUp(g *gocui.Gui, v *gocui.View) error {
|
|
jump := pageUpDownJumpCount(v)
|
|
if v != nil {
|
|
cx, _ := v.Cursor()
|
|
ox, oy := v.Origin()
|
|
if err := v.SetOrigin(ox, oy-jump); err != nil {
|
|
if err := v.SetOrigin(ox, 0); err != nil {
|
|
return err
|
|
}
|
|
_ = v.SetCursor(cx, 0)
|
|
|
|
}
|
|
_ = onMovingCursorRedrawView(g, v)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func previousSummary(g *gocui.Gui, v *gocui.View) error {
|
|
if v != nil {
|
|
// cursor to summary
|
|
if err := g.SetCurrentView("summary"); err != nil {
|
|
return err
|
|
}
|
|
// move next line
|
|
if err := cursorUp(g, g.CurrentView()); err != nil {
|
|
return err
|
|
}
|
|
// cursor to detail
|
|
if err := g.SetCurrentView("detail"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func nextSummary(g *gocui.Gui, v *gocui.View) error {
|
|
if v != nil {
|
|
// cursor to summary
|
|
if err := g.SetCurrentView("summary"); err != nil {
|
|
return err
|
|
}
|
|
// move next line
|
|
if err := cursorDown(g, g.CurrentView()); err != nil {
|
|
return err
|
|
}
|
|
// cursor to detail
|
|
if err := g.SetCurrentView("detail"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func changeHost(g *gocui.Gui, v *gocui.View) error {
|
|
|
|
if err := g.DeleteView("summary"); err != nil {
|
|
return err
|
|
}
|
|
if err := g.DeleteView("detail"); err != nil {
|
|
return err
|
|
}
|
|
if err := g.DeleteView("changelog"); err != nil {
|
|
return err
|
|
}
|
|
|
|
_, cy := v.Cursor()
|
|
l, err := v.Line(cy)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
serverName := strings.TrimSpace(l)
|
|
|
|
for _, r := range scanResults {
|
|
if serverName == strings.TrimSpace(r.ServerInfoTui()) {
|
|
currentScanResult = r
|
|
vinfos = r.ScannedCves.ToSortedSlice()
|
|
break
|
|
}
|
|
}
|
|
|
|
if err := setSummaryLayout(g); err != nil {
|
|
return err
|
|
}
|
|
if err := setDetailLayout(g); err != nil {
|
|
return err
|
|
}
|
|
return setChangelogLayout(g)
|
|
}
|
|
|
|
func redrawDetail(g *gocui.Gui) error {
|
|
if err := g.DeleteView("detail"); err != nil {
|
|
return err
|
|
}
|
|
|
|
return setDetailLayout(g)
|
|
}
|
|
|
|
func redrawChangelog(g *gocui.Gui) error {
|
|
if err := g.DeleteView("changelog"); err != nil {
|
|
return err
|
|
}
|
|
|
|
return setChangelogLayout(g)
|
|
}
|
|
|
|
func getLine(g *gocui.Gui, v *gocui.View) error {
|
|
var l string
|
|
var err error
|
|
|
|
_, cy := v.Cursor()
|
|
if l, err = v.Line(cy); err != nil {
|
|
l = ""
|
|
}
|
|
|
|
maxX, maxY := g.Size()
|
|
if v, err := g.SetView("msg", maxX/2-30, maxY/2, maxX/2+30, maxY/2+2); err != nil {
|
|
if err != gocui.ErrUnknownView {
|
|
return err
|
|
}
|
|
fmt.Fprintln(v, l)
|
|
if err := g.SetCurrentView("msg"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func showMsg(g *gocui.Gui, v *gocui.View) error {
|
|
jump := 8
|
|
_, cy := v.Cursor()
|
|
_, oy := v.Origin()
|
|
ok, yLimit := movable(v, oy+cy+jump)
|
|
// maxX, maxY := v.Size()
|
|
_, maxY := v.Size()
|
|
|
|
l := fmt.Sprintf("cy: %d, oy: %d, maxY: %d, yLimit: %d, curCve %d, ok: %v",
|
|
cy, oy, maxY, yLimit, currentVinfo, ok)
|
|
// if v, err := g.SetView("msg", maxX/2-30, maxY/2, maxX/2+30, maxY/2+2); err != nil {
|
|
if v, err := g.SetView("msg", 10, maxY/2, 10+50, maxY/2+2); err != nil {
|
|
if err != gocui.ErrUnknownView {
|
|
return err
|
|
}
|
|
fmt.Fprintln(v, l)
|
|
if err := g.SetCurrentView("msg"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func delMsg(g *gocui.Gui, _ *gocui.View) error {
|
|
if err := g.DeleteView("msg"); err != nil {
|
|
return err
|
|
}
|
|
err := g.SetCurrentView("summary")
|
|
return err
|
|
}
|
|
|
|
func quit(_ *gocui.Gui, _ *gocui.View) error {
|
|
return gocui.ErrQuit
|
|
}
|
|
|
|
func layout(g *gocui.Gui) error {
|
|
if err := setSideLayout(g); err != nil {
|
|
return err
|
|
}
|
|
if err := setSummaryLayout(g); err != nil {
|
|
return err
|
|
}
|
|
if err := setDetailLayout(g); err != nil {
|
|
return err
|
|
}
|
|
return setChangelogLayout(g)
|
|
}
|
|
|
|
func debug(g *gocui.Gui, str string) error {
|
|
if config.Conf.Debug {
|
|
maxX, maxY := g.Size()
|
|
if _, err := g.View("debug"); err != gocui.ErrUnknownView {
|
|
if err := g.DeleteView("debug"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if v, err := g.SetView("debug", maxX/2-7, maxY/2, maxX/2+7, maxY/2+2); err != nil {
|
|
fmt.Fprint(v, str)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func setSideLayout(g *gocui.Gui) error {
|
|
_, maxY := g.Size()
|
|
if v, err := g.SetView("side", -1, -1, 40, int(float64(maxY)*0.2)); err != nil {
|
|
if err != gocui.ErrUnknownView {
|
|
return err
|
|
}
|
|
v.Highlight = true
|
|
|
|
for _, result := range scanResults {
|
|
fmt.Fprintln(v, result.ServerInfoTui())
|
|
}
|
|
if len(scanResults) == 0 {
|
|
return xerrors.New("No scan results")
|
|
}
|
|
currentScanResult = scanResults[0]
|
|
vinfos = scanResults[0].ScannedCves.ToSortedSlice()
|
|
if err := g.SetCurrentView("side"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func setSummaryLayout(g *gocui.Gui) error {
|
|
maxX, maxY := g.Size()
|
|
if v, err := g.SetView("summary", 40, -1, maxX, int(float64(maxY)*0.2)); err != nil {
|
|
if err != gocui.ErrUnknownView {
|
|
return err
|
|
}
|
|
|
|
lines := summaryLines(currentScanResult)
|
|
fmt.Fprint(v, lines)
|
|
|
|
v.Highlight = true
|
|
v.Editable = false
|
|
v.Wrap = false
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func summaryLines(r models.ScanResult) string {
|
|
stable := uitable.New()
|
|
stable.MaxColWidth = 1000
|
|
stable.Wrap = false
|
|
|
|
if len(r.Errors) != 0 {
|
|
return "Error: Scan with --debug to view the details"
|
|
}
|
|
|
|
indexFormat := ""
|
|
if len(r.ScannedCves) < 10 {
|
|
indexFormat = "[%1d]"
|
|
} else if len(r.ScannedCves) < 100 {
|
|
indexFormat = "[%2d]"
|
|
} else {
|
|
indexFormat = "[%3d]"
|
|
}
|
|
|
|
for i, vinfo := range r.ScannedCves.ToSortedSlice() {
|
|
max := vinfo.MaxCvssScore().Value.Score
|
|
cvssScore := "| "
|
|
if 0 < max {
|
|
cvssScore = fmt.Sprintf("| %4.1f", max)
|
|
}
|
|
|
|
pkgNames := vinfo.AffectedPackages.Names()
|
|
pkgNames = append(pkgNames, vinfo.CpeURIs...)
|
|
pkgNames = append(pkgNames, vinfo.GitHubSecurityAlerts.Names()...)
|
|
pkgNames = append(pkgNames, vinfo.WpPackageFixStats.Names()...)
|
|
pkgNames = append(pkgNames, vinfo.LibraryFixedIns.Names()...)
|
|
pkgNames = append(pkgNames, vinfo.WindowsKBFixedIns...)
|
|
|
|
av := vinfo.AttackVector()
|
|
for _, pname := range vinfo.AffectedPackages.Names() {
|
|
if r.Packages[pname].HasReachablePort() {
|
|
av = fmt.Sprintf("%s ◉", av)
|
|
break
|
|
}
|
|
}
|
|
|
|
exploits := ""
|
|
if 0 < len(vinfo.Metasploits) {
|
|
exploits = "EXP"
|
|
} else if 0 < len(vinfo.Exploits) {
|
|
exploits = "POC"
|
|
}
|
|
|
|
var cols []string
|
|
cols = []string{
|
|
fmt.Sprintf(indexFormat, i+1),
|
|
string(vinfo.DiffStatus),
|
|
vinfo.CveID,
|
|
cvssScore + " |",
|
|
fmt.Sprintf("%-6s |", av),
|
|
fmt.Sprintf("%3s |", exploits),
|
|
fmt.Sprintf("%9s |", vinfo.AlertDict.FormatSource()),
|
|
fmt.Sprintf("%7s |", vinfo.PatchStatus(r.Packages)),
|
|
strings.Join(pkgNames, ", "),
|
|
}
|
|
icols := make([]interface{}, len(cols))
|
|
for j := range cols {
|
|
icols[j] = cols[j]
|
|
}
|
|
stable.AddRow(icols...)
|
|
}
|
|
|
|
return fmt.Sprintf("%s", stable)
|
|
}
|
|
|
|
func setDetailLayout(g *gocui.Gui) error {
|
|
maxX, maxY := g.Size()
|
|
|
|
summaryView, err := g.View("summary")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, cy := summaryView.Cursor()
|
|
_, oy := summaryView.Origin()
|
|
currentVinfo = cy + oy
|
|
|
|
if v, err := g.SetView("detail", -1, int(float64(maxY)*0.2), int(float64(maxX)*0.5), maxY); err != nil {
|
|
if err != gocui.ErrUnknownView {
|
|
return err
|
|
}
|
|
text, err := detailLines()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprint(v, text)
|
|
v.Editable = false
|
|
v.Wrap = true
|
|
|
|
currentDetailLimitY = len(strings.Split(text, "\n")) - 1
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func setChangelogLayout(g *gocui.Gui) error {
|
|
summaryView, err := g.View("summary")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
maxX, maxY := g.Size()
|
|
if v, err := g.SetView("changelog", int(float64(maxX)*0.5), int(float64(maxY)*0.2), maxX, maxY); err != nil {
|
|
if err != gocui.ErrUnknownView {
|
|
return err
|
|
}
|
|
if len(currentScanResult.Errors) != 0 || len(currentScanResult.ScannedCves) == 0 {
|
|
return nil
|
|
}
|
|
|
|
lines := []string{
|
|
"Affected Packages, Processes",
|
|
"============================",
|
|
}
|
|
|
|
_, cy := summaryView.Cursor()
|
|
_, oy := summaryView.Origin()
|
|
currentVinfo = cy + oy
|
|
vinfo := vinfos[currentVinfo]
|
|
vinfo.AffectedPackages.Sort()
|
|
for _, affected := range vinfo.AffectedPackages {
|
|
// packages detected by OVAL may not be actually installed
|
|
if pack, ok := currentScanResult.Packages[affected.Name]; ok {
|
|
var line string
|
|
if pack.Repository != "" {
|
|
line = fmt.Sprintf("* %s (%s)",
|
|
pack.FormatVersionFromTo(affected),
|
|
pack.Repository)
|
|
} else {
|
|
line = fmt.Sprintf("* %s", pack.FormatVersionFromTo(affected))
|
|
}
|
|
lines = append(lines, line)
|
|
|
|
for _, p := range pack.AffectedProcs {
|
|
if len(p.ListenPortStats) == 0 {
|
|
lines = append(lines, fmt.Sprintf(" * PID: %s %s", p.PID, p.Name))
|
|
continue
|
|
}
|
|
|
|
var ports []string
|
|
for _, pp := range p.ListenPortStats {
|
|
if len(pp.PortReachableTo) == 0 {
|
|
ports = append(ports, fmt.Sprintf("%s:%s", pp.BindAddress, pp.Port))
|
|
} else {
|
|
ports = append(ports, fmt.Sprintf("%s:%s(◉ Scannable: %s)", pp.BindAddress, pp.Port, pp.PortReachableTo))
|
|
}
|
|
}
|
|
|
|
lines = append(lines, fmt.Sprintf(" * PID: %s %s Port: %s", p.PID, p.Name, ports))
|
|
}
|
|
}
|
|
}
|
|
sort.Strings(vinfo.CpeURIs)
|
|
for _, uri := range vinfo.CpeURIs {
|
|
lines = append(lines, "* "+uri)
|
|
}
|
|
|
|
for _, alert := range vinfo.GitHubSecurityAlerts {
|
|
lines = append(lines, "* "+alert.RepoURLPackageName())
|
|
}
|
|
|
|
r := currentScanResult
|
|
// check wordpress fixedin
|
|
if r.WordPressPackages != nil {
|
|
for _, wp := range vinfo.WpPackageFixStats {
|
|
if p, ok := r.WordPressPackages.Find(wp.Name); ok {
|
|
if p.Type == models.WPCore {
|
|
lines = append(lines, fmt.Sprintf("* %s-%s, FixedIn: %s",
|
|
wp.Name, p.Version, wp.FixedIn))
|
|
} else {
|
|
lines = append(lines,
|
|
fmt.Sprintf("* %s-%s, Update: %s, FixedIn: %s, %s",
|
|
wp.Name, p.Version, p.Update, wp.FixedIn, p.Status))
|
|
}
|
|
} else {
|
|
lines = append(lines, fmt.Sprintf("* %s", wp.Name))
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, l := range vinfo.LibraryFixedIns {
|
|
libs := r.LibraryScanners.Find(l.Path, l.Name)
|
|
for path, lib := range libs {
|
|
lines = append(lines, fmt.Sprintf("%s-%s, FixedIn: %s (%s)",
|
|
lib.Name, lib.Version, l.FixedIn, path))
|
|
}
|
|
}
|
|
|
|
for _, adv := range vinfo.DistroAdvisories {
|
|
lines = append(lines, "\n",
|
|
"Advisories",
|
|
"==========",
|
|
)
|
|
lines = append(lines, adv.Format())
|
|
}
|
|
|
|
m := map[string]struct{}{}
|
|
if len(vinfo.Exploits) != 0 {
|
|
lines = append(lines, "\n",
|
|
"PoC",
|
|
"=============",
|
|
)
|
|
for _, exploit := range vinfo.Exploits {
|
|
if _, ok := m[exploit.URL]; ok {
|
|
continue
|
|
}
|
|
lines = append(lines, fmt.Sprintf("* [%s](%s)", exploit.Description, exploit.URL))
|
|
m[exploit.URL] = struct{}{}
|
|
}
|
|
}
|
|
|
|
if len(vinfo.Metasploits) != 0 {
|
|
lines = append(lines, "\n",
|
|
"Metasploit Modules",
|
|
"==================",
|
|
)
|
|
for _, module := range vinfo.Metasploits {
|
|
lines = append(lines, fmt.Sprintf("* %s: %s", module.Name, module.Description))
|
|
if 0 < len(module.URLs) {
|
|
for _, u := range module.URLs {
|
|
lines = append(lines, fmt.Sprintf(" - %s", u))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(vinfo.AlertDict.CISA) > 0 {
|
|
lines = append(lines, "\n",
|
|
"CISA Alert",
|
|
"===========",
|
|
)
|
|
for _, alert := range vinfo.AlertDict.CISA {
|
|
lines = append(lines, fmt.Sprintf("* [%s](%s)", alert.Title, alert.URL))
|
|
}
|
|
}
|
|
|
|
if len(vinfo.AlertDict.USCERT) > 0 {
|
|
lines = append(lines, "\n",
|
|
"USCERT Alert",
|
|
"=============",
|
|
)
|
|
for _, alert := range vinfo.AlertDict.USCERT {
|
|
lines = append(lines, fmt.Sprintf("* [%s](%s)", alert.Title, alert.URL))
|
|
}
|
|
}
|
|
|
|
if len(vinfo.AlertDict.JPCERT) > 0 {
|
|
lines = append(lines, "\n",
|
|
"JPCERT Alert",
|
|
"=============",
|
|
)
|
|
for _, alert := range vinfo.AlertDict.JPCERT {
|
|
if r.Lang == "ja" {
|
|
lines = append(lines, fmt.Sprintf("* [%s](%s)", alert.Title, alert.URL))
|
|
} else {
|
|
lines = append(lines, fmt.Sprintf("* [JPCERT](%s)", alert.URL))
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(vinfo.Ctis) > 0 {
|
|
lines = append(lines, "\n",
|
|
"Cyber Threat Intelligence",
|
|
"=========================",
|
|
)
|
|
|
|
attacks := []string{}
|
|
capecs := []string{}
|
|
for _, techniqueID := range vinfo.Ctis {
|
|
technique, ok := cti.TechniqueDict[techniqueID]
|
|
if !ok {
|
|
continue
|
|
}
|
|
if strings.HasPrefix(techniqueID, "CAPEC-") {
|
|
capecs = append(capecs, fmt.Sprintf("* %s", technique.Name))
|
|
} else {
|
|
attacks = append(attacks, fmt.Sprintf("* %s", technique.Name))
|
|
}
|
|
}
|
|
slices.Sort(attacks)
|
|
slices.Sort(capecs)
|
|
lines = append(lines, append([]string{"MITRE ATT&CK:"}, attacks...)...)
|
|
lines = append(lines, "\n")
|
|
lines = append(lines, append([]string{"CAPEC:"}, capecs...)...)
|
|
}
|
|
|
|
if currentScanResult.Config.Scan.Servers[currentScanResult.ServerName].Mode.IsDeep() {
|
|
lines = append(lines, "\n",
|
|
"ChangeLogs",
|
|
"==========",
|
|
)
|
|
for _, affected := range vinfo.AffectedPackages {
|
|
pack := currentScanResult.Packages[affected.Name]
|
|
for _, p := range currentScanResult.Packages {
|
|
if pack.Name == p.Name {
|
|
lines = append(lines, p.FormatChangelog(), "\n")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
text := strings.Join(lines, "\n")
|
|
fmt.Fprint(v, text)
|
|
v.Editable = false
|
|
v.Wrap = true
|
|
|
|
currentChangelogLimitY = len(strings.Split(text, "\n")) - 1
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type dataForTmpl struct {
|
|
CveID string
|
|
Cvsses string
|
|
SSVC []models.CveContentSSVC
|
|
Exploits []models.Exploit
|
|
Metasploits []models.Metasploit
|
|
Summary string
|
|
Mitigation string
|
|
PatchURLs []string
|
|
Confidences models.Confidences
|
|
Cwes []models.CweDictEntry
|
|
Alerts []models.Alert
|
|
Links []string
|
|
References []models.Reference
|
|
Packages []string
|
|
CpeURIs []string
|
|
PublishedDate time.Time
|
|
LastModifiedDate time.Time
|
|
}
|
|
|
|
func detailLines() (string, error) {
|
|
r := currentScanResult
|
|
if len(r.Errors) != 0 {
|
|
return "", nil
|
|
}
|
|
|
|
if len(r.ScannedCves) == 0 {
|
|
return "No vulnerable packages", nil
|
|
}
|
|
|
|
tmpl, err := template.New("detail").Parse(mdTemplate)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
vinfo := vinfos[currentVinfo]
|
|
links := []string{}
|
|
for _, r := range vinfo.CveContents.PrimarySrcURLs(r.Lang, r.Family, vinfo.CveID, vinfo.Confidences) {
|
|
links = append(links, r.Value)
|
|
}
|
|
|
|
refsMap := map[string]models.Reference{}
|
|
for _, rr := range vinfo.CveContents.References(r.Family) {
|
|
for _, ref := range rr.Value {
|
|
if ref.Source == "" {
|
|
ref.Source = "-"
|
|
}
|
|
refsMap[ref.Link] = ref
|
|
}
|
|
}
|
|
|
|
for _, ctype := range models.GetCveContentTypes(string(models.Trivy)) {
|
|
if conts, found := vinfo.CveContents[ctype]; found {
|
|
for _, cont := range conts {
|
|
for _, ref := range cont.References {
|
|
refsMap[ref.Link] = ref
|
|
}
|
|
}
|
|
}
|
|
}
|
|
refs := []models.Reference{}
|
|
for _, v := range refsMap {
|
|
refs = append(refs, v)
|
|
}
|
|
|
|
summary := vinfo.Summaries(r.Lang, r.Family)[0]
|
|
|
|
mitigations := []string{}
|
|
for _, m := range vinfo.Mitigations {
|
|
switch m.CveContentType {
|
|
case models.RedHatAPI, models.Microsoft:
|
|
mitigations = append(mitigations,
|
|
fmt.Sprintf("%s (%s)", m.Mitigation, m.CveContentType))
|
|
case models.Nvd:
|
|
mitigations = append(mitigations,
|
|
fmt.Sprintf("* %s (%s)", m.URL, m.CveContentType))
|
|
default:
|
|
logging.Log.Errorf("Unknown CveContentType: %s", m)
|
|
}
|
|
}
|
|
|
|
table := uitable.New()
|
|
table.MaxColWidth = 100
|
|
table.Wrap = true
|
|
scores := append(vinfo.Cvss40Scores(), append(vinfo.Cvss3Scores(), vinfo.Cvss2Scores()...)...)
|
|
var cols []interface{}
|
|
for _, score := range scores {
|
|
cols = []interface{}{
|
|
score.Value.Format(),
|
|
score.Type,
|
|
}
|
|
table.AddRow(cols...)
|
|
}
|
|
|
|
uniqCweIDs := vinfo.CveContents.UniqCweIDs(r.Family)
|
|
cwes := []models.CweDictEntry{}
|
|
for _, cweID := range uniqCweIDs {
|
|
if strings.HasPrefix(cweID.Value, "CWE-") {
|
|
if dict, ok := r.CweDict[strings.TrimPrefix(cweID.Value, "CWE-")]; ok {
|
|
cwes = append(cwes, dict)
|
|
}
|
|
}
|
|
}
|
|
|
|
data := dataForTmpl{
|
|
CveID: vinfo.CveID,
|
|
Cvsses: fmt.Sprintf("%s\n", table),
|
|
SSVC: vinfo.CveContents.SSVC(),
|
|
Summary: fmt.Sprintf("%s (%s)", summary.Value, summary.Type),
|
|
Mitigation: strings.Join(mitigations, "\n"),
|
|
PatchURLs: vinfo.CveContents.PatchURLs(),
|
|
Confidences: vinfo.Confidences,
|
|
Cwes: cwes,
|
|
Links: util.Distinct(links),
|
|
References: refs,
|
|
}
|
|
|
|
buf := bytes.NewBuffer(nil) // create empty buffer
|
|
if err := tmpl.Execute(buf, data); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return string(buf.Bytes()), nil
|
|
}
|
|
|
|
const mdTemplate = `
|
|
{{.CveID}}
|
|
================
|
|
|
|
CVSS Scores
|
|
-----------
|
|
{{.Cvsses }}
|
|
|
|
{{if .SSVC}}
|
|
SSVC
|
|
-----------
|
|
{{range $ssvc := .SSVC -}}
|
|
* {{$ssvc.Type}}
|
|
Exploitation : {{$ssvc.Value.Exploitation}}
|
|
Automatable : {{$ssvc.Value.Automatable}}
|
|
TechnicalImpact : {{$ssvc.Value.TechnicalImpact}}
|
|
{{end}}
|
|
{{end}}
|
|
|
|
Summary
|
|
-----------
|
|
{{.Summary }}
|
|
|
|
Mitigation
|
|
-----------
|
|
{{.Mitigation }}
|
|
|
|
Primary Src
|
|
-----------
|
|
{{range $link := .Links -}}
|
|
* {{$link}}
|
|
{{end}}
|
|
Patch
|
|
-----------
|
|
{{range $url := .PatchURLs -}}
|
|
* {{$url}}
|
|
{{end}}
|
|
CWE
|
|
-----------
|
|
{{range .Cwes -}}
|
|
* {{.En.CweID}} [{{.En.Name}}](https://cwe.mitre.org/data/definitions/{{.En.CweID}}.html)
|
|
{{end}}
|
|
{{range $name := .CpeURIs -}}
|
|
* {{$name}}
|
|
{{end}}
|
|
Confidence
|
|
-----------
|
|
{{range $confidence := .Confidences -}}
|
|
* {{$confidence.Score}} / {{$confidence.DetectionMethod}}
|
|
{{end}}
|
|
References
|
|
-----------
|
|
{{range .References -}}
|
|
* [{{.Source}}]({{.Link}})
|
|
{{end}}
|
|
|
|
`
|