diff --git a/.gitignore b/.gitignore index 57b7d41..0588182 100644 --- a/.gitignore +++ b/.gitignore @@ -56,7 +56,7 @@ Desktop.ini # Local build / test output (not tracked) /spread-kits/ -server/coverage/ -server/builder_cov/ -server/$cov/ -server/internal/builder/cov/ +/server/coverage +/server/builder_cov +/server/internal/builder/cov +/server/*cov* diff --git a/server/web/src/pages/CruciblePage.css b/server/web/src/pages/CruciblePage.css index 0af0195..9376f56 100644 --- a/server/web/src/pages/CruciblePage.css +++ b/server/web/src/pages/CruciblePage.css @@ -715,3 +715,157 @@ letter-spacing: 0.05em; } .crucible-btn-muted:hover { color: var(--text-primary); border-color: rgba(255,255,255,0.25); } + +/* ── Rich terminal renderers ──────────────────────────────────────────────── */ + +.rich-line { + align-items: flex-start !important; +} + +.ctl-rich { + flex: 1; + overflow-x: auto; +} + +.rich-block { + margin: 0.2rem 0 0.4rem 0; + padding: 0.5rem 0.75rem; + border-left: 2px solid rgba(0, 245, 255, 0.3); + background: rgba(0, 0, 0, 0.35); + border-radius: 0 4px 4px 0; + font-family: var(--font-tech); + font-size: 0.76rem; + max-width: 720px; +} + +.rich-ports { border-left-color: rgba(0, 245, 255, 0.5); } +.rich-patch { border-left-color: rgba(255, 200, 0, 0.5); } +.rich-posture { border-left-color: rgba(150, 120, 255, 0.5); } + +.rich-header { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 0.45rem; +} + +.rich-label { + color: var(--neon-cyan); + font-weight: 700; + letter-spacing: 0.1em; + font-size: 0.72rem; +} + +.rich-count { + color: var(--text-muted); + font-size: 0.68rem; +} + +.rich-score { + font-weight: 700; + letter-spacing: 0.05em; + padding: 0.1rem 0.45rem; + border-radius: 3px; + font-size: 0.76rem; +} +.rich-score-ok { color: #00ff9d; background: rgba(0,255,100,0.1); } +.rich-score-warn { color: #ffc800; background: rgba(255,200,0,0.1); } +.rich-score-bad { color: #ff4444; background: rgba(255,50,0,0.12); } + +.rich-tag { + font-size: 0.68rem; + padding: 0.1rem 0.4rem; + border-radius: 3px; + font-weight: 700; + letter-spacing: 0.08em; +} +.rich-tag-warn { color: #ff4444; background: rgba(255, 50, 0, 0.15); border: 1px solid rgba(255,50,0,0.3); } + +.rich-empty { + color: var(--text-muted); + font-style: italic; + font-size: 0.72rem; + padding: 0.25rem 0; +} + +/* Port/Patch/Posture tables */ +.rich-table { + border-collapse: collapse; + width: 100%; + font-size: 0.74rem; +} + +.rich-table th { + color: var(--text-muted); + text-align: left; + padding: 0.1rem 0.6rem 0.15rem 0; + letter-spacing: 0.08em; + font-size: 0.66rem; + border-bottom: 1px solid rgba(255,255,255,0.07); + white-space: nowrap; +} + +.rich-table td { + padding: 0.18rem 0.6rem 0.18rem 0; + white-space: nowrap; + color: var(--text-primary); +} + +.rich-table tbody tr:hover { background: rgba(255,255,255,0.03); } + +.rich-row-ssh td { color: #00ff9d; } +.rich-row-sys td:first-child { color: var(--neon-cyan); } + +.rich-port { font-weight: 700; min-width: 48px; color: var(--neon-cyan) !important; } +.rich-addr { color: var(--text-muted) !important; min-width: 100px; } +.rich-proto { color: rgba(180,160,255,0.85) !important; min-width: 50px; } +.rich-proc { color: var(--text-primary); min-width: 100px; } +.rich-pid { color: var(--text-muted) !important; font-size: 0.68rem; } + +/* Key-value rows */ +.rich-kv-row { + display: flex; + gap: 0.5rem; + padding: 0.12rem 0; + align-items: baseline; +} + +.rich-key { + color: var(--text-muted); + min-width: 140px; + font-size: 0.71rem; + flex-shrink: 0; +} + +.rich-val { color: var(--text-primary); font-size: 0.74rem; } +.rich-val-ok { color: #00ff9d; } +.rich-val-warn { color: #ffc800; } +.rich-val-bad { color: #ff4444; } + +.rich-posture-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0 1.5rem; +} + +.rich-sub-label { + margin: 0.5rem 0 0.2rem; + color: var(--neon-cyan); + font-size: 0.66rem; + letter-spacing: 0.1em; + opacity: 0.7; +} + +.rich-svc-status { + font-weight: 700; + font-size: 0.7rem; + letter-spacing: 0.05em; +} + +.blink-slow { + animation: blink-slow 1.5s step-end infinite; +} +@keyframes blink-slow { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.3; } +} diff --git a/server/web/src/pages/CruciblePage.tsx b/server/web/src/pages/CruciblePage.tsx index d6521c9..00b64d8 100644 --- a/server/web/src/pages/CruciblePage.tsx +++ b/server/web/src/pages/CruciblePage.tsx @@ -579,6 +579,137 @@ export default function CruciblePage() { }; }; + // ── Rich terminal renderers ──────────────────────────────────────────── + + const RichListenPortsTable = ({ d }: { d: RichListenPorts }) => ( +
+
+ LISTEN PORTS + {d.count} listener{d.count !== 1 ? 's' : ''} +
+ {d.ports.length === 0 ? ( +
No listeners found
+ ) : ( + + + + + + + + + + + + {d.ports.map((p, i) => ( + + + + + + + + ))} + +
PORTADDRPROTOPROCESSPID
{p.port}{p.addr || '*'}{p.proto.toUpperCase()}{p.process || '—'}{p.pid ?? '—'}
+ )} +
+ ); + + const RichPatchStatusBlock = ({ d }: { d: RichPatchStatus }) => ( +
+
+ PATCH STATUS + {d.reboot_pending && REBOOT REQUIRED} +
+
+ Pending Updates + 0 ? 'rich-val-warn' : 'rich-val-ok'}`}> + {d.pending_updates ?? 0} + +
+
+ Last Patch + 60 ? 'rich-val-bad' : (d.last_patch_days ?? 0) > 30 ? 'rich-val-warn' : 'rich-val-ok'}`}> + {d.last_patch ?? '—'}{d.last_patch_days != null ? ` (${d.last_patch_days}d ago)` : ''} + +
+
+ Reboot Pending + + {d.reboot_pending ? 'YES' : 'NO'} + +
+
+ ); + + const fwIcon = (on?: boolean) => on ? ON : OFF; + + const RichPostureSummaryBlock = ({ d }: { d: RichPostureSummary }) => ( +
+
+ POSTURE REPORT + {d.posture_score != null && ( + = 70 ? 'rich-score-ok' : d.posture_score >= 40 ? 'rich-score-warn' : 'rich-score-bad'}`}> + {d.posture_score} / 100 + + )} +
+
+
Defender + {d.defender_enabled ? 'ON' : 'OFF'} +
+
Real-Time Prot + {d.defender_rtp ? 'ON' : 'OFF'} +
+
Firewall Domain/Priv/Pub + {fwIcon(d.firewall_domain)} / {fwIcon(d.firewall_private)} / {fwIcon(d.firewall_public)} +
+
AV Products + {d.av_products?.join(', ') || '—'} +
+
SSH Listening + {d.ssh_listening ? 'YES' : 'NO'} +
+
Agent Elevated + {d.agent_elevated ? 'YES (ADMIN)' : 'no'} +
+ {d.last_patch_days != null && ( +
Last Patch + 60 ? 'rich-val-bad' : d.last_patch_days > 30 ? 'rich-val-warn' : 'rich-val-ok'}`}> + {d.last_patch_days}d ago{d.pending_updates ? ` · ${d.pending_updates} pending` : ''} + {d.reboot_pending ? ' · REBOOT!' : ''} + +
+ )} +
+ {d.services && d.services.length > 0 && ( + <> +
SERVICES
+ + + + {d.services.map((s, i) => ( + + + + + + ))} + +
SERVICESTATUSSTART
{s.display_name || s.name}{s.status.toUpperCase()}{s.start_type}
+ + )} +
+ ); + + const renderRichData = (d: RichTermData) => { + if (d.type === 'listen_ports') return ; + if (d.type === 'patch_status') return ; + if (d.type === 'posture') return ; + return null; + }; + // ── Render ───────────────────────────────────────────────────────────── return ( @@ -929,7 +1060,7 @@ export default function CruciblePage() { return (
{line.agentName.slice(0, 12).padEnd(12)} @@ -937,7 +1068,11 @@ export default function CruciblePage() { {line.isCmd ? '▶' : '◀'} - {line.text} + {line.richData ? ( + {renderRichData(line.richData)} + ) : ( + {line.text} + )} {line.ts.toLocaleTimeString()}
);