feat(monitor/frontend): KEV panel (recent CISA additions, 6-row tight table)
KevStore (zustand) accumulates KevEntry items dedup-by-cveID, capped at 200. Panel seeds the store from snapshot.kev_added on mount. 6-row table — CVE-ID mono / vendor·product sans (--fg-2) / dateAdded mono muted (--fg-3). table-layout: fixed + per-column widths (38% / 40% / 22%) + ellipsis on overflow. Hover --bg-row-hover (subtle, no transition). No severity column, no fluff per the plan: every row in this catalog is a known-exploited vulnerability, the existence of the row carries the weight. No "ransomware-use" badge — that's the separate Ransomware panel's job. No CTA — operator clicks the catalog via the raw link. Real KevEntry shape verified against live snapshot.kev_added: camelCase keys cveID/vendorProject/product/vulnerabilityName/dateAdded/dueDate/ knownRansomwareCampaignUse/shortDescription/requiredAction. Mirrored exactly in the TS interface (no transform layer where bugs hide). Plan 5 Task 16 Design QA: six rows max, CVE-ID mono primary text, vendor · product body sans secondary, date mono muted, no severity badge.
This commit is contained in:
parent
79cbdaaa51
commit
4724f7fe92
|
|
@ -4,6 +4,7 @@
|
|||
import { Globe } from '@/pages/globe/Globe'
|
||||
import { CVEVelocityPanel } from '@/pages/panels/CVEVelocityPanel'
|
||||
import { DShieldPanel } from '@/pages/panels/DShieldPanel'
|
||||
import { KEVPanel } from '@/pages/panels/KEVPanel'
|
||||
import { useUIStore } from '@/stores/ui'
|
||||
import { AlertBanner } from './AlertBanner'
|
||||
import { BottomTicker } from './BottomTicker'
|
||||
|
|
@ -30,6 +31,7 @@ export function Dashboard(): React.ReactElement {
|
|||
</section>
|
||||
<aside className={styles.right}>
|
||||
<CVEVelocityPanel />
|
||||
<KEVPanel />
|
||||
<DShieldPanel />
|
||||
</aside>
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
// ©AngelaMos | 2026
|
||||
// KEVPanel.module.scss
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.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);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.table tbody tr:hover {
|
||||
background: var(--bg-row-hover);
|
||||
}
|
||||
|
||||
.cveId {
|
||||
font-family: var(--font-mono);
|
||||
width: 38%;
|
||||
}
|
||||
|
||||
.vp {
|
||||
color: var(--fg-2);
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.date {
|
||||
font-family: var(--font-mono);
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--fg-3);
|
||||
width: 22%;
|
||||
text-align: right;
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
// ©AngelaMos | 2026
|
||||
// KEVPanel.tsx
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import { useSnapshot } from '@/api/snapshot'
|
||||
import { type KevEntry, useKevStore } from '@/stores/kev'
|
||||
import styles from './KEVPanel.module.scss'
|
||||
import { Panel } from './Panel'
|
||||
|
||||
const KEV_ROW_LIMIT = 6
|
||||
|
||||
export function KEVPanel(): React.ReactElement {
|
||||
const { data } = useSnapshot()
|
||||
const items = useKevStore((s) => s.items)
|
||||
const push = useKevStore((s) => s.push)
|
||||
|
||||
const seed = data?.kev_added as KevEntry | undefined
|
||||
useEffect(() => {
|
||||
if (seed?.cveID) push(seed)
|
||||
}, [seed, push])
|
||||
|
||||
const recent = items.slice(0, KEV_ROW_LIMIT)
|
||||
|
||||
return (
|
||||
<Panel
|
||||
title="KEV"
|
||||
subtitle="CISA EXPLOITED"
|
||||
rawHref="https://www.cisa.gov/known-exploited-vulnerabilities-catalog"
|
||||
rawLabel="CISA KEV catalog"
|
||||
>
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className={styles.cveId}>CVE</th>
|
||||
<th className={styles.vp}>Vendor · Product</th>
|
||||
<th className={styles.date}>Added</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{recent.map((k) => (
|
||||
<tr key={k.cveID}>
|
||||
<td className={styles.cveId}>{k.cveID}</td>
|
||||
<td className={styles.vp}>
|
||||
{k.vendorProject} · {k.product}
|
||||
</td>
|
||||
<td className={styles.date}>{k.dateAdded}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
|
||||
KEVPanel.displayName = 'KEVPanel'
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
// ©AngelaMos | 2026
|
||||
// kev.ts
|
||||
|
||||
import { create } from 'zustand'
|
||||
|
||||
export interface KevEntry {
|
||||
cveID: string
|
||||
vendorProject: string
|
||||
product: string
|
||||
vulnerabilityName: string
|
||||
dateAdded: string
|
||||
dueDate?: string
|
||||
knownRansomwareCampaignUse?: string
|
||||
shortDescription?: string
|
||||
requiredAction?: string
|
||||
}
|
||||
|
||||
interface KevStore {
|
||||
items: KevEntry[]
|
||||
push: (item: KevEntry) => void
|
||||
clear: () => void
|
||||
}
|
||||
|
||||
const KEV_CAP = 200
|
||||
|
||||
export const useKevStore = create<KevStore>((set) => ({
|
||||
items: [],
|
||||
push: (item) =>
|
||||
set((s) => {
|
||||
const filtered = s.items.filter((i) => i.cveID !== item.cveID)
|
||||
return { items: [item, ...filtered].slice(0, KEV_CAP) }
|
||||
}),
|
||||
clear: () => set({ items: [] }),
|
||||
}))
|
||||
Loading…
Reference in New Issue