Initial commit: AegisSight-Monitor (OSINT-Monitoringsystem)

Dieser Commit ist enthalten in:
claude-dev
2026-03-04 17:53:18 +01:00
Commit 8312d24912
51 geänderte Dateien mit 19355 neuen und 0 gelöschten Zeilen

0
src/email_utils/__init__.py Normale Datei
Datei anzeigen

Datei anzeigen

@@ -0,0 +1,102 @@
"""In-Memory Rate-Limiting fuer Magic-Link-Anfragen und Code-Verifizierung."""
import time
from collections import defaultdict
class RateLimiter:
"""Rate-Limiter mit zwei Ebenen: pro E-Mail und pro IP."""
def __init__(
self,
max_per_email: int = 3,
email_window_seconds: int = 900, # 15 Minuten
max_per_ip: int = 10,
ip_window_seconds: int = 3600, # 1 Stunde
):
self.max_per_email = max_per_email
self.email_window = email_window_seconds
self.max_per_ip = max_per_ip
self.ip_window = ip_window_seconds
self._email_requests: dict[str, list[float]] = defaultdict(list)
self._ip_requests: dict[str, list[float]] = defaultdict(list)
def _clean(self, entries: list[float], window: int) -> list[float]:
cutoff = time.time() - window
return [t for t in entries if t > cutoff]
def check(self, email: str, ip: str) -> tuple[bool, str]:
"""Prueft ob die Anfrage erlaubt ist.
Returns:
(erlaubt, grund) - True wenn OK, False mit Grund wenn blockiert.
"""
now = time.time()
# E-Mail-Limit
self._email_requests[email] = self._clean(self._email_requests[email], self.email_window)
if len(self._email_requests[email]) >= self.max_per_email:
return False, "Zu viele Anfragen fuer diese E-Mail-Adresse. Bitte warten."
# IP-Limit
self._ip_requests[ip] = self._clean(self._ip_requests[ip], self.ip_window)
if len(self._ip_requests[ip]) >= self.max_per_ip:
return False, "Zu viele Anfragen von dieser IP-Adresse. Bitte warten."
return True, ""
def record(self, email: str, ip: str):
"""Zeichnet eine erfolgreiche Anfrage auf."""
now = time.time()
self._email_requests[email].append(now)
self._ip_requests[ip].append(now)
class VerifyCodeLimiter:
"""Rate-Limiter fuer Code-Verifizierung (Brute-Force-Schutz).
Zaehlt Fehlversuche pro E-Mail und pro IP.
Nach max_attempts wird gesperrt bis das Zeitfenster ablaeuft.
"""
def __init__(
self,
max_attempts_per_email: int = 5,
max_attempts_per_ip: int = 15,
window_seconds: int = 600, # 10 Minuten (= Magic-Link-Ablaufzeit)
):
self.max_per_email = max_attempts_per_email
self.max_per_ip = max_attempts_per_ip
self.window = window_seconds
self._email_failures: dict[str, list[float]] = defaultdict(list)
self._ip_failures: dict[str, list[float]] = defaultdict(list)
def _clean(self, entries: list[float]) -> list[float]:
cutoff = time.time() - self.window
return [t for t in entries if t > cutoff]
def check(self, email: str, ip: str) -> tuple[bool, str]:
"""Prueft ob ein Verifizierungsversuch erlaubt ist."""
self._email_failures[email] = self._clean(self._email_failures[email])
if len(self._email_failures[email]) >= self.max_per_email:
return False, "Zu viele Fehlversuche. Bitte neuen Code anfordern."
self._ip_failures[ip] = self._clean(self._ip_failures[ip])
if len(self._ip_failures[ip]) >= self.max_per_ip:
return False, "Zu viele Fehlversuche von dieser IP-Adresse."
return True, ""
def record_failure(self, email: str, ip: str):
"""Zeichnet einen fehlgeschlagenen Versuch auf."""
now = time.time()
self._email_failures[email].append(now)
self._ip_failures[ip].append(now)
def clear(self, email: str):
"""Loescht Zaehler nach erfolgreichem Login."""
self._email_failures.pop(email, None)
# Singleton-Instanzen
magic_link_limiter = RateLimiter()
verify_code_limiter = VerifyCodeLimiter()

