initial commit

This commit is contained in:
2026-05-28 18:27:41 -06:00
commit 6d0035721e
45 changed files with 15082 additions and 0 deletions

24
dashboard/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

16
dashboard/README.md Normal file
View File

@@ -0,0 +1,16 @@
# React + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.

View File

@@ -0,0 +1,29 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{js,jsx}'],
extends: [
js.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
rules: {
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
},
},
])

16
dashboard/index.html Normal file
View File

@@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet" />
<title>Apophis SOC | Security Operations Center</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

3852
dashboard/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

31
dashboard/package.json Normal file
View File

@@ -0,0 +1,31 @@
{
"name": "dashboard",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.18",
"date-fns": "^4.1.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"recharts": "^3.7.0",
"tailwindcss": "^4.1.18"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"vite": "^7.3.1"
}
}

BIN
dashboard/public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 MiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

1
dashboard/src/App.css Normal file
View File

@@ -0,0 +1 @@
/* Styles handled by Tailwind + index.css */

100
dashboard/src/App.jsx Normal file
View File

@@ -0,0 +1,100 @@
import { useState, useEffect } from 'react';
import Header from './components/Header';
import ThreatFeed from './components/ThreatFeed';
import NetworkTraffic from './components/NetworkTraffic';
import SystemHealth from './components/SystemHealth';
import MitreHeatmap from './components/MitreHeatmap';
import TopThreats from './components/TopThreats';
import IncidentTracker from './components/IncidentTracker';
import VulnSummary from './components/VulnSummary';
import {
generateAlertBatch,
generateAlert,
generateTrafficData,
generateSystemHealth,
generateMitreData,
generateTopThreats,
generateIncidents,
generateVulnSummary,
getOverallThreatLevel,
} from './data/mockData';
function App() {
const [alerts, setAlerts] = useState(() => generateAlertBatch(25));
const [traffic, setTraffic] = useState(() => generateTrafficData(24));
const [systems, setSystems] = useState(() => generateSystemHealth());
const [mitre, setMitre] = useState(() => generateMitreData());
const [threats, setThreats] = useState(() => generateTopThreats(8));
const [incidents, setIncidents] = useState(() => generateIncidents());
const [vulns, setVulns] = useState(() => generateVulnSummary());
const threatLevel = getOverallThreatLevel(alerts);
// Simulate live alert feed - new alert every 3-6 seconds
useEffect(() => {
const id = setInterval(() => {
setAlerts((prev) => {
const newAlert = generateAlert();
const updated = [newAlert, ...prev];
return updated.slice(0, 50);
});
}, 3000 + Math.random() * 3000);
return () => clearInterval(id);
}, []);
// Refresh slower-changing data periodically
useEffect(() => {
const id = setInterval(() => {
setSystems(generateSystemHealth());
setTraffic(generateTrafficData(24));
}, 15000);
return () => clearInterval(id);
}, []);
useEffect(() => {
const id = setInterval(() => {
setThreats(generateTopThreats(8));
setVulns(generateVulnSummary());
setMitre(generateMitreData());
setIncidents(generateIncidents());
}, 30000);
return () => clearInterval(id);
}, []);
return (
<div className="flex flex-col h-screen bg-ap-dark">
<Header threatLevel={threatLevel} />
<div className="flex-1 grid grid-cols-12 grid-rows-[1fr_1fr_1fr] gap-px bg-white/5 min-h-0 overflow-hidden">
{/* Row 1: Threat Feed | Network Traffic | System Health */}
<div className="col-span-3 min-h-0 overflow-hidden">
<ThreatFeed alerts={alerts} />
</div>
<div className="col-span-6 min-h-0 overflow-hidden">
<NetworkTraffic data={traffic} />
</div>
<div className="col-span-3 min-h-0 overflow-hidden">
<SystemHealth systems={systems} />
</div>
{/* Row 2: MITRE ATT&CK Heatmap | Top Threats Table */}
<div className="col-span-6 min-h-0 overflow-hidden">
<MitreHeatmap data={mitre} />
</div>
<div className="col-span-6 min-h-0 overflow-hidden">
<TopThreats threats={threats} />
</div>
{/* Row 3: Incident Tracker | Vulnerability Summary */}
<div className="col-span-7 min-h-0 overflow-hidden">
<IncidentTracker incidents={incidents} />
</div>
<div className="col-span-5 min-h-0 overflow-hidden">
<VulnSummary data={vulns} />
</div>
</div>
</div>
);
}
export default App;

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -0,0 +1,57 @@
import { useState, useEffect } from 'react';
const THREAT_LEVEL_STYLES = {
CRITICAL: 'bg-ap-red text-white animate-pulse-red',
HIGH: 'bg-ap-orange text-white',
ELEVATED: 'bg-ap-yellow text-ap-black',
GUARDED: 'bg-ap-green text-ap-black',
};
function formatTime(date) {
const h = String(date.getHours()).padStart(2, '0');
const m = String(date.getMinutes()).padStart(2, '0');
const s = String(date.getSeconds()).padStart(2, '0');
return `${h}:${m}:${s}`;
}
export default function Header({ threatLevel = 'GUARDED' }) {
const [time, setTime] = useState(() => formatTime(new Date()));
useEffect(() => {
const id = setInterval(() => {
setTime(formatTime(new Date()));
}, 1000);
return () => clearInterval(id);
}, []);
const badgeStyle = THREAT_LEVEL_STYLES[threatLevel] || THREAT_LEVEL_STYLES.GUARDED;
return (
<header className="flex items-center justify-between bg-ap-black border-b border-white/10 px-4 py-2">
{/* Left -- Logo + Title */}
<div className="flex items-center gap-3">
<img src="/logo.png" alt="Apophis" className="h-9 w-auto" />
<span className="font-mono font-bold tracking-wider text-ap-red text-sm select-none">
APOPHIS SOC
</span>
</div>
{/* Center -- System Clock */}
<div className="font-mono text-ap-silver-dim text-sm tabular-nums tracking-wide select-none">
{time}
</div>
{/* Right -- Threat Level Indicator */}
<div className="flex flex-col items-end gap-0.5">
<span className="text-[10px] text-ap-silver-dim font-mono uppercase tracking-widest select-none">
Threat Level
</span>
<span
className={`px-3 py-1 font-mono text-xs font-bold uppercase select-none ${badgeStyle}`}
>
{threatLevel}
</span>
</div>
</header>
);
}

