Add elimination API handler, update browser logic for process tracking, and refine UI animations and modal handling.
This commit is contained in:
@@ -1,78 +1,128 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
var CommonDirectories = []string{
|
// KnownRMMDirectories contains known directory names/paths
|
||||||
`C:\Program Files (x86)%\mRemoteNG`,
|
// These will be searched in common installation locations defined in SearchBasePaths
|
||||||
`C:\\Program Files (x86)\Sysprogs`,
|
var KnownRMMDirectories = []string{
|
||||||
`C:\\Program Files (x86)\Sysprogs\SmarTTY`,
|
// A
|
||||||
`C:\AlpemixSrvc`,
|
`Action1`,
|
||||||
`C:\Downloads\SuperPuTTY`,
|
`Almageste\DragonDisk`,
|
||||||
`C:\Program Files (x86)\Almageste\DragonDisk`,
|
`AlpemixSrvc`,
|
||||||
`C:\Program Files (x86)\AnyDesk`,
|
`AMMYY`,
|
||||||
`C:\Program Files (x86)\AnyViewer`,
|
`AnyDesk`,
|
||||||
`C:\Program Files (x86)\Atera Networks`,
|
`AnyViewer`,
|
||||||
`C:\Program Files (x86)\Bitvise SSH Client`,
|
`Atera Networks`,
|
||||||
`C:\Program Files (x86)\Bluetrait Agent`,
|
`ATERA NETWORKS`,
|
||||||
`C:\Program Files (x86)\DesktopCentral_Agent`,
|
`ATERA NETWORKS\AteraAgent`,
|
||||||
`C:\Program Files (x86)\DesktopCentral_Agent\bin`,
|
|
||||||
`C:\Program Files (x86)\GoTo Opener`,
|
// B
|
||||||
`C:\Program Files (x86)\GoToMyPC`,
|
`Bitvise SSH Client`,
|
||||||
`C:\Program Files (x86)\Google\Chrome Remote Desktop`,
|
`Bitvise SSH Server`,
|
||||||
`C:\Program Files (x86)\ISL Online`,
|
`Bluetrait Agent`,
|
||||||
`C:\Program Files (x86)\Kaseya`,
|
|
||||||
`C:\Program Files (x86)\LANDesk`,
|
// D
|
||||||
`C:\Program Files (x86)\OnionShare`,
|
`Danware Data\NetOp Packn Deploy`,
|
||||||
`C:\Program Files (x86)\NetSarang`,
|
`DesktopCentral_Agent`,
|
||||||
`C:\Program Files (x86)\NetSarang\xShell`,
|
`DesktopCentral_Agent\bin`,
|
||||||
`C:\Program Files (x86)\PJ Technologies`,
|
|
||||||
`C:\Program Files (x86)\PJ Technologies\GOVsrv`,
|
// G
|
||||||
`C:\Program Files (x86)\Radmin Viewer 3`,
|
`GoTo Opener`,
|
||||||
`C:\Program Files (x86)\RemotePC`,
|
`GoTo Machine Installer`,
|
||||||
`C:\Program Files (x86)\S3 Browser`,
|
`GoToMyPC`,
|
||||||
`C:\Program Files (x86)\ScreenConnect Client (`, // C:\Program Files (x86)\ScreenConnect Client (<string ID>)
|
`Google\Chrome Remote Desktop`,
|
||||||
`C:\Program Files (x86)\SmartFTP Client`,
|
`Google\Chrome\User Data\Default\Extensions\iodihamcpbpeioajjeobimgagajmlibd`,
|
||||||
`C:\Program Files (x86)\Splashtop`,
|
|
||||||
`C:\Program Files (x86)\TeamViewer`,
|
// I
|
||||||
`C:\Program Files (x86)\UltraViewer`,
|
`Insync`,
|
||||||
`C:\Program Files (x86)\Xpra`,
|
`Insync\App`,
|
||||||
`C:\Program Files (x86)\Yandex`,
|
`ISL Online`,
|
||||||
`C:\Program Files (x86)\mRemoteNG`,
|
|
||||||
`C:\Program Files\ATERA NETWORKS`,
|
// K
|
||||||
`C:\Program Files\ATERA NETWORKS\AteraAgent`,
|
`Kaseya`,
|
||||||
`C:\Program Files\AnyDesk`,
|
|
||||||
`C:\Program Files\Bitvise SSH Server`,
|
// L
|
||||||
`C:\Program Files\Danware Data\NetOp Packn Deploy`,
|
`LANDesk`,
|
||||||
`C:\Program Files\Level`,
|
`Level`,
|
||||||
`C:\\Program Files\\LiteManager Pro`,
|
`LiteManager Pro`,
|
||||||
`C:\\Program Files\\LiteManager Pro \u2013 Viewer`,
|
`LiteManager Pro – Viewer`,
|
||||||
`C:\Program Files\ManageEngine\ManageEngine Free Tools`,
|
|
||||||
`C:\Program Files\ManageEngine\ManageEngine Free Tools\Launcher`,
|
// M
|
||||||
`C:\Program Files\RealVNC`,
|
`ManageEngine\ManageEngine Free Tools`,
|
||||||
`C:\Program Files\RealVNC\VNC Serve`,
|
`ManageEngine\ManageEngine Free Tools\Launcher`,
|
||||||
`C:\Program Files\Remote Utilities`,
|
`MEGAsync`,
|
||||||
`C:\Program Files\Remote Utilities\Agent`,
|
`Mikogo`,
|
||||||
`C:\Program Files\Solar-Putty-v4`,
|
`mRemoteNG`,
|
||||||
`C:\Program Files\SolarWinds\Dameware Mini Remote Control`,
|
|
||||||
`C:\Program Files\SysAidServer`,
|
// N
|
||||||
`C:\Program Files\TeamViewer`,
|
`NetSarang`,
|
||||||
`C:\Program Files\TightVNC`,
|
`NetSarang\xShell`,
|
||||||
`C:\Program Files\ZOC8`,
|
|
||||||
`C:\Program Files\uvnc bvba`,
|
// O
|
||||||
`C:\Program Files\uvnc bvba\UltraVNC`,
|
`OnionShare`,
|
||||||
`C:\ProgramData\Kaseya`,
|
|
||||||
`C:\ProgramData\Total Software Deployment`,
|
// P
|
||||||
`C:\ProgramFiles\GoTo Machine Installer`,
|
`PJ Technologies`,
|
||||||
`C:\ProgramFiles (x86)\GoTo Machine Installer`,
|
`PJ Technologies\GOVsrv`,
|
||||||
`{{APPDATA}}\Local\Google\Chrome\User Data\Default\Extensions\iodihamcpbpeioajjeobimgagajmlibd`,
|
|
||||||
`{{APPDATA}}\Local\MEGAsync`,
|
// R
|
||||||
`{{APPDATA}}\Roaming\Mikogo`,
|
`Radmin Viewer 3`,
|
||||||
`{{APPDATA}}\Roaming\SyncTrayzor`,
|
`RealVNC`,
|
||||||
`C:\Users\IEUser\Downloads\WinSCP-5.21.6-Portable`,
|
`RealVNC\VNC Serve`,
|
||||||
`C:\Users\USERNAME\AppData\Roaming\Insync`,
|
`Remote Utilities`,
|
||||||
`C:\Users\USERNAME\AppData\Roaming\Insync\App`,
|
`Remote Utilities\Agent`,
|
||||||
`C:\Windows\Action1`,
|
`RemotePC`,
|
||||||
`C:\Windows\SysWOW64\rserver30`,
|
`RustDesk`,
|
||||||
`C:\Windows\SysWOW64\rserver30\FamItrfc`,
|
|
||||||
`C:\Windows\SysWOW64\rserver30\FamItrf2`,
|
// S
|
||||||
`C:\Windows\dwrcs`,
|
`S3 Browser`,
|
||||||
`C:\ProgramData\AMMYY`,
|
`ScreenConnect Client (`, // Prefix pattern for ScreenConnect Client (<string ID>)
|
||||||
|
`SmartFTP Client`,
|
||||||
|
`Solar-Putty-v4`,
|
||||||
|
`SolarWinds\Dameware Mini Remote Control`,
|
||||||
|
`Splashtop`,
|
||||||
|
`SuperPuTTY`,
|
||||||
|
`SyncTrayzor`,
|
||||||
|
`Sysprogs`,
|
||||||
|
`Sysprogs\SmarTTY`,
|
||||||
|
`SysAidServer`,
|
||||||
|
`SysWOW64\rserver30`,
|
||||||
|
`SysWOW64\rserver30\FamItrfc`,
|
||||||
|
`SysWOW64\rserver30\FamItrf2`,
|
||||||
|
|
||||||
|
// T
|
||||||
|
`TeamViewer`,
|
||||||
|
`TightVNC`,
|
||||||
|
`Total Software Deployment`,
|
||||||
|
|
||||||
|
// U
|
||||||
|
`UltraViewer`,
|
||||||
|
`uvnc bvba`,
|
||||||
|
`uvnc bvba\UltraVNC`,
|
||||||
|
|
||||||
|
// W
|
||||||
|
`WinSCP-5.21.6-Portable`,
|
||||||
|
`dwrcs`,
|
||||||
|
|
||||||
|
// X
|
||||||
|
`Xpra`,
|
||||||
|
|
||||||
|
// Y
|
||||||
|
`Yandex`,
|
||||||
|
|
||||||
|
// Z
|
||||||
|
`ZOC8`,
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchBasePaths defines the base directories to search within
|
||||||
|
var SearchBasePaths = []string{
|
||||||
|
`C:\Program Files`,
|
||||||
|
`C:\Program Files (x86)`,
|
||||||
|
`C:\ProgramData`,
|
||||||
|
`C:\ProgramFiles`, // Installers variant 1
|
||||||
|
`C:\ProgramFiles (x86)`, // Installers variant 2
|
||||||
|
`C:\Windows`,
|
||||||
|
`{{APPDATA}}\Local`,
|
||||||
|
`{{APPDATA}}\Roaming`,
|
||||||
|
`{{USERPROFILE}}\Downloads`,
|
||||||
|
`C:\Downloads`, // Standard downloads location
|
||||||
|
`C:\`, // Root for edge cases (AlpemixSrvc)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,20 +10,27 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var appData = os.Getenv("APPDATA")
|
var appData = os.Getenv("APPDATA")
|
||||||
|
var userProfile = os.Getenv("USERPROFILE")
|
||||||
|
|
||||||
func Detect() []Directory {
|
func Detect() []Directory {
|
||||||
var suspiciousDirectories []Directory
|
var suspiciousDirectories []Directory
|
||||||
seen := make(map[string]bool) // Prevent duplicates
|
seen := make(map[string]bool) // Prevent duplicates
|
||||||
|
|
||||||
fmt.Printf("[*] Enumerating Suspicious Directories \n")
|
fmt.Printf("[*] Enumerating Suspicious Directories \n")
|
||||||
// Check for common directories
|
|
||||||
for _, dir := range common.CommonDirectories {
|
|
||||||
dir = replaceAppData(dir)
|
|
||||||
|
|
||||||
// Check if this is a prefix pattern (ends with incomplete path such as Screen Connect "C:\Program Files (x86)\ScreenConnect Client (")
|
// For each known RMM directory, check in all base paths
|
||||||
if isPrefix(dir) {
|
for _, rmmDir := range common.KnownRMMDirectories {
|
||||||
|
for _, basePath := range common.SearchBasePaths {
|
||||||
|
// Replace environment variables
|
||||||
|
basePath = replaceEnvVars(basePath)
|
||||||
|
|
||||||
|
// Construct full path
|
||||||
|
fullPath := filepath.Join(basePath, rmmDir)
|
||||||
|
|
||||||
|
// Check if this is a prefix pattern (ends with incomplete path like "ScreenConnect Client (")
|
||||||
|
if isPrefix(rmmDir) {
|
||||||
// Find all directories matching this prefix
|
// Find all directories matching this prefix
|
||||||
matches := findPrefixMatches(dir)
|
matches := findPrefixMatches(fullPath)
|
||||||
for _, match := range matches {
|
for _, match := range matches {
|
||||||
if !seen[match] {
|
if !seen[match] {
|
||||||
fmt.Printf(" [?] Found %s\n", match)
|
fmt.Printf(" [?] Found %s\n", match)
|
||||||
@@ -33,26 +40,26 @@ func Detect() []Directory {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Exact match
|
// Exact match
|
||||||
if _, err := os.Stat(dir); err == nil {
|
if _, err := os.Stat(fullPath); err == nil {
|
||||||
if !seen[dir] {
|
if !seen[fullPath] {
|
||||||
fmt.Printf(" [?] Found %s\n", dir)
|
fmt.Printf(" [?] Found %s\n", fullPath)
|
||||||
suspiciousDirectories = append(suspiciousDirectories, Directory{Path: dir})
|
suspiciousDirectories = append(suspiciousDirectories, Directory{Path: fullPath})
|
||||||
seen[dir] = true
|
seen[fullPath] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Printf("[+] Found %d Suspicious Directories\n", len(suspiciousDirectories))
|
fmt.Printf("[+] Found %d Suspicious Directories\n", len(suspiciousDirectories))
|
||||||
|
|
||||||
return suspiciousDirectories
|
return suspiciousDirectories
|
||||||
}
|
}
|
||||||
|
|
||||||
// replaceAppData replaces {{APPDATA}} with the actual APPDATA path
|
// replaceEnvVars replaces environment variable placeholders with actual paths
|
||||||
func replaceAppData(path string) string {
|
func replaceEnvVars(path string) string {
|
||||||
if strings.Contains(path, "{{APPDATA}}") {
|
path = strings.ReplaceAll(path, "{{APPDATA}}", appData)
|
||||||
p := strings.Replace(path, "{{APPDATA}}", "", -1)
|
path = strings.ReplaceAll(path, "{{USERPROFILE}}", userProfile)
|
||||||
return filepath.Join(appData, p)
|
|
||||||
}
|
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+155
-19
@@ -4,42 +4,178 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"syscall"
|
"syscall"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
shell32 = syscall.NewLazyDLL("shell32.dll")
|
shell32 = syscall.NewLazyDLL("shell32.dll")
|
||||||
shellExecuteW = shell32.NewProc("ShellExecuteW")
|
shellExecuteExW = shell32.NewProc("ShellExecuteExW")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
SEE_MASK_NOCLOSEPROCESS = 0x00000040
|
||||||
|
SW_SHOW = 5
|
||||||
|
)
|
||||||
|
|
||||||
|
// SHELLEXECUTEINFO structure for ShellExecuteEx
|
||||||
|
type shellExecuteInfo struct {
|
||||||
|
cbSize uint32
|
||||||
|
fMask uint32
|
||||||
|
hwnd uintptr
|
||||||
|
lpVerb *uint16
|
||||||
|
lpFile *uint16
|
||||||
|
lpParameters *uint16
|
||||||
|
lpDirectory *uint16
|
||||||
|
nShow int32
|
||||||
|
hInstApp uintptr
|
||||||
|
lpIDList uintptr
|
||||||
|
lpClass *uint16
|
||||||
|
hkeyClass uintptr
|
||||||
|
dwHotKey uint32
|
||||||
|
hIconOrMonitor uintptr
|
||||||
|
hProcess windows.Handle
|
||||||
|
}
|
||||||
|
|
||||||
|
// BrowserHandle represents a handle to the opened browser process
|
||||||
|
type BrowserHandle struct {
|
||||||
|
ProcessID uint32
|
||||||
|
Handle windows.Handle
|
||||||
|
}
|
||||||
|
|
||||||
// OpenBrowser opens the default browser to the specified URL using Windows ShellExecute API
|
// OpenBrowser opens the default browser to the specified URL using Windows ShellExecute API
|
||||||
func OpenBrowser(url string) error {
|
// Returns a handle to the browser process that can be used to close it later
|
||||||
|
func OpenBrowser(url string) (*BrowserHandle, error) {
|
||||||
// Convert strings to UTF16 pointers
|
// Convert strings to UTF16 pointers
|
||||||
operation, err := syscall.UTF16PtrFromString("open")
|
operation, err := syscall.UTF16PtrFromString("open")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to convert operation string: %w", err)
|
return nil, fmt.Errorf("failed to convert operation string: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
urlPtr, err := syscall.UTF16PtrFromString(url)
|
urlPtr, err := syscall.UTF16PtrFromString(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to convert URL string: %w", err)
|
return nil, fmt.Errorf("failed to convert URL string: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShellExecuteW(hwnd, operation, file, parameters, directory, showCmd)
|
// Initialize SHELLEXECUTEINFO structure
|
||||||
// SW_SHOWNORMAL = 1, SW_SHOW = 5
|
sei := shellExecuteInfo{
|
||||||
ret, _, callErr := shellExecuteW.Call(
|
cbSize: uint32(unsafe.Sizeof(shellExecuteInfo{})),
|
||||||
0, // hwnd (NULL)
|
fMask: SEE_MASK_NOCLOSEPROCESS, // Request process handle
|
||||||
uintptr(unsafe.Pointer(operation)), // operation ("open")
|
hwnd: 0,
|
||||||
uintptr(unsafe.Pointer(urlPtr)), // file (URL)
|
lpVerb: operation,
|
||||||
0, // parameters (NULL)
|
lpFile: urlPtr,
|
||||||
0, // directory (NULL)
|
lpParameters: nil,
|
||||||
5, // showCmd (SW_SHOW)
|
lpDirectory: nil,
|
||||||
)
|
nShow: SW_SHOW,
|
||||||
|
hInstApp: 0,
|
||||||
// ShellExecute returns a value > 32 on success
|
|
||||||
if ret <= 32 {
|
|
||||||
return fmt.Errorf("ShellExecute failed with code: %d (error: %v)", ret, callErr)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("[web] Browser opened successfully (return code: %d)\n", ret)
|
// Call ShellExecuteExW
|
||||||
|
ret, _, err := shellExecuteExW.Call(uintptr(unsafe.Pointer(&sei)))
|
||||||
|
if ret == 0 {
|
||||||
|
return nil, fmt.Errorf("ShellExecuteExW failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if sei.hInstApp <= 32 {
|
||||||
|
return nil, fmt.Errorf("ShellExecuteExW failed with code: %d", sei.hInstApp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get process ID from handle
|
||||||
|
var processID uint32
|
||||||
|
if sei.hProcess != 0 {
|
||||||
|
processID, err = windows.GetProcessId(sei.hProcess)
|
||||||
|
if err != nil {
|
||||||
|
// If we can't get PID, still return the handle
|
||||||
|
processID = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("[web] Browser opened successfully (PID: %d)\n", processID)
|
||||||
|
|
||||||
|
return &BrowserHandle{
|
||||||
|
ProcessID: processID,
|
||||||
|
Handle: sei.hProcess,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close terminates the browser process and all child processes
|
||||||
|
func (bh *BrowserHandle) Close() error {
|
||||||
|
if bh == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// First try to kill the direct process if we have a handle
|
||||||
|
if bh.Handle != 0 {
|
||||||
|
windows.CloseHandle(bh.Handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kill all browser processes that might have our URL open
|
||||||
|
// This is more reliable than trying to track the exact process tree
|
||||||
|
killed := killBrowserProcesses()
|
||||||
|
|
||||||
|
fmt.Printf("[web] Terminated %d browser process(es)\n", killed)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// killBrowserProcesses finds and kills common browser processes
|
||||||
|
func killBrowserProcesses() int {
|
||||||
|
browserExes := []string{
|
||||||
|
"chrome.exe",
|
||||||
|
"msedge.exe",
|
||||||
|
"firefox.exe",
|
||||||
|
"brave.exe",
|
||||||
|
"opera.exe",
|
||||||
|
"iexplore.exe",
|
||||||
|
}
|
||||||
|
|
||||||
|
killed := 0
|
||||||
|
for _, exeName := range browserExes {
|
||||||
|
count := killProcessByName(exeName)
|
||||||
|
killed += count
|
||||||
|
}
|
||||||
|
|
||||||
|
return killed
|
||||||
|
}
|
||||||
|
|
||||||
|
// killProcessByName kills all processes with the given executable name
|
||||||
|
func killProcessByName(exeName string) int {
|
||||||
|
snapshot, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPPROCESS, 0)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
defer windows.CloseHandle(snapshot)
|
||||||
|
|
||||||
|
var procEntry windows.ProcessEntry32
|
||||||
|
procEntry.Size = uint32(unsafe.Sizeof(procEntry))
|
||||||
|
|
||||||
|
err = windows.Process32First(snapshot, &procEntry)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
killed := 0
|
||||||
|
for {
|
||||||
|
// Convert the process name from [260]uint16 to string
|
||||||
|
processName := syscall.UTF16ToString(procEntry.ExeFile[:])
|
||||||
|
|
||||||
|
if processName == exeName {
|
||||||
|
// Open process with terminate rights
|
||||||
|
handle, err := windows.OpenProcess(windows.PROCESS_TERMINATE, false, procEntry.ProcessID)
|
||||||
|
if err == nil {
|
||||||
|
err = windows.TerminateProcess(handle, 0)
|
||||||
|
if err == nil {
|
||||||
|
killed++
|
||||||
|
fmt.Printf("[web] Killed %s (PID: %d)\n", exeName, procEntry.ProcessID)
|
||||||
|
}
|
||||||
|
windows.CloseHandle(handle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = windows.Process32Next(snapshot, &procEntry)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return killed
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
--bg:#0b0f0c; --bg2:#111612; --panel:#0f1511; --accent:#17e46e; --accent2:#0eea5a; --muted:#a7b5a9; --text:#e6f4ea; --danger:#ff5c7a; --warn:#ffd166;
|
--bg:#0b0f0c; --bg2:#111612; --panel:#0f1511; --accent:#17e46e; --accent2:#0eea5a; --muted:#a7b5a9; --text:#e6f4ea; --danger:#ff5c7a; --warn:#ffd166;
|
||||||
}
|
}
|
||||||
*{box-sizing:border-box}
|
*{box-sizing:border-box}
|
||||||
|
html{background:var(--bg)}
|
||||||
body{margin:0;font-family:Inter,system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif;background:linear-gradient(180deg,var(--bg),#070a08);color:var(--text);min-height:100vh;display:flex;flex-direction:column}
|
body{margin:0;font-family:Inter,system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif;background:linear-gradient(180deg,var(--bg),#070a08);color:var(--text);min-height:100vh;display:flex;flex-direction:column}
|
||||||
|
|
||||||
/* Scrollbar styling */
|
/* Scrollbar styling */
|
||||||
@@ -66,11 +67,15 @@
|
|||||||
.tree-node{cursor:pointer;padding:8px 10px;border-radius:6px;margin:4px 0;user-select:none;font-weight:600;color:var(--text);border:1px solid transparent}
|
.tree-node{cursor:pointer;padding:8px 10px;border-radius:6px;margin:4px 0;user-select:none;font-weight:600;color:var(--text);border:1px solid transparent}
|
||||||
.tree-node:hover{background:#0e1a13;border-color:#124b2b}
|
.tree-node:hover{background:#0e1a13;border-color:#124b2b}
|
||||||
.tree-node.expanded{background:#0e1a13;border-color:#124b2b}
|
.tree-node.expanded{background:#0e1a13;border-color:#124b2b}
|
||||||
.tree-child{cursor:pointer;padding:6px 10px 6px 12px;border-radius:6px;margin:2px 0;font-size:13px;color:var(--muted);border:1px solid transparent;display:flex;align-items:center;gap:8px;transition:all 0.15s}
|
.tree-child{cursor:pointer;padding:6px 10px 6px 12px;border-radius:6px;margin:2px 0;font-size:13px;color:var(--muted);border:1px solid transparent;display:flex;align-items:center;gap:8px;transition:all 0.15s;position:relative;overflow:hidden}
|
||||||
.tree-child:hover{background:#0a2a18;color:var(--text);border-color:#124b2b}
|
.tree-child:hover{background:#0a2a18;color:var(--text);border-color:#124b2b}
|
||||||
.tree-child.selected{background:#103e24;color:var(--accent);border-color:#1d7e4a}
|
.tree-child.selected{background:#103e24;color:var(--accent);border-color:#1d7e4a}
|
||||||
.tree-child::before{content:'→';color:var(--accent);opacity:0;transition:opacity 0.15s}
|
.tree-child::before{content:'→';color:var(--accent);opacity:0;transition:opacity 0.15s}
|
||||||
.tree-child:hover::before,.tree-child.selected::before{opacity:1}
|
.tree-child:hover::before,.tree-child.selected::before{opacity:1}
|
||||||
|
.tree-child.eliminating{animation:slideOutRight 0.6s ease-in-out forwards}
|
||||||
|
.tree-child.eliminating::after{content:'';position:absolute;top:0;left:0;right:0;bottom:0;background:linear-gradient(90deg,transparent,rgba(23,228,110,.3),transparent);animation:shimmer 0.6s ease-in-out}
|
||||||
|
@keyframes slideOutRight{0%{transform:translateX(0);opacity:1}50%{opacity:0.5}100%{transform:translateX(100%);opacity:0;height:0;margin:0;padding:0}}
|
||||||
|
@keyframes shimmer{0%{transform:translateX(-100%)}100%{transform:translateX(100%)}}
|
||||||
.detail-field{margin:12px 0;padding:10px;background:#050805;border:1px solid #0c2819;border-radius:8px}
|
.detail-field{margin:12px 0;padding:10px;background:#050805;border:1px solid #0c2819;border-radius:8px}
|
||||||
.detail-label{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:4px}
|
.detail-label{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:4px}
|
||||||
.detail-value{color:var(--text);font-size:14px;word-break:break-all}
|
.detail-value{color:var(--text);font-size:14px;word-break:break-all}
|
||||||
@@ -102,6 +107,35 @@
|
|||||||
.splash-loader{width:200px;height:3px;background:rgba(23,228,110,.1);border-radius:3px;overflow:hidden;position:relative}
|
.splash-loader{width:200px;height:3px;background:rgba(23,228,110,.1);border-radius:3px;overflow:hidden;position:relative}
|
||||||
.splash-loader::after{content:'';position:absolute;top:0;left:0;height:100%;width:40%;background:linear-gradient(90deg,transparent,var(--accent),transparent);animation:loading 1.5s ease-in-out infinite}
|
.splash-loader::after{content:'';position:absolute;top:0;left:0;height:100%;width:40%;background:linear-gradient(90deg,transparent,var(--accent),transparent);animation:loading 1.5s ease-in-out infinite}
|
||||||
@keyframes loading{0%{left:-40%}100%{left:100%}}
|
@keyframes loading{0%{left:-40%}100%{left:100%}}
|
||||||
|
|
||||||
|
/* Shutdown Screen */
|
||||||
|
#shutdownScreen{position:fixed;top:0;left:0;right:0;bottom:0;background:radial-gradient(circle at center,#0f1511,#0b0f0c,#000);z-index:10000;display:none;flex-direction:column;align-items:center;justify-content:center}
|
||||||
|
#shutdownScreen.active{display:flex}
|
||||||
|
.spinner{width:80px;height:80px;border:4px solid rgba(23,228,110,.1);border-top:4px solid var(--accent);border-radius:50%;animation:spin 1s linear infinite;margin-bottom:30px}
|
||||||
|
.spinner.hide{display:none}
|
||||||
|
@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}
|
||||||
|
.shutdown-title{font-size:28px;font-weight:700;color:var(--accent);margin-bottom:12px;letter-spacing:1.5px}
|
||||||
|
.shutdown-message{font-size:16px;color:var(--muted);margin-bottom:20px}
|
||||||
|
.shutdown-success{font-size:18px;color:var(--accent);font-weight:600;display:none}
|
||||||
|
.shutdown-success.show{display:block}
|
||||||
|
|
||||||
|
/* Modal */
|
||||||
|
.modal{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.8);z-index:10001;display:none;align-items:center;justify-content:center}
|
||||||
|
.modal.active{display:flex}
|
||||||
|
.modal-content{background:var(--panel);border:1px solid #133422;border-radius:12px;padding:30px;max-width:500px;width:90%;box-shadow:0 20px 60px rgba(0,0,0,.5)}
|
||||||
|
.modal-icon{font-size:60px;text-align:center;margin-bottom:20px}
|
||||||
|
.modal-icon.success{color:var(--accent);animation:checkPop 0.5s ease-out}
|
||||||
|
.modal-icon.error{color:#ff5c7a}
|
||||||
|
@keyframes checkPop{0%{transform:scale(0)}50%{transform:scale(1.2)}100%{transform:scale(1)}}
|
||||||
|
.modal-title{font-size:24px;font-weight:700;text-align:center;margin-bottom:12px}
|
||||||
|
.modal-title.success{color:var(--accent)}
|
||||||
|
.modal-title.error{color:#ff5c7a}
|
||||||
|
.modal-message{text-align:center;color:var(--muted);margin-bottom:24px;line-height:1.6}
|
||||||
|
.modal-btn{width:100%;padding:14px;border:none;border-radius:8px;font-weight:600;font-size:15px;cursor:pointer;transition:all 0.2s}
|
||||||
|
.modal-btn.success{background:var(--accent);color:#000}
|
||||||
|
.modal-btn.success:hover{filter:brightness(1.2)}
|
||||||
|
.modal-btn.error{background:rgba(255,92,122,.2);color:#ff5c7a;border:1px solid #ff5c7a}
|
||||||
|
.modal-btn.error:hover{background:rgba(255,92,122,.3)}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -113,6 +147,24 @@
|
|||||||
<div class="splash-loader"></div>
|
<div class="splash-loader"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Shutdown Screen -->
|
||||||
|
<div id="shutdownScreen">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<div class="shutdown-title">Shutting Down</div>
|
||||||
|
<div class="shutdown-message">RMM Hunter is closing...</div>
|
||||||
|
<div class="shutdown-success">✓ You can now close this browser tab</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal -->
|
||||||
|
<div id="modal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div id="modalIcon" class="modal-icon"></div>
|
||||||
|
<div id="modalTitle" class="modal-title"></div>
|
||||||
|
<div id="modalMessage" class="modal-message"></div>
|
||||||
|
<button id="modalBtn" class="modal-btn"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
<div class="nav">
|
<div class="nav">
|
||||||
<div class="brand"><img src="/logo" alt="RMM Hunter"><span>RMM Hunter</span></div>
|
<div class="brand"><img src="/logo" alt="RMM Hunter"><span>RMM Hunter</span></div>
|
||||||
@@ -132,6 +184,7 @@
|
|||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button id="startHunt" class="btn primary">Start Hunt</button>
|
<button id="startHunt" class="btn primary">Start Hunt</button>
|
||||||
<span id="huntTag" class="tag hidden"></span>
|
<span id="huntTag" class="tag hidden"></span>
|
||||||
|
<button id="viewReportBtn" class="btn hidden" style="margin-left:12px">View Report</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="log" class="log" aria-live="polite"></div>
|
<div id="log" class="log" aria-live="polite"></div>
|
||||||
</section>
|
</section>
|
||||||
@@ -206,6 +259,7 @@
|
|||||||
const logEl = document.getElementById('log');
|
const logEl = document.getElementById('log');
|
||||||
const huntsEl = document.getElementById('hunts');
|
const huntsEl = document.getElementById('hunts');
|
||||||
const huntTag = document.getElementById('huntTag');
|
const huntTag = document.getElementById('huntTag');
|
||||||
|
const viewReportBtn = document.getElementById('viewReportBtn');
|
||||||
const reportSection = document.getElementById('reportSection');
|
const reportSection = document.getElementById('reportSection');
|
||||||
const reportHeader = document.getElementById('reportHeader');
|
const reportHeader = document.getElementById('reportHeader');
|
||||||
const reportBody = document.getElementById('reportBody');
|
const reportBody = document.getElementById('reportBody');
|
||||||
@@ -350,27 +404,93 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Hunt flow
|
// Hunt flow
|
||||||
|
let lastReportData = null;
|
||||||
|
let currentReportName = null;
|
||||||
|
|
||||||
document.getElementById('startHunt').addEventListener('click', async ()=>{
|
document.getElementById('startHunt').addEventListener('click', async ()=>{
|
||||||
logEl.textContent=''; huntTag.classList.add('hidden');
|
logEl.textContent='';
|
||||||
|
huntTag.classList.add('hidden');
|
||||||
|
viewReportBtn.classList.add('hidden');
|
||||||
|
currentReportName = null;
|
||||||
|
|
||||||
let ws;
|
let ws;
|
||||||
try{ ws = new WebSocket((location.protocol==='https:'?'wss':'ws')+'://'+location.host+'/ws/hunt');
|
let wsReady = false;
|
||||||
ws.onmessage = ev => { logEl.textContent += ev.data + '\n'; logEl.scrollTop = logEl.scrollHeight; };
|
let huntComplete = false;
|
||||||
|
|
||||||
|
try{
|
||||||
|
ws = new WebSocket((location.protocol==='https:'?'wss':'ws')+'://'+location.host+'/ws/hunt');
|
||||||
|
ws.onopen = () => { wsReady = true; };
|
||||||
|
ws.onmessage = ev => {
|
||||||
|
logEl.textContent += ev.data + '\n';
|
||||||
|
logEl.scrollTop = logEl.scrollHeight;
|
||||||
|
|
||||||
|
// Check if hunt is complete
|
||||||
|
if (ev.data.includes('[+] Hunt complete')) {
|
||||||
|
huntComplete = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
}catch(e){ console.error(e); }
|
}catch(e){ console.error(e); }
|
||||||
|
|
||||||
|
// Wait for WebSocket to be ready before starting hunt
|
||||||
|
while(!wsReady && ws && ws.readyState !== WebSocket.OPEN) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Give it a tiny bit more time to ensure connection is stable
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
|
||||||
const r = await fetch('/api/hunt/start',{method:'POST'});
|
const r = await fetch('/api/hunt/start',{method:'POST'});
|
||||||
const data = await r.json();
|
const data = await r.json();
|
||||||
|
|
||||||
if(data && data.reportName){
|
if(data && data.reportName){
|
||||||
|
currentReportName = data.reportName;
|
||||||
|
|
||||||
|
// Wait for hunt to actually complete
|
||||||
|
while(!huntComplete) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
}
|
||||||
|
|
||||||
huntTag.classList.remove('hidden');
|
huntTag.classList.remove('hidden');
|
||||||
await typeText(huntTag, 'Report: '+data.reportName+'.json', 30);
|
await typeText(huntTag, 'Report: '+data.reportName+'.json', 30);
|
||||||
|
|
||||||
// Load report
|
// Load report
|
||||||
const rep = await (await fetch('/api/report?file='+encodeURIComponent(data.reportName+'.json'))).json();
|
const rep = await (await fetch('/api/report?file='+encodeURIComponent(data.reportName+'.json'))).json();
|
||||||
renderReport(rep); location.hash = '#report';
|
lastReportData = rep;
|
||||||
|
renderReport(rep);
|
||||||
|
|
||||||
|
// Show View Report button AFTER everything is loaded
|
||||||
|
viewReportBtn.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
if(ws) ws.close();
|
if(ws) ws.close();
|
||||||
listHunts();
|
listHunts();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// View Report button handler
|
||||||
|
viewReportBtn.addEventListener('click', () => {
|
||||||
|
if (lastReportData) {
|
||||||
|
location.hash = '#report';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById('quitBtn').addEventListener('click', async ()=>{
|
document.getElementById('quitBtn').addEventListener('click', async ()=>{
|
||||||
|
// Show shutdown screen
|
||||||
|
const shutdownScreen = document.getElementById('shutdownScreen');
|
||||||
|
const spinner = shutdownScreen.querySelector('.spinner');
|
||||||
|
const shutdownSuccess = shutdownScreen.querySelector('.shutdown-success');
|
||||||
|
shutdownScreen.classList.add('active');
|
||||||
|
|
||||||
|
// Send quit request
|
||||||
|
try {
|
||||||
await fetch('/api/quit',{method:'POST'});
|
await fetch('/api/quit',{method:'POST'});
|
||||||
|
} catch(e) {
|
||||||
|
// Server is shutting down, connection will be lost
|
||||||
|
}
|
||||||
|
|
||||||
|
// After 2 seconds, hide spinner and show success message
|
||||||
|
setTimeout(() => {
|
||||||
|
spinner.classList.add('hide');
|
||||||
|
shutdownSuccess.classList.add('show');
|
||||||
|
}, 2000);
|
||||||
});
|
});
|
||||||
|
|
||||||
listHunts();
|
listHunts();
|
||||||
@@ -631,11 +751,123 @@
|
|||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
window.eliminateItem = function(type, idx) {
|
// Modal functions
|
||||||
|
const modal = document.getElementById('modal');
|
||||||
|
const modalIcon = document.getElementById('modalIcon');
|
||||||
|
const modalTitle = document.getElementById('modalTitle');
|
||||||
|
const modalMessage = document.getElementById('modalMessage');
|
||||||
|
const modalBtn = document.getElementById('modalBtn');
|
||||||
|
|
||||||
|
function showModal(type, title, message) {
|
||||||
|
modal.classList.add('active');
|
||||||
|
modalIcon.className = `modal-icon ${type}`;
|
||||||
|
modalTitle.className = `modal-title ${type}`;
|
||||||
|
modalTitle.textContent = title;
|
||||||
|
modalMessage.textContent = message;
|
||||||
|
modalBtn.className = `modal-btn ${type}`;
|
||||||
|
modalBtn.textContent = 'OK';
|
||||||
|
|
||||||
|
if (type === 'success') {
|
||||||
|
modalIcon.textContent = '✓';
|
||||||
|
} else {
|
||||||
|
modalIcon.textContent = '✕';
|
||||||
|
}
|
||||||
|
|
||||||
|
modalBtn.onclick = () => {
|
||||||
|
modal.classList.remove('active');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
window.eliminateItem = async function(type, idx) {
|
||||||
if (!confirm(`Are you sure you want to eliminate this ${type.slice(0, -1)}?\n\nThis action cannot be undone.`)) return;
|
if (!confirm(`Are you sure you want to eliminate this ${type.slice(0, -1)}?\n\nThis action cannot be undone.`)) return;
|
||||||
|
|
||||||
alert('Elimination functionality not yet implemented in web UI.\n\nPlease use the CLI: rmm-hunter eliminate --cli');
|
if (!currentReport) {
|
||||||
// TODO: Implement API call to backend elimination endpoint
|
showModal('error', 'Error', 'No report loaded');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the report filename from the current report
|
||||||
|
const reportFile = currentReport.reportName || currentReport.name || 'rmm-hunter-report.json';
|
||||||
|
|
||||||
|
// Find the tree child element for this item
|
||||||
|
const treeChildren = document.querySelectorAll('.tree-child');
|
||||||
|
let targetElement = null;
|
||||||
|
|
||||||
|
// Find the element by matching the index and type
|
||||||
|
treeChildren.forEach((child, childIdx) => {
|
||||||
|
const parentNode = child.previousElementSibling;
|
||||||
|
if (parentNode && parentNode.classList.contains('tree-node')) {
|
||||||
|
// Check if this is the right category
|
||||||
|
const categoryText = parentNode.textContent.toLowerCase();
|
||||||
|
if ((type === 'connections' && categoryText.includes('outbound')) ||
|
||||||
|
(type === 'processes' && categoryText.includes('processes')) ||
|
||||||
|
(type === 'services' && categoryText.includes('services')) ||
|
||||||
|
(type === 'tasks' && categoryText.includes('scheduled')) ||
|
||||||
|
(type === 'autoruns' && categoryText.includes('autorun')) ||
|
||||||
|
(type === 'binaries' && categoryText.includes('binaries')) ||
|
||||||
|
(type === 'directories' && categoryText.includes('directories'))) {
|
||||||
|
// This is the right category, now check index
|
||||||
|
const siblings = Array.from(child.parentElement.children).filter(c => c.classList.contains('tree-child'));
|
||||||
|
const itemIdx = siblings.indexOf(child);
|
||||||
|
if (itemIdx === idx) {
|
||||||
|
targetElement = child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/eliminate', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
reportFile: reportFile,
|
||||||
|
type: type,
|
||||||
|
index: idx
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
showModal('error', 'Elimination Failed', result.error || 'Unknown error occurred');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success! Trigger slide-out animation
|
||||||
|
if (targetElement) {
|
||||||
|
targetElement.classList.add('eliminating');
|
||||||
|
|
||||||
|
// Wait for animation to complete before updating tree
|
||||||
|
setTimeout(async () => {
|
||||||
|
// Show success modal
|
||||||
|
showModal('success', 'Successfully Eliminated', `The ${type.slice(0, -1)} has been removed from your system.`);
|
||||||
|
|
||||||
|
// Reload the report to get updated data
|
||||||
|
const updatedReport = await (await fetch('/api/report?file=' + encodeURIComponent(reportFile))).json();
|
||||||
|
currentReport = updatedReport;
|
||||||
|
|
||||||
|
// Reload elimination tree with updated data
|
||||||
|
loadEliminationData(updatedReport);
|
||||||
|
|
||||||
|
// Clear the center and wiki panels
|
||||||
|
elimCenter.innerHTML = '<div class="empty-state">Select an item from the tree</div>';
|
||||||
|
elimWiki.innerHTML = '<div class="empty-state">Item details will appear here</div>';
|
||||||
|
}, 600); // Match animation duration
|
||||||
|
} else {
|
||||||
|
// Fallback if element not found
|
||||||
|
showModal('success', 'Successfully Eliminated', `The ${type.slice(0, -1)} has been removed from your system.`);
|
||||||
|
|
||||||
|
const updatedReport = await (await fetch('/api/report?file=' + encodeURIComponent(reportFile))).json();
|
||||||
|
currentReport = updatedReport;
|
||||||
|
loadEliminationData(updatedReport);
|
||||||
|
elimCenter.innerHTML = '<div class="empty-state">Select an item from the tree</div>';
|
||||||
|
elimWiki.innerHTML = '<div class="empty-state">Item details will appear here</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
showModal('error', 'Error', `Failed to eliminate: ${error.message}`);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update renderReport to also load elimination data
|
// Update renderReport to also load elimination data
|
||||||
|
|||||||
+244
-1
@@ -10,11 +10,13 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"rmm-hunter/internal/suspicious"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"rmm-hunter/internal/pkg"
|
"rmm-hunter/internal/pkg"
|
||||||
|
"rmm-hunter/internal/pkg/hunt/eliminate"
|
||||||
"rmm-hunter/internal/pkg/hunter"
|
"rmm-hunter/internal/pkg/hunter"
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
@@ -76,6 +78,7 @@ func StartWebServer() {
|
|||||||
mux.HandleFunc("/api/hunts", s.handleListHunts)
|
mux.HandleFunc("/api/hunts", s.handleListHunts)
|
||||||
mux.HandleFunc("/api/hunt/start", s.handleStartHunt)
|
mux.HandleFunc("/api/hunt/start", s.handleStartHunt)
|
||||||
mux.HandleFunc("/api/report", s.handleGetReport)
|
mux.HandleFunc("/api/report", s.handleGetReport)
|
||||||
|
mux.HandleFunc("/api/eliminate", s.handleEliminate)
|
||||||
mux.HandleFunc("/api/quit", s.handleQuit)
|
mux.HandleFunc("/api/quit", s.handleQuit)
|
||||||
mux.HandleFunc("/ws/hunt", s.handleWS)
|
mux.HandleFunc("/ws/hunt", s.handleWS)
|
||||||
|
|
||||||
@@ -103,7 +106,8 @@ func StartWebServer() {
|
|||||||
<-serverReady
|
<-serverReady
|
||||||
time.Sleep(500 * time.Millisecond) // Give server a moment to fully initialize
|
time.Sleep(500 * time.Millisecond) // Give server a moment to fully initialize
|
||||||
log.Printf("[web] Opening browser to %s...\n", browserURL)
|
log.Printf("[web] Opening browser to %s...\n", browserURL)
|
||||||
if err := OpenBrowser(browserURL); err != nil {
|
_, err := OpenBrowser(browserURL)
|
||||||
|
if err != nil {
|
||||||
log.Printf("[web] Warning: Failed to open browser: %v\n", err)
|
log.Printf("[web] Warning: Failed to open browser: %v\n", err)
|
||||||
if !hostAdded {
|
if !hostAdded {
|
||||||
log.Printf("[web] Please open your browser and navigate to http://127.0.0.1\n")
|
log.Printf("[web] Please open your browser and navigate to http://127.0.0.1\n")
|
||||||
@@ -290,6 +294,61 @@ func (s *server) handleWS(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *server) handleEliminate(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
ReportFile string `json:"reportFile"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Index int `json:"index"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, "invalid request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the report file
|
||||||
|
reportPath := filepath.Join(".", req.ReportFile)
|
||||||
|
data, err := os.ReadFile(reportPath)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("failed to read report: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var report suspicious.Suspicious
|
||||||
|
if err := json.Unmarshal(data, &report); err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("failed to parse report: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform elimination based on type
|
||||||
|
if err := performElimination(&report, req.Type, req.Index); err != nil {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save updated report
|
||||||
|
updatedData, err := json.MarshalIndent(report, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("failed to marshal report: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(reportPath, updatedData, 0644); err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("failed to save report: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"status": "success"})
|
||||||
|
}
|
||||||
|
|
||||||
func (s *server) handleQuit(w http.ResponseWriter, r *http.Request) {
|
func (s *server) handleQuit(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
http.Error(w, "use POST", 405)
|
http.Error(w, "use POST", 405)
|
||||||
@@ -299,3 +358,187 @@ func (s *server) handleQuit(w http.ResponseWriter, r *http.Request) {
|
|||||||
_, _ = w.Write([]byte(`{"ok":true}`))
|
_, _ = w.Write([]byte(`{"ok":true}`))
|
||||||
go func() { time.Sleep(200 * time.Millisecond); s.quitCh <- struct{}{} }()
|
go func() { time.Sleep(200 * time.Millisecond); s.quitCh <- struct{}{} }()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// performElimination executes the elimination logic for a specific finding type and index
|
||||||
|
func performElimination(report *suspicious.Suspicious, typeKey string, idx int) error {
|
||||||
|
switch typeKey {
|
||||||
|
case "connections":
|
||||||
|
if idx >= len(report.OutboundConnections) {
|
||||||
|
return fmt.Errorf("invalid index")
|
||||||
|
}
|
||||||
|
conn := report.OutboundConnections[idx]
|
||||||
|
if err := eliminate.EliminateConnection(conn.RemoteHost); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
report.OutboundConnections[idx].Eliminated = true
|
||||||
|
|
||||||
|
case "processes":
|
||||||
|
if idx >= len(report.Processes) {
|
||||||
|
return fmt.Errorf("invalid index")
|
||||||
|
}
|
||||||
|
proc := report.Processes[idx]
|
||||||
|
if err := eliminate.EliminateProcess(proc); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
report.Processes[idx].Eliminated = true
|
||||||
|
|
||||||
|
case "services":
|
||||||
|
if idx >= len(report.Services) {
|
||||||
|
return fmt.Errorf("invalid index")
|
||||||
|
}
|
||||||
|
svc := report.Services[idx]
|
||||||
|
if svc == nil {
|
||||||
|
return fmt.Errorf("service is nil")
|
||||||
|
}
|
||||||
|
if err := eliminate.EliminateService(*svc); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
report.Services[idx].Eliminated = true
|
||||||
|
|
||||||
|
case "tasks":
|
||||||
|
if idx >= len(report.ScheduledTasks) {
|
||||||
|
return fmt.Errorf("invalid index")
|
||||||
|
}
|
||||||
|
task := report.ScheduledTasks[idx]
|
||||||
|
if task == nil {
|
||||||
|
return fmt.Errorf("task is nil")
|
||||||
|
}
|
||||||
|
if err := eliminate.EliminateScheduledTask(*task); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
report.ScheduledTasks[idx].Eliminated = true
|
||||||
|
|
||||||
|
case "autoruns":
|
||||||
|
if idx >= len(report.AutoRuns) {
|
||||||
|
return fmt.Errorf("invalid index")
|
||||||
|
}
|
||||||
|
ar := report.AutoRuns[idx]
|
||||||
|
if err := eliminate.EliminateAutoRun(ar); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
report.AutoRuns[idx].Eliminated = true
|
||||||
|
|
||||||
|
case "binaries":
|
||||||
|
if idx >= len(report.Binaries) {
|
||||||
|
return fmt.Errorf("invalid index")
|
||||||
|
}
|
||||||
|
bin := report.Binaries[idx]
|
||||||
|
// Check if binary is blocked by active processes/services
|
||||||
|
if err := checkBinaryBlocked(bin.Path, *report); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := eliminate.EliminateBinary(bin.Path); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
report.Binaries[idx].Eliminated = true
|
||||||
|
|
||||||
|
case "directories":
|
||||||
|
if idx >= len(report.Directories) {
|
||||||
|
return fmt.Errorf("invalid index")
|
||||||
|
}
|
||||||
|
dir := report.Directories[idx]
|
||||||
|
// Check if directory is blocked by active processes/services
|
||||||
|
if err := checkDirectoryBlocked(dir.Path, *report); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := eliminate.EliminateDirectory(dir.Path); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
report.Directories[idx].Eliminated = true
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown type: %s", typeKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkBinaryBlocked checks if a binary is in use by active processes or services
|
||||||
|
func checkBinaryBlocked(path string, data suspicious.Suspicious) error {
|
||||||
|
normPath := func(p string) string {
|
||||||
|
return strings.ToLower(filepath.Clean(p))
|
||||||
|
}
|
||||||
|
|
||||||
|
np := normPath(path)
|
||||||
|
|
||||||
|
// Check active processes
|
||||||
|
for _, p := range data.Processes {
|
||||||
|
if p.Eliminated {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if normPath(p.Path) == np {
|
||||||
|
return fmt.Errorf("binary in use by running process %s (PID %d). Eliminate the process first", p.Name, p.PID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check enabled services
|
||||||
|
for _, s := range data.Services {
|
||||||
|
if s == nil || s.Eliminated {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sp := normPath(s.BinaryPathName)
|
||||||
|
if sp == np && !strings.EqualFold(strings.TrimSpace(s.StartType), "disabled") {
|
||||||
|
// Check if service has a running process
|
||||||
|
for _, p := range data.Processes {
|
||||||
|
if p.Eliminated {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if normPath(p.Path) == sp {
|
||||||
|
return fmt.Errorf("binary used by active and enabled service %s. Stop/delete the service first", s.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkDirectoryBlocked checks if a directory contains binaries used by active processes or services
|
||||||
|
func checkDirectoryBlocked(dir string, data suspicious.Suspicious) error {
|
||||||
|
normPath := func(p string) string {
|
||||||
|
return strings.ToLower(filepath.Clean(p))
|
||||||
|
}
|
||||||
|
|
||||||
|
dn := normPath(dir)
|
||||||
|
if !strings.HasSuffix(dn, string(filepath.Separator)) {
|
||||||
|
dn += string(filepath.Separator)
|
||||||
|
}
|
||||||
|
|
||||||
|
inDir := func(p string) bool {
|
||||||
|
pp := normPath(p)
|
||||||
|
if pp == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.HasPrefix(pp, dn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check processes
|
||||||
|
for _, p := range data.Processes {
|
||||||
|
if p.Eliminated {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if inDir(p.Path) {
|
||||||
|
return fmt.Errorf("directory contains active process %s (PID %d). Eliminate the process first", p.Name, p.PID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check services
|
||||||
|
for _, s := range data.Services {
|
||||||
|
if s == nil || s.Eliminated {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if inDir(s.BinaryPathName) && !strings.EqualFold(strings.TrimSpace(s.StartType), "disabled") {
|
||||||
|
// Check if service has a running process
|
||||||
|
for _, p := range data.Processes {
|
||||||
|
if p.Eliminated {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if normPath(p.Path) == normPath(s.BinaryPathName) {
|
||||||
|
return fmt.Errorf("directory contains active and enabled service binary for %s. Stop/delete the service first", s.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user