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:
CarterPerez-dev 2026-05-03 09:22:02 -04:00
parent 79cbdaaa51
commit 4724f7fe92
4 changed files with 141 additions and 0 deletions

View File

@ -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>

View File

@ -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;
}

View File

@ -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'

View File

@ -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: [] }),
}))