168 lines
5.1 KiB
Go
168 lines
5.1 KiB
Go
package tui
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
|
|
"rmm-hunter/internal/suspicious"
|
|
|
|
"github.com/charmbracelet/bubbles/list"
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
"github.com/charmbracelet/lipgloss"
|
|
)
|
|
|
|
// ListSelectedMsg indicates which index/type was selected for detail
|
|
type ListSelectedMsg struct {
|
|
TypeKey string
|
|
Index int
|
|
}
|
|
|
|
type listItem struct {
|
|
title string
|
|
desc string
|
|
eliminated bool
|
|
}
|
|
|
|
func (i listItem) Title() string { return i.title }
|
|
func (i listItem) Description() string { return i.desc }
|
|
func (i listItem) FilterValue() string { return i.title }
|
|
|
|
// customDelegate is a custom list item delegate that renders eliminated items in green
|
|
type customDelegate struct {
|
|
list.DefaultDelegate
|
|
}
|
|
|
|
func (d customDelegate) Render(w io.Writer, m list.Model, index int, item list.Item) {
|
|
i, ok := item.(listItem)
|
|
if !ok {
|
|
d.DefaultDelegate.Render(w, m, index, item)
|
|
return
|
|
}
|
|
|
|
title := i.Title()
|
|
desc := i.Description()
|
|
|
|
// Style for eliminated items (green)
|
|
if i.eliminated {
|
|
titleStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("10"))
|
|
descStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("10"))
|
|
|
|
if index == m.Index() {
|
|
// Selected item - add background
|
|
titleStyle = titleStyle.Background(lipgloss.Color("240"))
|
|
descStyle = descStyle.Background(lipgloss.Color("240"))
|
|
}
|
|
|
|
fmt.Fprintf(w, "%s\n%s", titleStyle.Render("✓ "+title), descStyle.Render(" "+desc))
|
|
} else {
|
|
// Normal rendering for non-eliminated items
|
|
d.DefaultDelegate.Render(w, m, index, item)
|
|
}
|
|
}
|
|
|
|
type ListViewModel struct {
|
|
typeKey string
|
|
list list.Model
|
|
header string
|
|
eliminated map[string]map[int]bool
|
|
}
|
|
|
|
func NewListView(typeKey string, sus suspicious.Suspicious, width, height int, eliminated map[string]map[int]bool) ListViewModel {
|
|
defaultDelegate := list.NewDefaultDelegate()
|
|
defaultDelegate.ShowDescription = true
|
|
delegate := customDelegate{DefaultDelegate: defaultDelegate}
|
|
l := list.New([]list.Item{}, delegate, 0, 0)
|
|
if width > 0 && height > 0 {
|
|
l.SetSize(width, height-2)
|
|
} else {
|
|
l.SetSize(80, 20)
|
|
}
|
|
l.Styles.Title = lipgloss.NewStyle().Bold(true)
|
|
|
|
header := ""
|
|
var items []list.Item
|
|
switch typeKey {
|
|
case "autoruns":
|
|
header = "Suspicious AutoRuns"
|
|
for i, ar := range sus.AutoRuns {
|
|
title := ar.ImageName
|
|
if title == "" {
|
|
title = ar.Entry
|
|
}
|
|
desc := fmt.Sprintf("%s (%s)", ar.ImagePath, ar.Location)
|
|
isEliminated := eliminated[typeKey] != nil && eliminated[typeKey][i]
|
|
items = append(items, listItem{title: title, desc: desc, eliminated: isEliminated})
|
|
}
|
|
case "binaries":
|
|
header = "Suspicious Binaries"
|
|
for i, b := range sus.Binaries {
|
|
isEliminated := eliminated[typeKey] != nil && eliminated[typeKey][i]
|
|
items = append(items, listItem{title: b.Path, desc: "binary file", eliminated: isEliminated})
|
|
}
|
|
case "connections":
|
|
header = "Suspicious Connections"
|
|
for i, c := range sus.OutboundConnections {
|
|
label := fmt.Sprintf("%s -> %s (%s)", c.LocalAddr, c.RemoteAddr, c.RemoteHost)
|
|
isEliminated := eliminated[typeKey] != nil && eliminated[typeKey][i]
|
|
items = append(items, listItem{title: label, desc: fmt.Sprintf("PID %s %s", c.PID, c.Process), eliminated: isEliminated})
|
|
}
|
|
case "directories":
|
|
header = "Suspicious Directories"
|
|
for i, d := range sus.Directories {
|
|
isEliminated := eliminated[typeKey] != nil && eliminated[typeKey][i]
|
|
items = append(items, listItem{title: d.Path, desc: "directory", eliminated: isEliminated})
|
|
}
|
|
case "processes":
|
|
header = "Suspicious Processes"
|
|
for i, p := range sus.Processes {
|
|
label := fmt.Sprintf("%s (PID %d)", p.Name, p.PID)
|
|
desc := p.Path
|
|
isEliminated := eliminated[typeKey] != nil && eliminated[typeKey][i]
|
|
items = append(items, listItem{title: label, desc: desc, eliminated: isEliminated})
|
|
}
|
|
case "scheduledTasks":
|
|
header = "Suspicious Scheduled Tasks"
|
|
for i, t := range sus.ScheduledTasks {
|
|
label := t.Name
|
|
desc := t.Path
|
|
isEliminated := eliminated[typeKey] != nil && eliminated[typeKey][i]
|
|
items = append(items, listItem{title: label, desc: desc, eliminated: isEliminated})
|
|
}
|
|
case "services":
|
|
header = "Suspicious Services"
|
|
for i, s := range sus.Services {
|
|
label := fmt.Sprintf("%s (%s)", s.Name, s.DisplayName)
|
|
desc := s.BinaryPathName
|
|
isEliminated := eliminated[typeKey] != nil && eliminated[typeKey][i]
|
|
items = append(items, listItem{title: label, desc: desc, eliminated: isEliminated})
|
|
}
|
|
}
|
|
|
|
l.Title = header + " — Left: Back Enter: Details q: Quit"
|
|
l.SetItems(items)
|
|
return ListViewModel{typeKey: typeKey, list: l, header: header, eliminated: eliminated}
|
|
}
|
|
|
|
func (m ListViewModel) Init() tea.Cmd { return nil }
|
|
|
|
func (m ListViewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
switch msg := msg.(type) {
|
|
case tea.WindowSizeMsg:
|
|
m.list.SetSize(msg.Width, msg.Height-2)
|
|
case tea.KeyMsg:
|
|
switch msg.String() {
|
|
case "left":
|
|
return m, func() tea.Msg { return BackMsg{} }
|
|
case "q", "esc", "ctrl+c":
|
|
return m, tea.Quit
|
|
case "enter":
|
|
return m, func() tea.Msg { return ListSelectedMsg{TypeKey: m.typeKey, Index: m.list.Index()} }
|
|
}
|
|
}
|
|
var cmd tea.Cmd
|
|
m.list, cmd = m.list.Update(msg)
|
|
return m, cmd
|
|
}
|
|
|
|
func (m ListViewModel) View() string { return m.list.View() }
|