feat(monitor/frontend): DShield panel (top ports + top source IPs, dense tables)
Two side-by-side tables fed from snapshot.scan_firehose: Top Ports (port + 24h hits) and Top Sources (IP + CC + hits). Below them a one-line muted mono summary "Xk records · Yk sources · Z targets — last 24h." Tables, not bar charts — operator wants to read the port number 22 / 3389, not stare at a horizontal bar. Port and IP and hit counts are mono with tabular-nums (digit alignment matters). Country code stays sans (text label, not a number). Hover row background var(--bg-row-hover) — subtle, no transform / no transition / no theatre. Defensive empty-state — when snapshot has no scan_firehose yet, tables render as empty (per ethos: no "no data" message). PORT_ROW_LIMIT / SOURCE_ROW_LIMIT / THOUSAND / MILLION constants at module scope (no inline magic). Renders in the right column as the bottom panel per spec §11.1 column ordering.
This commit is contained in:
parent
44f0ff3c3d
commit
a91f290595
|
|
@ -2,6 +2,7 @@
|
||||||
// Dashboard.tsx
|
// Dashboard.tsx
|
||||||
|
|
||||||
import { Globe } from '@/pages/globe/Globe'
|
import { Globe } from '@/pages/globe/Globe'
|
||||||
|
import { DShieldPanel } from '@/pages/panels/DShieldPanel'
|
||||||
import { useUIStore } from '@/stores/ui'
|
import { useUIStore } from '@/stores/ui'
|
||||||
import { AlertBanner } from './AlertBanner'
|
import { AlertBanner } from './AlertBanner'
|
||||||
import { BottomTicker } from './BottomTicker'
|
import { BottomTicker } from './BottomTicker'
|
||||||
|
|
@ -26,7 +27,9 @@ export function Dashboard(): React.ReactElement {
|
||||||
<section className={styles.center}>
|
<section className={styles.center}>
|
||||||
<Globe />
|
<Globe />
|
||||||
</section>
|
</section>
|
||||||
<aside className={styles.right} />
|
<aside className={styles.right}>
|
||||||
|
<DShieldPanel />
|
||||||
|
</aside>
|
||||||
</main>
|
</main>
|
||||||
<BottomTicker />
|
<BottomTicker />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
// ©AngelaMos | 2026
|
||||||
|
// DShieldPanel.module.scss
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1.2fr;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table thead th {
|
||||||
|
font-size: var(--type-label);
|
||||||
|
letter-spacing: var(--letter-spacing-label);
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--fg-3);
|
||||||
|
text-align: left;
|
||||||
|
padding: var(--row-py) var(--row-px);
|
||||||
|
border-bottom: 1px solid var(--fg-4);
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table tbody td {
|
||||||
|
padding: var(--row-py) var(--row-px);
|
||||||
|
font-size: var(--type-body);
|
||||||
|
color: var(--fg-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table tbody tr:hover {
|
||||||
|
background: var(--bg-row-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mono {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary {
|
||||||
|
margin: 8px;
|
||||||
|
font-size: var(--type-body);
|
||||||
|
color: var(--fg-3);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
// ©AngelaMos | 2026
|
||||||
|
// DShieldPanel.tsx
|
||||||
|
|
||||||
|
import { useSnapshot } from '@/api/snapshot'
|
||||||
|
import styles from './DShieldPanel.module.scss'
|
||||||
|
import { Panel } from './Panel'
|
||||||
|
|
||||||
|
const PORT_ROW_LIMIT = 8
|
||||||
|
const SOURCE_ROW_LIMIT = 8
|
||||||
|
const THOUSAND = 1_000
|
||||||
|
const MILLION = 1_000_000
|
||||||
|
|
||||||
|
interface DShieldPort {
|
||||||
|
port: number
|
||||||
|
records: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DShieldSource {
|
||||||
|
ip: string
|
||||||
|
country?: string
|
||||||
|
records: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DShieldDailySummary {
|
||||||
|
records: number
|
||||||
|
sources: number
|
||||||
|
targets: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DShieldData {
|
||||||
|
topports?: DShieldPort[]
|
||||||
|
topips?: DShieldSource[]
|
||||||
|
dailysummary?: DShieldDailySummary[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DShieldPanel(): React.ReactElement {
|
||||||
|
const { data } = useSnapshot()
|
||||||
|
const ds = (data?.scan_firehose as DShieldData | undefined) ?? {}
|
||||||
|
const summary = ds.dailysummary?.[0]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Panel
|
||||||
|
title="DSHIELD"
|
||||||
|
subtitle="MASS SCAN"
|
||||||
|
rawHref="https://isc.sans.edu/api/"
|
||||||
|
rawLabel="DShield API"
|
||||||
|
>
|
||||||
|
<div className={styles.row}>
|
||||||
|
<table className={styles.table}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Port</th>
|
||||||
|
<th>Hits / 24h</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{(ds.topports ?? []).slice(0, PORT_ROW_LIMIT).map((p) => (
|
||||||
|
<tr key={p.port}>
|
||||||
|
<td className={styles.mono}>{p.port}</td>
|
||||||
|
<td className={styles.mono}>{p.records.toLocaleString()}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<table className={styles.table}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Source IP</th>
|
||||||
|
<th>CC</th>
|
||||||
|
<th>Hits</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{(ds.topips ?? []).slice(0, SOURCE_ROW_LIMIT).map((s) => (
|
||||||
|
<tr key={s.ip}>
|
||||||
|
<td className={styles.mono}>{s.ip}</td>
|
||||||
|
<td>{s.country ?? '—'}</td>
|
||||||
|
<td className={styles.mono}>{s.records.toLocaleString()}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{summary && (
|
||||||
|
<p className={styles.summary}>
|
||||||
|
{fmtN(summary.records)} records · {fmtN(summary.sources)} sources ·{' '}
|
||||||
|
{fmtN(summary.targets)} targets — last 24h
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</Panel>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
DShieldPanel.displayName = 'DShieldPanel'
|
||||||
|
|
||||||
|
function fmtN(n: number): string {
|
||||||
|
if (n >= MILLION) return `${(n / MILLION).toFixed(1)}M`
|
||||||
|
if (n >= THOUSAND) return `${(n / THOUSAND).toFixed(1)}k`
|
||||||
|
return String(n)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue