package ui
import (
"bufio"
"fmt"
"os"
"strings"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
"github.com/xmirrorsecurity/opensca-cli/v3/cmd/detail"
"github.com/xmirrorsecurity/opensca-cli/v3/cmd/format"
"github.com/xmirrorsecurity/opensca-cli/v3/opensca/logs"
)
var (
colorVul = tcell.ColorPurple
colorPath = tcell.ColorBlue
colorDep = tcell.ColorGreen
colorDevDep = tcell.ColorGrey
colorVulDep = tcell.ColorRed
colorLicense = tcell.ColorYellow
colorLogMap = map[string]string{
"[TRACE]": "[grey]",
"[DEBUG]": "[green]",
"[INFO]": "[blue]",
"[WARN]": "[yellow]",
"[ERROR]": "[red]",
}
)
func OpenUI(report format.Report) {
flex := tview.NewFlex()
tree := DepTree(report)
tree.SetBorder(true).SetTitle(" dependency tree ")
log := TaskLog()
log.SetBorder(true).SetTitle(" log ")
info := TaskInfo(report)
info.SetBorder(true).SetTitle(" info ")
help := UIHelp()
help.SetBorder(true).SetTitle(" help ")
flex.SetDirection(tview.FlexRow).
AddItem(tview.NewFlex().
AddItem(tree, 0, 1, true).
AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
AddItem(info, 4, 1, false).
AddItem(log, 0, 1, false),
0, 1, false),
0, 1, true).
AddItem(help, 3, 1, false)
app := tview.NewApplication()
switchView := func() {
if app.GetFocus() == tree {
app.SetFocus(log)
} else {
app.SetFocus(tree)
}
}
app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
switch event.Rune() {
case 'h', 'l':
switchView()
case 'q':
app.Stop()
case ']':
tree.GetCurrentNode().ExpandAll()
case '[':
tree.GetCurrentNode().CollapseAll()
case '}':
tree.GetRoot().ExpandAll()
case '{':
tree.GetRoot().CollapseAll()
}
switch event.Key() {
case tcell.KeyLeft, tcell.KeyRight:
switchView()
}
return event
})
if err := app.SetRoot(flex, true).Run(); err != nil {
logs.Error(err)
}
}
func TaskInfo(report format.Report) *tview.TextView {
dep, vul := format.Statis(report)
info := tview.NewTextView().
SetText(fmt.Sprintf("%s\n%s", dep, vul))
info.SetTextColor(tcell.ColorBlue)
return info
}
func UIHelp() *tview.TextView {
tips := []string{"j:down", "k:up", "space:expand/collapse", "h/l:switch", "g:top", "G:bottom", "crtl+c/q:quit", "{:collapse_all", "}:expand_all", "[:collapse_node", "]:expand_node"}
text := tview.NewTextView().SetText(strings.Join(tips, " | "))
return text
}
func TaskLog() *tview.TextView {
log := tview.NewTextView().SetDynamicColors(true).ScrollToEnd()
if logs.LogFilePath == "" {
log.SetText("log file not found")
return log
}
f, err := os.Open(logs.LogFilePath)
if err != nil {
log.SetText(fmt.Sprintf("read log file err:\n%s", err))
return log
}
lines := []string{}
lineNum := 0
scanner := bufio.NewScanner(f)
color := "white"
for scanner.Scan() {
lineNum++
line := scanner.Text()
if i := strings.Index(line, " "); i != -1 {
if c, ok := colorLogMap[line[:i]]; ok {
color = c
line = fmt.Sprintf("%s[%s%s[white]", color, color, line[1:])
}
}
line = fmt.Sprintf("%s%s[white]", color, line)
lines = append(lines, fmt.Sprintf("[grey]%d:[white]%s", lineNum, line))
}
log.SetText(strings.Join(lines, "\r\n"))
return log
}
func DepTree(report format.Report) *tview.TreeView {
var root *tview.TreeNode
if report.DepDetailGraph != nil && report.DepDetailGraph.Name != "" {
root = newTreeNode(report.DepDetailGraph)
} else {
root = tview.NewTreeNode(report.TaskInfo.AppName).SetColor(colorPath)
}
depTreeRoot := report.DepDetailGraph
depTreeRoot.Expand = root
tree := tview.NewTreeView().SetRoot(root).SetCurrentNode(root)
tree.SetSelectedFunc(func(node *tview.TreeNode) {
if len(node.GetChildren()) > 0 {
node.SetExpanded(!node.IsExpanded())
}
})
depTreeRoot.ForEach(func(n *detail.DepDetailGraph) bool {
node := n.Expand.(*tview.TreeNode)
for _, c := range n.Children {
sub := newTreeNode(c)
c.Expand = sub
node.AddChild(sub)
}
return true
})
return tree
}
func newTreeNode(d *detail.DepDetailGraph) *tview.TreeNode {
dev := ""
if d.Develop {
dev = "<dev>"
}
dep := fmt.Sprintf("%s:%s", d.Name, d.Version)
if d.Vendor != "" {
dep = fmt.Sprintf("%s:%s", d.Vendor, dep)
}
info := fmt.Sprintf("%s%s<%s>", dev, dep, d.Language)
n := tview.NewTreeNode(info)
if len(d.Children) == 0 {
n.SetExpanded(false)
}
n.SetColor(colorDep)
if len(d.Vulnerabilities) > 0 {
n.SetColor(colorVulDep)
}
if d.Develop {
n.SetColor(colorDevDep)
}
if len(d.Paths) > 0 {
color := colorPath
if d.Develop {
color = colorDevDep
}
paths := tview.NewTreeNode("paths").SetColor(color).SetExpanded(!n.IsExpanded())
for _, p := range d.Paths {
paths.AddChild(tview.NewTreeNode(p).SetColor(color))
}
n.AddChild(paths)
}
if len(d.Licenses) > 0 {
color := colorLicense
if d.Develop {
color = colorDevDep
}
license := tview.NewTreeNode("license").SetColor(color).SetExpanded(!n.IsExpanded())
for _, lic := range d.Licenses {
license.AddChild(tview.NewTreeNode(lic.ShortName).SetColor(color))
}
n.AddChild(license)
}
if len(d.Vulnerabilities) > 0 {
color := colorVul
if d.Develop {
color = colorDevDep
}
vulns := tview.NewTreeNode("vulns").SetColor(color).SetExpanded(!n.IsExpanded())
for _, v := range d.Vulnerabilities {
vuln := tview.NewTreeNode(v.Id).SetColor(color)
vuln.AddChild(tview.NewTreeNode(fmt.Sprintf("name:%s", v.Name)).SetColor(color))
vuln.AddChild(tview.NewTreeNode(fmt.Sprintf("cve:%s", v.Cve)).SetColor(color))
vuln.AddChild(tview.NewTreeNode(fmt.Sprintf("description:%s", v.Description)).SetColor(color))
vuln.AddChild(tview.NewTreeNode(fmt.Sprintf("suggestion:%s", v.Suggestion)).SetColor(color))
vulns.AddChild(vuln)
}
n.AddChild(vulns)
}
return n
}