second push
هذا الالتزام موجود في:
12
q4-siem/alerts.json
Normal file
12
q4-siem/alerts.json
Normal file
@@ -0,0 +1,12 @@
|
||||
[
|
||||
{
|
||||
"ip": "192.168.1.50",
|
||||
"threat_score": 100,
|
||||
"events": [
|
||||
"Web Brute Force Attempt",
|
||||
"SSH Failed Login",
|
||||
"SQL Injection Attempt"
|
||||
],
|
||||
"status": "Critical"
|
||||
}
|
||||
]
|
||||
129
q4-siem/analyzer.py
Normal file
129
q4-siem/analyzer.py
Normal file
@@ -0,0 +1,129 @@
|
||||
"""
|
||||
Ghaymah SIEM - Log Correlation Analyzer
|
||||
-----------------------------------------
|
||||
Correlates events across 3 log sources (Web/Nginx, SSH/Auth, Database) to
|
||||
detect a single attacker IP behind multiple attack patterns.
|
||||
|
||||
Usage:
|
||||
python3 analyzer.py -> runs on built-in demo data
|
||||
python3 analyzer.py web.log auth.log db.log -> runs on real log files
|
||||
|
||||
When real file paths are given, only NEW lines since the last run are
|
||||
processed (tracked via analyzer_state.json), so this script is safe to run
|
||||
every minute from a cron job without re-scoring the same events twice.
|
||||
"""
|
||||
import json
|
||||
import re
|
||||
import os
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
|
||||
STATE_FILE = "analyzer_state.json"
|
||||
|
||||
# --- Demo data (used only when no real log paths are provided) ---
|
||||
DEMO_WEB_LOGS = [
|
||||
"192.168.1.50 - POST /login HTTP/1.1 401 Unauthorized",
|
||||
"192.168.1.50 - POST /login HTTP/1.1 401 Unauthorized",
|
||||
"192.168.1.50 - POST /login HTTP/1.1 401 Unauthorized",
|
||||
"10.0.0.5 - GET /index.html HTTP/1.1 200 OK",
|
||||
]
|
||||
DEMO_SSH_LOGS = [
|
||||
"Failed password for root from 192.168.1.50 port 22 ssh2",
|
||||
"Accepted password for admin from 10.0.0.2 port 22 ssh2",
|
||||
]
|
||||
DEMO_DB_LOGS = [
|
||||
"Query executed by 192.168.1.50: SELECT * FROM users WHERE id = '1' OR '1'='1'",
|
||||
"Query executed by 10.0.0.2: SELECT name FROM products WHERE id = 5",
|
||||
]
|
||||
|
||||
|
||||
def load_state():
|
||||
if os.path.exists(STATE_FILE):
|
||||
with open(STATE_FILE) as f:
|
||||
return json.load(f)
|
||||
return {}
|
||||
|
||||
|
||||
def save_state(state):
|
||||
with open(STATE_FILE, "w") as f:
|
||||
json.dump(state, f)
|
||||
|
||||
|
||||
def read_new_lines(path, state):
|
||||
"""Read only the lines appended to `path` since the last recorded offset."""
|
||||
if not path or not os.path.exists(path):
|
||||
return []
|
||||
last_offset = state.get(path, 0)
|
||||
with open(path, "r", errors="ignore") as f:
|
||||
f.seek(last_offset)
|
||||
new_lines = f.readlines()
|
||||
state[path] = f.tell()
|
||||
return [line.strip() for line in new_lines if line.strip()]
|
||||
|
||||
|
||||
def flag(bucket, reason, weight):
|
||||
"""Add score/reason to an IP bucket, without duplicating the same reason twice."""
|
||||
bucket["score"] += weight
|
||||
if reason not in bucket["reasons"]:
|
||||
bucket["reasons"].append(reason)
|
||||
|
||||
|
||||
def analyze_logs(web_path=None, ssh_path=None, db_path=None):
|
||||
using_real_files = any([web_path, ssh_path, db_path])
|
||||
state = load_state() if using_real_files else {}
|
||||
|
||||
web_logs = read_new_lines(web_path, state) if web_path else DEMO_WEB_LOGS
|
||||
ssh_logs = read_new_lines(ssh_path, state) if ssh_path else DEMO_SSH_LOGS
|
||||
db_logs = read_new_lines(db_path, state) if db_path else DEMO_DB_LOGS
|
||||
|
||||
suspicious_ips = defaultdict(lambda: {"score": 0, "reasons": []})
|
||||
|
||||
# Web logs -> Brute Force detection
|
||||
for log in web_logs:
|
||||
if "401 Unauthorized" in log:
|
||||
ip = log.split()[0]
|
||||
flag(suspicious_ips[ip], "Web Brute Force Attempt", 10)
|
||||
|
||||
# SSH logs -> server intrusion attempts
|
||||
for log in ssh_logs:
|
||||
if "Failed password" in log:
|
||||
match = re.search(r"from (\d+\.\d+\.\d+\.\d+)", log)
|
||||
if match:
|
||||
flag(suspicious_ips[match.group(1)], "SSH Failed Login", 20)
|
||||
|
||||
# DB logs -> SQL Injection patterns
|
||||
for log in db_logs:
|
||||
if "OR '1'='1'" in log or "DROP TABLE" in log.upper():
|
||||
match = re.search(r"by (\d+\.\d+\.\d+\.\d+):", log)
|
||||
if match:
|
||||
flag(suspicious_ips[match.group(1)], "SQL Injection Attempt", 50)
|
||||
|
||||
# Build alerts with severity tiers instead of a flat "Critical" for everything
|
||||
alerts = []
|
||||
for ip, data in suspicious_ips.items():
|
||||
if data["score"] >= 30:
|
||||
severity = "Critical" if data["score"] >= 50 else "Warning"
|
||||
alerts.append({
|
||||
"ip": ip,
|
||||
"threat_score": data["score"],
|
||||
"events": data["reasons"],
|
||||
"status": severity,
|
||||
})
|
||||
|
||||
alerts.sort(key=lambda a: a["threat_score"], reverse=True)
|
||||
|
||||
with open("alerts.json", "w") as f:
|
||||
json.dump(alerts, f, indent=4)
|
||||
|
||||
if using_real_files:
|
||||
save_state(state)
|
||||
|
||||
print(f"Analysis complete. {len(alerts)} alert(s) saved to alerts.json")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = sys.argv[1:]
|
||||
web_p = args[0] if len(args) > 0 else None
|
||||
ssh_p = args[1] if len(args) > 1 else None
|
||||
db_p = args[2] if len(args) > 2 else None
|
||||
analyze_logs(web_p, ssh_p, db_p)
|
||||
79
q4-siem/dashboard.html
Normal file
79
q4-siem/dashboard.html
Normal file
@@ -0,0 +1,79 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ar" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Ghaymah SIEM Dashboard</title>
|
||||
<style>
|
||||
body { font-family: system-ui, sans-serif; background-color: #1e1e2f; color: #fff; padding: 20px; }
|
||||
h1 { color: #00d2ff; text-align: center; }
|
||||
.meta { text-align: center; color: #9a9ab5; font-size: 0.9em; margin-bottom: 10px; }
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 20px; background-color: #2a2a40; }
|
||||
th, td { padding: 15px; text-align: right; border-bottom: 1px solid #444; }
|
||||
th { background-color: #3f3f5a; }
|
||||
.critical { color: #ff4d4d; font-weight: bold; }
|
||||
.warning { color: #ffb84d; font-weight: bold; }
|
||||
.score-critical { display: inline-block; padding: 5px 10px; background-color: #ff4d4d; border-radius: 5px; color: white; }
|
||||
.score-warning { display: inline-block; padding: 5px 10px; background-color: #ffb84d; border-radius: 5px; color: #1e1e2f; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1> لوحة تحكم SIEM - التنبيهات الأمنية</h1>
|
||||
<p class="meta" id="last-updated">جاري التحميل...</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>عنوان IP المشبوه</th>
|
||||
<th>درجة الخطورة</th>
|
||||
<th>الأحداث المرصودة (الأنماط)</th>
|
||||
<th>الحالة</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="alerts-table">
|
||||
<!-- سيتم حقن البيانات هنا بواسطة الجافاسكريبت -->
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<script>
|
||||
const REFRESH_INTERVAL_MS = 30000; // نفس منطق الـ cron كل دقيقة: نراجع كل 30 ثانية
|
||||
|
||||
function loadAlerts() {
|
||||
fetch('alerts.json', { cache: 'no-store' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const tableBody = document.getElementById('alerts-table');
|
||||
document.getElementById('last-updated').textContent =
|
||||
`آخر تحديث: ${new Date().toLocaleTimeString('ar-EG')} | عدد التنبيهات: ${data.length}`;
|
||||
|
||||
if (data.length === 0) {
|
||||
tableBody.innerHTML = '<tr><td colspan="4" style="text-align:center;">لا توجد تهديدات حالياً ✅</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tableBody.innerHTML = '';
|
||||
data.forEach(alert => {
|
||||
const isCritical = alert.status === 'Critical';
|
||||
const statusClass = isCritical ? 'critical' : 'warning';
|
||||
const scoreClass = isCritical ? 'score-critical' : 'score-warning';
|
||||
const icon = isCritical ? '⛔' : '⚠️';
|
||||
const row = `<tr>
|
||||
<td style="font-family: monospace; font-size: 1.1em;">${alert.ip}</td>
|
||||
<td><span class="${scoreClass}">${alert.threat_score}</span></td>
|
||||
<td>${alert.events.join(' <strong>+</strong> ')}</td>
|
||||
<td class="${statusClass}">${icon} ${alert.status}</td>
|
||||
</tr>`;
|
||||
tableBody.innerHTML += row;
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
document.getElementById('alerts-table').innerHTML =
|
||||
'<tr><td colspan="4" style="text-align:center;">تعذر تحميل alerts.json — تأكد إنك شغّلت analyzer.py وإن الصفحة متفتحة عن طريق سيرفر (مش file:// مباشرة).</td></tr>';
|
||||
});
|
||||
}
|
||||
|
||||
loadAlerts();
|
||||
setInterval(loadAlerts, REFRESH_INTERVAL_MS);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
30
q4-siem/deployment-plan.md
Normal file
30
q4-siem/deployment-plan.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# SIEM Deployment Strategy on Ghaymah Cloud
|
||||
|
||||
## 0. اختبار محلي قبل النشر (Local Testing Note)
|
||||
`dashboard.html` بيستخدم `fetch()` لقراءة `alerts.json`، وده **مش هيشتغل لو فتحت الملف مباشرة بدبل كليك** (`file://`) بسبب سياسة الـ CORS في المتصفحات الحديثة. للتجربة محلياً قبل الرفع:
|
||||
```bash
|
||||
python3 analyzer.py # يولّد alerts.json
|
||||
python3 -m http.server 8000 # يشغّل الملفين على HTTP بدل file://
|
||||
```
|
||||
ثم فتح `http://localhost:8000/dashboard.html`. بعد النشر على غيمة، NGINX هيحل المشكلة دي تلقائياً لأنه بيقدّم الملفات عبر HTTP فعلياً (تفاصيل في القسم 3).
|
||||
|
||||
## 1. Storage Architecture (هيكلية التخزين)
|
||||
نظراً لأن سجلات النظام (Logs) تتضخم بسرعة، لا يجب تخزينها على القرص الأساسي للخادم (OS Disk).
|
||||
- **الخطوة:** سيتم إنشاء `Block Storage Volume` بحجم مناسب (مثلاً 500GB) من لوحة تحكم "غيمة".
|
||||
- **التجهيز:** سيتم ربط (Attach) البلوك بالسيرفر وعمل Mount له في مسار مخصص، وليكن `/var/log/ghaymah_siem/`.
|
||||
- **الميزة:** هذا يضمن عدم توقف السيرفر عن العمل إذا امتلأت مساحة السجلات (Disk Full)، ويسمح بأخذ نسخ احتياطية للـ Volume بشكل مستقل.
|
||||
- **التشفير:** يتم تفعيل تشفير الـ Block Storage نفسه (Encryption at Rest)، ويُفضّل أن تكون النسخ الاحتياطية للـ Volume غير قابلة للحذف (Immutable) لمدة محددة، لضمان بقاء الأدلة الجنائية (Forensic Evidence) سليمة حتى في حال اختراق السيرفر نفسه.
|
||||
|
||||
## 2. Automation & Execution (التشغيل التلقائي)
|
||||
- سيتم تشغيل `analyzer.py` كـ `Cron Job` يعمل كل دقيقة، مع تمرير مسارات ملفات الـ Logs الحقيقية على الـ Block Storage كـ arguments:
|
||||
```bash
|
||||
* * * * * /usr/bin/python3 /opt/siem/analyzer.py /var/log/ghaymah_siem/web.log /var/log/ghaymah_siem/auth.log /var/log/ghaymah_siem/db.log
|
||||
```
|
||||
- السكريبت بيحتفظ بموقع آخر سطر اتقرا لكل ملف في `analyzer_state.json`، فكل تشغيل بيعالج **الأسطر الجديدة بس** — ده مهم جداً لأن الـ Logs بتتزايد باستمرار، ولو كل تشغيل أعاد قراءة الملف كامل، الـ CPU هتتحمّل فوق طاقتها والـ Alerts هتتكرر لنفس الحدث.
|
||||
- السكريبت بيحدّث `alerts.json` بشكل دوري، والداشبورد بيعمل Auto-Refresh كل 30 ثانية.
|
||||
|
||||
## 3. Dashboard Hosting (استضافة الواجهة)
|
||||
- سيتم استخدام `NGINX` كـ Web Server خفيف لاستضافة `dashboard.html` و `alerts.json` (وده بيحل مشكلة الـ CORS في القسم 0 تلقائياً لأن الاستضافة بقت عبر HTTP وليس فتح ملف محلي).
|
||||
- سيتم توجيه NGINX لقراءة الواجهة وملف الـ JSON من المسار المعزول على الـ Block Storage.
|
||||
- سيتم تأمين مسار الداشبورد باستخدام Basic Authentication و HTTPS (شهادة Let's Encrypt) لضمان عدم وصول أي شخص غير مصرح له لبيانات التنبيهات الأمنية.
|
||||
- **تكامل مع Rate Limiting:** بما إن الداشبورد بيعرض بيانات حساسة عن هجمات فعلية، يُنصح بتطبيق نفس مبدأ Rate Limiting المستخدم في تأمين الـ API (راجع تقرير الـ Postmortem) على مسار تسجيل الدخول لصفحة الداشبورد نفسها.
|
||||
المرجع في مشكلة جديدة
حظر مستخدم