الملفات
2026-07-27 00:08:02 +03:00

580 أسطر
18 KiB
HTML
خام الرابط الدائم اللوم التاريخ

هذا الملف يحتوي على أحرف Unicode غامضة

هذا الملف يحتوي على أحرف Unicode قد تُخلط مع أحرف أخرى. إذا كنت تعتقد أن هذا مقصود، يمكنك تجاهل هذا التحذير بأمان. استخدم زر الهروب للكشف عنها.

<!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>