feat(phases): implement webhooks (6.1), email (7.1), user preferences (7.2)
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
- Phase 6.1: WebhookConfig model, CRUD router (/api/v1/webhooks, admin-only), dispatch_webhook() with HMAC signing; integrated into test validation, campaign completion, and MITRE sync job - Phase 7.1: SMTP email service with send_test_validated_email, send_campaign_completed_email, send_new_mitre_techniques_email; notify_role_with_email() added to notification_service - Phase 7.2: notification_preferences and jira_account_id on User model; PATCH /users/me/preferences endpoint; Alembic migrations b031phase6 and b032phase7 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
79
backend/app/services/email_service.py
Normal file
79
backend/app/services/email_service.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"""Email notification service using SMTP.
|
||||
|
||||
Sending is silently skipped when SMTP_ENABLED=False (default).
|
||||
All errors are caught and logged — email failures never crash the caller.
|
||||
"""
|
||||
import logging
|
||||
import smtplib
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
from app.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def send_email(to: str, subject: str, html_body: str) -> bool:
|
||||
"""Send an HTML email. Returns True on success, False on skip/error."""
|
||||
if not settings.SMTP_ENABLED:
|
||||
logger.debug("SMTP disabled — skipping email to %s: %s", to, subject)
|
||||
return False
|
||||
if not to:
|
||||
return False
|
||||
try:
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg["Subject"] = f"[Aegis] {subject}"
|
||||
msg["From"] = settings.SMTP_FROM_EMAIL
|
||||
msg["To"] = to
|
||||
msg.attach(MIMEText(html_body, "html"))
|
||||
with smtplib.SMTP(settings.SMTP_HOST, settings.SMTP_PORT, timeout=10) as server:
|
||||
if settings.SMTP_USE_TLS:
|
||||
server.starttls()
|
||||
if settings.SMTP_USERNAME:
|
||||
server.login(settings.SMTP_USERNAME, settings.SMTP_PASSWORD)
|
||||
server.send_message(msg)
|
||||
logger.info("Email sent to %s: %s", to, subject)
|
||||
return True
|
||||
except Exception:
|
||||
logger.exception("Failed to send email to %s: %s", to, subject)
|
||||
return False
|
||||
|
||||
|
||||
def send_test_validated_email(to: str, test_name: str, technique_id: str, test_id: str) -> bool:
|
||||
"""Notify that a test was validated."""
|
||||
url = f"{settings.PLATFORM_URL}/tests/{test_id}"
|
||||
html = f"""
|
||||
<html><body style="font-family:sans-serif;color:#1a1a2e">
|
||||
<h2 style="color:#22d3ee">✅ Test Validated</h2>
|
||||
<p>Test <strong>{test_name}</strong> for technique <code>{technique_id}</code> has been validated.</p>
|
||||
<p><a href="{url}" style="background:#22d3ee;color:#000;padding:8px 16px;border-radius:4px;text-decoration:none">View Test</a></p>
|
||||
<p style="color:#666;font-size:12px">Aegis ATT&CK Coverage Platform</p>
|
||||
</body></html>"""
|
||||
return send_email(to, f"Test Validated: {test_name}", html)
|
||||
|
||||
|
||||
def send_campaign_completed_email(to: str, campaign_name: str, campaign_id: str) -> bool:
|
||||
"""Notify that a campaign was completed."""
|
||||
url = f"{settings.PLATFORM_URL}/campaigns/{campaign_id}"
|
||||
html = f"""
|
||||
<html><body style="font-family:sans-serif;color:#1a1a2e">
|
||||
<h2 style="color:#22d3ee">🎯 Campaign Completed</h2>
|
||||
<p>Campaign <strong>{campaign_name}</strong> has been completed.</p>
|
||||
<p><a href="{url}" style="background:#22d3ee;color:#000;padding:8px 16px;border-radius:4px;text-decoration:none">View Campaign</a></p>
|
||||
<p style="color:#666;font-size:12px">Aegis ATT&CK Coverage Platform</p>
|
||||
</body></html>"""
|
||||
return send_email(to, f"Campaign Completed: {campaign_name}", html)
|
||||
|
||||
|
||||
def send_new_mitre_techniques_email(to: str, created: int, updated: int) -> bool:
|
||||
"""Notify of new MITRE techniques after sync."""
|
||||
if created == 0:
|
||||
return False
|
||||
html = f"""
|
||||
<html><body style="font-family:sans-serif;color:#1a1a2e">
|
||||
<h2 style="color:#22d3ee">🔄 MITRE ATT&CK Updated</h2>
|
||||
<p><strong>{created}</strong> new techniques added, <strong>{updated}</strong> updated.</p>
|
||||
<p><a href="{settings.PLATFORM_URL}/techniques" style="background:#22d3ee;color:#000;padding:8px 16px;border-radius:4px;text-decoration:none">View Techniques</a></p>
|
||||
<p style="color:#666;font-size:12px">Aegis ATT&CK Coverage Platform</p>
|
||||
</body></html>"""
|
||||
return send_email(to, f"MITRE ATT&CK Updated: {created} new techniques", html)
|
||||
Reference in New Issue
Block a user