Files
RMM-Hunter/internal/tui/actions.go
T
Evan Hosinski 192ce28d89 Add warning modal support and checks for blocked binaries and directories
Introduce `WarnBlock` to handle non-fatal warnings displayed in a warning modal. Add pre-elimination checks to identify blocked binaries and directories based on running processes or enabled services. Enhance path normalization for robust comparisons.
2025-10-10 22:53:20 -04:00

124 lines
4.4 KiB
Go

package tui
import (
"fmt"
"path/filepath"
"rmm-hunter/internal/suspicious"
"strings"
)
// WarnBlock is a non-fatal warning condition (rendered as a warning modal)
type WarnBlock struct{ Reason string }
func (w WarnBlock) Error() string { return w.Reason }
// normalize a Windows-like path for robust comparisons
func normPath(p string) string {
p = strings.TrimSpace(p)
p = strings.Trim(p, "\"") // strip surrounding quotes if any
p = strings.ReplaceAll(p, "\\", "/")
return strings.ToLower(p)
}
// extract the executable path from a command/BinaryPathName that may include quotes/args
func exeFromCommand(cmd string) string {
s := strings.TrimSpace(cmd)
if s == "" {
return s
}
if strings.HasPrefix(s, "\"") {
s = s[1:]
if i := strings.Index(s, "\""); i >= 0 {
return s[:i]
}
return s
}
// no quotes; split on space
if i := strings.IndexAny(s, " \t"); i >= 0 {
return s[:i]
}
return s
}
// CheckBinaryBlocked returns a WarnBlock if the path is in use by an active process or enabled+active service
func CheckBinaryBlocked(path string, data suspicious.Suspicious) error {
np := normPath(path)
// active process: listed in data.Processes
for _, p := range data.Processes {
if normPath(p.Path) == np {
return WarnBlock{Reason: fmt.Sprintf("Binary in use by running process %s (PID %d). Eliminate the process first.", p.Name, p.PID)}
}
}
// enabled+active service: service uses this binary AND a running process exists for it
for _, s := range data.Services {
sp := normPath(exeFromCommand(s.BinaryPathName))
if sp == np && !strings.EqualFold(strings.TrimSpace(s.StartType), "disabled") {
// Is it active? infer by checking matching running process
for _, p := range data.Processes {
if normPath(p.Path) == sp {
return WarnBlock{Reason: fmt.Sprintf("Binary used by active and enabled service %s. Stop/delete the service first.", s.Name)}
}
}
}
}
return nil
}
// CheckDirectoryBlocked returns a WarnBlock if any process or enabled+active service binary is inside the directory
func CheckDirectoryBlocked(dir string, data suspicious.Suspicious) error {
dn := normPath(dir)
if !strings.HasSuffix(dn, "/") {
dn += "/"
}
inDir := func(p string) bool {
pp := normPath(p)
if pp == "" {
return false
}
if strings.HasPrefix(pp, dn) {
return true
}
// try with filepath.Rel for robustness
rel, err := filepath.Rel(dn, pp)
return err == nil && rel != ".." && !strings.HasPrefix(rel, "../")
}
for _, p := range data.Processes {
if inDir(p.Path) {
return WarnBlock{Reason: fmt.Sprintf("Directory contains running process %s (PID %d). Eliminate the process first.", p.Name, p.PID)}
}
}
for _, s := range data.Services {
sp := exeFromCommand(s.BinaryPathName)
if inDir(sp) && !strings.EqualFold(strings.TrimSpace(s.StartType), "disabled") {
// infer active via running process
for _, p := range data.Processes {
if normPath(p.Path) == normPath(sp) {
return WarnBlock{Reason: fmt.Sprintf("Directory contains active and enabled service binary for %s. Stop/delete the service first.", s.Name)}
}
}
}
}
return nil
}
// Elimination placeholders; TODO: replace with internal/pkg/hunt/eliminate/*
var (
EliminateAutoRun = func(ar suspicious.AutoRun) error { return eliminateAutoRun(ar) }
EliminateBinary = func(path string) error { return eliminateBinary(path) }
EliminateConnection = func(conn suspicious.NetworkConnection) error { return eliminateConnection(conn) }
EliminateDirectory = func(path string) error { return eliminateDirectory(path) }
EliminateProcess = func(p suspicious.Process) error { return eliminateProcess(p) }
EliminateScheduledTask = func(t suspicious.ScheduledTask) error { return eliminateScheduledTask(t) }
EliminateService = func(s suspicious.Service) error { return eliminateService(s) }
)
func eliminateAutoRun(ar suspicious.AutoRun) error {
return fmt.Errorf("eliminate autorun not implemented")
}
func eliminateBinary(path string) error { return nil }
func eliminateConnection(conn suspicious.NetworkConnection) error { return nil }
func eliminateDirectory(path string) error { return nil }
func eliminateProcess(p suspicious.Process) error { return nil }
func eliminateScheduledTask(t suspicious.ScheduledTask) error { return nil }
func eliminateService(s suspicious.Service) error { return nil }