View File

@@ -0,0 +1,140 @@
import { formatDistanceToNow } from 'date-fns';
const PRIORITY_STYLE = {
P1: 'bg-ap-red/20 text-ap-red',
P2: 'bg-ap-orange/20 text-ap-orange',
P3: 'bg-ap-yellow/20 text-ap-yellow',
P4: 'bg-ap-blue/20 text-ap-blue',
};
const STATUS_COLOR = {
Open: 'text-ap-red',
Investigating: 'text-ap-orange',
Contained: 'text-ap-yellow',
Resolved: 'text-ap-green',
};
const STATUS_DOT = {
Open: 'bg-ap-red',
Investigating: 'bg-ap-orange',
Contained: 'bg-ap-yellow',
Resolved: 'bg-ap-green',
};
const STATUS_ORDER = ['Open', 'Investigating', 'Contained', 'Resolved'];
function getRelativeTime(created) {
if (!created) return '--';
const date = created instanceof Date ? created : new Date(created);
if (Number.isNaN(date.getTime())) return '--';
return formatDistanceToNow(date, { addSuffix: true });
}
function PriorityBadge({ priority }) {
const style = PRIORITY_STYLE[priority] || PRIORITY_STYLE.P4;
return (
<span className={`font-mono text-[10px] font-bold px-1.5 py-0.5 select-none ${style}`}>
{priority}
</span>
);
}
function StatusBadge({ status }) {
const color = STATUS_COLOR[status] || STATUS_COLOR.Open;
return (
<span className={`font-mono text-[10px] uppercase select-none ${color}`}>
{status}
</span>
);
}
function IncidentCard({ incident }) {
const { id, description, status, priority, assignee, created } = incident;
return (
<div className="mx-3 my-2 p-3 bg-ap-dark border border-white/5">
{/* Top row: priority + ID + status */}
<div className="flex items-center gap-2">
<PriorityBadge priority={priority} />
<span className="font-mono text-xs text-ap-silver">
{id}
</span>
<span className="ml-auto shrink-0">
<StatusBadge status={status} />
</span>
</div>
{/* Description */}
<p className="text-xs text-ap-silver mt-1 truncate">
{description}
</p>
{/* Bottom row: assignee + relative time */}
<div className="flex items-center gap-2 mt-2">
{assignee && (
<span className="font-mono text-[10px] text-ap-silver-dim px-1.5 py-0.5 bg-white/5">
{assignee}
</span>
)}
<span className="ml-auto font-mono text-[10px] text-ap-silver-dim tabular-nums">
{getRelativeTime(created)}
</span>
</div>
</div>
);
}
function StatsBar({ incidents }) {
const counts = STATUS_ORDER.reduce((acc, status) => {
acc[status] = incidents.filter((i) => i.status === status).length;
return acc;
}, {});
return (
<div className="flex items-center gap-4 px-3 py-2 border-b border-white/5">
{STATUS_ORDER.map((status) => (
<div key={status} className="flex items-center gap-1.5">
<span className={`inline-block w-1.5 h-1.5 ${STATUS_DOT[status]}`} />
<span className="font-mono text-[10px] text-ap-silver-dim">
{status}
</span>
<span className="font-mono text-[10px] font-bold text-ap-silver tabular-nums">
{counts[status]}
</span>
</div>
))}
</div>
);
}
export default function IncidentTracker({ incidents = [] }) {
const activeCount = incidents.filter((i) => i.status !== 'Resolved').length;
return (
<div className="panel h-full flex flex-col">
{/* Panel header */}
<div className="panel-header">
<span className="dot" />
<span>Active Incidents</span>
<span className="ml-auto px-1.5 py-0.5 text-[9px] font-mono font-bold tabular-nums bg-ap-red-dim text-ap-red select-none">
{activeCount}
</span>
</div>
{/* Stats bar */}
<StatsBar incidents={incidents} />
{/* Scrollable incident list */}
<div className="flex-1 overflow-y-auto">
{incidents.length === 0 && (
<div className="px-3 py-6 text-center font-mono text-xs text-ap-silver-dim">
No incidents
</div>
)}
{incidents.map((incident) => (
<IncidentCard key={incident.id} incident={incident} />
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,128 @@
import { useMemo } from 'react';
const TACTIC_ORDER = [
'Reconnaissance',
'Resource Dev',
'Initial Access',
'Execution',
'Persistence',
'Priv Escalation',
'Defense Evasion',
'Credential Access',
'Discovery',
'Lateral Movement',
'Collection',
'C2',
'Exfiltration',
'Impact',
];
const CELL_STYLES = [
'bg-white/5', // 0 — not seen
'bg-ap-blue/40', // 1 — low
'bg-ap-yellow/50', // 2 — medium
'bg-ap-red/70', // 3 — high
];
const LEGEND = [
{ label: 'None', style: 'bg-white/5' },
{ label: 'Low', style: 'bg-ap-blue/40' },
{ label: 'Med', style: 'bg-ap-yellow/50' },
{ label: 'High', style: 'bg-ap-red/70' },
];
function HeatCell({ technique, value }) {
const bg = CELL_STYLES[value] ?? CELL_STYLES[0];
return (
<div
className={`w-7 h-7 ${bg} border border-white/5 transition-colors hover:border-ap-silver-dim cursor-default`}
title={`${technique} (${value})`}
/>
);
}
export default function MitreHeatmap({ data = {} }) {
// Build a stable ordered structure from the data prop
const { tactics, maxRows } = useMemo(() => {
const ordered = TACTIC_ORDER
.filter((t) => data[t] != null)
.map((tactic) => ({
tactic,
techniques: Object.entries(data[tactic]).map(([name, value]) => ({
name,
value: Math.max(0, Math.min(3, Number(value) || 0)),
})),
}));
const max = ordered.reduce(
(m, t) => Math.max(m, t.techniques.length),
0,
);
return { tactics: ordered, maxRows: max };
}, [data]);
return (
<div className="panel h-full flex flex-col">
{/* Panel header */}
<div className="panel-header">
<span className="dot" />
<span>MITRE ATT&CK Coverage</span>
</div>
{/* Scrollable heatmap area */}
<div className="flex-1 overflow-x-auto overflow-y-auto p-3">
{tactics.length === 0 ? (
<div className="px-3 py-6 text-center font-mono text-xs text-ap-silver-dim">
No MITRE data
</div>
) : (
<div className="inline-flex gap-px">
{tactics.map(({ tactic, techniques }) => (
<div key={tactic} className="flex flex-col items-center gap-px">
{/* Tactic header — vertical text */}
<div className="h-24 flex items-end justify-center pb-1">
<span
className="font-mono text-[9px] text-ap-silver-dim uppercase tracking-wide whitespace-nowrap select-none"
style={{
writingMode: 'vertical-rl',
transform: 'rotate(180deg)',
}}
>
{tactic}
</span>
</div>
{/* Technique cells */}
{techniques.map(({ name, value }) => (
<HeatCell key={name} technique={name} value={value} />
))}
{/* Pad empty rows so columns align */}
{Array.from(
{ length: maxRows - techniques.length },
(_, i) => (
<div key={`pad-${i}`} className="w-7 h-7" />
),
)}
</div>
))}
</div>
)}
</div>
{/* Legend */}
<div className="flex items-center gap-3 px-3 py-2 border-t border-white/10">
{LEGEND.map(({ label, style }) => (
<div key={label} className="flex items-center gap-1.5">
<span className={`inline-block w-3 h-3 ${style} border border-white/10`} />
<span className="font-mono text-[9px] text-ap-silver-dim uppercase select-none">
{label}
</span>
</div>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,112 @@
import {
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
Legend,
} from 'recharts';
const AREA_CONFIG = [
{ dataKey: 'inbound', stroke: '#0056B3', fill: '#0056B344', name: 'Inbound' },
{ dataKey: 'outbound', stroke: '#E0E0E2', fill: '#E0E0E222', name: 'Outbound' },
{ dataKey: 'blocked', stroke: '#D72638', fill: '#D7263844', name: 'Blocked' },
];
const TICK_STYLE = {
fontFamily: "'JetBrains Mono', monospace",
fontSize: '0.75rem',
fill: '#E0E0E266',
};
function CustomTooltip({ active, payload, label }) {
if (!active || !payload?.length) return null;
return (
<div
style={{
background: '#1B1B1E',
border: '1px solid #E0E0E220',
fontFamily: "'JetBrains Mono', monospace",
fontSize: '0.75rem',
padding: '8px 12px',
}}
>
<p style={{ color: '#E0E0E266', marginBottom: 4 }}>{label}</p>
{payload.map((entry) => (
<p key={entry.dataKey} style={{ color: entry.stroke }}>
{entry.name}: {entry.value} Mbps
</p>
))}
</div>
);
}
export default function NetworkTraffic({ data }) {
return (
<div className="panel h-full flex flex-col">
{/* Header */}
<div className="panel-header">
<span className="dot" />
<span>Network Traffic</span>
<span
className="ml-auto font-mono text-[10px] tracking-widest px-1.5 py-0.5 border border-ap-red/30 text-ap-red"
>
24H
</span>
</div>
{/* Chart */}
<div className="flex-1 min-h-0 p-2">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={data} margin={{ top: 8, right: 12, left: 0, bottom: 0 }}>
<CartesianGrid stroke="#E0E0E210" strokeDasharray="3 3" />
<XAxis
dataKey="time"
tick={TICK_STYLE}
axisLine={{ stroke: '#E0E0E210' }}
tickLine={{ stroke: '#E0E0E210' }}
/>
<YAxis
tick={TICK_STYLE}
unit=" Mbps"
axisLine={{ stroke: '#E0E0E210' }}
tickLine={{ stroke: '#E0E0E210' }}
width={72}
/>
<Tooltip content={<CustomTooltip />} cursor={{ stroke: '#E0E0E220' }} />
<Legend
verticalAlign="bottom"
iconType="rect"
wrapperStyle={{
fontFamily: "'JetBrains Mono', monospace",
fontSize: '0.75rem',
paddingTop: 8,
}}
/>
{AREA_CONFIG.map(({ dataKey, stroke, fill, name }) => (
<Area
key={dataKey}
type="monotone"
dataKey={dataKey}
stroke={stroke}
fill={fill}
strokeWidth={1.5}
name={name}
dot={false}
activeDot={{ r: 3, strokeWidth: 0, fill: stroke }}
/>
))}
</AreaChart>
</ResponsiveContainer>
</div>
</div>
);
}

View File

@@ -0,0 +1,78 @@
const STATUS_COLOR = {
online: 'bg-ap-green',
warning: 'bg-ap-yellow',
offline: 'bg-ap-red',
};
function barColor(pct) {
if (pct > 80) return 'bg-ap-red';
if (pct >= 50) return 'bg-ap-yellow';
return 'bg-ap-green';
}
function MetricBar({ label, value }) {
return (
<div className="flex items-center gap-2">
<span className="font-mono text-[10px] text-ap-silver-dim w-7 shrink-0">
{label}
</span>
<div className="flex-1 h-1 bg-white/10">
<div
className={`h-full ${barColor(value)}`}
style={{ width: `${Math.min(value, 100)}%` }}
/>
</div>
<span className="font-mono text-[10px] text-ap-silver-dim w-8 text-right shrink-0">
{value}%
</span>
</div>
);
}
function SystemCard({ system }) {
const { name, status, cpu, mem, uptime } = system;
return (
<div className="bg-ap-dark p-2.5 border border-white/5">
{/* Name + status dot */}
<div className="flex items-center justify-between gap-2 mb-2">
<span className="font-mono text-xs font-semibold truncate text-ap-silver">
{name}
</span>
<span
className={`inline-block w-2 h-2 shrink-0 ${STATUS_COLOR[status] ?? 'bg-ap-silver-dim'}`}
/>
</div>
{/* Metrics */}
<div className="flex flex-col gap-1.5">
<MetricBar label="CPU" value={cpu} />
<MetricBar label="MEM" value={mem} />
</div>
{/* Uptime */}
<p className="font-mono text-[10px] text-ap-silver-dim mt-2">
UP: {uptime}
</p>
</div>
);
}
export default function SystemHealth({ systems }) {
return (
<div className="panel h-full flex flex-col">
{/* Header */}
<div className="panel-header">
<span className="dot" />
<span>System Health</span>
</div>
{/* Grid */}
<div className="grid grid-cols-2 gap-2 p-3 flex-1 content-start">
{systems.map((system) => (
<SystemCard key={system.name} system={system} />
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,129 @@
const SEVERITY_COLOR = {
critical: 'bg-ap-red',
high: 'bg-ap-orange',
medium: 'bg-ap-yellow',
low: 'bg-ap-blue',
info: 'bg-ap-silver-dim',
};
const SEVERITY_TEXT = {
critical: 'text-ap-red',
high: 'text-ap-orange',
medium: 'text-ap-yellow',
low: 'text-ap-blue',
info: 'text-ap-silver-dim',
};
function formatTimestamp(ts) {
if (!ts) return '--:--:--';
const d = ts instanceof Date ? ts : new Date(ts);
if (Number.isNaN(d.getTime())) return '--:--:--';
const h = String(d.getHours()).padStart(2, '0');
const m = String(d.getMinutes()).padStart(2, '0');
const s = String(d.getSeconds()).padStart(2, '0');
return `${h}:${m}:${s}`;
}
function SeverityDot({ severity }) {
const bg = SEVERITY_COLOR[severity] || SEVERITY_COLOR.info;
return <span className={`inline-block w-1.5 h-1.5 ${bg} shrink-0`} />;
}
function BlockedBadge({ blocked }) {
if (blocked) {
return (
<span className="px-1.5 py-0.5 text-[9px] font-mono font-bold uppercase bg-ap-green/20 text-ap-green select-none">
Blocked
</span>
);
}
return (
<span className="px-1.5 py-0.5 text-[9px] font-mono font-bold uppercase bg-ap-red/20 text-ap-red select-none">
Allowed
</span>
);
}
function AlertRow({ alert }) {
const {
timestamp,
severity = 'info',
attackType,
sourceIP,
targetIP,
source,
country,
port,
blocked,
} = alert;
const sevText = SEVERITY_TEXT[severity] || SEVERITY_TEXT.info;
return (
<div className="px-3 py-2 border-b border-white/5 hover:bg-white/5 transition-colors cursor-default">
{/* Top line */}
<div className="flex items-center gap-2">
<SeverityDot severity={severity} />
<span className={`font-mono text-xs font-semibold ${sevText}`}>
{severity.toUpperCase()}
</span>
<span className="font-mono text-xs text-ap-silver truncate">
{attackType}
</span>
<span className="ml-auto font-mono text-[11px] text-ap-silver-dim tabular-nums shrink-0">
{formatTimestamp(timestamp)}
</span>
</div>
{/* Bottom line */}
<div className="flex items-center gap-2 mt-1">
<span className="font-mono text-[11px] text-ap-silver-dim truncate">
{sourceIP}
{port != null ? `:${port}` : ''}
<span className="text-ap-silver-dim/50 mx-1">{'->'}</span>
{targetIP}
</span>
{country && (
<span className="font-mono text-[9px] text-ap-silver-dim uppercase shrink-0">
[{country}]
</span>
)}
{source && (
<span className="font-mono text-[10px] text-ap-silver-dim/70 truncate">
{source}
</span>
)}
<span className="ml-auto shrink-0">
<BlockedBadge blocked={blocked} />
</span>
</div>
</div>
);
}
export default function ThreatFeed({ alerts = [] }) {
return (
<div className="panel h-full flex flex-col">
{/* Panel header */}
<div className="panel-header">
<span className="dot" />
<span>Threat Feed</span>
<span className="ml-auto px-1.5 py-0.5 text-[9px] font-mono font-bold tabular-nums bg-ap-red-dim text-ap-red select-none">
{alerts.length}
</span>
</div>
{/* Scrollable alert list */}
<div className="flex-1 overflow-y-auto">
{alerts.length === 0 && (
<div className="px-3 py-6 text-center font-mono text-xs text-ap-silver-dim">
No alerts
</div>
)}
{alerts.map((alert) => (
<AlertRow key={alert.id} alert={alert} />
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,115 @@
function CountCell({ count }) {
let color = 'text-ap-silver';
if (count > 200) color = 'text-ap-red font-bold';
else if (count > 100) color = 'text-ap-orange font-bold';
return (
<td className="px-3 py-2 border-b border-white/5">
<span className={`font-mono text-xs tabular-nums ${color}`}>
{count.toLocaleString()}
</span>
</td>
);
}
function StatusBadge({ blocked }) {
if (blocked) {
return (
<span className="text-[10px] px-1.5 py-0.5 font-mono font-bold uppercase bg-ap-green/20 text-ap-green select-none">
Blocked
</span>
);
}
return (
<span className="text-[10px] px-1.5 py-0.5 font-mono font-bold uppercase bg-ap-red/20 text-ap-red select-none">
Active
</span>
);
}
export default function TopThreats({ threats = [] }) {
return (
<div className="panel h-full flex flex-col">
{/* Panel header */}
<div className="panel-header">
<span className="dot" />
<span>Top Threat Sources</span>
</div>
{/* Table area */}
<div className="flex-1 overflow-y-auto">
{threats.length === 0 ? (
<div className="px-3 py-6 text-center font-mono text-xs text-ap-silver-dim">
No threat data
</div>
) : (
<table className="w-full font-mono text-xs">
<thead>
<tr>
<th className="text-left px-3 py-2 text-ap-silver-dim text-[10px] uppercase tracking-wider border-b border-white/10 bg-ap-dark sticky top-0">
IP
</th>
<th className="text-left px-3 py-2 text-ap-silver-dim text-[10px] uppercase tracking-wider border-b border-white/10 bg-ap-dark sticky top-0">
Country
</th>
<th className="text-left px-3 py-2 text-ap-silver-dim text-[10px] uppercase tracking-wider border-b border-white/10 bg-ap-dark sticky top-0">
Attack Type
</th>
<th className="text-left px-3 py-2 text-ap-silver-dim text-[10px] uppercase tracking-wider border-b border-white/10 bg-ap-dark sticky top-0">
Count
</th>
<th className="text-left px-3 py-2 text-ap-silver-dim text-[10px] uppercase tracking-wider border-b border-white/10 bg-ap-dark sticky top-0">
Status
</th>
</tr>
</thead>
<tbody>
{threats.map((threat) => (
<tr
key={threat.ip}
className="hover:bg-white/5 transition-colors cursor-default"
>
{/* IP */}
<td className="px-3 py-2 border-b border-white/5">
<span className="text-ap-silver font-medium whitespace-nowrap">
{threat.ip}
</span>
</td>
{/* Country */}
<td className="px-3 py-2 border-b border-white/5">
<span
className="text-ap-silver-dim uppercase"
title={threat.countryName}
>
{threat.country}
</span>
</td>
{/* Attack Type */}
<td className="px-3 py-2 border-b border-white/5">
<span
className="block max-w-[140px] truncate text-ap-silver"
title={threat.attackType}
>
{threat.attackType}
</span>
</td>
{/* Count */}
<CountCell count={threat.count} />
{/* Status */}
<td className="px-3 py-2 border-b border-white/5">
<StatusBadge blocked={threat.blocked} />
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,110 @@
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from 'recharts';
const SEVERITY_CONFIG = [
{ key: 'critical', label: 'Critical', color: '#D72638' },
{ key: 'high', label: 'High', color: '#FF6D00' },
{ key: 'medium', label: 'Medium', color: '#FFD600' },
{ key: 'low', label: 'Low', color: '#0056B3' },
{ key: 'info', label: 'Info', color: '#E0E0E244' },
];
function CustomTooltip({ active, payload }) {
if (!active || !payload?.length) return null;
const { name, value } = payload[0];
return (
<div
style={{
background: '#1B1B1E',
border: '1px solid #E0E0E220',
fontFamily: "'JetBrains Mono', monospace",
fontSize: '0.75rem',
padding: '6px 10px',
}}
>
<span style={{ color: payload[0].payload.fill }}>{name}</span>
<span style={{ color: '#E0E0E2', marginLeft: 8 }}>{value}</span>
</div>
);
}
export default function VulnSummary({ data = {} }) {
const {
critical = 0,
high = 0,
medium = 0,
low = 0,
info = 0,
} = data;
const total = critical + high + medium + low + info;
const chartData = SEVERITY_CONFIG.map(({ key, label, color }) => ({
name: label,
value: data[key] || 0,
fill: color,
}));
return (
<div className="panel h-full flex flex-col">
{/* Panel header */}
<div className="panel-header">
<span className="dot" />
<span>Vulnerability Summary</span>
</div>
{/* Chart area */}
<div className="relative flex-shrink-0" style={{ height: 180 }}>
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={chartData}
cx="50%"
cy="50%"
innerRadius={50}
outerRadius={75}
dataKey="value"
stroke="none"
paddingAngle={1}
>
{chartData.map((entry, index) => (
<Cell key={SEVERITY_CONFIG[index].key} fill={entry.fill} />
))}
</Pie>
<Tooltip content={<CustomTooltip />} />
</PieChart>
</ResponsiveContainer>
{/* Center label */}
<div
className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none"
>
<span className="font-mono text-2xl font-bold text-ap-silver tabular-nums leading-none">
{total}
</span>
<span className="font-mono text-[10px] text-ap-silver-dim uppercase tracking-widest mt-1">
Total
</span>
</div>
</div>
{/* Legend */}
<div className="grid grid-cols-2 gap-x-4 gap-y-2 px-4 pb-4 pt-2">
{SEVERITY_CONFIG.map(({ key, label, color }) => (
<div key={key} className="flex items-center gap-2">
<span
className="inline-block w-2 h-2 shrink-0"
style={{ backgroundColor: color }}
/>
<span className="font-mono text-xs text-ap-silver-dim">
{label}
</span>
<span className="ml-auto font-mono text-xs font-bold text-ap-silver tabular-nums">
{data[key] || 0}
</span>
</div>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,236 @@
// =============================================================================
// APOPHIS SOC - Mock Data Generators
// Generates realistic-looking security operations data for the dashboard
// =============================================================================
const ATTACK_TYPES = [
'SSH Brute Force', 'SQL Injection', 'XSS Attempt', 'Port Scan',
'DNS Tunneling', 'C2 Beacon', 'Credential Stuffing', 'DDoS SYN Flood',
'Malware Download', 'Privilege Escalation', 'Lateral Movement',
'Data Exfiltration', 'Phishing Link Click', 'RDP Brute Force',
'SMB Exploit', 'ARP Spoofing', 'ICMP Flood', 'Directory Traversal',
'Buffer Overflow', 'Zero-Day Exploit'
];
const SEVERITIES = ['critical', 'high', 'medium', 'low', 'info'];
const SEVERITY_WEIGHTS = [0.05, 0.15, 0.35, 0.30, 0.15];
const SOURCES = [
'Suricata IDS', 'pfSense Firewall', 'Wazuh SIEM', 'ClamAV',
'Zeek Network Monitor', 'OSSEC HIDS', 'Fail2Ban', 'Snort IPS',
'ModSecurity WAF', 'OpenVAS Scanner'
];
const COUNTRIES = [
{ code: 'CN', name: 'China', weight: 0.20 },
{ code: 'RU', name: 'Russia', weight: 0.18 },
{ code: 'US', name: 'United States', weight: 0.12 },
{ code: 'KP', name: 'North Korea', weight: 0.08 },
{ code: 'IR', name: 'Iran', weight: 0.07 },
{ code: 'BR', name: 'Brazil', weight: 0.07 },
{ code: 'IN', name: 'India', weight: 0.06 },
{ code: 'DE', name: 'Germany', weight: 0.05 },
{ code: 'NL', name: 'Netherlands', weight: 0.05 },
{ code: 'RO', name: 'Romania', weight: 0.04 },
{ code: 'UA', name: 'Ukraine', weight: 0.04 },
{ code: 'VN', name: 'Vietnam', weight: 0.04 },
];
const MITRE_TACTICS = [
'Reconnaissance', 'Resource Dev', 'Initial Access', 'Execution',
'Persistence', 'Priv Escalation', 'Defense Evasion', 'Credential Access',
'Discovery', 'Lateral Movement', 'Collection', 'C2', 'Exfiltration', 'Impact'
];
const MITRE_TECHNIQUES = {
'Reconnaissance': ['Active Scanning', 'Search Open Websites', 'Gather Victim Info', 'Phishing for Info'],
'Resource Dev': ['Acquire Infrastructure', 'Develop Capabilities', 'Stage Capabilities', 'Compromise Accounts'],
'Initial Access': ['Phishing', 'Exploit Public App', 'Valid Accounts', 'Drive-by Compromise'],
'Execution': ['Command & Script', 'Scheduled Task', 'User Execution', 'WMI'],
'Persistence': ['Boot Autostart', 'Create Account', 'Scheduled Task', 'Server Software'],
'Priv Escalation': ['Abuse Elevation', 'Access Token', 'Boot Autostart', 'Exploitation'],
'Defense Evasion': ['Obfuscation', 'Masquerading', 'Rootkit', 'Process Injection'],
'Credential Access': ['Brute Force', 'OS Credential Dump', 'Keylogging', 'Network Sniffing'],
'Discovery': ['Account Discovery', 'Network Scan', 'System Info', 'Process Discovery'],
'Lateral Movement': ['Remote Services', 'SMB/Admin Share', 'Pass the Hash', 'RDP'],
'Collection': ['Data from Local', 'Screen Capture', 'Clipboard Data', 'Email Collection'],
'C2': ['Application Layer', 'Encrypted Channel', 'Proxy', 'Web Service'],
'Exfiltration': ['Exfil Over C2', 'Exfil Over Web', 'Automated Exfil', 'Transfer Size Limits'],
'Impact': ['Data Destruction', 'Data Encrypted', 'Service Stop', 'Defacement'],
};
// --- Utility functions ---
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function randomFloat(min, max) {
return Math.random() * (max - min) + min;
}
function randomChoice(arr) {
return arr[Math.floor(Math.random() * arr.length)];
}
function weightedChoice(items, weights) {
const r = Math.random();
let sum = 0;
for (let i = 0; i < items.length; i++) {
sum += weights[i];
if (r <= sum) return items[i];
}
return items[items.length - 1];
}
function randomIP() {
// Generate realistic-looking external IPs (avoiding private ranges)
const firstOctet = randomChoice([1, 2, 5, 14, 23, 31, 37, 41, 45, 46, 49, 58, 59, 60, 61, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 91, 92, 93, 94, 95, 103, 104, 106, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223]);
return `${firstOctet}.${randomInt(1, 254)}.${randomInt(1, 254)}.${randomInt(1, 254)}`;
}
let alertIdCounter = 1000;
// --- Public API ---
export function generateAlert() {
const severity = weightedChoice(SEVERITIES, SEVERITY_WEIGHTS);
const country = weightedChoice(
COUNTRIES.map(c => c),
COUNTRIES.map(c => c.weight)
);
return {
id: `APH-${String(alertIdCounter++).padStart(5, '0')}`,
timestamp: new Date(),
severity,
attackType: randomChoice(ATTACK_TYPES),
sourceIP: randomIP(),
targetIP: `10.0.${randomInt(1, 5)}.${randomInt(1, 254)}`,
source: randomChoice(SOURCES),
country: country.code,
countryName: country.name,
port: randomChoice([22, 80, 443, 445, 3389, 8080, 8443, 3306, 5432, 53, 25, 110, 143, 993, 995]),
blocked: Math.random() > 0.3,
};
}
export function generateAlertBatch(count = 20) {
const alerts = [];
for (let i = 0; i < count; i++) {
const alert = generateAlert();
alert.timestamp = new Date(Date.now() - randomInt(0, 3600000));
alerts.push(alert);
}
return alerts.sort((a, b) => b.timestamp - a.timestamp);
}
export function generateTrafficData(points = 24) {
const data = [];
const now = new Date();
for (let i = points - 1; i >= 0; i--) {
const time = new Date(now.getTime() - i * 3600000);
const hour = time.getHours();
// Simulate higher traffic during business hours
const multiplier = (hour >= 8 && hour <= 18) ? 1.5 : (hour >= 0 && hour <= 5) ? 0.4 : 1.0;
data.push({
time: time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
inbound: Math.round(randomFloat(120, 450) * multiplier),
outbound: Math.round(randomFloat(80, 320) * multiplier),
blocked: Math.round(randomFloat(5, 60) * multiplier),
});
}
return data;
}
export function generateSystemHealth() {
return [
{ name: 'pfSense Firewall', status: Math.random() > 0.05 ? 'online' : 'warning', cpu: randomInt(8, 45), mem: randomInt(30, 70), uptime: `${randomInt(10, 90)}d` },
{ name: 'Suricata IDS', status: Math.random() > 0.08 ? 'online' : 'warning', cpu: randomInt(15, 65), mem: randomInt(40, 80), uptime: `${randomInt(5, 60)}d` },
{ name: 'Wazuh SIEM', status: Math.random() > 0.05 ? 'online' : 'warning', cpu: randomInt(20, 55), mem: randomInt(50, 85), uptime: `${randomInt(3, 45)}d` },
{ name: 'ClamAV Scanner', status: Math.random() > 0.10 ? 'online' : 'offline', cpu: randomInt(5, 30), mem: randomInt(20, 50), uptime: `${randomInt(1, 30)}d` },
{ name: 'Zeek Monitor', status: Math.random() > 0.05 ? 'online' : 'warning', cpu: randomInt(10, 50), mem: randomInt(35, 75), uptime: `${randomInt(7, 60)}d` },
{ name: 'OpenVAS', status: Math.random() > 0.12 ? 'online' : 'offline', cpu: randomInt(5, 25), mem: randomInt(15, 45), uptime: `${randomInt(2, 20)}d` },
];
}
export function generateMitreData() {
const data = {};
for (const tactic of MITRE_TACTICS) {
data[tactic] = {};
for (const technique of MITRE_TECHNIQUES[tactic]) {
// 0 = not seen, 1 = low, 2 = medium, 3 = high detection count
data[tactic][technique] = Math.random() > 0.4 ? randomInt(0, 3) : 0;
}
}
return data;
}
export function generateTopThreats(count = 8) {
const threats = [];
const usedIPs = new Set();
for (let i = 0; i < count; i++) {
let ip;
do { ip = randomIP(); } while (usedIPs.has(ip));
usedIPs.add(ip);
const country = weightedChoice(
COUNTRIES.map(c => c),
COUNTRIES.map(c => c.weight)
);
threats.push({
ip,
country: country.code,
countryName: country.name,
attackType: randomChoice(ATTACK_TYPES),
count: randomInt(15, 500),
lastSeen: new Date(Date.now() - randomInt(0, 7200000)),
blocked: Math.random() > 0.2,
});
}
return threats.sort((a, b) => b.count - a.count);
}
export function generateIncidents() {
const statuses = ['Open', 'Investigating', 'Contained', 'Resolved'];
const priorities = ['P1', 'P2', 'P3', 'P4'];
const descriptions = [
'Suspicious outbound C2 traffic detected on endpoint WS-04',
'Multiple failed SSH logins from external IP on DMZ server',
'Malware signature detected in email attachment',
'Unauthorized port scan from internal host 10.0.2.15',
'Privilege escalation attempt on domain controller',
'Data exfiltration alert: large DNS TXT queries',
'Brute force attack on VPN gateway',
'Phishing campaign targeting finance department',
'Anomalous lateral movement between VLANs',
'Ransomware indicator detected on file server',
];
return descriptions.slice(0, randomInt(4, 8)).map((desc, i) => ({
id: `INC-${String(2024001 + i).padStart(7, '0')}`,
description: desc,
status: i === 0 ? 'Open' : randomChoice(statuses),
priority: i < 2 ? priorities[i] : randomChoice(priorities),
assignee: randomChoice(['analyst-1', 'analyst-2', 'lead-soc', 'ir-team']),
created: new Date(Date.now() - randomInt(3600000, 86400000 * 3)),
}));
}
export function generateVulnSummary() {
return {
critical: randomInt(2, 8),
high: randomInt(10, 35),
medium: randomInt(25, 80),
low: randomInt(40, 120),
info: randomInt(50, 200),
};
}
export function getOverallThreatLevel(alerts) {
const critCount = alerts.filter(a => a.severity === 'critical').length;
const highCount = alerts.filter(a => a.severity === 'high').length;
if (critCount >= 3) return 'CRITICAL';
if (critCount >= 1 || highCount >= 5) return 'HIGH';
if (highCount >= 2) return 'ELEVATED';
return 'GUARDED';
}
export { MITRE_TACTICS, MITRE_TECHNIQUES, SEVERITIES };

89
dashboard/src/index.css Normal file
View File

@@ -0,0 +1,89 @@
@import "tailwindcss";
@theme {
--color-ap-red: #D72638;
--color-ap-red-dim: #D7263844;
--color-ap-black: #1B1B1E;
--color-ap-dark: #121214;
--color-ap-panel: #222226;
--color-ap-silver: #E0E0E2;
--color-ap-silver-dim: #E0E0E266;
--color-ap-blue: #0056B3;
--color-ap-blue-dim: #0056B344;
--color-ap-green: #00C853;
--color-ap-yellow: #FFD600;
--color-ap-orange: #FF6D00;
--font-mono: 'JetBrains Mono', monospace;
--font-sans: 'Inter', sans-serif;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--font-sans);
background-color: var(--color-ap-dark);
color: var(--color-ap-silver);
overflow-x: hidden;
}
#root {
min-height: 100vh;
}
::-webkit-scrollbar {
width: 4px;
height: 4px;
}
::-webkit-scrollbar-track {
background: var(--color-ap-black);
}
::-webkit-scrollbar-thumb {
background: var(--color-ap-red);
}
@keyframes pulse-red {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.animate-pulse-red {
animation: pulse-red 2s ease-in-out infinite;
}
.panel {
background-color: var(--color-ap-panel);
border: 1px solid #E0E0E215;
position: relative;
overflow: hidden;
}
.panel-header {
font-family: var(--font-mono);
font-size: 0.65rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.12em;
color: #E0E0E288;
padding: 0.65rem 0.85rem;
border-bottom: 1px solid #E0E0E210;
display: flex;
align-items: center;
gap: 0.5rem;
}
.panel-header .dot {
width: 5px;
height: 5px;
background: var(--color-ap-red);
display: inline-block;
}
.severity-critical { color: #D72638; }
.severity-high { color: #FF6D00; }
.severity-medium { color: #FFD600; }
.severity-low { color: #0056B3; }
.severity-info { color: #E0E0E266; }

10
dashboard/src/main.jsx Normal file
View File

@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)

7
dashboard/vite.config.js Normal file
View File

@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [react(), tailwindcss()],
})