From 4724f7fe9238f047e16e8aab19a0987baa2f04db Mon Sep 17 00:00:00 2001 From: CarterPerez-dev Date: Sun, 3 May 2026 09:22:02 -0400 Subject: [PATCH] feat(monitor/frontend): KEV panel (recent CISA additions, 6-row tight table) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../src/pages/dashboard/Dashboard.tsx | 2 + .../src/pages/panels/KEVPanel.module.scss | 50 +++++++++++++++++ .../frontend/src/pages/panels/KEVPanel.tsx | 55 +++++++++++++++++++ .../frontend/src/stores/kev.ts | 34 ++++++++++++ 4 files changed, 141 insertions(+) create mode 100644 PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/KEVPanel.module.scss create mode 100644 PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/KEVPanel.tsx create mode 100644 PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/stores/kev.ts diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/dashboard/Dashboard.tsx b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/dashboard/Dashboard.tsx index 6f449d57..8e2a998f 100644 --- a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/dashboard/Dashboard.tsx +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/dashboard/Dashboard.tsx @@ -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 { diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/KEVPanel.module.scss b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/KEVPanel.module.scss new file mode 100644 index 00000000..0eecb582 --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/KEVPanel.module.scss @@ -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; +} diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/KEVPanel.tsx b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/KEVPanel.tsx new file mode 100644 index 00000000..e0968901 --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/KEVPanel.tsx @@ -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 ( + + + + + + + + + + + {recent.map((k) => ( + + + + + + ))} + +
CVEVendor · ProductAdded
{k.cveID} + {k.vendorProject} · {k.product} + {k.dateAdded}
+
+ ) +} + +KEVPanel.displayName = 'KEVPanel' diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/stores/kev.ts b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/stores/kev.ts new file mode 100644 index 00000000..0682169f --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/stores/kev.ts @@ -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((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: [] }), +}))