Exposure panel - person risk and OSINT findings
exposure-person-panel/ · 4 files
componentskill: design-taste-frontendsecuritydashboarddenseevidencedark
Brief
The prompt that produced this. Re-run it to generate more in the same taste.
- Aesthetic
- security console - dark, banded severity, dense, badge-led, evidence-first, unsentimental
- Reference
- attack-surface monitor person detail
- Intent
- hold everything known about one person’s exposure in a single column, so a responder can judge severity and act without opening a second view
- Guardrails
- always band severity by colour and by label, never colour alone, show when the exposure was last checked, not just what it found, keep each finding beside the tool that produced it, let the score be traced back to the events that moved itnever present a breach as a person’s fault, show a risk score without its band, hide the source of a finding, imply a check is current when it has not been run
Install
pnpm dlx shadcn@latest add http://localhost:3040/r/exposure-person-panel.json
Source
Meta block and library type import stripped - this is what pastes cleanly elsewhere.
'use client'
import { useState } from 'react'
import type { EventInput, Person, SecurityEvent } from './lib'
import { PersonDetailPanel } from './person-panel'
/**
* Invented person, invented employer, invented breaches. Everything a panel
* like this renders is by nature personal data, so nothing here corresponds
* to a real person, address, account or organisation.
*/
const BASE: Person = {
id: 'p-4471',
name: 'Robin Aleixo',
email: 'r.aleixo@example.com',
personalEmail: 'robin.aleixo@example.net',
extraEmails: ['r.aleixo@contractor.example'],
phoneNumbers: ['+33 6 00 00 00 00'],
role: 'Platform Engineer',
department: 'Infrastructure',
domain: 'example.com',
skills: ['Kubernetes', 'Terraform', 'Go'],
links: {
github: 'https://github.com/example',
linkedin: 'https://www.linkedin.com/in/example',
},
riskLevel: 'high',
note: 'Holds production cluster credentials; treat any credential exposure as urgent.',
addedAt: '2026-02-11T09:00:00Z',
leakStatus: 'exposed',
lastChecked: '2026-07-29T22:14:00Z',
breaches: [
{
name: 'ExampleForum',
title: 'Example Forum',
domain: 'forum.example',
breachDate: '2024-03-02',
pwnCount: 8_400_000,
dataClasses: ['Email', 'Password hash'],
},
{
name: 'DemoTracker',
title: 'Demo Tracker',
domain: 'demotracker.example',
breachDate: '2022-11-19',
pwnCount: 1_120_000,
dataClasses: ['Email', 'IP address'],
},
],
events: [
{
id: 'e-1',
type: 'phishing_fail',
date: '2026-07-02',
points: 12,
note: 'Clicked a credential-harvest link in a simulated campaign.',
},
{ id: 'e-2', type: 'training_completed', date: '2026-07-14', points: -8 },
{ id: 'e-3', type: 'mfa_disabled', date: '2026-06-28', points: 15 },
],
osint: {
checkedAt: '2026-07-29T22:14:00Z',
findings: [
{
id: 'f-1',
kind: 'github',
tool: 'gitleaks',
target: 'github.com/example',
title: 'Cloud key committed to a public repository',
value: 'AKIA…REDACTED',
severity: 'warning',
},
{
id: 'f-2',
kind: 'email-security',
tool: 'dmarc',
target: 'example.com',
title: 'DMARC policy set to none',
severity: 'warning',
},
{
id: 'f-3',
kind: 'account',
tool: 'sherlock',
target: 'r-aleixo',
title: 'Account reuse across 4 public services',
severity: 'info',
},
],
},
riskScore: 74,
riskBand: 'high',
}
export default function ExposurePersonPanel() {
const [person, setPerson] = useState<Person>(BASE)
const [checking, setChecking] = useState(false)
const addEvent = (input: EventInput) => {
const event: SecurityEvent = {
id: `e-${person.events.length + 1}`,
type: input.type,
date: input.date ?? new Date().toISOString().slice(0, 10),
points: 0,
note: input.note,
}
setPerson((p) => ({ ...p, events: [event, ...p.events] }))
}
return (
// The panel themes itself with semantic tokens (bg-card, text-muted-
// foreground), so the wrapper must not force a palette - hardcoding a dark
// surface here put near-white inherited text on the panel's light card.
<main className="min-h-[100dvh] bg-background p-6 text-foreground">
<div className="mx-auto max-w-3xl">
<p className="font-mono text-[11px] text-muted-foreground uppercase tracking-widest">
Attack surface / people
</p>
<h1 className="mt-1 font-semibold text-2xl tracking-[-0.02em]">Exposure review</h1>
<p className="mt-3 max-w-prose text-muted-foreground text-sm">
The panel is a fixed slide-over with its own scrim; this page is what it covers.
</p>
<PersonDetailPanel
person={person}
checking={checking}
enriching={false}
onClose={() => undefined}
// Flips the spinner so the checking state is reachable in the preview.
onCheck={() => {
setChecking(true)
setTimeout(() => setChecking(false), 1200)
}}
onEnrich={() => undefined}
onEdit={() => undefined}
onAddEvent={addEvent}
onRemoveEvent={(id) =>
setPerson((p) => ({ ...p, events: p.events.filter((e) => e.id !== id) }))
}
/>
</div>
</main>
)
}