Extend owned-fleet control with scheduled tasks, audit log, file browser, HTTPS beacon when WS drops, protocol tunnels, registry/autostart forge options, KEV exposure in full sys check with Telegram alerts, and UI/tests.
291 lines
12 KiB
TypeScript
291 lines
12 KiB
TypeScript
import type { ReactNode } from 'react';
|
|
import type { FullSysCheckReport } from '../../types/syscheck';
|
|
import './FullSysCheckPanel.css';
|
|
|
|
function Row({ label, value }: { label: string; value: ReactNode }) {
|
|
if (value === undefined || value === null || value === '') return null;
|
|
return (
|
|
<div className="syscheck-kv">
|
|
<span className="syscheck-k">{label}</span>
|
|
<span className="syscheck-v">{value}</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Section({ title, children }: { title: string; children: ReactNode }) {
|
|
return (
|
|
<section className="syscheck-section">
|
|
<h4 className="syscheck-section-title font-tech">{title}</h4>
|
|
<div className="syscheck-section-body">{children}</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function BoolBadge({ v, yes = 'YES', no = 'NO' }: { v?: boolean; yes?: string; no?: string }) {
|
|
if (v === undefined) return <span className="syscheck-muted">—</span>;
|
|
return <span className={v ? 'syscheck-ok' : 'syscheck-bad'}>{v ? yes : no}</span>;
|
|
}
|
|
|
|
export default function FullSysCheckPanel({
|
|
report,
|
|
agentName,
|
|
onClose,
|
|
}: {
|
|
report: FullSysCheckReport;
|
|
agentName: string;
|
|
onClose?: () => void;
|
|
}) {
|
|
const geo = report.network?.geo;
|
|
const geoLine =
|
|
geo &&
|
|
[geo.city, geo.region, geo.country].filter(Boolean).join(', ') +
|
|
(geo.isp ? ` · ${geo.isp}` : '') +
|
|
(geo.lat != null && geo.lon != null ? ` (${geo.lat.toFixed(2)}, ${geo.lon.toFixed(2)})` : '');
|
|
|
|
return (
|
|
<div className="syscheck-panel">
|
|
<div className="syscheck-header">
|
|
<div>
|
|
<h3 className="font-display">Full System Check</h3>
|
|
<p className="syscheck-sub font-tech">
|
|
{agentName} · {report.generated_at} · {report.platform}/{report.arch}
|
|
</p>
|
|
</div>
|
|
{onClose && (
|
|
<button type="button" className="btn btn-outline btn-sm" onClick={onClose}>
|
|
Close
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
<div className="syscheck-grid">
|
|
<Section title="Network & Location">
|
|
<Row label="External IP" value={report.network?.external_ip} />
|
|
<Row label="IP Source" value={report.network?.external_ip_source} />
|
|
<Row label="Location" value={geoLine} />
|
|
<Row label="Primary LAN IP" value={report.network?.primary_local_ip} />
|
|
<Row label="Default Gateway" value={report.network?.default_gateway} />
|
|
<Row label="DNS" value={report.network?.dns?.servers?.join(', ')} />
|
|
<Row label="DNS Search" value={report.network?.dns?.search_domains?.join(', ')} />
|
|
<Row label="ARP Neighbors" value={report.neighbors?.arp_count != null ? `${report.neighbors.arp_count} host(s)` : undefined} />
|
|
{report.neighbors?.arp_hosts && report.neighbors.arp_hosts.length > 0 && (
|
|
<pre className="syscheck-pre">{report.neighbors.arp_hosts.join('\n')}</pre>
|
|
)}
|
|
{report.neighbors?.subnet_scan && (
|
|
<>
|
|
<div className="syscheck-k syscheck-subhead">Subnet scan</div>
|
|
<pre className="syscheck-pre">{report.neighbors.subnet_scan}</pre>
|
|
</>
|
|
)}
|
|
</Section>
|
|
|
|
<Section title="Security & Firewall">
|
|
<Row
|
|
label="Posture Score"
|
|
value={
|
|
report.security?.posture_score != null ? (
|
|
<span className="syscheck-score">{report.security.posture_score} / 100</span>
|
|
) : undefined
|
|
}
|
|
/>
|
|
<Row label="Defender" value={<BoolBadge v={report.security?.defender_enabled} yes="ON" no="OFF" />} />
|
|
<Row label="Real-time" value={<BoolBadge v={report.security?.defender_rtp} yes="ON" no="OFF" />} />
|
|
<Row
|
|
label="Firewall D / P / Pub"
|
|
value={
|
|
<>
|
|
<BoolBadge v={report.security?.firewall_domain} yes="on" no="off" /> /{' '}
|
|
<BoolBadge v={report.security?.firewall_private} yes="on" no="off" /> /{' '}
|
|
<BoolBadge v={report.security?.firewall_public} yes="on" no="off" />
|
|
</>
|
|
}
|
|
/>
|
|
<Row label="AV Products" value={report.security?.av_products?.join(', ')} />
|
|
<Row label="SSH" value={<BoolBadge v={report.security?.ssh_listening} />} />
|
|
<Row label="Elevated" value={<BoolBadge v={report.identity?.agent_elevated} yes="ADMIN" no="user" />} />
|
|
<Row label="Pending Updates" value={report.security?.pending_updates} />
|
|
<Row
|
|
label="Last Patch"
|
|
value={
|
|
report.security?.last_patch
|
|
? `${report.security.last_patch}${report.security.last_patch_days != null ? ` (${report.security.last_patch_days}d)` : ''}`
|
|
: undefined
|
|
}
|
|
/>
|
|
<Row label="Reboot Pending" value={<BoolBadge v={report.security?.reboot_pending} />} />
|
|
</Section>
|
|
|
|
{report.kev_exposure && (
|
|
<Section title="CISA KEV Exposure (heuristic)">
|
|
<Row
|
|
label="Risk score"
|
|
value={
|
|
<span
|
|
className={
|
|
report.kev_exposure.risk_score >= 50
|
|
? 'syscheck-bad'
|
|
: report.kev_exposure.risk_score >= 25
|
|
? 'syscheck-warn'
|
|
: 'syscheck-ok'
|
|
}
|
|
>
|
|
{report.kev_exposure.risk_score} / 100
|
|
</span>
|
|
}
|
|
/>
|
|
<Row
|
|
label="Indicators"
|
|
value={`${report.kev_exposure.exposed_count} exposed · ${report.kev_exposure.likely_count} likely · ${report.kev_exposure.critical_count} critical`}
|
|
/>
|
|
{report.kev_exposure.summary && (
|
|
<p className="syscheck-kev-summary">{report.kev_exposure.summary}</p>
|
|
)}
|
|
<ul className="syscheck-kev-list">
|
|
{report.kev_exposure.findings
|
|
?.filter((f) => f.status === 'exposed' || f.status === 'likely')
|
|
.map((f) => (
|
|
<li key={f.cve} className={`syscheck-kev-item syscheck-kev-${f.status}`}>
|
|
<span className="syscheck-kev-cve">{f.cve}</span>
|
|
<span className="syscheck-kev-name">{f.name}</span>
|
|
<span className="syscheck-kev-status">{f.status}</span>
|
|
{f.detail && <span className="syscheck-kev-detail">{f.detail}</span>}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
<p className="syscheck-muted" style={{ marginTop: '0.5rem' }}>
|
|
Read-only checks aligned with CISA known-exploited CVE families (Log4Shell, ProxyLogon, Zerologon, Citrix, Pulse, F5, Confluence, etc.). Verify patches on any "likely" or "exposed" row.
|
|
</p>
|
|
</Section>
|
|
)}
|
|
|
|
<Section title="Hardware">
|
|
<Row label="System" value={[report.hardware?.manufacturer, report.hardware?.model].filter(Boolean).join(' ')} />
|
|
<Row label="Serial / BIOS" value={[report.hardware?.serial, report.hardware?.bios_version].filter(Boolean).join(' · ')} />
|
|
<Row label="RAM" value={report.hardware?.memory_gb != null ? `${report.hardware.memory_gb} GB` : undefined} />
|
|
<Row label="Uptime" value={report.hardware?.uptime_hours != null ? `${report.hardware.uptime_hours} h` : undefined} />
|
|
{report.hardware?.cpus?.map((c, i) => (
|
|
<Row
|
|
key={i}
|
|
label={`CPU ${i + 1}`}
|
|
value={`${c.name ?? 'CPU'} · ${c.cores ?? '?'}c/${c.logical ?? '?'}t · ${c.current_mhz ?? '?'}/${c.max_mhz ?? '?'} MHz`}
|
|
/>
|
|
))}
|
|
{report.hardware?.gpus?.map((g, i) => (
|
|
<Row key={i} label={`GPU ${i + 1}`} value={`${g.name ?? 'GPU'} · driver ${g.driver ?? '—'} · ${g.vram_mb ?? 0} MB`} />
|
|
))}
|
|
{report.hardware?.disks?.map((d, i) => (
|
|
<Row
|
|
key={i}
|
|
label={`Disk ${d.mount ?? i}`}
|
|
value={`${d.free_gb ?? '?'} / ${d.total_gb ?? '?'} GB free (${d.free_pct ?? '?'}%) ${d.fs_type ?? ''}`}
|
|
/>
|
|
))}
|
|
</Section>
|
|
|
|
<Section title="Identity & Agent">
|
|
<Row label="Hostname" value={report.hostname} />
|
|
<Row label="OS" value={report.os_version} />
|
|
<Row label="User" value={report.identity?.username} />
|
|
<Row label="Domain" value={report.identity?.domain} />
|
|
<Row label="Computer" value={report.identity?.computer_name} />
|
|
<Row label="MAC" value={report.identity?.mac_address} />
|
|
<Row label="Worker / Build" value={`${report.worker_name ?? '—'} / ${report.build_id ?? '—'}`} />
|
|
<Row label="Install Dir" value={report.environment?.install_dir} />
|
|
</Section>
|
|
|
|
<Section title="Live Resources">
|
|
<Row label="CPU Freq" value={report.resources?.cpu_freq_mhz != null ? `${report.resources.cpu_freq_mhz} MHz` : undefined} />
|
|
<Row label="Throttle" value={<BoolBadge v={report.resources?.cpu_throttle} yes="YES" no="no" />} />
|
|
<Row label="CPU Temp" value={report.resources?.cpu_temp_c != null ? `${report.resources.cpu_temp_c}°C` : undefined} />
|
|
<Row
|
|
label="Disk (miner vol)"
|
|
value={
|
|
report.resources?.disk_free_pct != null
|
|
? `${report.resources.disk_free_gb} / ${report.resources.disk_total_gb} GB (${report.resources.disk_free_pct}%)`
|
|
: undefined
|
|
}
|
|
/>
|
|
<Row label="GPU" value={report.resources?.gpu_usage_pct != null ? `${report.resources.gpu_usage_pct}% · ${report.resources.gpu_temp_c ?? '?'}°C` : undefined} />
|
|
</Section>
|
|
|
|
<Section title="Listeners">
|
|
<Row label="Open TCP ports" value={report.listen_ports?.count} />
|
|
{report.listen_ports?.ports && report.listen_ports.ports.length > 0 && (
|
|
<div className="syscheck-table-wrap">
|
|
<table className="syscheck-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Port</th>
|
|
<th>Bind</th>
|
|
<th>Process</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{report.listen_ports.ports.slice(0, 40).map((p, i) => (
|
|
<tr key={i}>
|
|
<td>{p.port}</td>
|
|
<td>{p.addr}</td>
|
|
<td>{p.process || p.pid}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</Section>
|
|
|
|
<Section title="Interfaces">
|
|
{report.network?.interfaces?.map((iface, i) => (
|
|
<div key={i} className="syscheck-iface">
|
|
<strong>{iface.name}</strong> {iface.mac && <span className="syscheck-muted"> {iface.mac}</span>}
|
|
{iface.ipv4?.map((ip) => (
|
|
<div key={ip} className="syscheck-muted">
|
|
{ip}
|
|
</div>
|
|
))}
|
|
</div>
|
|
))}
|
|
{report.network?.routes_summary && (
|
|
<>
|
|
<div className="syscheck-k syscheck-subhead">Routes</div>
|
|
<pre className="syscheck-pre">{report.network.routes_summary}</pre>
|
|
</>
|
|
)}
|
|
</Section>
|
|
</div>
|
|
|
|
{(report.raw_sysinfo || report.raw_ipconfig || report.raw_netstat) && (
|
|
<details className="syscheck-raw">
|
|
<summary className="font-tech">Raw dumps (sysinfo / ipconfig / netstat)</summary>
|
|
{report.raw_sysinfo && (
|
|
<>
|
|
<div className="syscheck-k">systeminfo / uname</div>
|
|
<pre className="syscheck-pre">{report.raw_sysinfo}</pre>
|
|
</>
|
|
)}
|
|
{report.raw_ipconfig && (
|
|
<>
|
|
<div className="syscheck-k">ipconfig / ip addr</div>
|
|
<pre className="syscheck-pre">{report.raw_ipconfig}</pre>
|
|
</>
|
|
)}
|
|
{report.raw_netstat && (
|
|
<>
|
|
<div className="syscheck-k">netstat</div>
|
|
<pre className="syscheck-pre">{report.raw_netstat}</pre>
|
|
</>
|
|
)}
|
|
</details>
|
|
)}
|
|
|
|
{report.probe_errors && report.probe_errors.length > 0 && (
|
|
<div className="syscheck-errors">
|
|
{report.probe_errors.map((e, i) => (
|
|
<div key={i}>{e}</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|