54
src/email_utils/sender.py Normale Datei
Datei anzeigen

@@ -0,0 +1,54 @@
"""Async E-Mail-Versand via SMTP."""
import logging
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import aiosmtplib
from config import (
SMTP_HOST,
SMTP_PORT,
SMTP_USER,
SMTP_PASSWORD,
SMTP_FROM_EMAIL,
SMTP_FROM_NAME,
SMTP_USE_TLS,
)
logger = logging.getLogger("osint.email")
async def send_email(to_email: str, subject: str, html_body: str) -> bool:
"""Sendet eine HTML-E-Mail.
Returns:
True bei Erfolg, False bei Fehler.
"""
if not SMTP_HOST:
logger.warning(f"SMTP nicht konfiguriert - E-Mail an {to_email} nicht gesendet: {subject}")
return False
msg = MIMEMultipart("alternative")
msg["From"] = f"{SMTP_FROM_NAME} <{SMTP_FROM_EMAIL}>"
msg["To"] = to_email
msg["Subject"] = subject
# Text-Fallback (simpel)
text_content = f"Betreff: {subject}\n\nBitte oeffnen Sie diese E-Mail in einem HTML-faehigen E-Mail-Client."
msg.attach(MIMEText(text_content, "plain", "utf-8"))
msg.attach(MIMEText(html_body, "html", "utf-8"))
try:
await aiosmtplib.send(
msg,
hostname=SMTP_HOST,
port=SMTP_PORT,
username=SMTP_USER if SMTP_USER else None,
password=SMTP_PASSWORD if SMTP_PASSWORD else None,
start_tls=SMTP_USE_TLS,
)
logger.info(f"E-Mail gesendet an {to_email}: {subject}")
return True
except Exception as e:
logger.error(f"E-Mail-Versand fehlgeschlagen an {to_email}: {e}")
return False

138
src/email_utils/templates.py Normale Datei
Datei anzeigen

