Add Script Manager + Operator Panels; overhaul pivot/jump graph

- Script Manager (scripts.go): 30+ lateral-move/privesc/persistence/creds/enum recipes, dry-run preview, MITRE ATT&CK + OpSec labels, spawn with listener pre-flight + post-spawn verification
- Operator Panels (panels.go): file/process browser, kill-chain tracker, engagement timer, IOC tracker, cleanup + report generators
- Graph: correct pivot direction (session-id), jump/lateral lineage, firewall->root edges, working Reset Layout, tidy arrow spacing
- Safe native file-dialog wrappers; spawn beacon-interval fix; README Operator Toolkit section
This commit is contained in:
Raj Kumar Mullapudi
2026-07-24 17:34:12 -04:00
parent 1701f57108
commit edede574e9
8 changed files with 4547 additions and 131 deletions
+37 -4
View File
@@ -46,10 +46,6 @@ directly: no grpc-web proxy, no protocol reimplementation, no drift when upstrea
<img width="1912" height="832" alt="image" src="https://github.com/user-attachments/assets/5db41809-17f4-40e2-bf7c-20a5a4901666" />
<br><br>
<img width="1912" height="756" alt="image" src="https://github.com/user-attachments/assets/b779ca54-7b43-4d0d-b066-66e25d7f73aa" />
<br><br>
<img width="1912" height="802" alt="image" src="https://github.com/user-attachments/assets/e43bcdc2-3c84-4ad2-8010-aa43c88d4b5d" />
<br><br>
<img width="1913" height="807" alt="Screenshot 2026-07-18 014308" src="https://github.com/user-attachments/assets/5780d98a-8e5e-4683-90bc-3649587d4b00" />
---
@@ -86,6 +82,43 @@ kill-session · kill-beacon · version · mtls/http/https/dns/wg`
---
## Operator Toolkit — Script Manager, Panels & Pivot Graph
> A cohesive operator layer on top of Sliver's RPCs — **designed and built by
> [Mr-In4inci3le](https://github.com/Mr-In4inci3le) aka Raj Kumar Mullapudi.** No C2 logic is
> reimplemented; every action composes existing Sliver RPCs.
### Script Manager
One-click post-exploitation **recipes** (Cobalt-Strike-style jump / spawn / remote-exec), each with a
**dry-run preview**, a **MITRE ATT&CK ID**, and an **OpSec noise** rating:
- **Spawn** — `spawn <os> <arch> <profile>` builds + runs a fresh agent on the current host, with a
pre-flight **listener check** and **post-spawn verification** (reports the real new agent ID).
- **Lateral Movement** — SSH deploy, PsExec, WMI, WinRM, SC, SMB (`jump ssh|psexec|winrm|wmi`).
- **Privilege Escalation** — Linux (sudo / SUID), Windows (privesc check, token impersonation,
GetSystem, UAC bypass).
- **Persistence** — cron · SSH-key · systemd · Registry Run · scheduled task · service · WMI · startup.
- **Credentials** — Linux & Windows harvest, Kerberoast, DCSync (harvested creds routed to Loot).
- **Enumeration** — network scan, Active Directory enumeration, local Windows enum.
### Operator Panels
- **File Browser** — visual remote filesystem (list / delete).
- **Process Browser** — visual process list + kill.
- **Kill-Chain Tracker** — record and visualise the current engagement stage.
- **Engagement Timer** — start / elapsed stopwatch.
- **IOC Tracker** — log indicators as you drop them (files, registry keys, services).
- **Cleanup Script Generator** — emit a teardown script from the tracked IOCs.
- **Engagement Report Generator** — one-click engagement report.
### Interactive Pivot / Jump Graph
A Cobalt-Strike-style topology view: a **firewall / egress boundary** on the left, agents laid out
left-to-right in their **real pivot order** (chains joined by session id so same-host pivots render
correctly), edges colour-coded by agent (**green** session · **red** SYSTEM/privileged · **blue**
beacon), and **orange dashed lateral-move edges** that show exactly where each jump came from.
Drag nodes · scroll-zoom · pan · per-teamserver saved layout · one-click **Reset Layout**.
---
## Quick start
**Prerequisites**
+35 -7
View File
@@ -64,8 +64,36 @@ func (a *App) shutdown(ctx context.Context) {
// ─── Connection ───────────────────────────────────────────────────────────────
// safeOpenFileDialog wraps runtime.OpenFileDialog with a nil-ctx check and panic
// recovery to prevent app crashes if native OS file dialogs fail on Windows/Linux.
func (a *App) safeOpenFileDialog(opts runtime.OpenDialogOptions) (res string, err error) {
if a.ctx == nil {
return "", fmt.Errorf("application context not initialized")
}
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("file dialog error: %v", r)
}
}()
return runtime.OpenFileDialog(a.ctx, opts)
}
// safeSaveFileDialog wraps runtime.SaveFileDialog with a nil-ctx check and panic
// recovery to prevent app crashes if native OS file dialogs fail on Windows/Linux.
func (a *App) safeSaveFileDialog(opts runtime.SaveDialogOptions) (res string, err error) {
if a.ctx == nil {
return "", fmt.Errorf("application context not initialized")
}
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("file dialog error: %v", r)
}
}()
return runtime.SaveFileDialog(a.ctx, opts)
}
func (a *App) PickConfigFile() (string, error) {
return runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
return a.safeOpenFileDialog(runtime.OpenDialogOptions{
Title: "Select Sliver operator config (.cfg)",
Filters: []runtime.FileFilter{{DisplayName: "Sliver Config (*.cfg)", Pattern: "*.cfg"}},
})
@@ -868,7 +896,7 @@ func (a *App) ProcessDump(sessionID string, pid int32) TransferResult {
if err != nil {
return TransferResult{Error: err.Error()}
}
savePath, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
savePath, err := a.safeSaveFileDialog(runtime.SaveDialogOptions{
DefaultFilename: fmt.Sprintf("procdump_%d.dmp", pid),
Title: "Save process dump",
})
@@ -918,7 +946,7 @@ func (a *App) DownloadFile(sessionID, remotePath string) TransferResult {
if err != nil {
return TransferResult{Error: "decode: " + err.Error()}
}
savePath, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
savePath, err := a.safeSaveFileDialog(runtime.SaveDialogOptions{
DefaultFilename: filepath.Base(remotePath),
Title: "Save downloaded file",
})
@@ -950,7 +978,7 @@ func (a *App) UploadFile(sessionID, remotePath string) TransferResult {
if err != nil {
return TransferResult{Error: err.Error()}
}
localPath, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
localPath, err := a.safeOpenFileDialog(runtime.OpenDialogOptions{
Title: "Select file to upload",
})
if err != nil || localPath == "" {
@@ -1295,7 +1323,7 @@ func (a *App) RegenerateBuild(name string) TransferResult {
if resp.File == nil {
return TransferResult{Error: "no stored build for " + name}
}
savePath, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
savePath, err := a.safeSaveFileDialog(runtime.SaveDialogOptions{
DefaultFilename: resp.File.Name,
Title: "Save regenerated implant",
})
@@ -1660,7 +1688,7 @@ func (a *App) DownloadLoot(lootID string) TransferResult {
if resp.File == nil {
return TransferResult{Error: "this loot item has no file content"}
}
savePath, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
savePath, err := a.safeSaveFileDialog(runtime.SaveDialogOptions{
DefaultFilename: resp.File.Name,
Title: "Save loot",
})
@@ -2522,7 +2550,7 @@ type AssemblyResult struct {
// an explicit path (execute-assembly <path>) or fall back to a picker.
func (a *App) readLocalOrDialog(localPath, title string) ([]byte, error) {
if localPath == "" {
p, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{Title: title})
p, err := a.safeOpenFileDialog(runtime.OpenDialogOptions{Title: title})
if err != nil || p == "" {
return nil, fmt.Errorf("selection cancelled")
}
+3 -3
View File
@@ -210,7 +210,7 @@ func (a *App) RegistryReadHiveExport(sessionID, rootHive, requestedHive string)
data = dec
}
}
savePath, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
savePath, err := a.safeSaveFileDialog(runtime.SaveDialogOptions{
DefaultFilename: strings.ToLower(rootHive) + ".hive",
Title: "Save registry hive",
})
@@ -959,7 +959,7 @@ func (a *App) ConvertDLLToShellcode(dllPath, functionName, args string) (string,
if err != nil {
return "", err
}
savePath, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
savePath, err := a.safeSaveFileDialog(runtime.SaveDialogOptions{
DefaultFilename: strings.TrimSuffix(filepath.Base(dllPath), filepath.Ext(dllPath)) + ".bin",
Title: "Save shellcode",
})
@@ -999,7 +999,7 @@ func (a *App) EncodeShellcode(inPath, architecture string, iterations int) (stri
if err != nil {
return "", err
}
savePath, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
savePath, err := a.safeSaveFileDialog(runtime.SaveDialogOptions{
DefaultFilename: strings.TrimSuffix(filepath.Base(inPath), filepath.Ext(inPath)) + ".enc.bin",
Title: "Save encoded shellcode",
})
+16 -8
View File
@@ -39,6 +39,10 @@
<button class="tb-btn" data-view="creds">Creds</button>
<button class="tb-btn" data-view="hosts">Hosts</button>
<button class="tb-btn" data-view="operators">Operators</button>
<button class="tb-btn" data-view="scripts">Scripts</button>
<button class="tb-btn" data-view="iocs">IOCs</button>
<button class="tb-btn" data-view="report">Report</button>
<button class="tb-btn" data-view="c2profiles">C2 Profiles</button>
</div>
<div class="toolbar-right">
<span id="server-version" class="version-tag"></span>
@@ -67,8 +71,8 @@
<div class="table-wrap">
<table class="data-table" id="agents-table">
<thead><tr>
<th>Type</th><th>Name / ID</th><th>Host</th><th>User</th><th>OS/Arch</th>
<th>PID</th><th>Transport</th><th>Remote</th><th>Last Checkin</th><th>Status</th>
<th>Type</th><th>Name / ID</th><th>Host</th><th>User</th><th>Remote IP</th><th>OS/Arch</th>
<th>PID</th><th>Transport</th><th>Last Checkin</th><th>Status</th>
</tr></thead>
<tbody id="agents-body"></tbody>
</table>
@@ -79,14 +83,16 @@
<div id="graph-view" class="panel-content hidden">
<svg id="graph-svg" xmlns="http://www.w3.org/2000/svg"></svg>
<div class="graph-legend">
<span class="leg-item"><span class="leg-line priv"></span> Privileged (red)</span>
<span class="leg-item"><span class="leg-line user"></span> User session (green)</span>
<span class="leg-item"><span class="leg-line beacon"></span> Beacon (blue dashed)</span>
<span class="leg-item"><span class="leg-line dead"></span> Dead (grey)</span>
<span class="leg-item"><span class="leg-line" style="border-top:3px solid #35c46b"></span> User session</span>
<span class="leg-item"><span class="leg-line" style="border-top:3px solid #e23c4e"></span> Privileged session</span>
<span class="leg-item"><span class="leg-line" style="border-top:3px solid #4d9fe6"></span> Beacon</span>
<span class="leg-item"><span class="leg-line" style="border-top:3px dashed #f5a623"></span> Jump / Lateral move</span>
<span class="leg-item"><span class="leg-line" style="border-top:3px solid #6b7280;opacity:.6"></span> Dead agent</span>
<span class="sep">|</span>
<span class="leg-item">OS logo + integrity shown per node</span>
<span class="leg-item"><span style="display:inline-block;width:10px;height:10px;border:1.5px dashed #35c46b;border-radius:2px;vertical-align:middle;margin-right:4px"></span> Active selection</span>
<span class="leg-item"><span style="display:inline-block;width:10px;height:10px;border:1.2px dashed #f5a623;border-radius:2px;vertical-align:middle;margin-right:4px"></span> Jump-linked node</span>
<span class="sep">|</span>
<span class="leg-item">drag nodes · scroll to zoom · drag bg to pan</span>
<span class="leg-item" style="color:var(--muted)">drag · scroll zoom · drag bg to pan</span>
<span class="spacer"></span>
<button id="graph-reset-btn" class="btn small">Reset Layout</button>
</div>
@@ -151,6 +157,8 @@
<!-- Context menu -->
<div id="ctx-menu" class="ctx-menu hidden">
<div class="ctx-item" id="ctx-interact">Interact</div>
<div class="ctx-item" id="ctx-files">File Browser</div>
<div class="ctx-item" id="ctx-processes">Process Browser</div>
<div class="ctx-item" id="ctx-integrity">Check Integrity</div>
<div class="ctx-item" id="ctx-rename">Rename</div>
<div class="ctx-divider"></div>
+1558 -108
View File
File diff suppressed because it is too large Load Diff
+300 -1
View File
@@ -192,7 +192,7 @@ input::placeholder{color:var(--muted);}
.check-label{display:flex;align-items:center;gap:7px;font-size:13px;color:var(--text-dim);cursor:pointer;}
.modal-body .btn.accent{padding:10px 18px;font-size:13px;font-weight:700;}
.gen-status{font-size:12px;color:var(--text-dim);display:flex;align-items:center;gap:8px;}
.spinner{width:13px;height:13px;border:2px solid var(--border-hi);border-top-color:var(--accent);border-radius:50%;animation:spin 1s steps(12) infinite;}
.spinner{width:13px;height:13px;border:2px solid var(--border-hi);border-top-color:var(--accent);border-radius:50%;animation:spin .6s linear infinite;}
@keyframes spin{to{transform:rotate(360deg)}}
.result-box{font-size:12px;font-family:var(--mono);}
.shell-out{flex:1;overflow-y:auto;margin:0;padding:10px 12px;font-family:var(--mono);font-size:12.5px;line-height:1.55;background:#08090e;color:var(--text);white-space:pre-wrap;word-break:break-word;min-height:0;}
@@ -232,3 +232,302 @@ input::placeholder{color:var(--muted);}
.status-msg{font-size:11px;margin-top:6px;}
.status-msg.ok{color:var(--ok);}
.status-msg.err{color:var(--accent);}
/* ── Script Manager Panel ──────────────────────────────────────────────────── */
.scripts-panel { padding: 8px; display: flex; flex-direction: column; gap: 14px; height: 100%; box-sizing: border-border-box; }
.scr-top-bar { display: flex; gap: 12px; align-items: center; background: var(--panel-alt); padding: 10px 14px; border: 1px solid var(--border); border-radius: 8px; }
.scr-main-layout { display: grid; grid-template-columns: minmax(360px, 1.2fr) minmax(320px, 1fr); gap: 16px; flex: 1; min-height: 0; }
.scr-left-pane { display: flex; flex-direction: column; gap: 14px; overflow-y: auto; padding-right: 4px; }
.scr-right-pane { display: flex; flex-direction: column; gap: 10px; min-height: 0; }
.scr-input { width: 100%; padding: 7px 10px; background: var(--bg); border: 1px solid var(--border); color: var(--text); border-radius: 6px; font-size: 12px; margin-top: 4px; font-family: var(--font); box-sizing: border-box; }
.scr-input:focus { border-color: var(--cyan); outline: none; box-shadow: 0 0 0 2px rgba(56,189,248,0.2); }
.scr-categories { display: flex; flex-direction: column; gap: 16px; }
.scr-cat { background: var(--panel-alt); border: 1px solid var(--border); border-radius: 8px; padding: 12px 14px; }
.scr-cat h4 { color: var(--cyan); font-size: 11px; text-transform: uppercase; letter-spacing: 0.8px; margin-bottom: 10px; font-weight: 700; display: flex; align-items: center; gap: 6px; }
.scr-cat h4::before { content: ""; width: 3px; height: 10px; background: var(--cyan); border-radius: 2px; }
.scr-btns { display: flex; flex-wrap: wrap; gap: 8px; }
.scr-btn { display: inline-flex; align-items: center; gap: 8px; padding: 7px 12px; background: var(--panel-hi); border: 1px solid var(--border); color: var(--text-dim); border-radius: 6px; font-size: 11.5px; cursor: pointer; transition: all 0.2s ease; }
.scr-btn:hover { background: var(--panel-elev); color: var(--text); border-color: var(--cyan); transform: translateY(-1px); }
.scr-btn.active { background: var(--panel-elev); color: var(--text); border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent), 0 4px 12px rgba(0,0,0,0.3); }
.scr-btn:disabled { opacity: 0.5; cursor: wait; }
.badge-attck { background: rgba(56,188,203,0.12); color: var(--cyan); border: 1px solid rgba(56,188,203,0.3); font-family: var(--mono); font-size: 9.5px; padding: 2px 5px; border-radius: 4px; }
.badge-opsec { font-size: 9.5px; padding: 2px 6px; border-radius: 4px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.3px; }
.opsec-low { background: rgba(53,196,107,0.12); color: var(--ok); border: 1px solid rgba(53,196,107,0.3); }
.opsec-medium { background: rgba(224,163,58,0.12); color: var(--warn); border: 1px solid rgba(224,163,58,0.3); }
.opsec-high { background: var(--accent-dim); color: var(--accent); border: 1px solid rgba(226,60,78,0.4); }
.scr-param-card { background: var(--panel-alt); border: 1px solid var(--border-hi); border-radius: 8px; padding: 14px 16px; box-shadow: 0 4px 16px rgba(0,0,0,0.25); }
.scr-param-card h3 { font-size: 13.5px; color: var(--text); margin-bottom: 4px; display: flex; align-items: center; gap: 8px; font-weight: 600; }
.scr-param-card p { font-size: 11.5px; color: var(--text-dim); margin-bottom: 14px; line-height: 1.4; }
.scr-form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 12px; margin-bottom: 14px; }
.scr-actions { display: flex; gap: 10px; justify-content: flex-end; }
.scr-action-btn { padding: 8px 16px; border-radius: 6px; font-size: 12px; font-weight: 600; cursor: pointer; border: 1px solid transparent; transition: all 0.2s ease; display: inline-flex; align-items: center; gap: 6px; }
.scr-btn-preview { background: var(--panel-hi); color: var(--cyan); border-color: var(--border-hi); }
.scr-btn-preview:hover { border-color: var(--cyan); background: var(--panel-elev); shadow: 0 2px 8px rgba(56,189,248,0.2); }
.scr-btn-exec { background: var(--accent); color: #0d1117; }
.scr-btn-exec:hover { filter: brightness(1.15); box-shadow: 0 2px 10px rgba(53,196,107,0.3); }
.scr-output-wrap { display: flex; flex-direction: column; flex: 1; height: 100%; min-height: 0; background: var(--panel-alt); border: 1px solid var(--border); border-radius: 8px; padding: 12px; }
.scr-output { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 12px; font-family: var(--mono); font-size: 11.5px; color: var(--text); flex: 1; overflow-y: auto; white-space: pre-wrap; word-break: break-word; line-height: 1.55; margin: 0; }
/* ── Console-Pinned Script Manager ───────────────────────────────────────── */
.console-script-bar {
background: var(--panel-elev, #161b22);
border-bottom: 1px solid var(--border, #30363d);
padding: 8px 12px;
flex-shrink: 0;
}
.csb-header {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.csb-title {
display: flex;
align-items: center;
gap: 8px;
}
.csb-badge {
background: var(--accent, #4ade80);
color: #0d1117;
font-size: 10px;
font-weight: 700;
padding: 2px 6px;
border-radius: 4px;
letter-spacing: 0.5px;
}
.csb-target {
font-size: 11px;
color: var(--text-dim, #8b949e);
font-family: var(--mono, monospace);
}
.csb-cats {
display: flex;
gap: 4px;
flex: 1;
overflow-x: auto;
}
.csb-cat-tab {
background: transparent;
border: 1px solid transparent;
color: var(--muted, #8b949e);
font-size: 11px;
padding: 3px 8px;
border-radius: 4px;
cursor: pointer;
white-space: nowrap;
transition: all 0.15s;
}
.csb-cat-tab:hover {
color: var(--text, #e6edf3);
background: var(--panel-hi, #2a2e3b);
}
.csb-cat-tab.active {
color: var(--cyan, #38bdf8);
background: var(--panel-hi, #2a2e3b);
border-color: rgba(56, 189, 248, 0.3);
}
.csb-toggle-btn {
background: var(--bg-3, #252535);
border: 1px solid var(--border, #30363d);
color: var(--text-dim, #8b949e);
font-size: 10.5px;
padding: 3px 8px;
border-radius: 4px;
cursor: pointer;
}
.csb-toggle-btn:hover {
color: var(--text, #e6edf3);
}
.csb-body {
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid rgba(48, 54, 61, 0.5);
}
.csb-grid {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 8px;
max-height: 120px;
overflow-y: auto;
}
.csb-recipe-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
background: var(--bg-3, #252535);
border: 1px solid var(--border, #30363d);
color: var(--fg, #e0e0e0);
border-radius: 4px;
font-size: 11px;
cursor: pointer;
transition: all 0.15s;
}
.csb-recipe-btn:hover {
border-color: var(--cyan, #38bdf8);
background: var(--panel-hi, #2a2e3b);
}
.csb-recipe-btn.active {
border-color: var(--accent, #4ade80);
background: var(--panel-hi, #2a2e3b);
}
.csb-card {
background: var(--panel-alt, #0d1117);
border: 1px solid var(--border, #30363d);
border-radius: 6px;
padding: 10px 12px;
margin-top: 6px;
}
.csb-card-header h4 {
font-size: 12px;
color: var(--text, #e6edf3);
margin-bottom: 2px;
display: flex;
align-items: center;
gap: 8px;
}
.csb-card-header p {
font-size: 11px;
color: var(--text-dim, #8b949e);
margin-bottom: 8px;
}
.csb-form-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 8px;
margin-bottom: 8px;
}
.csb-actions {
display: flex;
gap: 8px;
}
.csb-btn {
padding: 5px 12px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
cursor: pointer;
border: none;
transition: opacity 0.15s;
}
.csb-btn:hover {
opacity: 0.9;
}
.csb-btn:disabled {
opacity: 0.5;
cursor: wait;
}
.csb-btn-preview {
background: var(--bg-3, #252535);
color: var(--cyan, #38bdf8);
border: 1px solid var(--border, #30363d);
}
.csb-btn-exec {
background: var(--accent, #4ade80);
color: #0d1117;
}
/* ── File Browser (Explorer-style) ────────────────────────────────────────── */
.file-browser { font-family: var(--font, 'Segoe UI', sans-serif); font-size: 12.5px; display: flex; flex-direction: column; height: 100%; }
.fb-toolbar { display: flex; align-items: center; gap: 4px; padding: 5px 8px; background: linear-gradient(180deg, rgba(30,33,44,1) 0%, rgba(22,24,32,1) 100%); border-bottom: 1px solid var(--border); flex-shrink: 0; }
.fb-toolbar .fb-nav-btn { width: 28px; height: 26px; display: flex; align-items: center; justify-content: center; background: none; border: 1px solid transparent; border-radius: 4px; color: var(--text-dim); cursor: pointer; font-size: 14px; transition: all .12s; }
.fb-toolbar .fb-nav-btn:hover { background: var(--panel-alt); border-color: var(--border); color: var(--text); }
.fb-toolbar .fb-nav-btn:active { background: var(--panel-hi); }
.fb-toolbar .fb-nav-btn[disabled] { opacity: .3; pointer-events: none; }
.fb-addressbar { flex: 1; display: flex; align-items: center; background: var(--bg); border: 1px solid var(--border); border-radius: 4px; padding: 0 8px; height: 26px; overflow: hidden; }
.fb-addressbar .fb-crumb { color: var(--text-dim); font-size: 11.5px; cursor: pointer; padding: 2px 3px; border-radius: 3px; white-space: nowrap; transition: color .1s, background .1s; }
.fb-addressbar .fb-crumb:hover { color: var(--text); background: var(--panel-alt); }
.fb-addressbar .fb-sep { color: var(--muted); font-size: 10px; margin: 0 1px; user-select: none; }
.fb-col-header { display: grid; grid-template-columns: 28px 1fr 90px 100px; padding: 4px 10px; background: var(--panel); border-bottom: 1px solid var(--border-hi); color: var(--muted); font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: .5px; flex-shrink: 0; user-select: none; }
.fb-col-header span { padding: 2px 0; }
.fb-list { flex: 1; overflow-y: auto; overflow-x: hidden; }
.fb-item { display: grid; grid-template-columns: 28px 1fr 90px 100px; align-items: center; padding: 3px 10px; cursor: default; border-bottom: 1px solid rgba(42,46,59,.25); transition: background .08s; user-select: none; min-height: 26px; }
.fb-item:nth-child(even) { background: rgba(255,255,255,.015); }
.fb-item:hover { background: rgba(77,159,230,.08); }
.fb-item.selected { background: rgba(77,159,230,.18); outline: 1px solid rgba(77,159,230,.35); outline-offset: -1px; }
.fb-item .fb-icon { font-size: 16px; text-align: center; line-height: 1; }
.fb-item .fb-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding-left: 4px; }
.fb-item.dir .fb-name { color: var(--info, #4d9fe6); font-weight: 600; }
.fb-item .fb-size { color: var(--text-dim); font-size: 11px; text-align: right; font-family: var(--mono); }
.fb-item .fb-date { color: var(--muted); font-size: 10.5px; }
.fb-status { padding: 4px 10px; background: var(--panel); border-top: 1px solid var(--border); color: var(--muted); font-size: 10.5px; flex-shrink: 0; display: flex; align-items: center; gap: 12px; }
/* ── Process Browser (Task Manager-style) ─────────────────────────────────── */
.proc-browser { font-family: var(--font, 'Segoe UI', sans-serif); font-size: 12px; display: flex; flex-direction: column; height: 100%; }
.proc-toolbar { display: flex; align-items: center; gap: 6px; padding: 5px 8px; background: linear-gradient(180deg, rgba(30,33,44,1) 0%, rgba(22,24,32,1) 100%); border-bottom: 1px solid var(--border); flex-shrink: 0; }
.proc-toolbar .proc-search { flex: 1; max-width: 240px; height: 26px; background: var(--bg); border: 1px solid var(--border); border-radius: 4px; color: var(--text); font-size: 11.5px; padding: 0 8px; font-family: var(--font); outline: none; transition: border-color .12s; }
.proc-toolbar .proc-search:focus { border-color: var(--accent); }
.proc-toolbar .proc-search::placeholder { color: var(--muted); }
.proc-toolbar .proc-tb-btn { height: 26px; padding: 0 10px; background: none; border: 1px solid var(--border); border-radius: 4px; color: var(--text-dim); cursor: pointer; font-size: 11px; font-weight: 600; transition: all .12s; display: flex; align-items: center; gap: 4px; }
.proc-toolbar .proc-tb-btn:hover { background: var(--panel-alt); border-color: var(--muted); color: var(--text); }
.proc-toolbar .proc-tb-btn.danger { color: var(--accent); border-color: rgba(226,60,78,.3); }
.proc-toolbar .proc-tb-btn.danger:hover { background: rgba(226,60,78,.12); border-color: #e23c4e; color: #e23c4e; }
.proc-col-header { display: grid; grid-template-columns: 28px 65px 65px 1fr 160px 60px; padding: 4px 10px; background: var(--panel); border-bottom: 1px solid var(--border-hi); color: var(--muted); font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: .5px; flex-shrink: 0; user-select: none; }
.proc-col-header span { padding: 2px 0; cursor: pointer; transition: color .1s; }
.proc-col-header span:hover { color: var(--text); }
.proc-list { flex: 1; overflow-y: auto; overflow-x: hidden; }
.proc-item { display: grid; grid-template-columns: 28px 65px 65px 1fr 160px 60px; align-items: center; padding: 2px 10px; cursor: default; border-bottom: 1px solid rgba(42,46,59,.25); transition: background .08s; user-select: none; min-height: 25px; font-size: 11.5px; }
.proc-item:nth-child(even) { background: rgba(255,255,255,.015); }
.proc-item:hover { background: rgba(77,159,230,.08); }
.proc-item.selected { background: rgba(77,159,230,.18); outline: 1px solid rgba(77,159,230,.35); outline-offset: -1px; }
.proc-item .proc-icon { font-size: 14px; text-align: center; line-height: 1; }
.proc-item .proc-pid { font-family: var(--mono); color: var(--text-dim); font-size: 11px; }
.proc-item .proc-exe { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; }
.proc-item .proc-owner { color: var(--text-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11px; }
.proc-item .proc-arch { color: var(--muted); font-size: 10.5px; font-family: var(--mono); }
.proc-item.proc-system .proc-exe { color: var(--accent); }
.proc-item.proc-highlight .proc-exe { color: var(--ok); }
.proc-status { padding: 4px 10px; background: var(--panel); border-top: 1px solid var(--border); color: var(--muted); font-size: 10.5px; flex-shrink: 0; display: flex; align-items: center; gap: 12px; }
/* ── IOC Tracker ───────────────────────────────────────────────────────────── */
.ioc-panel { padding: 4px; }
[class^="badge-"] { display: inline-block; padding: 1px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; }
.badge-file { background: #1e3a5f; color: #93c5fd; }
.badge-service { background: #4c1d95; color: #c4b5fd; }
.badge-regkey { background: #713f12; color: #fde047; }
.badge-schtask { background: #7c2d12; color: #fdba74; }
.badge-cron { background: #14532d; color: #86efac; }
.badge-user { background: #7f1d1d; color: #fca5a5; }
/* ── File Browser Split Layout ────────────────────────────────────────────── */
.fb-body-split { display: flex; flex: 1; overflow: hidden; min-height: 0; }
.fb-left-pane { display: flex; flex-direction: column; flex: 0 0 65%; min-width: 0; border-right: 1px solid var(--border); overflow: hidden; }
.fb-left-pane .fb-col-header { flex-shrink: 0; }
.fb-left-pane .fb-list { flex: 1; overflow-y: auto; overflow-x: hidden; }
.fb-right-pane { flex: 1; display: flex; flex-direction: column; overflow-y: auto; background: var(--bg); }
/* ── File Browser Preview Panel ───────────────────────────────────────────── */
.fb-preview-empty { display: flex; align-items: center; justify-content: center; height: 100%; color: var(--muted); font-size: 11.5px; text-align: center; padding: 20px; }
.fb-preview-icon { font-size: 40px; text-align: center; padding: 24px 0 8px; }
.fb-preview-name { font-size: 13px; font-weight: 700; text-align: center; padding: 0 12px 4px; word-break: break-all; color: var(--text); }
.fb-preview-type { font-size: 10.5px; text-align: center; color: var(--muted); padding-bottom: 12px; }
.fb-preview-divider { height: 1px; background: var(--border); margin: 0 12px 12px; }
.fb-preview-row { display: flex; flex-direction: column; padding: 4px 14px; gap: 2px; }
.fb-preview-label { font-size: 9.5px; text-transform: uppercase; letter-spacing: .5px; color: var(--muted); font-weight: 600; }
.fb-preview-val { font-size: 11px; color: var(--text-dim); font-family: var(--mono); word-break: break-all; }
.fb-preview-tip { text-align: center; color: var(--muted); font-size: 10.5px; padding: 16px 12px; font-style: italic; }
.fb-preview-actions { display: flex; flex-direction: column; gap: 6px; padding: 14px 14px 0; }
.fb-preview-btn { padding: 6px 12px; border: 1px solid var(--border); background: var(--panel); color: var(--text); border-radius: 4px; cursor: pointer; font-size: 11.5px; font-weight: 500; transition: all .12s; text-align: center; }
.fb-preview-btn:hover { background: var(--panel-alt); border-color: var(--muted); }
.fb-preview-btn.danger { color: var(--accent); border-color: rgba(226,60,78,.3); }
.fb-preview-btn.danger:hover { background: rgba(226,60,78,.12); border-color: #e23c4e; color: #e23c4e; }
/* ── C2 Profile Editor ────────────────────────────────────────────────────── */
.c2-editor { display: flex; gap: 0; height: 100%; min-height: 460px; overflow: hidden; }
.c2-left { display: flex; flex-direction: column; width: 200px; flex-shrink: 0; border-right: 1px solid var(--border); background: var(--panel); }
.c2-list-header { padding: 8px 12px; font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .6px; color: var(--muted); border-bottom: 1px solid var(--border); flex-shrink: 0; }
.c2-profile-list { flex: 1; overflow-y: auto; }
.c2p-item { padding: 8px 12px; font-size: 12px; cursor: pointer; color: var(--text-dim); border-bottom: 1px solid rgba(42,46,59,.3); transition: background .1s, color .1s; }
.c2p-item:hover { background: var(--panel-alt); color: var(--text); }
.c2p-item.active { background: rgba(77,159,230,.15); color: var(--info); border-left: 2px solid var(--info); font-weight: 600; }
.c2-list-footer { padding: 8px; border-top: 1px solid var(--border); flex-shrink: 0; }
.c2-right { display: flex; flex-direction: column; flex: 1; overflow: hidden; }
.c2-editor-toolbar { display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: var(--panel); border-bottom: 1px solid var(--border); flex-shrink: 0; }
.c2-editor-name { font-size: 12px; font-weight: 600; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.c2-json-editor { flex: 1; resize: none; background: var(--bg); color: var(--text); font-family: var(--mono); font-size: 12px; border: none; outline: none; padding: 12px 14px; line-height: 1.6; overflow: auto; }
.c2-json-editor::placeholder { color: var(--muted); }
.c2-editor-status { padding: 5px 12px; background: var(--panel); border-top: 1px solid var(--border); font-size: 10.5px; color: var(--muted); flex-shrink: 0; min-height: 24px; }
+598
View File
@@ -0,0 +1,598 @@
package main
import (
"fmt"
"net"
"regexp"
"strings"
"sync"
"time"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
)
// panels.go implements: File Browser, Process Browser, Credential Auto-Populate,
// Kill Chain Tracker, Engagement Timer, Internal/External IP resolution,
// and Builder Streaming.
// ─── File Browser ─────────────────────────────────────────────────────────────
type FileEntry struct {
Name string `json:"name"`
IsDir bool `json:"isDir"`
Size int64 `json:"size"`
Mode string `json:"mode"`
}
type FileBrowserResult struct {
Path string `json:"path"`
Files []FileEntry `json:"files"`
Error string `json:"error,omitempty"`
}
// FileBrowserList lists files at a path for the visual file browser.
func (a *App) FileBrowserList(sessionID, path string) FileBrowserResult {
client, err := a.requireClient()
if err != nil {
return FileBrowserResult{Error: err.Error()}
}
if path == "" {
path = "."
}
a.audit.log("file-browse", sessionID, path)
resp, err := client.RPC.Ls(a.ctx, &sliverpb.LsReq{
Path: path,
Request: &commonpb.Request{SessionID: sessionID},
})
if err != nil {
return FileBrowserResult{Error: err.Error()}
}
files := make([]FileEntry, 0, len(resp.Files))
for _, f := range resp.Files {
files = append(files, FileEntry{
Name: f.Name,
IsDir: f.IsDir,
Size: f.Size,
Mode: f.Mode,
})
}
return FileBrowserResult{Path: resp.Path, Files: files}
}
// FileBrowserDelete removes a file/directory.
func (a *App) FileBrowserDelete(sessionID, path string) error {
client, err := a.requireClient()
if err != nil {
return err
}
a.audit.log("file-delete", sessionID, path)
_, err = client.RPC.Rm(a.ctx, &sliverpb.RmReq{
Path: path,
Recursive: true,
Request: &commonpb.Request{SessionID: sessionID},
})
return err
}
// ─── Process Browser ──────────────────────────────────────────────────────────
type ProcessEntry struct {
PID int32 `json:"pid"`
PPID int32 `json:"ppid"`
Executable string `json:"executable"`
Owner string `json:"owner"`
Arch string `json:"arch"`
SessionID string `json:"sessionID"`
}
type ProcessBrowserResult struct {
Processes []ProcessEntry `json:"processes"`
Error string `json:"error,omitempty"`
}
// ProcessBrowserList lists all processes for the visual process browser.
func (a *App) ProcessBrowserList(sessionID string) ProcessBrowserResult {
client, err := a.requireClient()
if err != nil {
return ProcessBrowserResult{Error: err.Error()}
}
a.audit.log("process-browse", sessionID, "")
resp, err := client.RPC.Ps(a.ctx, &sliverpb.PsReq{
Request: &commonpb.Request{SessionID: sessionID},
})
if err != nil {
return ProcessBrowserResult{Error: err.Error()}
}
procs := make([]ProcessEntry, 0, len(resp.Processes))
for _, p := range resp.Processes {
procs = append(procs, ProcessEntry{
PID: p.Pid,
PPID: p.Ppid,
Executable: p.Executable,
Owner: p.Owner,
Arch: p.Architecture,
SessionID: sessionID,
})
}
return ProcessBrowserResult{Processes: procs}
}
// ProcessBrowserKill kills a remote process.
func (a *App) ProcessBrowserKill(sessionID string, pid int32) error {
client, err := a.requireClient()
if err != nil {
return err
}
a.audit.log("process-kill", sessionID, fmt.Sprintf("PID %d", pid))
_, err = client.RPC.Terminate(a.ctx, &sliverpb.TerminateReq{
Pid: pid,
Request: &commonpb.Request{SessionID: sessionID},
})
return err
}
// ─── Internal/External IP Resolution ──────────────────────────────────────────
type AgentIPs struct {
InternalIP string `json:"internalIP"`
ExternalIP string `json:"externalIP"`
Error string `json:"error,omitempty"`
}
// GetAgentIPs returns the internal (private) and external (public/remote) IPs
// for an agent. Internal is pulled from ifconfig; external is the peer address.
func (a *App) GetAgentIPs(sessionID string) AgentIPs {
client, err := a.requireClient()
if err != nil {
return AgentIPs{Error: err.Error()}
}
// Get internal IPs from ifconfig
resp, err := client.RPC.Ifconfig(a.ctx, &sliverpb.IfconfigReq{
Request: &commonpb.Request{SessionID: sessionID},
})
internalIP := ""
if err == nil && resp != nil {
for _, iface := range resp.NetInterfaces {
for _, addr := range iface.IPAddresses {
ip := strings.Split(addr, "/")[0]
if isPrivateIP(ip) && internalIP == "" {
internalIP = ip
}
}
}
}
// Get external IP from the session's remote address (handles IPv4 and IPv6)
externalIP := ""
sessions, _ := client.ListSessions(a.ctx)
for _, s := range sessions {
if s.ID == sessionID {
host, _, err := net.SplitHostPort(s.RemoteAddress)
if err != nil {
// Fallback: might be bare IP without port
host = s.RemoteAddress
}
externalIP = host
break
}
}
return AgentIPs{InternalIP: internalIP, ExternalIP: externalIP}
}
func isPrivateIP(ip string) bool {
privates := []string{"10.", "172.16.", "172.17.", "172.18.", "172.19.",
"172.20.", "172.21.", "172.22.", "172.23.", "172.24.", "172.25.",
"172.26.", "172.27.", "172.28.", "172.29.", "172.30.", "172.31.",
"192.168.", "169.254."}
for _, prefix := range privates {
if strings.HasPrefix(ip, prefix) {
return true
}
}
return false
}
// ─── Credential Auto-Populate ─────────────────────────────────────────────────
type ParsedCredential struct {
Username string `json:"username"`
Password string `json:"password"`
Hash string `json:"hash"`
Source string `json:"source"`
}
// ParseAndStoreCredentials parses script output for well-known credential formats.
// It does NOT auto-store into Sliver's cred DB — call ConfirmAndStoreCredentials
// with the returned slice after the operator reviews them.
func (a *App) ParseAndStoreCredentials(output, source string) []ParsedCredential {
creds := parseCredentials(output)
for i := range creds {
creds[i].Source = source
}
if len(creds) > 0 {
a.audit.log("creds-parsed", "", fmt.Sprintf("%d candidates from %s", len(creds), source))
}
return creds
}
// ConfirmAndStoreCredentials stores operator-confirmed credentials in Sliver's DB.
func (a *App) ConfirmAndStoreCredentials(creds []ParsedCredential) error {
client, err := a.requireClient()
if err != nil {
return err
}
for _, c := range creds {
hashType := int32(0) // plaintext
if c.Hash != "" {
hashType = 1
}
_, _ = client.RPC.CredsAdd(a.ctx, &clientpb.Credentials{
Credentials: []*clientpb.Credential{{
Username: c.Username,
Plaintext: c.Password,
Hash: c.Hash,
HashType: clientpb.HashType(hashType),
Collection: c.Source,
}},
})
}
a.audit.log("creds-stored", "", fmt.Sprintf("%d credentials confirmed+stored", len(creds)))
return nil
}
// Compiled credential regexes — only match well-known output formats.
var (
// SAM dump: user:RID:LM_hash:NTLM_hash:::
reSAMHash = regexp.MustCompile(`(?m)^([\w$\.\-]+):\d+:[a-fA-F0-9]{32}:([a-fA-F0-9]{32})`)
// impacket secretsdump: DOMAIN\user:plaintext or DOMAIN/user:plaintext (after ":::")
reSecretsDump = regexp.MustCompile(`(?m)^([\w\.\-]+(?:[/\\][\w\.\-]+)?):[^:]+:[a-fA-F0-9]{32}:([a-fA-F0-9]{32})`)
// mimikatz: "Username : value" / "* Password : value" / "* NTLM : hex"
reMimikatzUser = regexp.MustCompile(`(?i)Username\s*:\s*(\S+)`)
reMimikatzPass = regexp.MustCompile(`(?i)\*\s*Password\s*:\s*(.+)$`)
reMimikatzNTLM = regexp.MustCompile(`(?i)\*\s*NTLM\s*:\s*([a-fA-F0-9]{32})`)
)
func parseCredentials(output string) []ParsedCredential {
var creds []ParsedCredential
seen := map[string]bool{}
add := func(user, pass, hash string) {
user = strings.TrimSpace(user)
pass = strings.TrimSpace(pass)
hash = strings.TrimSpace(hash)
if user == "" || (pass == "" && hash == "") {
return
}
// Skip obvious noise
if user == "(null)" || pass == "(null)" || pass == "(null" {
return
}
key := user + ":" + pass + ":" + hash
if seen[key] {
return
}
seen[key] = true
creds = append(creds, ParsedCredential{Username: user, Password: pass, Hash: hash})
}
// 1. SAM hash lines: user:RID:LM:NTLM
for _, m := range reSAMHash.FindAllStringSubmatch(output, -1) {
add(m[1], "", m[2])
}
// 2. secretsdump NTLM lines
for _, m := range reSecretsDump.FindAllStringSubmatch(output, -1) {
add(m[1], "", m[2])
}
// 3. mimikatz block parsing: Username line followed by Password/NTLM lines
lines := strings.Split(output, "\n")
var curUser string
for _, line := range lines {
if m := reMimikatzUser.FindStringSubmatch(line); m != nil {
curUser = m[1]
} else if curUser != "" {
if m := reMimikatzPass.FindStringSubmatch(line); m != nil {
p := strings.TrimSpace(m[1])
if p != "" && p != "(null)" {
add(curUser, p, "")
}
}
if m := reMimikatzNTLM.FindStringSubmatch(line); m != nil {
add(curUser, "", m[1])
}
}
// Reset on blank lines (new logon block)
if strings.TrimSpace(line) == "" {
curUser = ""
}
}
return creds
}
// ─── Kill Chain Tracker ───────────────────────────────────────────────────────
// KillChainStage represents the progress of an engagement.
type KillChainState struct {
Recon bool `json:"recon"`
Access bool `json:"access"`
PrivEsc bool `json:"privesc"`
Lateral bool `json:"lateral"`
DomainAdmin bool `json:"domainAdmin"`
Persistence bool `json:"persistence"`
LastUpdate string `json:"lastUpdate"`
}
var (
stateMu sync.Mutex
killChain = KillChainState{}
iocList []IOCEntry
iocCounter int
engagementStart *time.Time
)
// GetKillChain returns current kill chain progress.
func (a *App) GetKillChain() KillChainState {
stateMu.Lock()
defer stateMu.Unlock()
return killChain
}
// UpdateKillChain advances a kill chain stage.
func (a *App) UpdateKillChain(stage string) KillChainState {
stateMu.Lock()
defer stateMu.Unlock()
switch strings.ToLower(stage) {
case "recon":
killChain.Recon = true
case "access":
killChain.Access = true
case "privesc":
killChain.PrivEsc = true
case "lateral":
killChain.Lateral = true
case "domainadmin", "da":
killChain.DomainAdmin = true
case "persistence", "persist":
killChain.Persistence = true
}
killChain.LastUpdate = time.Now().Format(time.RFC3339)
a.audit.log("killchain", stage, "")
return killChain
}
// ResetKillChain resets all stages.
func (a *App) ResetKillChain() KillChainState {
stateMu.Lock()
defer stateMu.Unlock()
killChain = KillChainState{}
return killChain
}
// ResetEngagementState clears all per-engagement state (call on disconnect).
func (a *App) ResetEngagementState() {
stateMu.Lock()
defer stateMu.Unlock()
killChain = KillChainState{}
iocList = nil
iocCounter = 0
engagementStart = nil
a.audit.log("engagement", "reset", "")
}
// ─── Engagement Timer ─────────────────────────────────────────────────────────
// StartEngagementTimer marks the beginning of the engagement.
func (a *App) StartEngagementTimer() string {
stateMu.Lock()
defer stateMu.Unlock()
now := time.Now()
engagementStart = &now
a.audit.log("engagement", "start", now.Format(time.RFC3339))
return now.Format(time.RFC3339)
}
// GetEngagementElapsed returns seconds since engagement started.
func (a *App) GetEngagementElapsed() int64 {
stateMu.Lock()
defer stateMu.Unlock()
if engagementStart == nil {
return 0
}
return int64(time.Since(*engagementStart).Seconds())
}
// ─── Builder Streaming (placeholder — streams are complex in Wails) ───────────
// GetBuilders lists external builders connected to the teamserver.
func (a *App) GetBuilders() ([]map[string]interface{}, error) {
client, err := a.requireClient()
if err != nil {
return nil, err
}
resp, err := client.RPC.Builders(a.ctx, &commonpb.Empty{})
if err != nil {
return nil, err
}
var result []map[string]interface{}
for _, b := range resp.Builders {
result = append(result, map[string]interface{}{
"name": b.Name,
"operator": b.OperatorName,
"goos": b.GOOS,
"goarch": b.GOARCH,
"targets": b.CrossCompilers,
})
}
return result, nil
}
// ─── IOC Tracker ──────────────────────────────────────────────────────────────
// IOCEntry represents an Indicator of Compromise left on a target.
type IOCEntry struct {
ID int `json:"id"`
Timestamp string `json:"timestamp"`
Host string `json:"host"`
Type string `json:"type"` // file, service, regkey, schtask, user, cron
Path string `json:"path"`
Detail string `json:"detail"`
}
// AddIOC records a new IOC. Thread-safe.
func (a *App) AddIOC(host, iocType, path, detail string) IOCEntry {
stateMu.Lock()
defer stateMu.Unlock()
iocCounter++
entry := IOCEntry{
ID: iocCounter,
Timestamp: time.Now().Format("15:04:05"),
Host: host,
Type: iocType,
Path: path,
Detail: detail,
}
iocList = append(iocList, entry)
a.audit.log("ioc-added", host, fmt.Sprintf("%s: %s", iocType, path))
return entry
}
// GetIOCs returns all tracked IOCs.
func (a *App) GetIOCs() []IOCEntry {
stateMu.Lock()
defer stateMu.Unlock()
out := make([]IOCEntry, len(iocList))
copy(out, iocList)
return out
}
// ClearIOCs resets the IOC list.
func (a *App) ClearIOCs() {
stateMu.Lock()
defer stateMu.Unlock()
iocList = nil
iocCounter = 0
}
// GenerateCleanupScript produces a script to remove all IOCs.
func (a *App) GenerateCleanupScript() string {
if len(iocList) == 0 {
return "# No IOCs tracked"
}
var winCmds, linCmds []string
for _, ioc := range iocList {
switch ioc.Type {
case "file":
winCmds = append(winCmds, fmt.Sprintf(`del /f "%s"`, ioc.Path))
linCmds = append(linCmds, fmt.Sprintf(`rm -f "%s"`, ioc.Path))
case "service":
winCmds = append(winCmds, fmt.Sprintf(`sc stop %s & sc delete %s`, ioc.Path, ioc.Path))
linCmds = append(linCmds, fmt.Sprintf(`systemctl stop %s && systemctl disable %s && rm /etc/systemd/system/%s.service`, ioc.Path, ioc.Path, ioc.Path))
case "regkey":
winCmds = append(winCmds, fmt.Sprintf(`reg delete "%s" /f`, ioc.Path))
case "schtask":
winCmds = append(winCmds, fmt.Sprintf(`schtasks /delete /tn "%s" /f`, ioc.Path))
case "cron":
linCmds = append(linCmds, fmt.Sprintf(`sed -i '/%s/d' /etc/crontab`, strings.ReplaceAll(ioc.Path, "/", `\/`)))
case "user":
winCmds = append(winCmds, fmt.Sprintf(`net user %s /delete`, ioc.Path))
linCmds = append(linCmds, fmt.Sprintf(`userdel -r %s`, ioc.Path))
}
}
script := "# ═══ VulnNetRed IOC Cleanup Script ═══\n"
script += fmt.Sprintf("# Generated: %s\n", time.Now().Format(time.RFC3339))
script += fmt.Sprintf("# IOCs tracked: %d\n\n", len(iocList))
if len(winCmds) > 0 {
script += "# ── Windows Cleanup ──\n"
for _, c := range winCmds {
script += c + "\n"
}
script += "\n"
}
if len(linCmds) > 0 {
script += "# ── Linux Cleanup ──\n"
for _, c := range linCmds {
script += c + "\n"
}
}
return script
}
// ─── Engagement Report ────────────────────────────────────────────────────────
// GenerateReport creates a Markdown engagement report from audit log + IOCs + kill chain.
func (a *App) GenerateReport() string {
elapsed := a.GetEngagementElapsed()
h := elapsed / 3600
m := (elapsed % 3600) / 60
s := elapsed % 60
report := "# Engagement Report\n\n"
report += fmt.Sprintf("**Duration:** %02d:%02d:%02d\n\n", h, m, s)
report += fmt.Sprintf("**Generated:** %s\n\n", time.Now().Format(time.RFC3339))
// Kill Chain
report += "## Kill Chain Progress\n\n"
report += "| Stage | Status |\n|-------|--------|\n"
stages := []struct {
name string
done bool
}{
{"Reconnaissance", killChain.Recon},
{"Initial Access", killChain.Access},
{"Privilege Escalation", killChain.PrivEsc},
{"Lateral Movement", killChain.Lateral},
{"Domain Admin", killChain.DomainAdmin},
{"Persistence", killChain.Persistence},
}
for _, st := range stages {
status := "[----]"
if st.done {
status = "[DONE]"
}
report += fmt.Sprintf("| %s | %s |\n", st.name, status)
}
// IOCs
report += "\n## Indicators of Compromise\n\n"
if len(iocList) == 0 {
report += "No IOCs tracked.\n"
} else {
report += "| Time | Host | Type | Path | Detail |\n|------|------|------|------|--------|\n"
for _, ioc := range iocList {
report += fmt.Sprintf("| %s | %s | %s | `%s` | %s |\n",
ioc.Timestamp, ioc.Host, ioc.Type, ioc.Path, ioc.Detail)
}
}
// Audit trail (last 50 entries)
report += "\n## Operator Actions (Last 50)\n\n"
entries, _ := a.RecentAudit(50)
if len(entries) > 0 {
report += "| Time | Action | Target | Detail |\n|------|--------|--------|--------|\n"
for _, e := range entries {
report += fmt.Sprintf("| %s | %s | %s | %s |\n",
e.Time, e.Action, e.Target, e.Detail)
}
} else {
report += "No audit entries.\n"
}
// Cleanup script
report += "\n## Cleanup Script\n\n```bash\n"
report += a.GenerateCleanupScript()
report += "\n```\n"
return report
}
+2000
View File
File diff suppressed because it is too large Load Diff