murshid / murshid_frontend /index.html
devorbit's picture
Initial deployment - secrets removed
26e1c2e
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Murshid | مُرشِد</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🛡️</text></svg>" />
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
:root { --accent: #3B82F6; }
[data-theme="dark"] { color-scheme: dark; }
[data-theme="dark"] body { background: #0f172a; color: #e2e8f0; }
[data-theme="dark"] .card { background: #1e293b; border-color: #334155; }
[data-theme="dark"] .sidebar { background: #1e293b; border-color: #334155; }
[data-theme="dark"] .topbar { background: #1e293b; border-color: #334155; }
[data-theme="dark"] input, [data-theme="dark"] textarea, [data-theme="dark"] select {
background: #0f172a; border-color: #334155; color: #e2e8f0;
}
[data-theme="dark"] table tr:hover td { background: #1e3a5f; }
.theme-blue { --accent: #3B82F6; }
.theme-purple { --accent: #8B5CF6; }
.theme-green { --accent: #10B981; }
.theme-orange { --accent: #F59E0B; }
.accent-btn { background-color: var(--accent); }
.accent-text { color: var(--accent); }
.accent-border { border-color: var(--accent); }
.sidebar-link.active { background: var(--accent); color: white; }
.sidebar-link { transition: all .2s; }
.sidebar-link:not(.active):hover { background: rgba(var(--accent-rgb,59,130,246),.12); }
.card { background: white; border: 1px solid #e2e8f0; border-radius: 12px; }
.spinner { border: 3px solid #e2e8f0; border-top-color: var(--accent); border-radius: 50%; width: 32px; height: 32px; animation: spin .8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
.fade-in { animation: fadeIn .3s ease; }
@keyframes fadeIn { from { opacity:0; transform: translateY(8px); } to { opacity:1; transform:none; } }
.tag-high { background: #d1fae5; color: #065f46; }
.tag-medium { background: #fef3c7; color: #92400e; }
.tag-low { background: #fee2e2; color: #991b1b; }
[data-theme="dark"] .tag-high { background: #064e3b; color: #6ee7b7; }
[data-theme="dark"] .tag-medium { background: #451a03; color: #fcd34d; }
[data-theme="dark"] .tag-low { background: #450a0a; color: #fca5a5; }
.wql-block { font-family: 'Courier New', monospace; font-size: 12px; white-space: pre-wrap; }
.copy-btn { transition: all .15s; }
.copy-btn:hover { transform: scale(1.05); }
pre { overflow-x: auto; }
</style>
<script>
tailwind.config = {
darkMode: 'class',
theme: { extend: { colors: { accent: 'var(--accent)' } } }
}
</script>
</head>
<body class="bg-gray-50 min-h-screen font-sans">
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect, useRef, useCallback } = React;
// ─── API ──────────────────────────────────────────────────────────────────
// URL
const BASE = window.location.origin;
const api = axios.create({ baseURL: BASE });
const getHealth = () => api.get('/health');
const getStats = () => api.get('/api/stats');
const analyzeRule = (rule_xml) => api.post('/rules/analyze', { rule_xml });
const getResults = (rule_id) => api.get(`/results/${rule_id}`);
const getQueries = (technique_id) => api.get(`/queries/${technique_id}`);
const addTemplate = (data) => api.post('/admin/templates', data);
const updateTemplate = (id, data) => api.patch(`/admin/templates/${id}`, data);
// DB Viewer
const getDbSummary = () => api.get('/api/db/summary');
const getDbRules = () => api.get('/api/db/rules');
const getDbMappings = () => api.get('/api/db/mappings');
const getDbTemplates = () => api.get('/api/db/templates');
const getDbTechniques = () => api.get('/api/db/techniques');
// ─── Storage helpers ──────────────────────────────────────────────────────
const store = {
get: (k, def) => { try { const v = localStorage.getItem(k); return v ? JSON.parse(v) : def; } catch { return def; } },
set: (k, v) => localStorage.setItem(k, JSON.stringify(v)),
};
// ─── Sidebar icon map ─────────────────────────────────────────────────────
const ICONS = {
dashboard: '🏠',
rules: '🔍',
admin: '⚙️',
templates: '📋',
settings: '👤',
};
// ─── StatsCard ────────────────────────────────────────────────────────────
function StatsCard({ icon, label, value, sub, color }) {
return (
<div className="card p-5 flex items-start gap-4 fade-in">
<div className={`text-3xl p-2 rounded-xl ${color}`}>{icon}</div>
<div>
<p className="text-xs text-gray-500 font-medium uppercase tracking-wide">{label}</p>
<p className="text-2xl font-bold">{value}</p>
{sub && <p className="text-xs text-gray-400 mt-0.5">{sub}</p>}
</div>
</div>
);
}
// ─── Chart: Technique Frequency ───────────────────────────────────────────
function TechFreqChart({ data }) {
const ref = useRef(null);
const chart = useRef(null);
useEffect(() => {
if (!data?.length || !ref.current) return;
chart.current?.destroy();
const accent = getComputedStyle(document.documentElement).getPropertyValue('--accent').trim() || '#3B82F6';
chart.current = new Chart(ref.current, {
type: 'bar',
data: {
labels: data.map(d => d.technique_id),
datasets: [{ label: 'Rules Mapped', data: data.map(d => d.count),
backgroundColor: accent + 'cc', borderColor: accent, borderWidth: 1, borderRadius: 6 }]
},
options: { plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, ticks: { stepSize: 1 } } }, responsive: true, maintainAspectRatio: false }
});
return () => chart.current?.destroy();
}, [data]);
if (!data?.length) return <p className="text-gray-400 text-sm py-8 text-center">No data yet — analyze rules first.</p>;
return <div style={{ height: 220 }}><canvas ref={ref} /></div>;
}
// ─── Figure 4-11: MITRE ATT&CK Techniques Distribution (Top 5 Horizontal Bar) ──────
// Report §4.2.3: "A visual chart showing the distribution of MITRE ATT&CK techniques
// related to the entered rule, categorized by confidence levels (High, Medium, Low)."
function TechniqueDistributionChart({ mappings }) {
const ref = useRef(null);
const chart = useRef(null);
useEffect(() => {
if (!mappings?.length || !ref.current) return;
chart.current?.destroy();
// Top 5 techniques sorted by confidence (already sorted from API)
const top5 = mappings.slice(0, 5);
const getColor = (pct) => {
if (pct >= 70) return { bg: 'rgba(16,185,129,0.8)', border: '#059669' }; // High - green
if (pct >= 40) return { bg: 'rgba(245,158,11,0.8)', border: '#d97706' }; // Medium - yellow
return { bg: 'rgba(239,68,68,0.8)', border: '#dc2626' }; // Low - red
};
const bgColors = top5.map(m => getColor(m.confidence_percent).bg);
const borderColors = top5.map(m => getColor(m.confidence_percent).border);
chart.current = new Chart(ref.current, {
type: 'bar',
data: {
labels: top5.map(m => m.technique_id),
datasets: [{
label: 'Confidence %',
data: top5.map(m => m.confidence_percent),
backgroundColor: bgColors,
borderColor: borderColors,
borderWidth: 1.5,
borderRadius: 6,
}]
},
options: {
indexAxis: 'y', // horizontal bars — technique on Y, confidence on X
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
label: ctx => {
const pct = ctx.raw;
const level = pct >= 70 ? '🟢 High' : pct >= 40 ? '🟡 Medium' : '🔴 Low';
return ` ${level} ${pct}%`;
}
}
}
},
scales: {
x: { beginAtZero: true, max: 100, ticks: { callback: v => v + '%' } },
y: { ticks: { font: { family: 'monospace', weight: 'bold' } } }
}
}
});
return () => chart.current?.destroy();
}, [mappings]);
if (!mappings?.length) return <p className="text-gray-400 text-sm py-8 text-center">No data.</p>;
return <div style={{ height: Math.max(mappings.slice(0,5).length * 50 + 40, 160) }}><canvas ref={ref} /></div>;
}
// ─── Confidence Badge ─────────────────────────────────────────────────────
function ConfBadge({ pct }) {
const cls = pct >= 70 ? 'tag-high' : pct >= 40 ? 'tag-medium' : 'tag-low';
const label = pct >= 70 ? 'High' : pct >= 40 ? 'Med' : 'Low';
return <span className={`text-xs font-semibold px-2 py-0.5 rounded-full ${cls}`}>{label} {pct}%</span>;
}
// ─── Toast ────────────────────────────────────────────────────────────────
function Toast({ msg, type, onClose }) {
useEffect(() => { const t = setTimeout(onClose, 3000); return () => clearTimeout(t); }, []);
const bg = type === 'success' ? 'bg-green-600' : type === 'error' ? 'bg-red-600' : 'bg-blue-600';
return (
<div className={`fixed bottom-5 right-5 z-50 flex items-center gap-3 px-5 py-3 rounded-xl text-white shadow-xl ${bg} fade-in`}>
<span>{msg}</span>
<button onClick={onClose} className="text-white/70 hover:text-white text-lg leading-none">×</button>
</div>
);
}
// ─── LOGIN PAGE ───────────────────────────────────────────────────────────
function LoginPage({ onLogin }) {
const [email, setEmail] = useState('admin@murshid.com');
const [pass, setPass] = useState('password');
const [role, setRole] = useState('analyst');
const [err, setErr] = useState('');
const [loading, setLoading] = useState(false);
async function handleSubmit(e) {
e.preventDefault();
if (!email || !pass) { setErr('Please fill all fields.'); return; }
setLoading(true);
try {
await getHealth();
onLogin({ email, role, username: email.split('@')[0] });
} catch {
setErr('Cannot connect to backend. Make sure the server is running on port 8000.');
} finally { setLoading(false); }
}
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-900 via-blue-950 to-slate-900 p-4">
<div className="w-full max-w-md">
<div className="text-center mb-8">
<div className="text-5xl mb-3">🛡️</div>
<h1 className="text-3xl font-bold text-white">Murshid | مُرشِد</h1>
<p className="text-blue-300 text-sm mt-1">From Alerts to Guidance</p>
</div>
<div className="bg-white/10 backdrop-blur-md border border-white/20 rounded-2xl p-8 shadow-2xl">
<h2 className="text-white text-xl font-semibold mb-6 text-center">Sign In</h2>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-blue-200 text-sm mb-1.5 font-medium">Email</label>
<input className="w-full rounded-xl px-4 py-2.5 bg-white/10 border border-white/20 text-white placeholder-white/40 focus:outline-none focus:border-blue-400 focus:ring-1 focus:ring-blue-400"
type="email" value={email} onChange={e => setEmail(e.target.value)} placeholder="analyst@soc.com" />
</div>
<div>
<label className="block text-blue-200 text-sm mb-1.5 font-medium">Password</label>
<input className="w-full rounded-xl px-4 py-2.5 bg-white/10 border border-white/20 text-white placeholder-white/40 focus:outline-none focus:border-blue-400 focus:ring-1 focus:ring-blue-400"
type="password" value={pass} onChange={e => setPass(e.target.value)} placeholder="••••••••" />
</div>
<div>
<label className="block text-blue-200 text-sm mb-1.5 font-medium">Role</label>
<select className="w-full rounded-xl px-4 py-2.5 bg-white/10 border border-white/20 text-white focus:outline-none focus:border-blue-400"
value={role} onChange={e => setRole(e.target.value)}>
<option value="analyst" className="bg-slate-800">SOC Analyst</option>
<option value="admin" className="bg-slate-800">Administrator</option>
</select>
</div>
{err && <p className="text-red-400 text-sm bg-red-900/30 px-3 py-2 rounded-lg">{err}</p>}
<button type="submit" disabled={loading}
className="w-full accent-btn text-white font-semibold py-3 rounded-xl hover:opacity-90 transition flex items-center justify-center gap-2 mt-2">
{loading ? <><span className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" /> Connecting...</> : 'Sign In →'}
</button>
</form>
</div>
</div>
</div>
);
}
// ─── SIDEBAR ──────────────────────────────────────────────────────────────
function Sidebar({ page, onNav, user, onLogout }) {
const links = [
{ id: 'dashboard', label: 'Dashboard', icon: '🏠' },
{ id: 'rules', label: 'Rule Lookup', icon: '🔍' },
{ id: 'dbviewer', label: 'نتائج DB', icon: '🗄️' },
...(user?.role === 'admin' ? [
{ id: 'admin', label: 'Rule Mapping', icon: '⚙️' },
{ id: 'templates',label: 'WQL Templates',icon: '📋' },
] : []),
{ id: 'settings', label: 'Settings', icon: '👤' },
];
return (
<aside className="sidebar w-60 min-h-screen flex flex-col border-r shadow-sm">
<div className="p-5 border-b">
<div className="flex items-center gap-2.5">
<span className="text-2xl">🛡️</span>
<div>
<p className="font-bold text-sm">Murshid</p>
<p className="text-xs text-gray-500">مُرشِد</p>
</div>
</div>
</div>
<nav className="flex-1 p-3 space-y-1">
{links.map(l => (
<button key={l.id} onClick={() => onNav(l.id)}
className={`sidebar-link w-full text-left px-3 py-2.5 rounded-lg flex items-center gap-3 text-sm font-medium ${page === l.id ? 'active' : 'text-gray-600'}`}>
<span>{l.icon}</span> {l.label}
</button>
))}
</nav>
<div className="p-4 border-t">
<div className="flex items-center gap-3 mb-3">
<div className="w-8 h-8 rounded-full accent-btn flex items-center justify-center text-white text-sm font-bold">
{user?.username?.[0]?.toUpperCase() || 'U'}
</div>
<div className="overflow-hidden">
<p className="text-sm font-medium truncate">{user?.username}</p>
<p className="text-xs text-gray-400 capitalize">{user?.role}</p>
</div>
</div>
<button onClick={onLogout} className="text-xs text-gray-400 hover:text-red-500 flex items-center gap-1 transition">
🚪 Sign out
</button>
</div>
</aside>
);
}
// ─── HEADER ───────────────────────────────────────────────────────────────
function Header({ title, backendOk }) {
return (
<header className="topbar border-b px-6 py-4 flex items-center justify-between">
<h1 className="text-lg font-semibold">{title}</h1>
<div className="flex items-center gap-2">
<span className={`w-2 h-2 rounded-full ${backendOk ? 'bg-green-500' : 'bg-red-500'}`}></span>
<span className="text-xs text-gray-500">Backend {backendOk ? 'online' : 'offline'}</span>
</div>
</header>
);
}
// ─── DASHBOARD PAGE ───────────────────────────────────────────────────────
function DashboardPage({ user }) {
const [stats, setStats] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
getStats().then(r => setStats(r.data)).catch(() => {}).finally(() => setLoading(false));
}, []);
if (loading) return <div className="flex justify-center mt-20"><div className="spinner"></div></div>;
return (
<div className="p-6 space-y-6 fade-in">
<div>
<h2 className="text-xl font-bold">Welcome, {user?.username} 👋</h2>
<p className="text-gray-500 text-sm mt-0.5">Here's a real-time overview of Murshid's analytical performance.</p>
</div>
<div className="grid grid-cols-2 gap-4 lg:grid-cols-4">
<StatsCard icon="📌" label="Rules Mapped" value={stats?.total_rules_mapped ?? 0} color="bg-blue-50" />
<StatsCard icon="🔗" label="Total Mappings" value={stats?.total_mappings ?? 0} color="bg-purple-50" />
<StatsCard icon="🧩" label="Techniques" value={stats?.total_techniques ?? 0} color="bg-green-50" />
<StatsCard icon="📄" label="WQL Templates" value={stats?.total_queries ?? 0} color="bg-orange-50" />
</div>
<div className="card p-5">
<h3 className="font-semibold mb-4 text-sm uppercase tracking-wide text-gray-500">MITRE ATT&CK Technique Frequency (Top 10)</h3>
<TechFreqChart data={stats?.technique_frequency} />
</div>
</div>
);
}
// ─── RULES LOOKUP PAGE ────────────────────────────────────────────────────
// Figure 4-10: Search bar
// Figure 4-11: MITRE ATT&CK Techniques Distribution chart (Top 5 horizontal bar)
// Figure 4-12: Returned Investigation Queries (WQL table)
function RulesPage({ addToast }) {
const [ruleId, setRuleId] = useState('');
const [results, setResults] = useState(null);
const [queries, setQueries] = useState({});
const [loading, setLoading] = useState(false);
const [err, setErr] = useState('');
async function fetchMapping() {
if (!ruleId.trim()) { setErr('Enter a Rule ID.'); return; }
setErr(''); setLoading(true); setResults(null); setQueries({});
try {
const r = await getResults(ruleId.trim());
setResults(r.data);
// auto-fetch WQL queries for detected techniques (primary + secondary)
const qMap = {};
for (const m of (r.data.detected || [])) {
try {
const qr = await getQueries(m.technique_id);
qMap[m.technique_id] = qr.data;
} catch { qMap[m.technique_id] = []; }
}
setQueries(qMap);
} catch (e) {
const msg = e.response?.data?.detail || 'No results found. Make sure the rule has been analyzed first.';
setErr(msg);
} finally { setLoading(false); }
}
async function copyQuery(q) {
await navigator.clipboard.writeText(q);
addToast('Query copied to clipboard!', 'success');
}
const detected = results?.detected || [];
const allMappings = results?.mappings || [];
return (
<div className="p-6 space-y-6 fade-in">
<div>
<h2 className="text-xl font-bold">Rule Lookup</h2>
<p className="text-gray-500 text-sm">Enter a Wazuh Rule ID to view MITRE ATT&CK mappings and WQL investigation queries.</p>
</div>
{/* Figure 4-10: Search bar */}
<div className="card p-5">
<label className="block font-medium text-sm mb-2">Rule ID</label>
<div className="flex gap-3">
<input
className="flex-1 border rounded-xl px-4 py-2.5 text-sm focus:outline-none focus:ring-2"
style={{ '--tw-ring-color': 'var(--accent)' }}
placeholder="e.g. 18205, 597" value={ruleId}
onChange={e => setRuleId(e.target.value)}
onKeyDown={e => e.key === 'Enter' && fetchMapping()} />
<button onClick={fetchMapping} disabled={loading}
className="accent-btn text-white px-6 py-2.5 rounded-xl font-medium hover:opacity-90 transition text-sm flex items-center gap-2">
{loading
? <span className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
: '🔍'}
Fetch Mapping
</button>
</div>
{err && <p className="text-red-500 text-sm mt-3 bg-red-50 px-3 py-2 rounded-lg">{err}</p>}
</div>
{/* Figure 4-11 + 4-12 */}
{results && (
<div className="space-y-6 fade-in">
{/* Figure 4-11: MITRE ATT&CK Techniques Distribution */}
<div className="card p-5">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="font-semibold text-base">MITRE ATT&CK Techniques Distribution</h3>
<p className="text-gray-500 text-xs mt-0.5">Rule ID: <span className="font-mono font-semibold accent-text">{results.rule_id}</span> — Top 5 techniques by confidence level</p>
</div>
<div className="flex gap-3 text-xs">
<span className="flex items-center gap-1"><span className="w-3 h-3 rounded bg-emerald-500 inline-block" />High ≥70%</span>
<span className="flex items-center gap-1"><span className="w-3 h-3 rounded bg-amber-400 inline-block" />Med 40–70%</span>
<span className="flex items-center gap-1"><span className="w-3 h-3 rounded bg-red-500 inline-block" />Low &lt;40%</span>
</div>
</div>
<TechniqueDistributionChart mappings={allMappings} />
</div>
{/* Figure 4-12: Returned Investigation Queries */}
<div className="card p-5">
<div className="mb-4">
<h3 className="font-semibold text-base">Returned Investigation Queries</h3>
<p className="text-gray-500 text-xs mt-0.5">
WQL templates for primary technique
{detected.length > 1 ? ' + secondary (confidence ≥ 50%)' : ''}.
Replace <span className="font-mono bg-gray-100 px-1 rounded text-xs">${'{HOST}'}</span>,{' '}
<span className="font-mono bg-gray-100 px-1 rounded text-xs">${'{USER}'}</span>,{' '}
<span className="font-mono bg-gray-100 px-1 rounded text-xs">${'{IP}'}</span>{' '}
with actual alert values.
</p>
</div>
{detected.length === 0 ? (
<div className="text-center py-8">
<p className="text-gray-400 text-sm">No techniques with confidence ≥ 50%.</p>
<p className="text-gray-300 text-xs mt-1">All techniques had low confidence scores.</p>
</div>
) : (
<div className="space-y-5">
{detected.map((m, i) => {
const tQueries = queries[m.technique_id] || [];
const badge = m.is_primary
? <span className="text-xs bg-blue-600 text-white px-2 py-0.5 rounded-full font-medium">Primary</span>
: <span className="text-xs bg-amber-500 text-white px-2 py-0.5 rounded-full font-medium">Secondary</span>;
return (
<div key={i} className={`border rounded-xl p-4 ${m.is_primary ? 'border-blue-300 bg-blue-50/40' : 'border-amber-200 bg-amber-50/30'}`}>
{/* Technique header */}
<div className="flex items-center gap-2 mb-4">
{badge}
<span className="font-mono font-bold text-sm">{m.technique_id}</span>
<ConfBadge pct={m.confidence_percent} />
<span className="text-xs text-gray-400 ml-auto">Rank #{m.rank}</span>
</div>
{/* WQL queries table */}
{tQueries.length === 0 ? (
<div className="bg-gray-50 border border-dashed rounded-lg p-4 text-center">
<p className="text-gray-400 text-sm">No WQL templates stored for <span className="font-mono">{m.technique_id}</span>.</p>
<p className="text-gray-300 text-xs mt-1">Admin can add templates in the WQL Templates panel.</p>
</div>
) : (
<div className="space-y-3">
{tQueries.map((q, j) => (
<div key={j} className="rounded-xl overflow-hidden border border-gray-200">
{/* Template info row */}
<div className="flex items-center justify-between bg-gray-50 px-4 py-2 border-b border-gray-200">
<div className="flex items-center gap-3">
<span className="text-xs text-gray-400 font-mono">Template #{q.template_id}</span>
{q.purpose && <span className="text-xs font-medium text-gray-600">{q.purpose}</span>}
</div>
<button
onClick={() => copyQuery(q.wql_query)}
className="copy-btn text-xs bg-gray-200 hover:bg-gray-300 text-gray-700 px-3 py-1 rounded-lg font-medium transition flex items-center gap-1">
📋 Copy
</button>
</div>
{/* WQL block */}
<div className="bg-gray-900 px-4 py-3">
<pre className="wql-block text-green-400 text-xs leading-relaxed">{q.wql_query}</pre>
</div>
{/* Note */}
{q.note && (
<div className="bg-amber-50 px-4 py-2 border-t border-amber-100">
<p className="text-amber-700 text-xs">💡 {q.note}</p>
</div>
)}
</div>
))}
</div>
)}
</div>
);
})}
</div>
)}
{/* Ignored techniques note */}
{allMappings.length > detected.length && (
<div className="mt-4 pt-4 border-t border-dashed">
<p className="text-xs text-gray-400">
{allMappings.length - detected.length} technique(s) excluded from queries (confidence &lt; 50%) —{' '}
{allMappings.slice(detected.length).map(m => m.technique_id).join(', ')}
</p>
</div>
)}
</div>
</div>
)}
</div>
);
}
// ─── ADMIN PANEL PAGE (Figure 4-13, 4-14, 4-15) ──────────────────────────
function AdminPage({ addToast }) {
const [singleXml, setSingleXml] = useState('');
const [analyzing, setAnalyzing] = useState(false);
const [result, setResult] = useState(null);
const [jobs, setJobs] = useState(store.get('jobs', []));
const [dragOver, setDragOver] = useState(false);
const [uploadedFile, setUploadedFile] = useState(null);
const [uploadingFile, setUploadingFile] = useState(false);
const fileInputRef = useRef(null);
// ── helpers ─────────────────────────────────────────────────────────────
function addJob(file, status, rules = 1) {
const job = { id: Date.now(), file, status, progress: status === 'done' ? 100 : 0, rules, ts: new Date().toLocaleString() };
const updated = [job, ...jobs];
setJobs(updated); store.set('jobs', updated);
return { job, updated };
}
function updateJob(updated, jobId, patch) {
const next = updated.map(j => j.id === jobId ? { ...j, ...patch } : j);
setJobs(next); store.set('jobs', next);
}
// ── 1. Single Rule Submit ────────────────────────────────────────────────
async function submitSingleRule() {
if (!singleXml.trim()) { addToast('Paste a Wazuh rule XML first.', 'error'); return; }
setAnalyzing(true); setResult(null);
const { job, updated } = addJob('Single Rule', 'running', 1);
try {
const r = await analyzeRule(singleXml);
setResult(r.data);
updateJob(updated, job.id, { status: 'done', progress: 100 });
addToast(`✅ Rule ${r.data.rule_id}${r.data.detected.length} technique(s) detected.`, 'success');
} catch (e) {
const msg = e.response?.data?.detail || 'Analysis failed.';
updateJob(updated, job.id, { status: 'failed', progress: 0 });
addToast(msg, 'error');
} finally { setAnalyzing(false); }
}
// ── 2. XML File Upload ───────────────────────────────────────────────────
function parseRulesFromXml(xmlText) {
const parser = new DOMParser();
const doc = parser.parseFromString(xmlText, 'text/xml');
const rules = doc.querySelectorAll('rule');
const xmls = [];
rules.forEach(rule => {
const serializer = new XMLSerializer();
xmls.push(serializer.serializeToString(rule));
});
return xmls;
}
async function processFileContent(file) {
if (!file.name.endsWith('.xml')) { addToast('Only .xml files are supported.', 'error'); return; }
setUploadedFile(file);
setUploadingFile(true);
setResult(null);
const text = await file.text();
const ruleXmls = parseRulesFromXml(text);
if (ruleXmls.length === 0) { addToast('No <rule> elements found in file.', 'error'); setUploadingFile(false); return; }
const { job, updated } = addJob(file.name, 'running', ruleXmls.length);
addToast(`📦 Processing ${ruleXmls.length} rule(s) from ${file.name}...`, 'info');
let done = 0, failed = 0;
for (const ruleXml of ruleXmls) {
try {
await analyzeRule(ruleXml);
done++;
} catch { failed++; }
const pct = Math.round(((done + failed) / ruleXmls.length) * 100);
updateJob(updated, job.id, { progress: pct });
}
const finalStatus = failed === ruleXmls.length ? 'failed' : 'done';
updateJob(updated, job.id, { status: finalStatus, progress: 100 });
setUploadingFile(false);
addToast(`✅ ${done} rule(s) processed${failed ? `, ${failed} failed` : ''}.`, finalStatus === 'done' ? 'success' : 'error');
}
function handleFileInput(e) { if (e.target.files[0]) processFileContent(e.target.files[0]); }
function handleDrop(e) { e.preventDefault(); setDragOver(false); if (e.dataTransfer.files[0]) processFileContent(e.dataTransfer.files[0]); }
function handleDragOver(e) { e.preventDefault(); setDragOver(true); }
function handleDragLeave() { setDragOver(false); }
const isProcessing = analyzing || uploadingFile;
return (
<div className="p-6 space-y-6 fade-in">
{/* Header */}
<div>
<h2 className="text-xl font-bold">Rule Mapping Panel</h2>
<p className="text-gray-500 text-sm">Upload Wazuh XML rule files or submit a single rule to map them to MITRE ATT&CK techniques.</p>
</div>
{/* KPIs */}
<div className="grid grid-cols-2 gap-4 lg:grid-cols-3">
<StatsCard icon="📊" label="Total Rules Processed" value={jobs.filter(j=>j.status==='done').reduce((a,j)=>a+j.rules,0)} color="bg-blue-50" />
<StatsCard icon="✅" label="Jobs Completed" value={jobs.filter(j=>j.status==='done').length} color="bg-green-50" />
<StatsCard icon="⏳" label="Processing Queue" value={jobs.filter(j=>j.status==='running').length} color="bg-yellow-50" />
</div>
{/* Upload Section — Figure 4-13 */}
<div className="card p-6">
<h3 className="font-semibold mb-4">Upload Wazuh Rules</h3>
{/* Drag & Drop Zone */}
<div
onClick={() => fileInputRef.current?.click()}
onDrop={handleDrop}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
className={`border-2 border-dashed rounded-2xl p-10 text-center cursor-pointer transition-all select-none
${dragOver ? 'border-blue-400 bg-blue-50' : 'border-gray-200 hover:border-blue-300 hover:bg-gray-50'}`}>
<input ref={fileInputRef} type="file" accept=".xml" className="hidden" onChange={handleFileInput} />
<div className={`w-14 h-14 rounded-full flex items-center justify-center mx-auto mb-4 text-2xl
${dragOver ? 'bg-blue-500 text-white' : 'bg-blue-100 text-blue-600'}`}>
{uploadingFile ? '⏳' : '⬆'}
</div>
{uploadedFile && !uploadingFile ? (
<div>
<p className="font-semibold text-green-700">✅ {uploadedFile.name}</p>
<p className="text-xs text-gray-400 mt-1">Click to upload a different file</p>
</div>
) : uploadingFile ? (
<div>
<p className="font-semibold text-blue-700">Processing {uploadedFile?.name}...</p>
<p className="text-xs text-gray-400 mt-1">Analyzing rules with LLaMA + LogReg</p>
</div>
) : (
<div>
<p className="font-semibold text-gray-700">Drag and drop XML file(s) here, or click to browse</p>
<p className="text-xs text-gray-400 mt-1">Supported format: .xml | Max 10 MB per file</p>
</div>
)}
</div>
<button
onClick={() => fileInputRef.current?.click()}
disabled={isProcessing}
className="w-full mt-4 accent-btn text-white py-3 rounded-xl font-medium hover:opacity-90 transition flex items-center justify-center gap-2">
{uploadingFile
? <><span className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />Processing...</>
: '📁 Upload File'}
</button>
</div>
{/* Divider */}
<div className="flex items-center gap-4">
<div className="flex-1 border-t border-gray-200" />
<span className="text-xs text-gray-400 font-medium">Or paste rule(s) manually</span>
<div className="flex-1 border-t border-gray-200" />
</div>
{/* Single Rule Input — Figure 4-15 */}
<div className="card p-6">
<h3 className="font-semibold mb-3">Single Rule Input</h3>
<p className="text-xs text-gray-400 mb-3">Paste a single Wazuh rule in XML format</p>
<textarea
rows={6}
value={singleXml}
onChange={e => setSingleXml(e.target.value)}
placeholder={'<rule id="597" level="5">\n <description>Registry Key Entry Deleted.</description>\n <mitre><id>T1070.004</id></mitre>\n</rule>'}
className="w-full border rounded-xl px-4 py-3 font-mono text-sm focus:outline-none focus:ring-2 resize-none" />
<button
onClick={submitSingleRule}
disabled={isProcessing}
className="mt-3 accent-btn text-white px-6 py-2.5 rounded-xl font-medium hover:opacity-90 transition text-sm flex items-center gap-2">
{analyzing
? <><span className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />Analyzing...</>
: '⚡ Submit Rule'}
</button>
</div>
{/* Result Card — Figure 4-15 (Successful submission) */}
{result && (
<div className="card p-5 fade-in border-l-4 border-green-500">
<h3 className="font-semibold text-green-700 mb-3">✅ Successful Submission — Rule #{result.rule_id}</h3>
<div className="bg-gray-50 rounded-lg p-3 mb-3">
<p className="text-xs text-gray-500 mb-1">LLaMA Summary</p>
<p className="text-sm font-medium">{result.summary}</p>
<p className="text-xs text-gray-400 mt-1">Pipeline mode: <span className="font-mono font-semibold">{result.pipeline_mode}</span></p>
</div>
{result.detected.length === 0 ? (
<p className="text-gray-400 text-sm">No techniques detected (confidence below thresholds).</p>
) : (
<table className="w-full text-sm">
<thead><tr className="text-left text-gray-500 text-xs uppercase border-b">
<th className="pb-2 pr-4">Technique</th>
<th className="pb-2 pr-4">Confidence</th>
<th className="pb-2 pr-4">Proba</th>
<th className="pb-2">Gap</th>
</tr></thead>
<tbody>
{result.detected.map((d, i) => (
<tr key={i} className="border-b last:border-0">
<td className="py-2 pr-4 font-mono font-semibold">{d.technique_id}</td>
<td className="py-2 pr-4"><ConfBadge pct={d.confidence_percent} /></td>
<td className="py-2 pr-4 text-gray-500">{(d.proba * 100).toFixed(1)}%</td>
<td className="py-2 text-green-600">+{d.gap.toFixed(3)}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
)}
{/* Mapping Progress Table — Figure 4-14 */}
{jobs.length > 0 && (
<div className="card p-5">
<div className="flex items-center justify-between mb-4">
<h3 className="font-semibold text-sm uppercase tracking-wide text-gray-500">Mapping Progress Table</h3>
<button onClick={() => { setJobs([]); store.set('jobs', []); }} className="text-xs text-gray-400 hover:text-red-500 transition">Clear</button>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead><tr className="text-left text-gray-500 text-xs uppercase border-b bg-gray-50">
<th className="px-3 py-2">Job ID</th>
<th className="px-3 py-2">File Name</th>
<th className="px-3 py-2">Status</th>
<th className="px-3 py-2">Progress</th>
<th className="px-3 py-2">Number of Rules</th>
<th className="px-3 py-2">Timestamp</th>
</tr></thead>
<tbody>
{jobs.map((j, i) => (
<tr key={i} className="border-b last:border-0 hover:bg-gray-50">
<td className="px-3 py-2 font-mono text-xs text-gray-400">{j.id.toString().slice(-6)}</td>
<td className="px-3 py-2 font-medium">{j.file}</td>
<td className="px-3 py-2">
<span className={`px-2 py-0.5 rounded-full text-xs font-semibold
${j.status === 'done' ? 'tag-high' : j.status === 'running' ? 'bg-blue-100 text-blue-700' : j.status === 'failed' ? 'tag-low' : 'bg-gray-100 text-gray-600'}`}>
{j.status}
</span>
</td>
<td className="px-3 py-2">
<div className="flex items-center gap-2">
<div className="w-20 bg-gray-200 rounded-full h-1.5">
<div className="h-1.5 rounded-full accent-btn transition-all" style={{ width: j.progress + '%' }} />
</div>
<span className="text-xs text-gray-400">{j.progress}%</span>
</div>
</td>
<td className="px-3 py-2 text-center">{j.rules}</td>
<td className="px-3 py-2 text-gray-400 text-xs">{j.ts}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</div>
);
}
// ─── TEMPLATES PAGE ───────────────────────────────────────────────────────
function TemplatesPage({ addToast }) {
const [templates, setTemplates] = useState([]);
const [loading, setLoading] = useState(false);
const [techId, setTechId] = useState('');
const [form, setForm] = useState({ technique_id: '', purpose: '', wql_query: '', note: '' });
const [showForm, setShowForm] = useState(false);
async function search() {
if (!techId.trim()) return;
setLoading(true);
try {
const r = await getQueries(techId.trim());
setTemplates(r.data);
} catch { setTemplates([]); addToast(`No templates found for ${techId}`, 'error'); }
finally { setLoading(false); }
}
async function save() {
if (!form.technique_id || !form.wql_query) { addToast('Technique ID and WQL are required.', 'error'); return; }
try {
await addTemplate(form);
addToast('Template saved!', 'success');
setShowForm(false);
setForm({ technique_id: '', purpose: '', wql_query: '', note: '' });
} catch { addToast('Failed to save template.', 'error'); }
}
async function toggleActive(tpl) {
try {
await updateTemplate(tpl.template_id, { is_active: !tpl.is_active });
addToast(`Template ${tpl.is_active ? 'disabled' : 'enabled'}.`, 'success');
search();
} catch { addToast('Update failed.', 'error'); }
}
return (
<div className="p-6 space-y-6 fade-in">
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold">WQL Templates</h2>
<p className="text-gray-500 text-sm">Manage static investigation query templates linked to MITRE techniques.</p>
</div>
<button onClick={() => setShowForm(!showForm)}
className="accent-btn text-white px-4 py-2 rounded-xl text-sm font-medium hover:opacity-90 transition">
{showForm ? '✕ Cancel' : '+ Add Template'}
</button>
</div>
{showForm && (
<div className="card p-5 fade-in border-blue-200">
<h3 className="font-semibold text-sm mb-4">New WQL Template</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-xs font-medium text-gray-500 block mb-1">MITRE Technique ID *</label>
<input className="w-full border rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1"
placeholder="T1070.004" value={form.technique_id} onChange={e => setForm(f => ({ ...f, technique_id: e.target.value }))} />
</div>
<div>
<label className="text-xs font-medium text-gray-500 block mb-1">Purpose</label>
<input className="w-full border rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1"
placeholder="Detect file deletion" value={form.purpose} onChange={e => setForm(f => ({ ...f, purpose: e.target.value }))} />
</div>
</div>
<div className="mt-3">
<label className="text-xs font-medium text-gray-500 block mb-1">WQL Query *</label>
<textarea rows={3} className="w-full border rounded-lg px-3 py-2 font-mono text-sm focus:outline-none focus:ring-1 resize-none"
placeholder="agent.name:${HOST} AND rule.description:&quot;File deleted&quot;"
value={form.wql_query} onChange={e => setForm(f => ({ ...f, wql_query: e.target.value }))} />
</div>
<div className="mt-3">
<label className="text-xs font-medium text-gray-500 block mb-1">Note</label>
<input className="w-full border rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1"
placeholder="Replace ${HOST} with actual hostname" value={form.note} onChange={e => setForm(f => ({ ...f, note: e.target.value }))} />
</div>
<button onClick={save} className="mt-4 accent-btn text-white px-5 py-2 rounded-xl text-sm font-medium hover:opacity-90 transition">
💾 Save Template
</button>
</div>
)}
<div className="card p-5">
<div className="flex gap-3 mb-4">
<input className="flex-1 border rounded-xl px-4 py-2.5 text-sm focus:outline-none focus:ring-1"
placeholder="Enter technique ID (e.g. T1484)" value={techId}
onChange={e => setTechId(e.target.value)} onKeyDown={e => e.key === 'Enter' && search()} />
<button onClick={search} disabled={loading}
className="accent-btn text-white px-5 py-2.5 rounded-xl text-sm font-medium hover:opacity-90 transition">
Search
</button>
</div>
{templates.length === 0
? <p className="text-gray-400 text-sm text-center py-6">Search for a technique ID to view its templates.</p>
: (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead><tr className="text-left text-gray-500 text-xs uppercase border-b bg-gray-50">
<th className="px-3 py-2">Technique</th>
<th className="px-3 py-2">Template ID</th>
<th className="px-3 py-2">Purpose</th>
<th className="px-3 py-2">Investigation Query (WQL)</th>
<th className="px-3 py-2">Notes</th>
<th className="px-3 py-2">Action</th>
</tr></thead>
<tbody>
{templates.map((t, i) => (
<tr key={i} className="border-b last:border-0 hover:bg-gray-50">
<td className="px-3 py-3 font-mono font-semibold text-xs">{t.technique_id}</td>
<td className="px-3 py-3 text-gray-500 text-xs">{t.template_id}</td>
<td className="px-3 py-3 text-xs">{t.purpose || '—'}</td>
<td className="px-3 py-3 max-w-xs">
<div className="bg-gray-900 text-green-400 rounded-lg px-3 py-2 font-mono text-xs overflow-x-auto">{t.wql_query}</div>
</td>
<td className="px-3 py-3 text-xs text-gray-500">{t.note || '—'}</td>
<td className="px-3 py-3">
<button onClick={() => toggleActive(t)}
className="text-xs px-2 py-1 rounded-lg border hover:bg-gray-100 transition">
Disable
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
);
}
// ─── SETTINGS PAGE ────────────────────────────────────────────────────────
function SettingsPage({ user, setUser, theme, setTheme }) {
const [username, setUsername] = useState(user?.username || '');
const [email, setEmail] = useState(user?.email || '');
const [dark, setDark] = useState(document.documentElement.getAttribute('data-theme') === 'dark');
const [saved, setSaved] = useState(false);
function toggleDark(val) {
setDark(val);
document.documentElement.setAttribute('data-theme', val ? 'dark' : 'light');
document.body.classList.toggle('dark', val);
store.set('dark', val);
}
function saveProfile() {
setUser(u => ({ ...u, username, email }));
store.set('user', { ...user, username, email });
setSaved(true);
setTimeout(() => setSaved(false), 2000);
}
const themes = ['blue', 'purple', 'green', 'orange'];
const themeColors = { blue: '#3B82F6', purple: '#8B5CF6', green: '#10B981', orange: '#F59E0B' };
return (
<div className="p-6 space-y-6 fade-in max-w-2xl">
<h2 className="text-xl font-bold">Settings</h2>
<div className="card p-5">
<h3 className="font-semibold mb-4">Profile Settings</h3>
<div className="flex items-center gap-4 mb-5">
<div className="w-16 h-16 rounded-full accent-btn flex items-center justify-center text-white text-2xl font-bold">
{username?.[0]?.toUpperCase() || 'U'}
</div>
<div>
<p className="font-medium">{username}</p>
<p className="text-sm text-gray-500 capitalize">{user?.role}</p>
</div>
</div>
<div className="space-y-3">
<div>
<label className="text-sm font-medium text-gray-600 block mb-1">Username</label>
<input className="w-full border rounded-xl px-4 py-2.5 text-sm focus:outline-none focus:ring-1"
value={username} onChange={e => setUsername(e.target.value)} />
</div>
<div>
<label className="text-sm font-medium text-gray-600 block mb-1">Email</label>
<input className="w-full border rounded-xl px-4 py-2.5 text-sm focus:outline-none focus:ring-1"
type="email" value={email} onChange={e => setEmail(e.target.value)} />
</div>
</div>
<button onClick={saveProfile} className="mt-4 accent-btn text-white px-5 py-2 rounded-xl text-sm font-medium hover:opacity-90 transition">
{saved ? '✅ Saved!' : 'Save Changes'}
</button>
</div>
<div className="card p-5">
<h3 className="font-semibold mb-4">Appearance Settings</h3>
<div className="flex items-center justify-between mb-5">
<div>
<p className="font-medium text-sm">Dark Mode</p>
<p className="text-xs text-gray-500">Toggle dark/light interface</p>
</div>
<button onClick={() => toggleDark(!dark)}
className={`w-12 h-6 rounded-full transition-all relative ${dark ? 'accent-btn' : 'bg-gray-200'}`}>
<span className={`absolute top-0.5 w-5 h-5 bg-white rounded-full shadow transition-all ${dark ? 'left-6' : 'left-0.5'}`} />
</button>
</div>
<div>
<p className="font-medium text-sm mb-3">Color Theme</p>
<div className="flex gap-3">
{themes.map(t => (
<button key={t} onClick={() => { setTheme(t); document.documentElement.style.setProperty('--accent', themeColors[t]); store.set('theme', t); }}
className={`w-9 h-9 rounded-full border-2 transition-transform hover:scale-110 ${theme === t ? 'scale-110 border-gray-800' : 'border-transparent'}`}
style={{ background: themeColors[t] }} title={t} />
))}
</div>
</div>
</div>
</div>
);
}
// ─── DB VIEWER PAGE ───────────────────────────────────────────────────────
function DbViewerPage() {
const [tab, setTab] = useState('summary');
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const TABS = [
{ id: 'summary', label: 'ملخص', icon: '📊', fn: getDbSummary },
{ id: 'rules', label: 'القواعد', icon: '📌', fn: getDbRules },
{ id: 'mappings', label: 'المطابقات', icon: '🔗', fn: getDbMappings },
{ id: 'techniques', label: 'التقنيات', icon: '🧩', fn: getDbTechniques },
{ id: 'templates', label: 'قوالب WQL', icon: '📄', fn: getDbTemplates },
];
async function loadTab(id) {
setTab(id);
setLoading(true);
setData(null);
const t = TABS.find(x => x.id === id);
try {
const r = await t.fn();
setData(r.data);
} catch (e) {
setData({ error: e.response?.data?.detail || 'Error loading data' });
} finally { setLoading(false); }
}
useEffect(() => { loadTab('summary'); }, []);
function renderSummary(d) {
const items = [
{ label: 'Rules', value: d.rules, icon: '📌', color: 'bg-blue-50 text-blue-700' },
{ label: 'Techniques', value: d.techniques, icon: '🧩', color: 'bg-purple-50 text-purple-700' },
{ label: 'Rule Mappings', value: d.rule_mappings, icon: '🔗', color: 'bg-green-50 text-green-700' },
{ label: 'WQL Templates', value: d.query_templates, icon: '📄', color: 'bg-orange-50 text-orange-700' },
{ label: 'Mapping Jobs', value: d.mapping_jobs, icon: '⚙️', color: 'bg-gray-50 text-gray-700' },
];
return (
<div>
<div className="grid grid-cols-2 lg:grid-cols-5 gap-4">
{items.map((x, i) => (
<div key={i} className={`rounded-xl p-4 ${x.color} border border-current/10`}>
<div className="text-2xl mb-1">{x.icon}</div>
<div className="text-2xl font-bold">{x.value}</div>
<div className="text-xs font-medium opacity-70 mt-0.5">{x.label}</div>
</div>
))}
</div>
<div className="mt-6 bg-gray-50 rounded-xl p-4 text-xs text-gray-500 space-y-1">
<p>📍 Database: <span className="font-mono font-semibold">SQLite (murshid.db)</span></p>
<p>📍 Models dir: <span className="font-mono font-semibold">d:\GP\Needed</span></p>
</div>
</div>
);
}
function renderRules(rows) {
if (!rows.length) return <p className="text-gray-400 text-sm text-center py-8">No rules in database yet. Use POST /rules/analyze first.</p>;
return (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead><tr className="text-left text-xs text-gray-500 uppercase border-b bg-gray-50">
<th className="px-3 py-2">Rule ID</th>
<th className="px-3 py-2">Job ID</th>
<th className="px-3 py-2">Embedding</th>
</tr></thead>
<tbody>
{rows.map((r, i) => (
<tr key={i} className="border-b last:border-0 hover:bg-gray-50">
<td className="px-3 py-2 font-mono font-bold accent-text">{r.rule_id}</td>
<td className="px-3 py-2 text-gray-400">{r.job_id ?? '—'}</td>
<td className="px-3 py-2">
<span className={`text-xs px-2 py-0.5 rounded-full ${r.has_embedding ? 'tag-high' : 'bg-gray-100 text-gray-500'}`}>
{r.has_embedding ? '✓ stored' : 'none'}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
function renderMappings(rows) {
if (!rows.length) return <p className="text-gray-400 text-sm text-center py-8">No mappings yet.</p>;
const grouped = rows.reduce((acc, m) => {
if (!acc[m.rule_id]) acc[m.rule_id] = [];
acc[m.rule_id].push(m);
return acc;
}, {});
return (
<div className="space-y-4">
{Object.entries(grouped).map(([ruleId, ms]) => (
<div key={ruleId} className="border rounded-xl overflow-hidden">
<div className="bg-gray-50 px-4 py-2 border-b flex items-center gap-2">
<span className="font-mono font-bold accent-text text-sm">Rule #{ruleId}</span>
<span className="text-xs text-gray-400">— {ms.length} techniques</span>
</div>
<table className="w-full text-xs">
<thead><tr className="text-left text-gray-400 uppercase border-b">
<th className="px-3 py-2">Rank</th>
<th className="px-3 py-2">Technique</th>
<th className="px-3 py-2">Confidence %</th>
<th className="px-3 py-2">Score</th>
<th className="px-3 py-2">Level</th>
</tr></thead>
<tbody>
{ms.map((m, j) => {
const pct = m.confidence_pct;
const level = pct >= 70 ? { lbl: 'High', cls: 'tag-high' } : pct >= 40 ? { lbl: 'Med', cls: 'tag-medium' } : { lbl: 'Low', cls: 'tag-low' };
return (
<tr key={j} className="border-b last:border-0 hover:bg-gray-50">
<td className="px-3 py-2 text-gray-400">#{j+1}</td>
<td className="px-3 py-2 font-mono font-semibold">{m.technique_id}</td>
<td className="px-3 py-2">
<div className="flex items-center gap-2">
<div className="w-20 bg-gray-200 rounded-full h-1.5">
<div className="h-1.5 rounded-full" style={{ width: Math.min(pct,100)+'%', background: pct>=70?'#10b981':pct>=40?'#f59e0b':'#ef4444' }} />
</div>
<span className="font-medium">{pct}%</span>
</div>
</td>
<td className="px-3 py-2 font-mono">{m.confidence_score}</td>
<td className="px-3 py-2"><span className={`text-xs px-2 py-0.5 rounded-full ${level.cls}`}>{level.lbl}</span></td>
</tr>
);
})}
</tbody>
</table>
</div>
))}
</div>
);
}
function renderTechniques(rows) {
if (!rows.length) return <p className="text-gray-400 text-sm text-center py-8">No techniques stored yet.</p>;
return (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead><tr className="text-left text-xs text-gray-500 uppercase border-b bg-gray-50">
<th className="px-3 py-2">Technique ID</th>
<th className="px-3 py-2">Name</th>
<th className="px-3 py-2">Tactic</th>
</tr></thead>
<tbody>
{rows.map((t, i) => (
<tr key={i} className="border-b last:border-0 hover:bg-gray-50">
<td className="px-3 py-2 font-mono font-bold accent-text">{t.technique_id}</td>
<td className="px-3 py-2">{t.technique_name || '—'}</td>
<td className="px-3 py-2 text-gray-400">{t.tactic || '—'}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
function renderTemplates(rows) {
if (!rows.length) return <p className="text-gray-400 text-sm text-center py-8">No WQL templates stored yet.</p>;
return (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead><tr className="text-left text-xs text-gray-500 uppercase border-b bg-gray-50">
<th className="px-3 py-2">ID</th>
<th className="px-3 py-2">Technique</th>
<th className="px-3 py-2">Purpose</th>
<th className="px-3 py-2">WQL Query</th>
<th className="px-3 py-2">Note</th>
<th className="px-3 py-2">Active</th>
</tr></thead>
<tbody>
{rows.map((t, i) => (
<tr key={i} className={`border-b last:border-0 hover:bg-gray-50 ${!t.is_active ? 'opacity-40' : ''}`}>
<td className="px-3 py-2 text-gray-400 text-xs">#{t.template_id}</td>
<td className="px-3 py-2 font-mono font-bold accent-text">{t.technique_id}</td>
<td className="px-3 py-2 text-xs">{t.purpose || '—'}</td>
<td className="px-3 py-2 max-w-xs">
<div className="bg-gray-900 text-green-400 rounded px-2 py-1 font-mono text-xs overflow-x-auto max-w-xs">{t.wql_query}</div>
</td>
<td className="px-3 py-2 text-xs text-gray-500">{t.note || '—'}</td>
<td className="px-3 py-2">
<span className={`text-xs px-2 py-0.5 rounded-full ${t.is_active ? 'tag-high' : 'tag-low'}`}>
{t.is_active ? 'Active' : 'Disabled'}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
function renderContent() {
if (loading) return <div className="flex justify-center py-16"><div className="spinner" /></div>;
if (!data) return null;
if (data.error) return <p className="text-red-500 text-sm py-8 text-center">{data.error}</p>;
if (tab === 'summary') return renderSummary(data);
if (tab === 'rules') return renderRules(Array.isArray(data) ? data : []);
if (tab === 'mappings') return renderMappings(Array.isArray(data) ? data : []);
if (tab === 'techniques') return renderTechniques(Array.isArray(data) ? data : []);
if (tab === 'templates') return renderTemplates(Array.isArray(data) ? data : []);
return null;
}
return (
<div className="p-6 space-y-5 fade-in">
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold">نتائج قاعدة البيانات</h2>
<p className="text-gray-500 text-sm mt-0.5">استعراض محتويات قاعدة بيانات SQLite — للفحص والاختبار</p>
</div>
<button onClick={() => loadTab(tab)}
className="flex items-center gap-2 text-sm border px-3 py-2 rounded-xl hover:bg-gray-50 transition">
🔄 تحديث
</button>
</div>
{/* Tabs */}
<div className="flex gap-2 border-b overflow-x-auto">
{TABS.map(t => (
<button key={t.id} onClick={() => loadTab(t.id)}
className={`px-4 py-2 text-sm font-medium whitespace-nowrap border-b-2 transition
${tab === t.id ? 'border-current accent-text border-b-2' : 'border-transparent text-gray-500 hover:text-gray-700'}`}>
{t.icon} {t.label}
</button>
))}
</div>
{/* Content */}
<div className="card p-5">
{renderContent()}
</div>
</div>
);
}
// ─── APP ROOT ─────────────────────────────────────────────────────────────
function App() {
const [user, setUser] = useState(() => store.get('user', null));
const [page, setPage] = useState('dashboard');
const [theme, setTheme] = useState(() => store.get('theme', 'blue'));
const [toasts, setToasts] = useState([]);
const [backendOk, setBackendOk] = useState(false);
// apply saved theme + dark mode on mount
useEffect(() => {
const colors = { blue:'#3B82F6', purple:'#8B5CF6', green:'#10B981', orange:'#F59E0B' };
document.documentElement.style.setProperty('--accent', colors[theme] || colors.blue);
const dark = store.get('dark', false);
if (dark) document.documentElement.setAttribute('data-theme', 'dark');
}, []);
// poll backend health
useEffect(() => {
const check = () => getHealth().then(() => setBackendOk(true)).catch(() => setBackendOk(false));
check();
const id = setInterval(check, 15000);
return () => clearInterval(id);
}, []);
function addToast(msg, type = 'info') {
const id = Date.now();
setToasts(t => [...t, { id, msg, type }]);
}
function removeToast(id) { setToasts(t => t.filter(x => x.id !== id)); }
function handleLogin(u) {
setUser(u); store.set('user', u);
}
function handleLogout() {
setUser(null); store.set('user', null); setPage('dashboard');
}
const PAGE_TITLES = {
dashboard: '🏠 Dashboard',
rules: '🔍 Rule Lookup',
dbviewer: '🗄️ نتائج قاعدة البيانات',
admin: '⚙️ Rule Mapping',
templates: '📋 WQL Templates',
settings: '👤 Settings',
};
if (!user) return <LoginPage onLogin={handleLogin} />;
return (
<div className="flex min-h-screen">
<Sidebar page={page} onNav={setPage} user={user} onLogout={handleLogout} />
<div className="flex-1 flex flex-col overflow-hidden">
<Header title={PAGE_TITLES[page] || 'Murshid'} backendOk={backendOk} />
<main className="flex-1 overflow-y-auto">
{page === 'dashboard' && <DashboardPage user={user} />}
{page === 'rules' && <RulesPage addToast={addToast} />}
{page === 'dbviewer' && <DbViewerPage />}
{page === 'admin' && <AdminPage addToast={addToast} />}
{page === 'templates' && <TemplatesPage addToast={addToast} />}
{page === 'settings' && <SettingsPage user={user} setUser={setUser} theme={theme} setTheme={setTheme} />}
</main>
</div>
{toasts.map(t => <Toast key={t.id} msg={t.msg} type={t.type} onClose={() => removeToast(t.id)} />)}
</div>
);
}
// ─── Mount ────────────────────────────────────────────────────────────────
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
</script>
</body>
</html>