Files
RMM-Hunter/internal/tui/detailview.go
T
Evan Hosinski 2b6c4eb4cd Implement TUI for managing suspicious artifacts (FilePicker, TypePicker, ListView, and DetailView)
Introduce Bubble Tea-based terminal UI to manage suspicious artifact findings, including file selection, type filtering, list view, and details.
2025-10-10 22:43:47 -04:00

97 lines
3.4 KiB
Go

package tui
import (
"fmt"
"rmm-hunter/internal/suspicious"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
// RequestEliminateMsg is emitted by the detail view when '!' is pressed
type RequestEliminateMsg struct {
TypeKey string
Index int
}
// DeletedMsg is emitted after successful elimination to update lists
type DeletedMsg struct {
TypeKey string
Index int
}
type DetailViewModel struct {
typeKey string
index int
data suspicious.Suspicious
// When modalErr != "", show modal and require ESC to dismiss
modalErr string
}
func NewDetailView(typeKey string, index int, data suspicious.Suspicious) DetailViewModel {
return DetailViewModel{typeKey: typeKey, index: index, data: data}
}
func (m DetailViewModel) Init() tea.Cmd { return nil }
func (m DetailViewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch v := msg.(type) {
case tea.KeyMsg:
if m.modalErr != "" {
// Modal active: only ESC dismisses
if v.String() == "esc" {
m.modalErr = ""
}
return m, nil
}
switch v.String() {
case "left":
return m, func() tea.Msg { return BackMsg{} }
case "!":
return m, func() tea.Msg { return RequestEliminateMsg{TypeKey: m.typeKey, Index: m.index} }
case "q", "esc", "ctrl+c":
return m, tea.Quit
}
}
return m, nil
}
func (m DetailViewModel) View() string {
title := lipgloss.NewStyle().Bold(true).Render("Details — press ! to eliminate, Left to go back, q to quit")
body := m.renderDetails()
view := title + "\n\n" + body
if m.modalErr != "" {
modal := lipgloss.NewStyle().Padding(1, 2).Foreground(lipgloss.Color("203")).Border(lipgloss.RoundedBorder()).Render("Elimination failed:\n" + m.modalErr + "\n\nPress ESC to dismiss")
view += "\n\n" + modal
}
return view
}
func (m DetailViewModel) renderDetails() string {
switch m.typeKey {
case "autoruns":
ar := m.data.AutoRuns[m.index]
return fmt.Sprintf("Name: %s\nCommand: %s\nLocation: %s\nEnabled: %v\nDescription: %s", ar.Name, ar.Command, ar.Location, ar.Enabled, ar.Description)
case "binaries":
b := m.data.Binaries[m.index]
return fmt.Sprintf("Binary: %s\nAction: delete file", b)
case "connections":
c := m.data.OutboundConnections[m.index]
return fmt.Sprintf("Local: %s\nRemote: %s\nHost: %s\nState: %s\nPID: %s\nProcess: %s\nAction: add firewall block (placeholder)", c.LocalAddr, c.RemoteAddr, c.RemoteHost, c.State, c.PID, c.Process)
case "directories":
d := m.data.Directories[m.index]
return fmt.Sprintf("Directory: %s\nAction: delete recursively", d)
case "processes":
p := m.data.Processes[m.index]
return fmt.Sprintf("Name: %s\nPID: %d\nPPID: %d\nParent: %s\nArgs: %s\nCreated: %s\nPath: %s\nAction: stop then delete (placeholder)", p.Name, p.PID, p.PPID, p.Parent, p.Args, p.Created, p.Path)
case "scheduledTasks":
t := m.data.ScheduledTasks[m.index]
return fmt.Sprintf("Name: %s\nAuthor: %s\nState: %s\nEnabled: %v\nLastResult: %s\nNextRun: %s\nLastRun: %s\nPath: %s\nAction: disable then delete (placeholder)", t.Name, t.Author, t.State, t.Enabled, t.LastResult, t.NextRun, t.LastRun, t.Path)
case "services":
s := m.data.Services[m.index]
return fmt.Sprintf("Name: %s\nDisplay: %s\nType: %s\nStartType: %s\nBinPath: %s\nStartName: %s\nDescription: %s\nAction: stop then delete (placeholder)", s.Name, s.DisplayName, s.ServiceType, s.StartType, s.BinaryPathName, s.ServiceStartName, s.Description)
}
return ""
}