@@ -0,0 +1,138 @@
"""HTML-E-Mail-Vorlagen fuer Magic Links, Einladungen und Benachrichtigungen."""
def magic_link_login_email(username: str, code: str, link: str) -> tuple[str, str]:
"""Erzeugt Login-E-Mail mit Magic Link und Code.
Returns:
(subject, html_body)
"""
subject = f"AegisSight Monitor - Anmeldung"
html = f"""<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"></head>
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f172a; color: #e2e8f0; padding: 40px 20px;">
<div style="max-width: 480px; margin: 0 auto; background: #1e293b; border-radius: 12px; padding: 32px; border: 1px solid #334155;">
<h1 style="color: #f0b429; font-size: 20px; margin: 0 0 24px 0;">AegisSight Monitor</h1>
<p style="margin: 0 0 16px 0;">Hallo {username},</p>
<p style="margin: 0 0 24px 0;">Klicken Sie auf den Link oder geben Sie den Code ein, um sich anzumelden:</p>
<div style="background: #0f172a; border-radius: 8px; padding: 20px; text-align: center; margin: 0 0 24px 0;">
<div style="font-size: 32px; font-weight: 700; letter-spacing: 8px; color: #f0b429; font-family: monospace;">{code}</div>
</div>
<div style="text-align: center; margin: 0 0 24px 0;">
<a href="{link}" style="display: inline-block; background: #f0b429; color: #0f172a; padding: 12px 32px; border-radius: 6px; text-decoration: none; font-weight: 600;">Jetzt anmelden</a>
</div>
<p style="color: #94a3b8; font-size: 13px; margin: 0;">Dieser Link ist 10 Minuten gueltig. Falls Sie diese Anmeldung nicht angefordert haben, ignorieren Sie diese E-Mail.</p>
</div>
</body>
</html>"""
return subject, html
def invite_email(username: str, org_name: str, code: str, link: str) -> tuple[str, str]:
"""Erzeugt Einladungs-E-Mail fuer neue Nutzer.
Returns:
(subject, html_body)
"""
subject = f"Einladung zum AegisSight Monitor - {org_name}"
html = f"""<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"></head>
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f172a; color: #e2e8f0; padding: 40px 20px;">
<div style="max-width: 480px; margin: 0 auto; background: #1e293b; border-radius: 12px; padding: 32px; border: 1px solid #334155;">
<h1 style="color: #f0b429; font-size: 20px; margin: 0 0 24px 0;">AegisSight Monitor</h1>
<p style="margin: 0 0 16px 0;">Hallo {username},</p>
<p style="margin: 0 0 16px 0;">Sie wurden zur Organisation <strong>{org_name}</strong> im AegisSight Monitor eingeladen.</p>
<p style="margin: 0 0 24px 0;">Klicken Sie auf den Link, um Ihren Zugang zu aktivieren:</p>
<div style="background: #0f172a; border-radius: 8px; padding: 20px; text-align: center; margin: 0 0 24px 0;">
<div style="font-size: 32px; font-weight: 700; letter-spacing: 8px; color: #f0b429; font-family: monospace;">{code}</div>
</div>
<div style="text-align: center; margin: 0 0 24px 0;">
<a href="{link}" style="display: inline-block; background: #f0b429; color: #0f172a; padding: 12px 32px; border-radius: 6px; text-decoration: none; font-weight: 600;">Einladung annehmen</a>
</div>
<p style="color: #94a3b8; font-size: 13px; margin: 0;">Dieser Link ist 48 Stunden gueltig.</p>
</div>
</body>
</html>"""
return subject, html
def incident_notification_email(
username: str,
incident_title: str,
notifications: list[dict],
dashboard_url: str,
) -> tuple[str, str]:
"""Erzeugt Benachrichtigungs-E-Mail fuer Lagen-Updates.
Args:
username: Empfaenger-Name
incident_title: Titel der Lage/Recherche
notifications: Liste von {"text": ..., "icon": ...} Dicts
dashboard_url: Link zum Dashboard
Returns:
(subject, html_body)
"""
subject = f"AegisSight - {incident_title}"
icon_map = {
"success": "&#10003;", # Haekchen
"warning": "&#9888;", # Warndreieck
"error": "&#10007;", # Kreuz
"info": "&#9432;", # Info-Kreis
}
color_map = {
"success": "#22c55e",
"warning": "#f0b429",
"error": "#ef4444",
"info": "#94a3b8",
}
items_html = ""
for n in notifications:
icon = icon_map.get(n.get("icon", "info"), "&#9432;")
color = color_map.get(n.get("icon", "info"), "#94a3b8")
text = n.get("text", "")
items_html += f"""
<div style="display: flex; align-items: flex-start; gap: 10px; padding: 10px 0; border-bottom: 1px solid #334155;">
<span style="color: {color}; font-size: 18px; line-height: 1;">{icon}</span>
<span style="color: #e2e8f0; font-size: 14px; line-height: 1.4;">{text}</span>
</div>"""
html = f"""<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"></head>
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f172a; color: #e2e8f0; padding: 40px 20px;">
<div style="max-width: 480px; margin: 0 auto; background: #1e293b; border-radius: 12px; padding: 32px; border: 1px solid #334155;">
<h1 style="color: #f0b429; font-size: 20px; margin: 0 0 8px 0;">AegisSight Monitor</h1>
<p style="color: #94a3b8; font-size: 12px; margin: 0 0 24px 0;">Lagebericht-Benachrichtigung</p>
<p style="margin: 0 0 8px 0;">Hallo {username},</p>
<p style="margin: 0 0 20px 0;">es gibt Neuigkeiten zur Lage <strong style="color: #f0b429;">{incident_title}</strong>:</p>
<div style="background: #0f172a; border-radius: 8px; padding: 4px 16px; margin: 0 0 24px 0;">
{items_html}
</div>
<div style="text-align: center; margin: 0 0 24px 0;">
<a href="{dashboard_url}" style="display: inline-block; background: #f0b429; color: #0f172a; padding: 12px 32px; border-radius: 6px; text-decoration: none; font-weight: 600;">Im Dashboard ansehen</a>
</div>
<p style="color: #64748b; font-size: 12px; margin: 0;">Diese Benachrichtigung kann in den Einstellungen im Dashboard deaktiviert werden.</p>
</div>
</body>
</html>"""
return subject, html