From 6e02acf47de02b5adb7e604376e21abac694ecdd Mon Sep 17 00:00:00 2001 From: solsr Date: Mon, 30 Mar 2026 17:16:12 +0200 Subject: [PATCH 1/4] Add Attack Surface Overview page with interactive radial threat map Visual security posture dashboard showing 12 attack vectors as a radial diagram. Nodes are color-coded (red/amber/green) by posture state with click-to-toggle and localStorage persistence. Clicking a node opens a detail card with description, attack tags, and framework guide links. Designed for CSOs to quickly assess and communicate security gaps. Co-Authored-By: Claude Opus 4.6 (1M context) --- components/attack-surface/AttackSurface.css | 397 ++++++++++++++++++ .../attack-surface/AttackSurfaceDashboard.tsx | 339 +++++++++++++++ components/attack-surface/threatData.ts | 259 ++++++++++++ components/index.ts | 1 + docs/pages/attack-surface.mdx | 15 + vocs.config.tsx | 1 + 6 files changed, 1012 insertions(+) create mode 100644 components/attack-surface/AttackSurface.css create mode 100644 components/attack-surface/AttackSurfaceDashboard.tsx create mode 100644 components/attack-surface/threatData.ts create mode 100644 docs/pages/attack-surface.mdx diff --git a/components/attack-surface/AttackSurface.css b/components/attack-surface/AttackSurface.css new file mode 100644 index 00000000..7abbe560 --- /dev/null +++ b/components/attack-surface/AttackSurface.css @@ -0,0 +1,397 @@ +/* Attack Surface — Radial Threat Map */ + +.as-wrap { + max-width: 860px; + margin: 0 auto; +} + +/* Summary Bar */ +.as-summary-bar { + display: flex; + align-items: center; + gap: 20px; + flex-wrap: wrap; + margin-bottom: 12px; + font-size: 14px; + color: var(--color-text-muted); +} + +.as-summary-count { + font-weight: 600; + color: var(--color-text-strong); + font-size: 15px; +} + +.as-summary-legend { + display: flex; + gap: 14px; + align-items: center; +} + +.as-legend-item { + display: flex; + align-items: center; + gap: 5px; + font-size: 13px; +} + +.as-legend-dot { + width: 10px; + height: 10px; + border-radius: 50%; + flex-shrink: 0; +} + +/* SVG Map Container — 10% bigger */ +.as-map { + width: 100%; + max-width: 770px; + margin: 0 auto; + display: block; +} + +.as-map svg { + width: 100%; + height: auto; +} + +/* Spokes */ +.as-spoke { + stroke-width: 2; + transition: stroke 0.3s ease; +} + +/* Hub */ +.as-hub { + fill: var(--color-primary); + transition: fill 0.3s ease; +} + +.as-hub-label { + fill: #ffffff; + font-size: 13px; + font-weight: 600; + pointer-events: none; + user-select: none; +} + +/* Nodes */ +.as-node-group { + cursor: pointer; +} + +.as-node { + cursor: pointer; + transition: fill 0.3s ease, stroke 0.3s ease, filter 0.3s ease; +} + +.as-node-group:hover .as-node { + filter: brightness(1.15); +} + +.as-node-ring { + fill: none; + stroke: transparent; + stroke-width: 3; + transition: stroke 0.2s ease; +} + +.as-node-group.selected .as-node-ring { + stroke: var(--color-text-strong); +} + +.as-node-check { + fill: #ffffff; + font-size: 18px; + font-weight: 700; + pointer-events: none; + user-select: none; +} + +.as-node-label { + fill: var(--color-text-primary); + font-size: 11px; + font-weight: 500; + user-select: none; + text-anchor: middle; + cursor: pointer; + transition: fill 0.15s ease; +} + +.as-node-group:hover .as-node-label { + fill: var(--color-primary); +} + +/* State colors */ +.as-node.state-no { fill: #ef4444; } +.as-node.state-yes { fill: #10b981; } +.as-node.state-partial { fill: #f59e0b; } + +.as-spoke.state-no { stroke: rgba(239, 68, 68, 0.3); } +.as-spoke.state-yes { stroke: rgba(16, 185, 129, 0.3); } +.as-spoke.state-partial { stroke: rgba(245, 158, 11, 0.3); } + +/* Critical pulse for unaddressed critical nodes */ +@keyframes as-pulse { + 0%, 100% { filter: drop-shadow(0 0 4px rgba(239, 68, 68, 0.4)); } + 50% { filter: drop-shadow(0 0 12px rgba(239, 68, 68, 0.7)); } +} + +.as-node-group.critical.state-no .as-node { + animation: as-pulse 2.5s ease-in-out infinite; +} + +/* ===== Detail Card ===== */ +.as-detail { + margin-top: 24px; + padding: 24px 28px; + background: var(--bg-card); + border: 1px solid var(--border-card); + border-radius: 8px; + animation: as-slide-in 0.2s ease-out; +} + +@keyframes as-slide-in { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} + +.as-detail-top { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + margin-bottom: 14px; +} + +.as-detail-left { + flex: 1; + min-width: 0; +} + +.as-detail-title { + font-size: 22px; + font-weight: 700; + color: var(--color-text-strong); + margin: 0 0 4px; +} + +.as-detail-sev { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + font-weight: 700; + letter-spacing: 0.06em; +} + +.as-detail-sev-dot { + width: 8px; + height: 8px; + border-radius: 50%; +} + +.as-detail-right { + display: flex; + align-items: center; + gap: 12px; + flex-shrink: 0; +} + +/* GAP / IN PROGRESS / SECURED toggle */ +.as-detail-toggle { + display: flex; + border: 1px solid var(--border-card); + border-radius: 4px; + overflow: hidden; +} + +.as-toggle-btn { + padding: 7px 16px; + font-size: 12px; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; + cursor: pointer; + border: none; + background: transparent; + color: var(--color-text-muted); + transition: all 0.15s ease; +} + +.as-toggle-btn:not(:last-child) { + border-right: 1px solid var(--border-card); +} + +.as-toggle-btn:hover { + background: rgba(255, 255, 255, 0.05); +} + +:root:not(.dark) .as-toggle-btn:hover { + background: rgba(0, 0, 0, 0.03); +} + +.as-toggle-btn.active.state-no { + background: #ef4444; + color: #ffffff; +} + +.as-toggle-btn.active.state-partial { + background: #f59e0b; + color: #ffffff; +} + +.as-toggle-btn.active.state-yes { + background: #10b981; + color: #ffffff; +} + +.as-detail-close { + background: none; + border: none; + color: var(--color-text-muted); + cursor: pointer; + font-size: 22px; + padding: 0 4px; + line-height: 1; + transition: color 0.15s ease; +} + +.as-detail-close:hover { + color: var(--color-text-strong); +} + +/* Description */ +.as-detail-desc { + font-size: 15px; + line-height: 1.6; + color: var(--color-text-muted); + margin: 0 0 16px; +} + +/* Bottom row: tags + CTA link */ +.as-detail-bottom { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 16px; + flex-wrap: wrap; +} + +.as-detail-tags { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.as-attack-tag { + font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; + font-size: 12px; + padding: 4px 12px; + border: 1px solid var(--border-card); + border-radius: 4px; + color: var(--color-text-muted); + white-space: nowrap; +} + +.as-detail-cta { + font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; + font-size: 13px; + padding: 8px 20px; + border: 1px solid var(--color-primary); + border-radius: 4px; + color: var(--color-primary); + text-decoration: none; + white-space: nowrap; + transition: all 0.15s ease; + flex-shrink: 0; +} + +.as-detail-cta:hover { + background: rgba(67, 57, 219, 0.08); +} + +/* ===== Mobile list ===== */ +.as-mobile-list { + display: none; + flex-direction: column; + gap: 8px; +} + +.as-mobile-row { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 14px; + background: var(--bg-card); + border: 1px solid var(--border-card); + border-radius: 8px; + cursor: pointer; + transition: background 0.15s ease; +} + +.as-mobile-row:hover { + background: rgba(67, 57, 219, 0.04); +} + +.as-mobile-row.selected { + border-color: var(--color-text-strong); +} + +.as-mobile-dot { + width: 20px; + height: 20px; + border-radius: 50%; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + font-weight: 700; + color: #ffffff; + cursor: pointer; + transition: background 0.2s ease; +} + +.as-mobile-dot.state-no { background: #ef4444; } +.as-mobile-dot.state-yes { background: #10b981; } +.as-mobile-dot.state-partial { background: #f59e0b; } + +.as-mobile-name { + font-size: 14px; + font-weight: 500; + color: var(--color-text-strong); + flex: 1; +} + +.as-mobile-sev { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + padding: 2px 8px; + border-radius: 4px; + flex-shrink: 0; +} + +@media (max-width: 600px) { + .as-map { display: none; } + .as-mobile-list { display: flex; } + + .as-detail-top { + flex-direction: column; + } + + .as-detail-bottom { + flex-direction: column; + align-items: flex-start; + } +} + +/* Print */ +@media print { + .as-detail-close, + .as-detail-toggle { display: none; } + .as-mobile-dot { print-color-adjust: exact; -webkit-print-color-adjust: exact; } + .as-node { print-color-adjust: exact; -webkit-print-color-adjust: exact; } +} diff --git a/components/attack-surface/AttackSurfaceDashboard.tsx b/components/attack-surface/AttackSurfaceDashboard.tsx new file mode 100644 index 00000000..5c9a48c2 --- /dev/null +++ b/components/attack-surface/AttackSurfaceDashboard.tsx @@ -0,0 +1,339 @@ +import { useState, useCallback, useEffect, useMemo } from "react"; +import { threatVectors, severityMeta, type PostureState, type ThreatVector } from "./threatData"; +import "./AttackSurface.css"; + +const STORAGE_KEY = "attackSurface-posture"; +const CX = 420; +const CY = 420; +const RADIUS = 286; +const NODE_R = 39; +const HUB_R = 39; + +type PostureMap = Record; + +const nextState: Record = { + no: "yes", + yes: "partial", + partial: "no", +}; + +const stateColors: Record = { + no: "#ef4444", + yes: "#10b981", + partial: "#f59e0b", +}; + +const spokeColors: Record = { + no: "rgba(239, 68, 68, 0.25)", + yes: "rgba(16, 185, 129, 0.25)", + partial: "rgba(245, 158, 11, 0.25)", +}; + +const checkMarks: Record = { + no: "", + yes: "\u2713", + partial: "", +}; + +const postureLabels: Record = { + no: "Gap", + yes: "Secured", + partial: "In Progress", +}; + +function loadPosture(): PostureMap { + try { + const raw = localStorage.getItem(STORAGE_KEY); + return raw ? JSON.parse(raw) : {}; + } catch { + return {}; + } +} + +function savePosture(posture: PostureMap) { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(posture)); + } catch {} +} + +function wrapLabel(title: string): string[] { + const words = title.split(" "); + const lines: string[] = []; + let current = ""; + for (const word of words) { + if (current && (current + " " + word).length > 16) { + lines.push(current); + current = word; + } else { + current = current ? current + " " + word : word; + } + } + if (current) lines.push(current); + return lines; +} + +function nodePosition(index: number, total: number) { + const angle = ((2 * Math.PI) / total) * index - Math.PI / 2; + return { + x: CX + RADIUS * Math.cos(angle), + y: CY + RADIUS * Math.sin(angle), + }; +} + +export function AttackSurfaceDashboard() { + const [posture, setPosture] = useState({}); + const [selected, setSelected] = useState(null); + + useEffect(() => { + setPosture(loadPosture()); + }, []); + + const handleToggle = useCallback((id: string) => { + setPosture((prev) => { + const current = prev[id] || "no"; + const next = { ...prev, [id]: nextState[current] }; + if (nextState[current] === "no") delete next[id]; + savePosture(next); + return next; + }); + }, []); + + const handleSetPosture = useCallback((id: string, state: PostureState) => { + setPosture((prev) => { + const next = { ...prev, [id]: state }; + if (state === "no") delete next[id]; + savePosture(next); + return next; + }); + }, []); + + const handleSelect = useCallback((id: string) => { + setSelected((prev) => (prev === id ? null : id)); + }, []); + + const counts = useMemo(() => { + const values = threatVectors.map((v) => posture[v.id] || "no"); + return { + yes: values.filter((s) => s === "yes").length, + partial: values.filter((s) => s === "partial").length, + no: values.filter((s) => s === "no").length, + }; + }, [posture]); + + const selectedVector = selected + ? threatVectors.find((v) => v.id === selected) || null + : null; + + return ( +
+ {/* Summary */} +
+ + {counts.yes + counts.partial} of {threatVectors.length} vectors covered + +
+ + + {counts.yes} secured + + + + {counts.partial} in progress + + + + {counts.no} gaps + +
+
+ + {/* SVG Radial Map */} +
+ + {/* Spokes */} + {threatVectors.map((v, i) => { + const pos = nodePosition(i, threatVectors.length); + const state = posture[v.id] || "no"; + return ( + + ); + })} + + {/* Hub */} + + + Your + + + Protocol + + + {/* Nodes */} + {threatVectors.map((v, i) => { + const pos = nodePosition(i, threatVectors.length); + const state = posture[v.id] || "no"; + const isCritical = v.severity === "critical"; + const isSelected = selected === v.id; + const lines = wrapLabel(v.title); + + return ( + handleSelect(v.id)} + style={{ cursor: "pointer" }} + > + {/* Selection ring */} + + {/* Main node */} + + {/* Check mark */} + + {checkMarks[state]} + + {/* Label */} + {lines.map((line, li) => ( + + {line} + + ))} + + ); + })} + +
+ + {/* Mobile list fallback */} +
+ {threatVectors.map((v) => { + const state = posture[v.id] || "no"; + const sev = severityMeta[v.severity]; + return ( +
handleSelect(v.id)} + > +
{ + e.stopPropagation(); + handleToggle(v.id); + }} + > + {checkMarks[state]} +
+ {v.title} + + {sev.label} + +
+ ); + })} +
+ + {/* Detail Card */} + {selectedVector && ( + handleSetPosture(selectedVector.id, state)} + onClose={() => setSelected(null)} + /> + )} +
+ ); +} + +function DetailCard({ + vector, + state, + onSetState, + onClose, +}: { + vector: ThreatVector; + state: PostureState; + onSetState: (state: PostureState) => void; + onClose: () => void; +}) { + const sev = severityMeta[vector.severity]; + + return ( +
+
+
+

{vector.title}

+
+ + {sev.label.toUpperCase()} SEVERITY +
+
+
+
+ {(["no", "partial", "yes"] as PostureState[]).map((s) => ( + + ))} +
+ +
+
+ +

{vector.description}

+ +
+
+ {vector.attackTags.map((tag) => ( + {tag} + ))} +
+ + {vector.primaryLinkLabel} → + +
+
+ ); +} diff --git a/components/attack-surface/threatData.ts b/components/attack-surface/threatData.ts new file mode 100644 index 00000000..16af821a --- /dev/null +++ b/components/attack-surface/threatData.ts @@ -0,0 +1,259 @@ +export type Severity = "critical" | "high" | "medium"; +export type PostureState = "no" | "yes" | "partial"; +export type Category = "smart-contract" | "operational" | "human" | "infrastructure" | "supply-chain" | "governance"; + +export interface FrameworkLink { + label: string; + href: string; +} + +export interface ThreatVector { + id: string; + title: string; + subtitle: string; + category: Category; + severity: Severity; + description: string; + example?: string; + /** Short attack-type tags shown in the detail card */ + attackTags: string[]; + /** The primary framework page for this vector — label text links here */ + primaryLink: string; + /** Label for the primary link button in the detail card */ + primaryLinkLabel: string; + frameworkLinks: FrameworkLink[]; +} + +export const categoryMeta: Record = { + "smart-contract": { label: "Smart Contract", color: "#ef4444" }, + operational: { label: "Operational", color: "#f97316" }, + human: { label: "Human", color: "#eab308" }, + infrastructure: { label: "Infrastructure", color: "#8b5cf6" }, + "supply-chain": { label: "Supply Chain", color: "#3b82f6" }, + governance: { label: "Governance", color: "#10b981" }, +}; + +export const severityMeta: Record = { + critical: { label: "Critical", color: "#ef4444", bg: "rgba(239, 68, 68, 0.12)" }, + high: { label: "High", color: "#f97316", bg: "rgba(249, 115, 22, 0.12)" }, + medium: { label: "Medium", color: "#eab308", bg: "rgba(234, 179, 8, 0.12)" }, +}; + +export const threatVectors: ThreatVector[] = [ + { + id: "smart-contract-exploits", + title: "Smart Contract Exploits", + subtitle: "Code vulnerabilities in on-chain logic", + category: "smart-contract", + severity: "critical", + description: + "Reentrancy attacks, logic bugs, oracle manipulation, flash loan exploits, and unaudited upgrade paths that drain protocol funds.", + example: "Euler Finance: $197M flash loan exploit", + attackTags: ["Reentrancy", "Flash loan exploit", "Oracle manipulation", "Logic bug"], + primaryLink: "/external-security-reviews/overview", + primaryLinkLabel: "Security Reviews Framework", + frameworkLinks: [ + { label: "Incident Playbooks", href: "/incident-management/playbooks" }, + { label: "DevSecOps", href: "/devsecops/overview" }, + { label: "External Security Reviews", href: "/external-security-reviews/overview" }, + { label: "Security Testing", href: "/security-testing/overview" }, + ], + }, + { + id: "multisig-failures", + title: "Multisig Operational Failures", + subtitle: "Signer and threshold mismanagement", + category: "operational", + severity: "critical", + description: + "Signer unavailability, lost keys, weak thresholds, no rotation policy, and lack of emergency procedures leave protocol treasuries exposed.", + example: "Ronin Bridge: $625M via compromised validator keys", + attackTags: ["Signer unavailability", "Weak threshold", "Key loss", "No rotation policy"], + primaryLink: "/multisig-for-protocols/overview", + primaryLinkLabel: "Multisig Framework", + frameworkLinks: [ + { label: "Multisig Overview", href: "/multisig-for-protocols/overview" }, + { label: "Emergency Procedures", href: "/multisig-for-protocols/emergency-procedures" }, + { label: "Signer Onboarding", href: "/multisig-for-protocols/signer-onboarding" }, + { label: "Multisig Runbooks", href: "/multisig-for-protocols/runbooks/overview" }, + ], + }, + { + id: "dprk-threat-actors", + title: "DPRK / Threat Actor Hiring", + subtitle: "Nation-state infiltration via hiring", + category: "human", + severity: "critical", + description: + "Unknowingly hiring North Korean IT workers or other threat actors as contractors who gain access to codebases, infrastructure, and signing keys.", + example: "Multiple protocols compromised via DPRK contractors", + attackTags: ["Fake identity", "Contractor infiltration", "Code backdoor", "Key theft"], + primaryLink: "/incident-management/playbooks/dprk-it-worker", + primaryLinkLabel: "DPRK Playbook", + frameworkLinks: [ + { label: "DPRK IT Worker Playbook", href: "/incident-management/playbooks/dprk-it-worker" }, + { label: "Insider Threat Mitigation", href: "/opsec/control-domains/people/insider-threat-mitigation" }, + { label: "Personnel Controls", href: "/opsec/control-domains/people/overview" }, + ], + }, + { + id: "leadership-phishing", + title: "Leadership Phishing", + subtitle: "Targeted spearphishing of key personnel", + category: "human", + severity: "critical", + description: + "Spearphishing campaigns targeting founders, executives, and multisig signers to steal credentials, signing keys, or trick them or employees into approving malicious transactions.", + attackTags: ["Spearphishing", "Credential theft", "Malicious signing", "Impersonation"], + primaryLink: "/awareness/understanding-threat-vectors", + primaryLinkLabel: "Threat Vectors Guide", + frameworkLinks: [ + { label: "Understanding Threat Vectors", href: "/awareness/understanding-threat-vectors" }, + { label: "Phishing & Social Engineering", href: "/user-team-security/phishing-social-engineering" }, + { label: "Security Training", href: "/user-team-security/security-training" }, + ], + }, + { + id: "infrastructure-compromise", + title: "Infrastructure Compromise", + subtitle: "Cloud, server, and network breaches", + category: "infrastructure", + severity: "critical", + description: + "Cloud misconfigurations, exposed APIs, server compromise, weak network segmentation, and missing zero-trust architecture leading to full infrastructure takeover.", + attackTags: ["AWS key leak", "RPC manipulation", "Server compromise", "API exfiltration"], + primaryLink: "/infrastructure/overview", + primaryLinkLabel: "Infrastructure Framework", + frameworkLinks: [ + { label: "Infrastructure Overview", href: "/infrastructure/overview" }, + { label: "Cloud Security", href: "/infrastructure/cloud-security" }, + { label: "Network Security", href: "/infrastructure/network-security" }, + { label: "Zero Trust Architecture", href: "/infrastructure/zero-trust-architecture" }, + ], + }, + { + id: "frontend-dns-hijacking", + title: "Frontend / DNS Hijacking", + subtitle: "Website and domain takeover attacks", + category: "infrastructure", + severity: "critical", + description: + "DNS hijacking, compromised frontend deployments, UI spoofing, and registrar account takeovers that redirect users to malicious interfaces draining wallets.", + example: "Curve Finance: DNS hijack redirected users to drainer", + attackTags: ["DNS hijack", "UI spoofing", "Registrar takeover", "Frontend injection"], + primaryLink: "/infrastructure/domain-and-dns-security/overview", + primaryLinkLabel: "DNS Security Framework", + frameworkLinks: [ + { label: "DNS Security", href: "/infrastructure/domain-and-dns-security/overview" }, + { label: "Monitoring & Alerting", href: "/infrastructure/domain-and-dns-security/monitoring-and-alerting" }, + { label: "Front-End Security", href: "/front-end-web-application/overview" }, + { label: "DNS Registrar Cert", href: "/certs/sfc-dns-registrar" }, + ], + }, + { + id: "opsec-failures", + title: "Operational Security Failures", + subtitle: "Day-to-day security hygiene gaps", + category: "operational", + severity: "high", + description: + "Poor device hygiene, leaked credentials, weak access controls, unencrypted communications, and lack of security policies across the team.", + attackTags: ["Leaked credentials", "Weak access controls", "Unencrypted comms", "No MFA"], + primaryLink: "/opsec/overview", + primaryLinkLabel: "OpSec Framework", + frameworkLinks: [ + { label: "OpSec Overview", href: "/opsec/overview" }, + { label: "Technical Controls", href: "/opsec/control-domains/technical/overview" }, + { label: "Device Hardening", href: "/opsec/control-domains/technical/device-hardening" }, + { label: "IAM", href: "/iam/overview" }, + ], + }, + { + id: "supply-chain-attacks", + title: "Supply Chain Attacks", + subtitle: "Compromised dependencies and CI/CD", + category: "supply-chain", + severity: "high", + description: + "Dependency poisoning, typosquatting, compromised CI/CD pipelines, malicious npm/crate packages, and build system backdoors that inject malicious code.", + example: "Ledger Connect Kit: compromised npm package", + attackTags: ["Dependency poisoning", "Typosquatting", "CI/CD backdoors", "Malicious package"], + primaryLink: "/supply-chain/overview", + primaryLinkLabel: "Supply Chain Framework", + frameworkLinks: [ + { label: "Supply Chain Overview", href: "/supply-chain/overview" }, + { label: "DevSecOps", href: "/devsecops/overview" }, + { label: "CI/CD Security", href: "/devsecops/ci-cd-security" }, + { label: "Code Signing", href: "/devsecops/code-signing" }, + ], + }, + { + id: "monitoring-gaps", + title: "Monitoring & Alerting Gaps", + subtitle: "Blind spots in threat detection", + category: "operational", + severity: "high", + description: + "No on-chain monitoring, slow anomaly detection, missing alerts for critical operations, and no automated response — letting attackers operate undetected.", + attackTags: ["No on-chain monitoring", "Slow detection", "Missing alerts", "No auto-response"], + primaryLink: "/monitoring/overview", + primaryLinkLabel: "Monitoring Framework", + frameworkLinks: [ + { label: "Monitoring Overview", href: "/monitoring/overview" }, + { label: "Threat Detection", href: "/security-automation/threat-detection-response" }, + { label: "DNS Monitoring", href: "/infrastructure/domain-and-dns-security/monitoring-and-alerting" }, + ], + }, + { + id: "social-engineering", + title: "Social Engineering", + subtitle: "Manipulation beyond phishing", + category: "human", + severity: "high", + description: + "Impersonation of partners or investors, fake collaboration requests, community manipulation, and trust exploitation to gain access or influence decisions.", + attackTags: ["Impersonation", "Fake partnership", "Community manipulation", "Trust exploit"], + primaryLink: "/awareness/overview", + primaryLinkLabel: "Awareness Framework", + frameworkLinks: [ + { label: "Awareness Overview", href: "/awareness/overview" }, + { label: "Threat Vectors", href: "/awareness/understanding-threat-vectors" }, + { label: "Security Culture", href: "/user-team-security/security-aware-culture" }, + { label: "Community Management", href: "/community-management/overview" }, + ], + }, + { + id: "duress-situations", + title: "Duress Situations", + subtitle: "Physical threats and coercion", + category: "human", + severity: "high", + description: + "Physical threats, kidnapping, extortion, and coercion targeting key personnel to force transaction signing or credential disclosure.", + attackTags: ["Physical threat", "Kidnapping", "Extortion", "Forced signing"], + primaryLink: "/opsec/travel/guide", + primaryLinkLabel: "Travel Security Guide", + frameworkLinks: [ + { label: "Travel Security", href: "/opsec/travel/guide" }, + { label: "Emergency Procedures", href: "/multisig-for-protocols/emergency-procedures" }, + { label: "Physical Controls", href: "/opsec/control-domains/physical-environmental/overview" }, + ], + }, + { + id: "governance-attacks", + title: "Governance Attacks", + subtitle: "Malicious proposals and vote manipulation", + category: "governance", + severity: "medium", + description: + "Proposal manipulation, vote buying, flash loan governance attacks, and unauthorized upgrades that alter protocol behavior or drain treasuries.", + attackTags: ["Proposal manipulation", "Vote buying", "Rogue upgrades"], + primaryLink: "/governance/overview", + primaryLinkLabel: "Governance Framework", + frameworkLinks: [ + { label: "Governance Overview", href: "/governance/overview" }, + { label: "Security Council Best Practices", href: "/governance/security-council-best-practices" }, + { label: "Multisig for Protocols", href: "/multisig-for-protocols/overview" }, + ], + }, +]; diff --git a/components/index.ts b/components/index.ts index 8ee66ac6..c5a48ed3 100644 --- a/components/index.ts +++ b/components/index.ts @@ -24,3 +24,4 @@ export { CertifiedProtocols } from './certified-protocols/CertifiedProtocols' export { CertifiedProtocolsWrapper } from './certified-protocols/CertifiedProtocolsWrapper' export { BadgeLegend } from './contributors/BadgeLegend' export { DevOnly } from './dev-only/DevOnly' +export { AttackSurfaceDashboard } from './attack-surface/AttackSurfaceDashboard' diff --git a/docs/pages/attack-surface.mdx b/docs/pages/attack-surface.mdx new file mode 100644 index 00000000..04de333b --- /dev/null +++ b/docs/pages/attack-surface.mdx @@ -0,0 +1,15 @@ +--- +title: "Attack Surface Overview | SEAL Security Frameworks" +description: "Visual overview of the Web3 threat landscape. See how protocols get compromised, assess your security posture, and find remediation guides." +tags: + - Security Specialist + - Operations & Strategy +--- + +import { AttackSurfaceDashboard } from '../../components' + +# Attack Surface Overview + +See where your protocol is most exposed. Click the checkbox on each vector to track your security posture. Your progress is saved locally in your browser. + + diff --git a/vocs.config.tsx b/vocs.config.tsx index 8f50b117..f6b4a885 100644 --- a/vocs.config.tsx +++ b/vocs.config.tsx @@ -37,6 +37,7 @@ const config = { text: 'Introduction', collapsed: false, items: [ + { text: 'Attack Surface Overview', link: '/attack-surface' }, { text: 'Introduction to Frameworks', link: '/intro/introduction' }, { text: 'How to Navigate the Website', link: '/intro/how-to-navigate-the-website' }, { text: 'Overview of each Framework', link: '/intro/overview-of-each-framework' }, From 28568823969d2f1f06c9ee18d916df3d82019bed Mon Sep 17 00:00:00 2001 From: solsr Date: Mon, 30 Mar 2026 17:17:44 +0200 Subject: [PATCH 2/4] Mark Attack Surface Overview as dev content Adds dev: true flag to sidebar entry per contributing guidelines. Co-Authored-By: Claude Opus 4.6 (1M context) --- vocs.config.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vocs.config.tsx b/vocs.config.tsx index f6b4a885..7159eb20 100644 --- a/vocs.config.tsx +++ b/vocs.config.tsx @@ -37,7 +37,7 @@ const config = { text: 'Introduction', collapsed: false, items: [ - { text: 'Attack Surface Overview', link: '/attack-surface' }, + { text: 'Attack Surface Overview', link: '/attack-surface', dev: true }, { text: 'Introduction to Frameworks', link: '/intro/introduction' }, { text: 'How to Navigate the Website', link: '/intro/how-to-navigate-the-website' }, { text: 'Overview of each Framework', link: '/intro/overview-of-each-framework' }, From 6a4af7c2234ae5ede950609b80c2a04ef80a4a1f Mon Sep 17 00:00:00 2001 From: solsr Date: Mon, 30 Mar 2026 17:30:53 +0200 Subject: [PATCH 3/4] Replace selection ring with scale + glow effect on selected nodes Selected nodes now scale up 10% and show a soft color-matched glow instead of a detached ring outline. Co-Authored-By: Claude Opus 4.6 (1M context) --- components/attack-surface/AttackSurface.css | 24 ++++++++++++------- .../attack-surface/AttackSurfaceDashboard.tsx | 7 ------ 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/components/attack-surface/AttackSurface.css b/components/attack-surface/AttackSurface.css index 7abbe560..32637cf7 100644 --- a/components/attack-surface/AttackSurface.css +++ b/components/attack-surface/AttackSurface.css @@ -78,26 +78,34 @@ /* Nodes */ .as-node-group { cursor: pointer; + transition: transform 0.2s ease, filter 0.2s ease; + transform-box: fill-box; + transform-origin: center; } .as-node { cursor: pointer; - transition: fill 0.3s ease, stroke 0.3s ease, filter 0.3s ease; + transition: fill 0.3s ease, stroke 0.3s ease; } .as-node-group:hover .as-node { filter: brightness(1.15); } -.as-node-ring { - fill: none; - stroke: transparent; - stroke-width: 3; - transition: stroke 0.2s ease; +.as-node-group.selected { + transform: scale(1.1); } -.as-node-group.selected .as-node-ring { - stroke: var(--color-text-strong); +.as-node-group.selected.state-no { + filter: drop-shadow(0 0 12px rgba(239, 68, 68, 0.6)); +} + +.as-node-group.selected.state-yes { + filter: drop-shadow(0 0 12px rgba(16, 185, 129, 0.6)); +} + +.as-node-group.selected.state-partial { + filter: drop-shadow(0 0 12px rgba(245, 158, 11, 0.6)); } .as-node-check { diff --git a/components/attack-surface/AttackSurfaceDashboard.tsx b/components/attack-surface/AttackSurfaceDashboard.tsx index 5c9a48c2..bc62050d 100644 --- a/components/attack-surface/AttackSurfaceDashboard.tsx +++ b/components/attack-surface/AttackSurfaceDashboard.tsx @@ -191,13 +191,6 @@ export function AttackSurfaceDashboard() { onClick={() => handleSelect(v.id)} style={{ cursor: "pointer" }} > - {/* Selection ring */} - {/* Main node */} Date: Mon, 30 Mar 2026 17:32:34 +0200 Subject: [PATCH 4/4] Add GitHub Actions reminder for attack surface threat data changes Posts an automated PR comment when threatData.ts is modified, reminding contributors to include all required fields and verify framework links. Follows the same pattern as the existing vocs-config-reminder workflow. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/attack-surface-reminder.yml | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 .github/workflows/attack-surface-reminder.yml diff --git a/.github/workflows/attack-surface-reminder.yml b/.github/workflows/attack-surface-reminder.yml new file mode 100644 index 00000000..3882524d --- /dev/null +++ b/.github/workflows/attack-surface-reminder.yml @@ -0,0 +1,85 @@ +name: Attack Surface Data Reminder + +on: + pull_request_target: + types: [opened, synchronize] + branches: + - develop + +permissions: + contents: read + pull-requests: write + +jobs: + threat-vector-reminder: + runs-on: ubuntu-latest + steps: + - name: Check for threat vector changes + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.1.0 + with: + script: | + const { owner, repo } = context.repo; + const pull_number = context.payload.pull_request.number; + const MARKER = ''; + const THREAT_DATA_FILE = 'components/attack-surface/threatData.ts'; + + // Get files changed in this PR + const { data: prFiles } = await github.rest.pulls.listFiles({ + owner, + repo, + pull_number, + per_page: 100 + }); + + const threatDataChanged = prFiles.find(f => f.filename === THREAT_DATA_FILE); + + if (!threatDataChanged) { + console.log('threatData.ts not modified. Skipping.'); + return; + } + + // Check if we already posted a reminder for this PR + const { data: comments } = await github.rest.issues.listComments({ + owner, + repo, + issue_number: pull_number, + per_page: 100 + }); + + const existingReminder = comments.find(c => + c.body.includes(MARKER) && + c.user.login === 'github-actions[bot]' + ); + + if (existingReminder) { + console.log('Reminder already posted. Skipping.'); + return; + } + + const body = `### Attack Surface Configuration Reminder + ${MARKER} + + This PR modifies the attack surface threat data (\`${THREAT_DATA_FILE}\`). + + If you are adding a new threat vector, please ensure: + - [ ] The vector has all required fields: \`id\`, \`title\`, \`subtitle\`, \`category\`, \`severity\`, \`description\`, \`attackTags\`, \`primaryLink\`, \`primaryLinkLabel\`, and \`frameworkLinks\` + - [ ] The \`primaryLink\` points to a valid, existing framework page + - [ ] The \`attackTags\` array contains 3-4 short example attack types + - [ ] The \`category\` is one of: \`smart-contract\`, \`operational\`, \`human\`, \`infrastructure\`, \`supply-chain\`, \`governance\` + - [ ] The \`severity\` is one of: \`critical\`, \`high\`, \`medium\` + + If you are modifying an existing vector, verify that all links still resolve correctly. + + See the [Attack Surface Overview page](/attack-surface) on the preview deployment to verify the radial map renders correctly. + + --- + This is an automated reminder. If this PR doesn't affect threat vectors, you can ignore this message.`; + + await github.rest.issues.createComment({ + owner, + repo, + issue_number: pull_number, + body + }); + + console.log('Posted attack surface reminder comment.');