580 أسطر
18 KiB
HTML
580 أسطر
18 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>Ghaymah — Lightweight SIEM Log Analyzer</title>
|
||
<style>
|
||
/* System font stack — no external font requests for privacy consistency */
|
||
|
||
:root {
|
||
--bg-base: #0a0e1a;
|
||
--bg-card: #111827;
|
||
--bg-card2: #1a2235;
|
||
--border: #1e2d45;
|
||
--accent: #3b82f6;
|
||
--critical: #ef4444;
|
||
--high: #f97316;
|
||
--medium: #eab308;
|
||
--low: #22c55e;
|
||
--text: #e2e8f0;
|
||
--text-muted: #64748b;
|
||
--glow-red: 0 0 20px rgba(239,68,68,0.3);
|
||
--glow-blue: 0 0 20px rgba(59,130,246,0.2);
|
||
}
|
||
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||
background: var(--bg-base);
|
||
color: var(--text);
|
||
min-height: 100vh;
|
||
}
|
||
|
||
/* ── Header ── */
|
||
header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 1.2rem 2rem;
|
||
background: var(--bg-card);
|
||
border-bottom: 1px solid var(--border);
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 100;
|
||
}
|
||
.logo {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: .75rem;
|
||
font-size: 1.25rem;
|
||
font-weight: 700;
|
||
letter-spacing: -.02em;
|
||
}
|
||
.logo-icon {
|
||
width: 36px; height: 36px;
|
||
background: linear-gradient(135deg, var(--accent), #6366f1);
|
||
border-radius: 8px;
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 1.1rem;
|
||
}
|
||
.status-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 1rem;
|
||
font-size: .85rem;
|
||
color: var(--text-muted);
|
||
}
|
||
.status-dot {
|
||
width: 8px; height: 8px;
|
||
border-radius: 50%;
|
||
background: #22c55e;
|
||
animation: pulse 2s infinite;
|
||
display: inline-block;
|
||
margin-right: 4px;
|
||
}
|
||
@keyframes pulse {
|
||
0%,100% { opacity: 1; }
|
||
50% { opacity: .4; }
|
||
}
|
||
#last-updated { font-family: ui-monospace, 'Cascadia Code', 'Courier New', monospace; }
|
||
|
||
/* ── Main Layout ── */
|
||
main { padding: 2rem; max-width: 1400px; margin: 0 auto; }
|
||
|
||
/* ── Stat Cards ── */
|
||
.stats-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
gap: 1rem;
|
||
margin-bottom: 2rem;
|
||
}
|
||
.stat-card {
|
||
background: var(--bg-card);
|
||
border: 1px solid var(--border);
|
||
border-radius: 12px;
|
||
padding: 1.4rem 1.6rem;
|
||
position: relative;
|
||
overflow: hidden;
|
||
transition: transform .2s, box-shadow .2s;
|
||
}
|
||
.stat-card:hover { transform: translateY(-2px); box-shadow: var(--glow-blue); }
|
||
.stat-card::before {
|
||
content: '';
|
||
position: absolute;
|
||
inset: 0;
|
||
background: linear-gradient(135deg, rgba(59,130,246,.05), transparent);
|
||
pointer-events: none;
|
||
}
|
||
.stat-card.critical::before { background: linear-gradient(135deg, rgba(239,68,68,.08), transparent); }
|
||
.stat-card.high::before { background: linear-gradient(135deg, rgba(249,115,22,.08), transparent); }
|
||
.stat-card.medium::before { background: linear-gradient(135deg, rgba(234,179,8,.08), transparent); }
|
||
|
||
.stat-label { font-size: .8rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: .05em; margin-bottom: .4rem; }
|
||
.stat-value { font-size: 2.4rem; font-weight: 700; line-height: 1; }
|
||
.stat-value.critical { color: var(--critical); text-shadow: var(--glow-red); }
|
||
.stat-value.high { color: var(--high); }
|
||
.stat-value.medium { color: var(--medium); }
|
||
.stat-value.ok { color: var(--low); }
|
||
.stat-sub { font-size: .75rem; color: var(--text-muted); margin-top: .4rem; }
|
||
|
||
/* ── Section Title ── */
|
||
.section-title {
|
||
font-size: .9rem;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
letter-spacing: .08em;
|
||
color: var(--text-muted);
|
||
margin-bottom: 1rem;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: .5rem;
|
||
}
|
||
|
||
/* ── Alert Table ── */
|
||
.alerts-panel {
|
||
background: var(--bg-card);
|
||
border: 1px solid var(--border);
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
margin-bottom: 2rem;
|
||
}
|
||
.alerts-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 1.2rem 1.6rem;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
.filter-group { display: flex; gap: .5rem; }
|
||
.filter-btn {
|
||
padding: .35rem .85rem;
|
||
border-radius: 6px;
|
||
border: 1px solid var(--border);
|
||
background: transparent;
|
||
color: var(--text-muted);
|
||
font-size: .8rem;
|
||
cursor: pointer;
|
||
transition: all .2s;
|
||
font-family: inherit;
|
||
}
|
||
.filter-btn:hover, .filter-btn.active {
|
||
background: var(--accent);
|
||
border-color: var(--accent);
|
||
color: #fff;
|
||
}
|
||
|
||
table { width: 100%; border-collapse: collapse; }
|
||
thead th {
|
||
padding: .9rem 1.4rem;
|
||
text-align: left;
|
||
font-size: .78rem;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
letter-spacing: .06em;
|
||
color: var(--text-muted);
|
||
background: var(--bg-card2);
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
tbody tr {
|
||
border-bottom: 1px solid rgba(30,45,69,.5);
|
||
transition: background .15s;
|
||
}
|
||
tbody tr:hover { background: var(--bg-card2); }
|
||
tbody tr:last-child { border-bottom: none; }
|
||
td {
|
||
padding: 1rem 1.4rem;
|
||
font-size: .88rem;
|
||
vertical-align: top;
|
||
}
|
||
|
||
.badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
padding: .2rem .7rem;
|
||
border-radius: 20px;
|
||
font-size: .75rem;
|
||
font-weight: 600;
|
||
letter-spacing: .03em;
|
||
}
|
||
.badge-CRITICAL { background: rgba(239,68,68,.15); color: #f87171; }
|
||
.badge-HIGH { background: rgba(249,115,22,.15); color: #fb923c; }
|
||
.badge-MEDIUM { background: rgba(234,179,8,.15); color: #facc15; }
|
||
.badge-LOW { background: rgba(34,197,94,.15); color: #4ade80; }
|
||
|
||
.type-tag {
|
||
font-family: ui-monospace, 'Cascadia Code', 'Courier New', monospace;
|
||
font-size: .78rem;
|
||
color: var(--accent);
|
||
background: rgba(59,130,246,.1);
|
||
padding: .15rem .5rem;
|
||
border-radius: 4px;
|
||
}
|
||
.ip-text {
|
||
font-family: ui-monospace, 'Cascadia Code', 'Courier New', monospace;
|
||
font-size: .83rem;
|
||
color: #94a3b8;
|
||
}
|
||
.count-badge {
|
||
background: var(--bg-base);
|
||
border: 1px solid var(--border);
|
||
border-radius: 4px;
|
||
padding: .1rem .45rem;
|
||
font-size: .78rem;
|
||
font-family: ui-monospace, 'Cascadia Code', 'Courier New', monospace;
|
||
}
|
||
.detail-text {
|
||
color: var(--text-muted);
|
||
font-size: .83rem;
|
||
max-width: 340px;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
.ts-text {
|
||
font-family: ui-monospace, 'Cascadia Code', 'Courier New', monospace;
|
||
font-size: .78rem;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
/* ── IP Breakdown ── */
|
||
.bottom-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 1.5rem;
|
||
}
|
||
@media (max-width: 900px) { .bottom-grid { grid-template-columns: 1fr; } }
|
||
|
||
.panel {
|
||
background: var(--bg-card);
|
||
border: 1px solid var(--border);
|
||
border-radius: 12px;
|
||
padding: 1.4rem 1.6rem;
|
||
}
|
||
|
||
.ip-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: .6rem 0;
|
||
border-bottom: 1px solid rgba(30,45,69,.5);
|
||
}
|
||
.ip-row:last-child { border-bottom: none; }
|
||
.ip-info { display: flex; align-items: center; gap: .75rem; }
|
||
.threat-dot {
|
||
width: 8px; height: 8px;
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
}
|
||
.alert-count-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: .5rem;
|
||
font-size: .82rem;
|
||
color: var(--text-muted);
|
||
}
|
||
.bar-track {
|
||
width: 80px;
|
||
height: 4px;
|
||
background: var(--border);
|
||
border-radius: 2px;
|
||
overflow: hidden;
|
||
}
|
||
.bar-fill { height: 100%; border-radius: 2px; background: var(--accent); }
|
||
|
||
/* ── Activity Log ── */
|
||
.log-entry {
|
||
display: flex;
|
||
gap: .75rem;
|
||
padding: .6rem 0;
|
||
border-bottom: 1px solid rgba(30,45,69,.4);
|
||
font-size: .83rem;
|
||
}
|
||
.log-entry:last-child { border-bottom: none; }
|
||
.log-time { font-family: ui-monospace, 'Cascadia Code', 'Courier New', monospace; color: var(--text-muted); flex-shrink: 0; }
|
||
.log-msg { color: var(--text); }
|
||
.log-src { color: var(--accent); font-size: .78rem; margin-left: auto; flex-shrink: 0; }
|
||
|
||
/* ── No alerts ── */
|
||
.empty-state {
|
||
padding: 3rem;
|
||
text-align: center;
|
||
color: var(--text-muted);
|
||
font-size: .9rem;
|
||
}
|
||
|
||
/* ── Refresh button ── */
|
||
#refresh-btn {
|
||
background: var(--accent);
|
||
color: #fff;
|
||
border: none;
|
||
padding: .45rem 1rem;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
font-size: .82rem;
|
||
font-family: inherit;
|
||
transition: opacity .2s;
|
||
}
|
||
#refresh-btn:hover { opacity: .85; }
|
||
|
||
/* ── Scrollbar ── */
|
||
::-webkit-scrollbar { width: 6px; }
|
||
::-webkit-scrollbar-track { background: var(--bg-base); }
|
||
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<header>
|
||
<div class="logo">
|
||
<div class="logo-icon">🛡</div>
|
||
Ghaymah — Log Analyzer
|
||
</div>
|
||
<div class="status-bar">
|
||
<span><span class="status-dot"></span>Near Real-Time (30s)</span>
|
||
<span>Last scan: <span id="last-updated">—</span></span>
|
||
<button id="refresh-btn" onclick="loadAlerts()">↻ Refresh</button>
|
||
</div>
|
||
</header>
|
||
|
||
<main>
|
||
|
||
<!-- Stats -->
|
||
<div class="stats-grid">
|
||
<div class="stat-card">
|
||
<div class="stat-label">Total Alerts</div>
|
||
<div class="stat-value ok" id="stat-total">0</div>
|
||
<div class="stat-sub">Active threats</div>
|
||
</div>
|
||
<div class="stat-card critical">
|
||
<div class="stat-label">Critical</div>
|
||
<div class="stat-value critical" id="stat-critical">0</div>
|
||
<div class="stat-sub">Immediate action required</div>
|
||
</div>
|
||
<div class="stat-card high">
|
||
<div class="stat-label">High</div>
|
||
<div class="stat-value high" id="stat-high">0</div>
|
||
<div class="stat-sub">Investigate soon</div>
|
||
</div>
|
||
<div class="stat-card medium">
|
||
<div class="stat-label">Medium</div>
|
||
<div class="stat-value medium" id="stat-medium">0</div>
|
||
<div class="stat-sub">Monitor closely</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">Unique IPs</div>
|
||
<div class="stat-value ok" id="stat-ips">0</div>
|
||
<div class="stat-sub">Suspicious sources</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Alerts Table -->
|
||
<div class="alerts-panel">
|
||
<div class="alerts-header">
|
||
<div class="section-title" style="margin:0">⚡ Security Alerts</div>
|
||
<div class="filter-group">
|
||
<button class="filter-btn active" onclick="filterAlerts('all', this)">All</button>
|
||
<button class="filter-btn" onclick="filterAlerts('CRITICAL', this)">Critical</button>
|
||
<button class="filter-btn" onclick="filterAlerts('HIGH', this)">High</button>
|
||
<button class="filter-btn" onclick="filterAlerts('MEDIUM', this)">Medium</button>
|
||
</div>
|
||
</div>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>#</th>
|
||
<th>Severity</th>
|
||
<th>Type</th>
|
||
<th>Source IP</th>
|
||
<th>Detail</th>
|
||
<th>Count</th>
|
||
<th>Last Seen</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="alerts-tbody"></tbody>
|
||
</table>
|
||
<div id="no-alerts" class="empty-state" style="display:none">
|
||
✅ No alerts at this time
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Bottom Grid -->
|
||
<div class="bottom-grid">
|
||
|
||
<!-- Top Threat IPs -->
|
||
<div class="panel">
|
||
<div class="section-title">🌐 Top Threat Sources</div>
|
||
<div id="ip-list"></div>
|
||
</div>
|
||
|
||
<!-- Activity Log -->
|
||
<div class="panel">
|
||
<div class="section-title">📋 Recent Activity</div>
|
||
<div id="activity-log"></div>
|
||
</div>
|
||
|
||
</div>
|
||
</main>
|
||
|
||
<script>
|
||
// ── Sample data used when alerts.json not available (demo mode) ──
|
||
const DEMO_DATA = {
|
||
generated_at: new Date().toISOString(),
|
||
total_alerts: 5,
|
||
critical_count: 2,
|
||
high_count: 2,
|
||
medium_count: 1,
|
||
alerts: [
|
||
{ id:1, timestamp: new Date(Date.now()-600000).toISOString(), last_seen: new Date(Date.now()-60000).toISOString(),
|
||
source:"auth.log", type:"brute_force_ssh", severity:"CRITICAL", ip:"203.0.113.42",
|
||
detail:"6 failed SSH login attempts detected", count:6, status:"open" },
|
||
{ id:2, timestamp: new Date(Date.now()-540000).toISOString(), last_seen: new Date(Date.now()-30000).toISOString(),
|
||
source:"nginx/access.log", type:"sql_injection_attempt", severity:"HIGH", ip:"203.0.113.42",
|
||
detail:"Possible SQL injection detected in request path: /api/v1/users?id=1 UNION SELECT * FROM users--", count:1, status:"open" },
|
||
{ id:3, timestamp: new Date(Date.now()-480000).toISOString(), last_seen: new Date(Date.now()-480000).toISOString(),
|
||
source:"nginx/access.log", type:"repeated_http_errors", severity:"HIGH", ip:"203.0.113.42",
|
||
detail:"3 HTTP error responses to IP in log window", count:3, status:"open" },
|
||
{ id:4, timestamp: new Date(Date.now()-300000).toISOString(), last_seen: new Date(Date.now()-300000).toISOString(),
|
||
source:"app/application.log", type:"application_error", severity:"CRITICAL", ip:"203.0.113.42",
|
||
detail:"JWT verification failed for token eyJ0eXAi... from IP 203.0.113.42", count:1, status:"open" },
|
||
{ id:5, timestamp: new Date(Date.now()-200000).toISOString(), last_seen: new Date(Date.now()-200000).toISOString(),
|
||
source:"app/application.log", type:"application_error", severity:"MEDIUM", ip:"unknown",
|
||
detail:"Unhandled exception in /api/v1/users/export: PermissionError", count:1, status:"open" },
|
||
]
|
||
};
|
||
|
||
let allAlerts = [];
|
||
let currentFilter = 'all';
|
||
|
||
function formatTime(iso) {
|
||
const d = new Date(iso);
|
||
return d.toLocaleTimeString('en-GB', {hour:'2-digit',minute:'2-digit',second:'2-digit'});
|
||
}
|
||
|
||
function timeSince(iso) {
|
||
const sec = Math.floor((Date.now() - new Date(iso)) / 1000);
|
||
if (sec < 60) return `${sec}s ago`;
|
||
if (sec < 3600) return `${Math.floor(sec/60)}m ago`;
|
||
return `${Math.floor(sec/3600)}h ago`;
|
||
}
|
||
|
||
function severityColor(s) {
|
||
return {CRITICAL:'#ef4444', HIGH:'#f97316', MEDIUM:'#eab308', LOW:'#22c55e'}[s] || '#94a3b8';
|
||
}
|
||
|
||
function renderAlerts(alerts) {
|
||
const tbody = document.getElementById('alerts-tbody');
|
||
const noAlerts = document.getElementById('no-alerts');
|
||
const visible = currentFilter === 'all' ? alerts : alerts.filter(a => a.severity === currentFilter);
|
||
|
||
if (!visible.length) {
|
||
tbody.innerHTML = '';
|
||
noAlerts.style.display = 'block';
|
||
return;
|
||
}
|
||
noAlerts.style.display = 'none';
|
||
|
||
tbody.innerHTML = visible.map(a => `
|
||
<tr>
|
||
<td class="ts-text">${String(a.id).padStart(3,'0')}</td>
|
||
<td><span class="badge badge-${a.severity}">${a.severity}</span></td>
|
||
<td><span class="type-tag">${a.type}</span></td>
|
||
<td><span class="ip-text">${a.ip}</span></td>
|
||
<td><div class="detail-text" title="${a.detail}">${a.detail}</div>
|
||
<div style="font-size:.75rem;color:var(--text-muted);margin-top:.25rem">${a.source}</div></td>
|
||
<td><span class="count-badge">×${a.count}</span></td>
|
||
<td class="ts-text">${timeSince(a.last_seen)}</td>
|
||
</tr>
|
||
`).join('');
|
||
}
|
||
|
||
function renderIPList(alerts) {
|
||
const ipMap = {};
|
||
alerts.forEach(a => {
|
||
if (a.ip === 'unknown') return;
|
||
if (!ipMap[a.ip]) ipMap[a.ip] = { count:0, maxSev:'LOW' };
|
||
ipMap[a.ip].count += a.count;
|
||
const order = {CRITICAL:4,HIGH:3,MEDIUM:2,LOW:1};
|
||
if ((order[a.severity]||0) > (order[ipMap[a.ip].maxSev]||0)) ipMap[a.ip].maxSev = a.severity;
|
||
});
|
||
|
||
const sorted = Object.entries(ipMap).sort((a,b) => b[1].count - a[1].count).slice(0,8);
|
||
const maxCount = sorted[0]?.[1]?.count || 1;
|
||
|
||
document.getElementById('ip-list').innerHTML = sorted.length
|
||
? sorted.map(([ip,{count,maxSev}]) => `
|
||
<div class="ip-row">
|
||
<div class="ip-info">
|
||
<div class="threat-dot" style="background:${severityColor(maxSev)}"></div>
|
||
<span class="ip-text">${ip}</span>
|
||
</div>
|
||
<div class="alert-count-bar">
|
||
<div class="bar-track"><div class="bar-fill" style="width:${Math.round(count/maxCount*100)}%;background:${severityColor(maxSev)}"></div></div>
|
||
<span>${count} events</span>
|
||
</div>
|
||
</div>`).join('')
|
||
: '<div style="color:var(--text-muted);font-size:.85rem;padding:.5rem 0">No suspicious IPs detected</div>';
|
||
}
|
||
|
||
function renderActivityLog(alerts) {
|
||
const recent = [...alerts]
|
||
.sort((a,b) => new Date(b.last_seen) - new Date(a.last_seen))
|
||
.slice(0,8);
|
||
|
||
document.getElementById('activity-log').innerHTML = recent.map(a => `
|
||
<div class="log-entry">
|
||
<span class="log-time">${formatTime(a.last_seen)}</span>
|
||
<span class="log-msg" style="color:${severityColor(a.severity)}">[${a.severity}]</span>
|
||
<span class="log-msg">${a.type.replace(/_/g,' ')}</span>
|
||
<span class="log-src">${a.source}</span>
|
||
</div>`).join('') || '<div style="color:var(--text-muted);font-size:.85rem">No activity</div>';
|
||
}
|
||
|
||
function updateStats(data) {
|
||
document.getElementById('stat-total').textContent = data.total_alerts;
|
||
document.getElementById('stat-critical').textContent = data.critical_count;
|
||
document.getElementById('stat-high').textContent = data.high_count;
|
||
document.getElementById('stat-medium').textContent = data.medium_count;
|
||
|
||
const uniqueIPs = new Set(data.alerts.map(a=>a.ip).filter(i=>i!=='unknown')).size;
|
||
document.getElementById('stat-ips').textContent = uniqueIPs;
|
||
|
||
document.getElementById('last-updated').textContent =
|
||
new Date(data.generated_at).toLocaleTimeString('en-GB');
|
||
}
|
||
|
||
async function loadAlerts() {
|
||
try {
|
||
const res = await fetch('./alerts.json?v=' + Date.now());
|
||
const data = await res.json();
|
||
allAlerts = data.alerts || [];
|
||
updateStats(data);
|
||
renderAlerts(allAlerts);
|
||
renderIPList(allAlerts);
|
||
renderActivityLog(allAlerts);
|
||
} catch {
|
||
// Demo mode — use built-in sample data
|
||
allAlerts = DEMO_DATA.alerts;
|
||
updateStats(DEMO_DATA);
|
||
renderAlerts(allAlerts);
|
||
renderIPList(allAlerts);
|
||
renderActivityLog(allAlerts);
|
||
}
|
||
}
|
||
|
||
function filterAlerts(severity, btn) {
|
||
currentFilter = severity;
|
||
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
renderAlerts(allAlerts);
|
||
}
|
||
|
||
// Auto-refresh every 30s
|
||
loadAlerts();
|
||
setInterval(loadAlerts, 30000);
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|