Add Crucible rich terminal renderers and ignore local coverage dirs.
Listen ports table UI and styles; gitignore for spread-kits and cov artifact files.
This commit is contained in:
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -579,6 +579,137 @@ export default function CruciblePage() {
|
||||
};
|
||||
};
|
||||
|
||||
// ── Rich terminal renderers ────────────────────────────────────────────
|
||||
|
||||
const RichListenPortsTable = ({ d }: { d: RichListenPorts }) => (
|
||||
<div className="rich-block rich-ports">
|
||||
<div className="rich-header">
|
||||
<span className="rich-label">LISTEN PORTS</span>
|
||||
<span className="rich-count">{d.count} listener{d.count !== 1 ? 's' : ''}</span>
|
||||
</div>
|
||||
{d.ports.length === 0 ? (
|
||||
<div className="rich-empty">No listeners found</div>
|
||||
) : (
|
||||
<table className="rich-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>PORT</th>
|
||||
<th>ADDR</th>
|
||||
<th>PROTO</th>
|
||||
<th>PROCESS</th>
|
||||
<th>PID</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{d.ports.map((p, i) => (
|
||||
<tr key={i} className={p.port === 22 ? 'rich-row-ssh' : p.port < 1024 ? 'rich-row-sys' : ''}>
|
||||
<td className="rich-port">{p.port}</td>
|
||||
<td className="rich-addr">{p.addr || '*'}</td>
|
||||
<td className="rich-proto">{p.proto.toUpperCase()}</td>
|
||||
<td className="rich-proc">{p.process || '—'}</td>
|
||||
<td className="rich-pid">{p.pid ?? '—'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const RichPatchStatusBlock = ({ d }: { d: RichPatchStatus }) => (
|
||||
<div className="rich-block rich-patch">
|
||||
<div className="rich-header">
|
||||
<span className="rich-label">PATCH STATUS</span>
|
||||
{d.reboot_pending && <span className="rich-tag rich-tag-warn blink-slow">REBOOT REQUIRED</span>}
|
||||
</div>
|
||||
<div className="rich-kv-row">
|
||||
<span className="rich-key">Pending Updates</span>
|
||||
<span className={`rich-val ${(d.pending_updates ?? 0) > 0 ? 'rich-val-warn' : 'rich-val-ok'}`}>
|
||||
{d.pending_updates ?? 0}
|
||||
</span>
|
||||
</div>
|
||||
<div className="rich-kv-row">
|
||||
<span className="rich-key">Last Patch</span>
|
||||
<span className={`rich-val ${(d.last_patch_days ?? 0) > 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)` : ''}
|
||||
</span>
|
||||
</div>
|
||||
<div className="rich-kv-row">
|
||||
<span className="rich-key">Reboot Pending</span>
|
||||
<span className={`rich-val ${d.reboot_pending ? 'rich-val-warn' : 'rich-val-ok'}`}>
|
||||
{d.reboot_pending ? 'YES' : 'NO'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const fwIcon = (on?: boolean) => on ? <span className="rich-val-ok">ON</span> : <span className="rich-val-bad">OFF</span>;
|
||||
|
||||
const RichPostureSummaryBlock = ({ d }: { d: RichPostureSummary }) => (
|
||||
<div className="rich-block rich-posture">
|
||||
<div className="rich-header">
|
||||
<span className="rich-label">POSTURE REPORT</span>
|
||||
{d.posture_score != null && (
|
||||
<span className={`rich-score ${d.posture_score >= 70 ? 'rich-score-ok' : d.posture_score >= 40 ? 'rich-score-warn' : 'rich-score-bad'}`}>
|
||||
{d.posture_score} / 100
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="rich-posture-grid">
|
||||
<div className="rich-kv-row"><span className="rich-key">Defender</span>
|
||||
<span className={`rich-val ${d.defender_enabled ? 'rich-val-ok' : 'rich-val-bad'}`}>{d.defender_enabled ? 'ON' : 'OFF'}</span>
|
||||
</div>
|
||||
<div className="rich-kv-row"><span className="rich-key">Real-Time Prot</span>
|
||||
<span className={`rich-val ${d.defender_rtp ? 'rich-val-ok' : 'rich-val-warn'}`}>{d.defender_rtp ? 'ON' : 'OFF'}</span>
|
||||
</div>
|
||||
<div className="rich-kv-row"><span className="rich-key">Firewall Domain/Priv/Pub</span>
|
||||
<span className="rich-val">{fwIcon(d.firewall_domain)} / {fwIcon(d.firewall_private)} / {fwIcon(d.firewall_public)}</span>
|
||||
</div>
|
||||
<div className="rich-kv-row"><span className="rich-key">AV Products</span>
|
||||
<span className="rich-val">{d.av_products?.join(', ') || '—'}</span>
|
||||
</div>
|
||||
<div className="rich-kv-row"><span className="rich-key">SSH Listening</span>
|
||||
<span className={`rich-val ${d.ssh_listening ? 'rich-val-ok' : 'rich-val-bad'}`}>{d.ssh_listening ? 'YES' : 'NO'}</span>
|
||||
</div>
|
||||
<div className="rich-kv-row"><span className="rich-key">Agent Elevated</span>
|
||||
<span className={`rich-val ${d.agent_elevated ? 'rich-val-warn' : 'rich-val-ok'}`}>{d.agent_elevated ? 'YES (ADMIN)' : 'no'}</span>
|
||||
</div>
|
||||
{d.last_patch_days != null && (
|
||||
<div className="rich-kv-row"><span className="rich-key">Last Patch</span>
|
||||
<span className={`rich-val ${d.last_patch_days > 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!' : ''}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{d.services && d.services.length > 0 && (
|
||||
<>
|
||||
<div className="rich-sub-label">SERVICES</div>
|
||||
<table className="rich-table">
|
||||
<thead><tr><th>SERVICE</th><th>STATUS</th><th>START</th></tr></thead>
|
||||
<tbody>
|
||||
{d.services.map((s, i) => (
|
||||
<tr key={i}>
|
||||
<td className="rich-proc">{s.display_name || s.name}</td>
|
||||
<td className={`rich-svc-status ${s.status === 'running' ? 'rich-val-ok' : 'rich-val-bad'}`}>{s.status.toUpperCase()}</td>
|
||||
<td className="rich-addr">{s.start_type}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderRichData = (d: RichTermData) => {
|
||||
if (d.type === 'listen_ports') return <RichListenPortsTable d={d} />;
|
||||
if (d.type === 'patch_status') return <RichPatchStatusBlock d={d} />;
|
||||
if (d.type === 'posture') return <RichPostureSummaryBlock d={d} />;
|
||||
return null;
|
||||
};
|
||||
|
||||
// ── Render ─────────────────────────────────────────────────────────────
|
||||
|
||||
return (
|
||||
@@ -929,7 +1060,7 @@ export default function CruciblePage() {
|
||||
return (
|
||||
<div
|
||||
key={line.id}
|
||||
className={`crucible-term-line ${line.isCmd ? 'cmd-line' : 'out-line'} ${line.success === false ? 'err-line' : ''}`}
|
||||
className={`crucible-term-line ${line.isCmd ? 'cmd-line' : 'out-line'} ${line.success === false ? 'err-line' : ''} ${line.richData ? 'rich-line' : ''}`}
|
||||
>
|
||||
<span className="ctl-agent" style={{ color }}>
|
||||
{line.agentName.slice(0, 12).padEnd(12)}
|
||||
@@ -937,7 +1068,11 @@ export default function CruciblePage() {
|
||||
<span className="ctl-arrow" style={{ color }}>
|
||||
{line.isCmd ? '▶' : '◀'}
|
||||
</span>
|
||||
<span className="ctl-text">{line.text}</span>
|
||||
{line.richData ? (
|
||||
<span className="ctl-text ctl-rich">{renderRichData(line.richData)}</span>
|
||||
) : (
|
||||
<span className="ctl-text">{line.text}</span>
|
||||
)}
|
||||
<span className="ctl-ts">{line.ts.toLocaleTimeString()}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user