Files
RMM-Hunter/internal/tui/detailview.go
T

139 lines
4.7 KiB
Go
Raw Normal View History

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
eliminated map[string]map[int]bool
// When modal* != "", show modal and require ESC to dismiss
modalErr string
modalWarn string
}
func NewDetailView(typeKey string, index int, data suspicious.Suspicious, eliminated map[string]map[int]bool) DetailViewModel {
return DetailViewModel{typeKey: typeKey, index: index, data: data, eliminated: eliminated}
}
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 != "" || m.modalWarn != "" {
// Modal active: only ESC dismisses
if v.String() == "esc" {
m.modalErr = ""
m.modalWarn = ""
}
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 {
isEliminated := m.eliminated[m.typeKey] != nil && m.eliminated[m.typeKey][m.index]
var title string
if isEliminated {
title = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("10")).Render("Details — ELIMINATED — Left to go back, q to quit")
} else {
title = lipgloss.NewStyle().Bold(true).Render("Details — press ! to eliminate, Left to go back, q to quit")
}
body := m.renderDetails()
// Apply green styling to body if eliminated
if isEliminated {
body = lipgloss.NewStyle().Foreground(lipgloss.Color("10")).Render(body)
}
view := title + "\n\n" + body
if m.modalWarn != "" {
modal := lipgloss.NewStyle().Padding(1, 2).Foreground(lipgloss.Color("214")).Border(lipgloss.RoundedBorder()).Render("Warning:\n" + m.modalWarn + "\n\nPress ESC to dismiss")
view += "\n\n" + modal
}
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":
if m.index >= len(m.data.AutoRuns) {
return "Item no longer available"
}
ar := m.data.AutoRuns[m.index]
return fmt.Sprintf("Type: %s\nEntry: %s\nLaunch: %s\nLocation: %s\nImage: %s\nArgs: %s\nMD5: %s\nSHA1: %s\nSHA256: %s", ar.Type, ar.Entry, ar.LaunchString, ar.Location, ar.ImagePath, ar.Arguments, ar.MD5, ar.SHA1, ar.SHA256)
case "binaries":
if m.index >= len(m.data.Binaries) {
return "Item no longer available"
}
b := m.data.Binaries[m.index]
return fmt.Sprintf("Binary: %s\nAction: delete file", b.Path)
case "connections":
if m.index >= len(m.data.OutboundConnections) {
return "Item no longer available"
}
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", c.LocalAddr, c.RemoteAddr, c.RemoteHost, c.State, c.PID, c.Process)
case "directories":
if m.index >= len(m.data.Directories) {
return "Item no longer available"
}
d := m.data.Directories[m.index]
return fmt.Sprintf("Directory: %s\nAction: delete recursively", d.Path)
case "processes":
if m.index >= len(m.data.Processes) {
return "Item no longer available"
}
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", p.Name, p.PID, p.PPID, p.Parent, p.Args, p.Created, p.Path)
case "scheduledTasks":
if m.index >= len(m.data.ScheduledTasks) {
return "Item no longer available"
}
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", t.Name, t.Author, t.State, t.Enabled, t.LastResult, t.NextRun, t.LastRun, t.Path)
case "services":
if m.index >= len(m.data.Services) {
return "Item no longer available"
}
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", s.Name, s.DisplayName, s.ServiceType, s.StartType, s.BinaryPathName, s.ServiceStartName, s.Description)
}
return ""
}