Design Library

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 it
never 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>
  )
}