هذا الالتزام موجود في:
2026-07-27 00:08:02 +03:00
الأصل 6a52386c7f
التزام c328b1e4f3
19 ملفات معدلة مع 2717 إضافات و1 حذوفات

33
q4-siem/alerts.json Normal file
عرض الملف

@@ -0,0 +1,33 @@
{
"generated_at": "2026-07-26T22:30:54.950560",
"total_alerts": 2,
"critical_count": 0,
"high_count": 0,
"medium_count": 2,
"alerts": [
{
"id": 1,
"timestamp": "2026-07-26T22:30:54.950367",
"last_seen": "2026-07-26T22:30:54.950548",
"source": "app/application.log",
"type": "application_error",
"severity": "MEDIUM",
"ip": "unknown",
"detail": "Database connection timeout after 30s \u2014 retrying",
"count": 3,
"status": "open"
},
{
"id": 2,
"timestamp": "2026-07-26T22:30:54.950462",
"last_seen": "2026-07-26T22:30:54.950465",
"source": "app/application.log",
"type": "application_error",
"severity": "MEDIUM",
"ip": "185.220.100.42",
"detail": "JWT verification failed for token eyJ0eXAi... from IP 185.220.100.42",
"count": 1,
"status": "open"
}
]
}

579
q4-siem/dashboard.html Normal file
عرض الملف

@@ -0,0 +1,579 @@
<!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>

324
q4-siem/deploy-guide.md Normal file
عرض الملف

