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
|
||||
|
||||
var CommonDirectories = []string{
|
||||
`C:\Program Files (x86)%\mRemoteNG`,
|
||||
`C:\\Program Files (x86)\Sysprogs`,
|
||||
`C:\\Program Files (x86)\Sysprogs\SmarTTY`,
|
||||
`C:\AlpemixSrvc`,
|
||||
`C:\Downloads\SuperPuTTY`,
|
||||
`C:\Program Files (x86)\Almageste\DragonDisk`,
|
||||
`C:\Program Files (x86)\AnyDesk`,
|
||||
`C:\Program Files (x86)\AnyViewer`,
|
||||
`C:\Program Files (x86)\Atera Networks`,
|
||||
`C:\Program Files (x86)\Bitvise SSH Client`,
|
||||
`C:\Program Files (x86)\Bluetrait Agent`,
|
||||
`C:\Program Files (x86)\DesktopCentral_Agent`,
|
||||
`C:\Program Files (x86)\DesktopCentral_Agent\bin`,
|
||||
`C:\Program Files (x86)\GoTo Opener`,
|
||||
`C:\Program Files (x86)\GoToMyPC`,
|
||||
`C:\Program Files (x86)\Google\Chrome Remote Desktop`,
|
||||
`C:\Program Files (x86)\ISL Online`,
|
||||
`C:\Program Files (x86)\Kaseya`,
|
||||
`C:\Program Files (x86)\LANDesk`,
|
||||
`C:\Program Files (x86)\OnionShare`,
|
||||
`C:\Program Files (x86)\NetSarang`,
|
||||
`C:\Program Files (x86)\NetSarang\xShell`,
|
||||
`C:\Program Files (x86)\PJ Technologies`,
|
||||
`C:\Program Files (x86)\PJ Technologies\GOVsrv`,
|
||||
`C:\Program Files (x86)\Radmin Viewer 3`,
|
||||
`C:\Program Files (x86)\RemotePC`,
|
||||
`C:\Program Files (x86)\S3 Browser`,
|
||||
`C:\Program Files (x86)\ScreenConnect Client (`, // C:\Program Files (x86)\ScreenConnect Client (<string ID>)
|
||||
`C:\Program Files (x86)\SmartFTP Client`,
|
||||
`C:\Program Files (x86)\Splashtop`,
|
||||
`C:\Program Files (x86)\TeamViewer`,
|
||||
`C:\Program Files (x86)\UltraViewer`,
|
||||
`C:\Program Files (x86)\Xpra`,
|
||||
`C:\Program Files (x86)\Yandex`,
|
||||
`C:\Program Files (x86)\mRemoteNG`,
|
||||
`C:\Program Files\ATERA NETWORKS`,
|
||||
`C:\Program Files\ATERA NETWORKS\AteraAgent`,
|
||||
`C:\Program Files\AnyDesk`,
|
||||
`C:\Program Files\Bitvise SSH Server`,
|
||||
`C:\Program Files\Danware Data\NetOp Packn Deploy`,
|
||||
`C:\Program Files\Level`,
|
||||
`C:\\Program Files\\LiteManager Pro`,
|
||||
`C:\\Program Files\\LiteManager Pro \u2013 Viewer`,
|
||||
`C:\Program Files\ManageEngine\ManageEngine Free Tools`,
|
||||
`C:\Program Files\ManageEngine\ManageEngine Free Tools\Launcher`,
|
||||
`C:\Program Files\RealVNC`,
|
||||
`C:\Program Files\RealVNC\VNC Serve`,
|
||||
`C:\Program Files\Remote Utilities`,
|
||||
`C:\Program Files\Remote Utilities\Agent`,
|
||||
`C:\Program Files\Solar-Putty-v4`,
|
||||
`C:\Program Files\SolarWinds\Dameware Mini Remote Control`,
|
||||
`C:\Program Files\SysAidServer`,
|
||||
`C:\Program Files\TeamViewer`,
|
||||
`C:\Program Files\TightVNC`,
|
||||
`C:\Program Files\ZOC8`,
|
||||
`C:\Program Files\uvnc bvba`,
|
||||
`C:\Program Files\uvnc bvba\UltraVNC`,
|
||||
`C:\ProgramData\Kaseya`,
|
||||
`C:\ProgramData\Total Software Deployment`,
|
||||
`C:\ProgramFiles\GoTo Machine Installer`,
|
||||
`C:\ProgramFiles (x86)\GoTo Machine Installer`,
|
||||
`{{APPDATA}}\Local\Google\Chrome\User Data\Default\Extensions\iodihamcpbpeioajjeobimgagajmlibd`,
|
||||
`{{APPDATA}}\Local\MEGAsync`,
|
||||
`{{APPDATA}}\Roaming\Mikogo`,
|
||||
`{{APPDATA}}\Roaming\SyncTrayzor`,
|
||||
`C:\Users\IEUser\Downloads\WinSCP-5.21.6-Portable`,
|
||||
`C:\Users\USERNAME\AppData\Roaming\Insync`,
|
||||
`C:\Users\USERNAME\AppData\Roaming\Insync\App`,
|
||||
`C:\Windows\Action1`,
|
||||
`C:\Windows\SysWOW64\rserver30`,
|
||||
`C:\Windows\SysWOW64\rserver30\FamItrfc`,
|
||||
`C:\Windows\SysWOW64\rserver30\FamItrf2`,
|
||||
`C:\Windows\dwrcs`,
|
||||
`C:\ProgramData\AMMYY`,
|
||||
// KnownRMMDirectories contains known directory names/paths
|
||||
// These will be searched in common installation locations defined in SearchBasePaths
|
||||
var KnownRMMDirectories = []string{
|
||||
// A
|
||||
`Action1`,
|
||||
`Almageste\DragonDisk`,
|
||||
`AlpemixSrvc`,
|
||||
`AMMYY`,
|
||||
`AnyDesk`,
|
||||
`AnyViewer`,
|
||||
`Atera Networks`,
|
||||
`ATERA NETWORKS`,
|
||||
`ATERA NETWORKS\AteraAgent`,
|
||||
|
||||
// B
|
||||
`Bitvise SSH Client`,
|
||||
`Bitvise SSH Server`,
|
||||
`Bluetrait Agent`,
|
||||
|
||||
// D
|
||||
`Danware Data\NetOp Packn Deploy`,
|
||||
`DesktopCentral_Agent`,
|
||||
`DesktopCentral_Agent\bin`,
|
||||
|
||||
// G
|
||||
`GoTo Opener`,
|
||||
`GoTo Machine Installer`,
|
||||
`GoToMyPC`,
|
||||
`Google\Chrome Remote Desktop`,
|
||||
`Google\Chrome\User Data\Default\Extensions\iodihamcpbpeioajjeobimgagajmlibd`,
|
||||
|
||||
// I
|
||||
`Insync`,
|
||||
`Insync\App`,
|
||||
`ISL Online`,
|
||||
|
||||
// K
|
||||
`Kaseya`,
|
||||
|
||||
// L
|
||||
`LANDesk`,
|
||||
`Level`,
|
||||
`LiteManager Pro`,
|
||||
`LiteManager Pro – Viewer`,
|
||||
|
||||
// M
|
||||
`ManageEngine\ManageEngine Free Tools`,
|
||||
`ManageEngine\ManageEngine Free Tools\Launcher`,
|
||||
`MEGAsync`,
|
||||
`Mikogo`,
|
||||
`mRemoteNG`,
|
||||
|
||||
// N
|
||||
`NetSarang`,
|
||||
`NetSarang\xShell`,
|
||||
|
||||
// O
|
||||
`OnionShare`,
|
||||
|
||||
// P
|
||||
`PJ Technologies`,
|
||||
`PJ Technologies\GOVsrv`,
|
||||
|
||||
// R
|
||||
`Radmin Viewer 3`,
|
||||
`RealVNC`,
|
||||
`RealVNC\VNC Serve`,
|
||||
`Remote Utilities`,
|
||||
`Remote Utilities\Agent`,
|
||||
`RemotePC`,
|
||||
`RustDesk`,
|
||||
|
||||
// S
|
||||
`S3 Browser`,
|
||||
`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,49 +10,56 @@ import (
|
||||
)
|
||||
|
||||
var appData = os.Getenv("APPDATA")
|
||||
var userProfile = os.Getenv("USERPROFILE")
|
||||
|
||||
func Detect() []Directory {
|
||||
var suspiciousDirectories []Directory
|
||||
seen := make(map[string]bool) // Prevent duplicates
|
||||
|
||||
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 (")
|
||||
if isPrefix(dir) {
|
||||
// Find all directories matching this prefix
|
||||
matches := findPrefixMatches(dir)
|
||||
for _, match := range matches {
|
||||
if !seen[match] {
|
||||
fmt.Printf(" [?] Found %s\n", match)
|
||||
suspiciousDirectories = append(suspiciousDirectories, Directory{Path: match})
|
||||
seen[match] = true
|
||||
// For each known RMM directory, check in all base paths
|
||||
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
|
||||
matches := findPrefixMatches(fullPath)
|
||||
for _, match := range matches {
|
||||
if !seen[match] {
|
||||
fmt.Printf(" [?] Found %s\n", match)
|
||||
suspiciousDirectories = append(suspiciousDirectories, Directory{Path: match})
|
||||
seen[match] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Exact match
|
||||
if _, err := os.Stat(dir); err == nil {
|
||||
if !seen[dir] {
|
||||
fmt.Printf(" [?] Found %s\n", dir)
|
||||
suspiciousDirectories = append(suspiciousDirectories, Directory{Path: dir})
|
||||
seen[dir] = true
|
||||
} else {
|
||||
// Exact match
|
||||
if _, err := os.Stat(fullPath); err == nil {
|
||||
if !seen[fullPath] {
|
||||
fmt.Printf(" [?] Found %s\n", fullPath)
|
||||
suspiciousDirectories = append(suspiciousDirectories, Directory{Path: fullPath})
|
||||
seen[fullPath] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("[+] Found %d Suspicious Directories\n", len(suspiciousDirectories))
|
||||
|
||||
return suspiciousDirectories
|
||||
}
|
||||
|
||||
// replaceAppData replaces {{APPDATA}} with the actual APPDATA path
|
||||
func replaceAppData(path string) string {
|
||||
if strings.Contains(path, "{{APPDATA}}") {
|
||||
p := strings.Replace(path, "{{APPDATA}}", "", -1)
|
||||
return filepath.Join(appData, p)
|
||||
}
|
||||
// replaceEnvVars replaces environment variable placeholders with actual paths
|
||||
func replaceEnvVars(path string) string {
|
||||
path = strings.ReplaceAll(path, "{{APPDATA}}", appData)
|
||||
path = strings.ReplaceAll(path, "{{USERPROFILE}}", userProfile)
|
||||
return path
|
||||
}
|
||||
|
||||
|
||||
+156
-20
@@ -4,42 +4,178 @@ import (
|
||||
"fmt"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var (
|
||||
shell32 = syscall.NewLazyDLL("shell32.dll")
|
||||
shellExecuteW = shell32.NewProc("ShellExecuteW")
|
||||
shell32 = syscall.NewLazyDLL("shell32.dll")
|
||||
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
|
||||
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
|
||||
operation, err := syscall.UTF16PtrFromString("open")
|
||||
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)
|
||||
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)
|
||||
// SW_SHOWNORMAL = 1, SW_SHOW = 5
|
||||
ret, _, callErr := shellExecuteW.Call(
|
||||
0, // hwnd (NULL)
|
||||
uintptr(unsafe.Pointer(operation)), // operation ("open")
|
||||
uintptr(unsafe.Pointer(urlPtr)), // file (URL)
|
||||
0, // parameters (NULL)
|
||||
0, // directory (NULL)
|
||||
5, // showCmd (SW_SHOW)
|
||||
)
|
||||
|
||||
// ShellExecute returns a value > 32 on success
|
||||
if ret <= 32 {
|
||||
return fmt.Errorf("ShellExecute failed with code: %d (error: %v)", ret, callErr)
|
||||
// Initialize SHELLEXECUTEINFO structure
|
||||
sei := shellExecuteInfo{
|
||||
cbSize: uint32(unsafe.Sizeof(shellExecuteInfo{})),
|
||||
fMask: SEE_MASK_NOCLOSEPROCESS, // Request process handle
|
||||
hwnd: 0,
|
||||
lpVerb: operation,
|
||||
lpFile: urlPtr,
|
||||
lpParameters: nil,
|
||||
lpDirectory: nil,
|
||||
nShow: SW_SHOW,
|
||||
hInstApp: 0,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
*{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}
|
||||
|
||||
/* 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:hover{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.selected{background:#103e24;color:var(--accent);border-color:#1d7e4a}
|
||||
.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.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-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}
|
||||
@@ -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::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%}}
|
||||
|
||||
/* 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>
|
||||
</head>
|
||||
<body>
|
||||
@@ -113,6 +147,24 @@
|
||||
<div class="splash-loader"></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>
|
||||
<div class="nav">
|
||||
<div class="brand"><img src="/logo" alt="RMM Hunter"><span>RMM Hunter</span></div>
|
||||
@@ -132,6 +184,7 @@
|
||||
<div class="actions">
|
||||
<button id="startHunt" class="btn primary">Start Hunt</button>
|
||||
<span id="huntTag" class="tag hidden"></span>
|
||||
<button id="viewReportBtn" class="btn hidden" style="margin-left:12px">View Report</button>
|
||||
</div>
|
||||
<div id="log" class="log" aria-live="polite"></div>
|
||||
</section>
|
||||
@@ -206,6 +259,7 @@
|
||||
const logEl = document.getElementById('log');
|
||||
const huntsEl = document.getElementById('hunts');
|
||||
const huntTag = document.getElementById('huntTag');
|
||||
const viewReportBtn = document.getElementById('viewReportBtn');
|
||||
const reportSection = document.getElementById('reportSection');
|
||||
const reportHeader = document.getElementById('reportHeader');
|
||||
const reportBody = document.getElementById('reportBody');
|
||||
@@ -350,27 +404,93 @@
|
||||
}
|
||||
|
||||
// Hunt flow
|
||||
let lastReportData = null;
|
||||
let currentReportName = null;
|
||||
|
||||
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;
|
||||
try{ ws = new WebSocket((location.protocol==='https:'?'wss':'ws')+'://'+location.host+'/ws/hunt');
|
||||
ws.onmessage = ev => { logEl.textContent += ev.data + '\n'; logEl.scrollTop = logEl.scrollHeight; };
|
||||
let wsReady = false;
|
||||
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); }
|
||||
|
||||
// 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 data = await r.json();
|
||||
|
||||
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');
|
||||
await typeText(huntTag, 'Report: '+data.reportName+'.json', 30);
|
||||
|
||||
// Load report
|
||||
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();
|
||||
listHunts();
|
||||
});
|
||||
|
||||
// View Report button handler
|
||||
viewReportBtn.addEventListener('click', () => {
|
||||
if (lastReportData) {
|
||||
location.hash = '#report';
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('quitBtn').addEventListener('click', async ()=>{
|
||||
await fetch('/api/quit',{method:'POST'});
|
||||
// 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'});
|
||||
} 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();
|
||||
@@ -631,11 +751,123 @@
|
||||
}).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;
|
||||
|
||||
alert('Elimination functionality not yet implemented in web UI.\n\nPlease use the CLI: rmm-hunter eliminate --cli');
|
||||
// TODO: Implement API call to backend elimination endpoint
|
||||
if (!currentReport) {
|
||||
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
|
||||
|
||||
+244
-1
@@ -10,11 +10,13 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"rmm-hunter/internal/suspicious"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"rmm-hunter/internal/pkg"
|
||||
"rmm-hunter/internal/pkg/hunt/eliminate"
|
||||
"rmm-hunter/internal/pkg/hunter"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
@@ -76,6 +78,7 @@ func StartWebServer() {
|
||||
mux.HandleFunc("/api/hunts", s.handleListHunts)
|
||||
mux.HandleFunc("/api/hunt/start", s.handleStartHunt)
|
||||
mux.HandleFunc("/api/report", s.handleGetReport)
|
||||
mux.HandleFunc("/api/eliminate", s.handleEliminate)
|
||||
mux.HandleFunc("/api/quit", s.handleQuit)
|
||||
mux.HandleFunc("/ws/hunt", s.handleWS)
|
||||
|
||||
@@ -103,7 +106,8 @@ func StartWebServer() {
|
||||
<-serverReady
|
||||
time.Sleep(500 * time.Millisecond) // Give server a moment to fully initialize
|
||||
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)
|
||||
if !hostAdded {
|
||||
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) {
|
||||
if r.Method != http.MethodPost {
|
||||
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}`))
|
||||
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