@@ -0,0 +1,324 @@
# Lightweight SIEM Deployment Guide on Ghaymah Cloud Infrastructure
## Overview
This guide explains how to deploy the `siem.py` log analyzer and `dashboard.html` on Ghaymah cloud infrastructure, using Block Storage to persist logs and alert data.
---
## Architecture
```mermaid
graph TD
BS["🗄️ Block Storage Volume\n/mnt/logs"]
BS --> AL["/mnt/logs/auth.log"]
BS --> NL["/mnt/logs/nginx/access.log"]
BS --> AP["/mnt/logs/app/application.log"]
AL & NL & AP --> SIEM["⚙️ siem.py\nsystemd service"]
SIEM -->|"writes every 30s"| AJ["📄 alerts.json\n/var/www/siem/"]
AJ -->|"served by Nginx"| DASH["🖥️ dashboard.html\nNear Real-Time UI"]
```
---
## Step 1: Create and Attach Block Storage
In the Ghaymah Cloud Console:
1. Go to **Storage → Block Storage → Create Volume**
2. Set name: `siem-logs-volume`
3. Set size: `50 GB` (adjust based on log retention needs)
4. Attach to your cloud instance
Then format and mount the volume:
```bash
# Find the new volume (usually /dev/sdb or /dev/vdb)
lsblk
# Format the volume
sudo mkfs.ext4 /dev/sdb
# Create mount point
sudo mkdir -p /mnt/logs
# Mount the volume
sudo mount /dev/sdb /mnt/logs
# Persist mount across reboots — use UUID to avoid /dev/sdX changes after reboot
# First find the UUID:
sudo blkid /dev/sdb
# Then replace UUID=xxxx with the actual value shown:
echo 'UUID=xxxx-xxxx /mnt/logs ext4 defaults,nofail 0 2' | sudo tee -a /etc/fstab
# Create log directories
sudo mkdir -p /mnt/logs/nginx /mnt/logs/app
sudo chown -R $USER:$USER /mnt/logs
```
> **Encryption:** Enable volume encryption during Block Storage creation in the Ghaymah console before writing any data to the volume.
---
## Step 2: Configure Services to Write Logs to Block Storage
### Nginx
```bash
# Edit nginx.conf to write access log to block storage
sudo nano /etc/nginx/nginx.conf
# Change:
# access_log /var/log/nginx/access.log;
# To:
# access_log /mnt/logs/nginx/access.log;
sudo systemctl reload nginx
```
### Application (Docker)
```yaml
# docker-compose.yml
services:
api:
volumes:
- /mnt/logs/app:/var/log/app
environment:
LOG_PATH: /var/log/app/application.log
```
> **Secrets management note:** For production deployments, avoid plain `.env` files on disk.
> Prefer **Kubernetes Secrets** (mounted as env vars or files) or a cloud secret manager
> (e.g., HashiCorp Vault, AWS Secrets Manager, or your cloud provider's equivalent).
> `.env` files are acceptable for local development but should never be committed to Git.
### SSH Auth Log Forwarding
```bash
# Create the auth log file with correct permissions for rsyslog
sudo touch /mnt/logs/auth.log
sudo chown syslog:adm /mnt/logs/auth.log
sudo chmod 640 /mnt/logs/auth.log
# Direct rsyslog to write auth logs to block storage
echo 'auth,authpriv.* /mnt/logs/auth.log' | sudo tee /etc/rsyslog.d/99-siem.conf
sudo systemctl restart rsyslog
```
---
## Step 3: Deploy siem.py as a Systemd Service
```bash
# Install Python 3
sudo apt-get update && sudo apt-get install -y python3
# Create a shared group so both siem.py (ubuntu) and nginx (www-data) can access alerts.json
sudo groupadd siem
sudo usermod -aG siem ubuntu
sudo usermod -aG siem www-data
# Copy SIEM files
sudo mkdir -p /opt/siem /var/www/siem
sudo cp q4-siem/siem.py /opt/siem/siem.py
sudo cp q4-siem/dashboard.html /var/www/siem/dashboard.html
# Set permissions: ubuntu writes, www-data reads via shared siem group
sudo touch /var/www/siem/alerts.json
sudo chown ubuntu:siem /var/www/siem/alerts.json
sudo chmod 640 /var/www/siem/alerts.json
# Edit log paths in siem.py to match block storage
sudo nano /opt/siem/siem.py
# Set:
# LOG_SOURCES = {
# "auth": "/mnt/logs/auth.log",
# "nginx": "/mnt/logs/nginx/access.log",
# "app": "/mnt/logs/app/application.log",
# }
# OUTPUT_FILE = "/var/www/siem/alerts.json"
```
Create the systemd service:
```ini
# /etc/systemd/system/siem.service
[Unit]
Description=Ghaymah SIEM Log Analyzer
After=network.target
[Service]
Type=simple
User=ubuntu
WorkingDirectory=/opt/siem
ExecStart=/usr/bin/python3 /opt/siem/siem.py
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
```
```bash
sudo systemctl daemon-reload
sudo systemctl enable siem
sudo systemctl start siem
sudo systemctl status siem
```
---
## Step 4: Serve the Dashboard via Nginx
```nginx
# /etc/nginx/sites-available/siem
server {
listen 8443 ssl;
server_name siem.ghaymah.systems;
ssl_certificate /etc/letsencrypt/live/siem.ghaymah.systems/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/siem.ghaymah.systems/privkey.pem;
root /var/www/siem;
index dashboard.html;
# Restrict access to internal network only
allow 10.0.0.0/8;
allow 192.168.0.0/16;
deny all;
# alerts.json is served to the dashboard browser client.
# The parent allow/deny block already restricts access to internal IPs only.
location = /alerts.json {
allow 10.0.0.0/8;
allow 192.168.0.0/16;
deny all;
add_header Content-Type application/json;
}
location / {
try_files $uri $uri/ =404;
}
}
```
```bash
sudo ln -s /etc/nginx/sites-available/siem /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
```
---
## Step 5: Log Rotation
```bash
# /etc/logrotate.d/siem-logs
/mnt/logs/*.log
/mnt/logs/app/*.log {
daily
rotate 30
compress
delaycompress
missingok
notifempty
create 640 syslog adm
postrotate
systemctl restart rsyslog
endscript
}
/mnt/logs/nginx/*.log {
daily
rotate 30
compress
delaycompress
missingok
notifempty
create 640 www-data adm
postrotate
systemctl reload nginx
endscript
}
```
---
## Verification
```bash
# Check SIEM is running
sudo systemctl status siem
# Check alerts output
cat /var/www/siem/alerts.json | python3 -m json.tool | head -30
# View SIEM logs
sudo journalctl -u siem -f
# Test dashboard
curl -k https://siem.ghaymah.systems:8443/dashboard.html
```
---
## Security Notes
| Control | Implementation |
|---|---|
| Dashboard access | Restricted to internal IP range (10.0.0.0/8) |
| HTTPS | TLS required, no HTTP access |
| `alerts.json` access | Internal IPs only via `allow 10.0.0.0/8; deny all;` inside `location = /alerts.json` |
| File permissions | `alerts.json` owned `ubuntu:siem`, mode `640` — owner writes, group (nginx) reads, others none |
| Block storage | Encrypted at rest (enable during volume creation in Ghaymah console) |
| Audit logs | All SIEM activity written to systemd journal |
---
## Future Integrations
The SIEM is designed to grow. The following integrations can be added as next steps:
| Integration | How |
|---|---|
| **AlertManager webhook** | Extend `siem.py` to POST critical alerts to a webhook endpoint. Pairs directly with the Prometheus alert rules in `q2-attack-simulation/alert-rule.yml`. |
| **Slack notifications** | Send alerts to a `#security-alerts` channel via Slack Incoming Webhooks when severity is CRITICAL or HIGH. |
| **PagerDuty / on-call** | Route CRITICAL alerts to PagerDuty for 24/7 on-call escalation using the Events API. |
| **Email alerts** | Use `smtplib` in `siem.py` to send summary emails to the security team at the end of each scan cycle. |
| **Cloud monitoring export** | Ship `alerts.json` to a cloud-native monitoring service (e.g., Ghaymah monitoring, Datadog, or CloudWatch) for long-term trend analysis and dashboards. |
| **Log shipping to SIEM platform** | Forward structured logs to a dedicated SIEM platform (e.g., Elastic/OpenSearch) for retention and correlation across multiple services. |
---
## Backup Strategy
Block Storage volumes are the persistence layer for all logs and alert data. Schedule regular snapshots to protect against data loss.
**Recommended snapshot schedule:**
| Frequency | Retention | Purpose |
|---|---|---|
| Daily | 7 days | Short-term recovery from accidental deletion or corruption |
| Weekly | 4 weeks | Recovery from persistent misconfiguration issues |
| Monthly | 3 months | Compliance and long-term trend analysis |
**How to enable in Ghaymah console:**
1. Go to **Storage → Block Storage → `siem-logs-volume`**
2. Navigate to **Snapshots → Create Snapshot Policy**
3. Set a daily schedule with 7-day retention
4. Enable email notification on snapshot failure
**Restore procedure (monthly test recommended):**
```bash
# Restore a snapshot to a new volume in the Ghaymah console,
# then mount it temporarily to verify data integrity:
sudo mkdir -p /mnt/logs-restore
sudo mount /dev/sdc /mnt/logs-restore
ls -lh /mnt/logs-restore/
sudo umount /mnt/logs-restore
```
> Restore tests should be performed monthly to confirm backups are valid and recovery time is within acceptable limits.

302
q4-siem/siem.py Normal file
عرض الملف

@@ -0,0 +1,302 @@
#!/usr/bin/env python3
"""
siem.py — Simplified SIEM for Ghaymah Storage
Collects logs from 3 sources, analyzes for suspicious patterns,
and exposes alerts as JSON for the dashboard.
"""
import re
import json
import os
import time
import random
import logging
from datetime import datetime, timedelta
from collections import defaultdict
from pathlib import Path
# ─── Configuration ─────────────────────────────────────────────
LOG_SOURCES = {
"auth": "/mnt/logs/auth.log",
"nginx": "/mnt/logs/nginx/access.log",
"app": "/mnt/logs/app/application.log",
}
OUTPUT_FILE = "/var/www/siem/alerts.json"
ALERT_THRESHOLD_FAIL = 5 # Failed logins before alert
ALERT_THRESHOLD_RATE = 100 # HTTP requests/min before alert
SCAN_INTERVAL_SEC = 30 # How often to re-scan logs
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s"
)
log = logging.getLogger("siem")
# ─── Patterns ──────────────────────────────────────────────────
PATTERNS = {
"brute_force": re.compile(
r"Failed password for .+ from (?P<ip>\d+\.\d+\.\d+\.\d+)"
),
"invalid_user": re.compile(
r"Invalid user .+ from (?P<ip>\d+\.\d+\.\d+\.\d+)"
),
"nginx_request": re.compile(
r'(?P<ip>\d+\.\d+\.\d+\.\d+) .+ "(?P<method>\w+) (?P<path>\S+) HTTP.+" (?P<status>\d{3})'
),
"app_error": re.compile(
r"\[(?P<level>ERROR|CRITICAL)\] (?P<msg>.+)"
),
"sql_injection": re.compile(
r"\b(select|union|insert|drop|update|delete|benchmark|sleep)\b\s*[\(%'\"()]",
re.IGNORECASE
),
}
# ─── State ─────────────────────────────────────────────────────
failed_logins: dict = defaultdict(int) # ip → count
request_counts: dict = defaultdict(int) # ip → count/window
sql_attempts: list = []
alerts: list = []
def generate_sample_logs():
"""
Generate sample log lines (used when real log files don't exist).
This allows the SIEM to demonstrate functionality without root access.
"""
attacker_ip = "185.220.100.42"
legit_ip = "192.168.1.10"
internal_ip = "10.0.0.5"
sample_lines = {
"auth": [
f"Jul 26 19:01:00 server sshd[1234]: Failed password for root from {attacker_ip} port 54321 ssh2",
f"Jul 26 19:01:02 server sshd[1234]: Failed password for admin from {attacker_ip} port 54321 ssh2",
f"Jul 26 19:01:04 server sshd[1234]: Failed password for ubuntu from {attacker_ip} port 54321 ssh2",
f"Jul 26 19:01:06 server sshd[1234]: Failed password for user from {attacker_ip} port 54321 ssh2",
f"Jul 26 19:01:08 server sshd[1234]: Failed password for deploy from {attacker_ip} port 54321 ssh2",
f"Jul 26 19:01:10 server sshd[1234]: Failed password for git from {attacker_ip} port 54321 ssh2",
f"Jul 26 19:01:12 server sshd[1234]: Invalid user hacker from {attacker_ip} port 54321",
f"Jul 26 19:05:00 server sshd[5678]: Accepted password for deploy from {legit_ip} port 22122 ssh2",
],
"nginx": [
f'185.220.100.42 - - [26/Jul/2026:19:01:00 +0300] "POST /api/v1/auth/login HTTP/1.1" 401 120',
f'185.220.100.42 - - [26/Jul/2026:19:01:01 +0300] "POST /api/v1/auth/login HTTP/1.1" 401 120',
f'185.220.100.42 - - [26/Jul/2026:19:01:02 +0300] "POST /api/v1/auth/login HTTP/1.1" 401 120',
f'185.220.100.42 - - [26/Jul/2026:19:01:03 +0300] "GET /api/v1/users?id=1 UNION SELECT * FROM users-- HTTP/1.1" 400 80',
f'192.168.1.10 - - [26/Jul/2026:19:02:00 +0300] "GET /dashboard HTTP/1.1" 200 4096',
f'10.0.0.5 - - [26/Jul/2026:19:03:00 +0300] "GET /health HTTP/1.1" 200 32',
],
"app": [
"[ERROR] Database connection timeout after 30s — retrying",
"[CRITICAL] Unhandled exception in /api/v1/users/export: PermissionError",
"[ERROR] JWT verification failed for token eyJ0eXAi... from IP 185.220.100.42",
"[INFO] User admin@ghaymah.systems logged in from 192.168.1.10",
"[ERROR] SQL query failed: syntax error near 'UNION'",
],
}
return sample_lines
def read_log_source(name: str, path: str) -> list[str]:
"""Read lines from a log file, or fall back to sample data."""
if os.path.exists(path):
try:
with open(path, "r", errors="replace") as f:
lines = f.readlines()
log.info(f"[{name}] Read {len(lines)} lines from {path}")
return lines
except PermissionError:
log.warning(f"[{name}] Permission denied reading {path} — using sample data")
else:
log.warning(f"[{name}] File not found: {path} — using sample data")
# Return sample data
return generate_sample_logs().get(name, [])
def analyze_auth_log(lines: list[str]):
"""Detect brute force SSH attempts."""
for line in lines:
m = PATTERNS["brute_force"].search(line)
if m:
ip = m.group("ip")
failed_logins[ip] += 1
m = PATTERNS["invalid_user"].search(line)
if m:
ip = m.group("ip")
failed_logins[ip] += 1
for ip, count in failed_logins.items():
if count >= ALERT_THRESHOLD_FAIL:
create_alert(
source="auth.log",
alert_type="brute_force_ssh",
severity="CRITICAL",
ip=ip,
detail=f"{count} failed SSH login attempts detected",
)
def analyze_nginx_log(lines: list[str]):
"""Detect HTTP flooding and SQL injection attempts."""
ip_requests = defaultdict(list)
for line in lines:
m = PATTERNS["nginx_request"].search(line)
if m:
ip = m.group("ip")
path = m.group("path")
status = m.group("status")
ip_requests[ip].append((path, status))
# Check for SQL injection in path
if PATTERNS["sql_injection"].search(path):
create_alert(
source="nginx/access.log",
alert_type="sql_injection_attempt",
severity="HIGH",
ip=ip,
detail=f"Possible SQL injection detected in request path: {path[:120]}",
)
# Check high request rate
for ip, reqs in ip_requests.items():
failed = [r for r in reqs if r[1].startswith(("4", "5"))]
if len(reqs) >= ALERT_THRESHOLD_RATE:
create_alert(
source="nginx/access.log",
alert_type="http_flood",
severity="HIGH",
ip=ip,
detail=f"{len(reqs)} requests detected in log window ({len(failed)} errors)",
)
elif len(failed) >= ALERT_THRESHOLD_FAIL:
create_alert(
source="nginx/access.log",
alert_type="repeated_http_errors",
severity="MEDIUM",
ip=ip,
detail=f"{len(failed)} HTTP error responses to IP in log window",
)
def analyze_app_log(lines: list[str]):
"""Detect application-level errors and anomalies."""
for line in lines:
m = PATTERNS["app_error"].search(line)
if m:
level = m.group("level")
msg = m.group("msg")
# Extract IP if present
ip_match = re.search(r"\d+\.\d+\.\d+\.\d+", msg)
ip = ip_match.group(0) if ip_match else "unknown"
if "JWT" in msg or "token" in msg.lower():
severity = "HIGH"
elif level == "CRITICAL":
severity = "CRITICAL"
else:
severity = "MEDIUM"
create_alert(
source="app/application.log",
alert_type="application_error",
severity=severity,
ip=ip,
detail=msg[:200],
)
def create_alert(source: str, alert_type: str, severity: str, ip: str, detail: str):
"""Create a deduplicated alert entry."""
# Deduplicate: only merge if same type + IP + first 60 chars of detail match.
# This prevents a low-severity alert from absorbing a different high-severity one.
for existing in alerts:
if (existing["type"] == alert_type
and existing["ip"] == ip
and existing["detail"][:60] == detail[:60]):
existing["count"] += 1
existing["last_seen"] = datetime.now().isoformat()
return
alert_entry = {
"id": len(alerts) + 1,
"timestamp": datetime.now().isoformat(),
"last_seen": datetime.now().isoformat(),
"source": source,
"type": alert_type,
"severity": severity,
"ip": ip,
"detail": detail,
"count": 1,
"status": "open",
}
alerts.append(alert_entry)
log.warning(f"[ALERT] [{severity}] {alert_type} — IP: {ip}{detail[:80]}")
def run_scan():
"""Main scan loop — reads all 3 log sources and runs analysis.
NOTE: This implementation is stateless — alert history resets on each
scan cycle. This is acceptable for demo/project use. Production deployments
should persist alert history in a database or SIEM backend (e.g.,
Elasticsearch/OpenSearch) to retain trends and support correlation rules.
"""
global alerts, failed_logins
alerts = []
failed_logins = defaultdict(int)
log.info("Starting SIEM scan cycle...")
# Source 1: Auth logs
auth_lines = read_log_source("auth", LOG_SOURCES["auth"])
analyze_auth_log(auth_lines)
# Source 2: Nginx access logs
nginx_lines = read_log_source("nginx", LOG_SOURCES["nginx"])
analyze_nginx_log(nginx_lines)
# Source 3: Application logs
app_lines = read_log_source("app", LOG_SOURCES["app"])
analyze_app_log(app_lines)
# Sort by severity
severity_order = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3}
alerts.sort(key=lambda a: severity_order.get(a["severity"], 9))
output = {
"generated_at": datetime.now().isoformat(),
"total_alerts": len(alerts),
"critical_count": sum(1 for a in alerts if a["severity"] == "CRITICAL"),
"high_count": sum(1 for a in alerts if a["severity"] == "HIGH"),
"medium_count": sum(1 for a in alerts if a["severity"] == "MEDIUM"),
"alerts": alerts,
}
with open(OUTPUT_FILE, "w") as f:
json.dump(output, f, indent=2)
log.info(f"Scan complete. {len(alerts)} alerts written to {OUTPUT_FILE}")
return output
if __name__ == "__main__":
log.info("=== Ghaymah SIEM Starting ===")
log.info(f"Monitoring: {', '.join(LOG_SOURCES.values())}")
log.info(f"Output: {OUTPUT_FILE}")
while True:
try:
results = run_scan()
print(f"\n[{datetime.now().strftime('%H:%M:%S')}] Alerts: {results['total_alerts']} "
f"(CRITICAL: {results['critical_count']}, HIGH: {results['high_count']}, "
f"MEDIUM: {results['medium_count']})")
except KeyboardInterrupt:
log.info("SIEM stopped by user.")
break
except Exception as e:
log.error(f"Scan error: {e}")
time.sleep(SCAN_INTERVAL_SEC)