{"slug": "llm-powered-siem-alert-triage-reduce-noise-by-80", "title": "LLM-Powered SIEM Alert Triage: Reduce Noise by 80%", "summary": "A developer built a Python pipeline that uses an LLM to triage SIEM alerts, reducing noise by up to 80%. The system enriches alerts with threat intel context and outputs a structured verdict with severity, disposition, and recommended actions.", "body_md": "Your SIEM fires 3,000 alerts on a Tuesday night. Your on-call analyst acknowledges 40 of them. The other 2,960? Noise — mostly. This is the alert fatigue problem, and it gets worse as environments scale. A language model can automate the first-pass triage that currently eats analyst hours, and it can do it with enough context to be genuinely useful rather than adding yet another filter layer.\n\nThis post walks through a working Python implementation that feeds SIEM alerts to an LLM, enriches them with threat intel context, and outputs a prioritized triage report.\n\nMost SIEM rules are threshold-based: if more than N failed logins happen in M minutes from the same IP, fire an alert. This works for known attack patterns but produces two failure modes at scale:\n\n`Failed SSH login from 185.220.101.47`\n\n. It doesn't tell you that this IP is a known Tor exit node, that the targeted user has admin privileges, and that this is the third attempt in 6 hours from that subnet.A language model can synthesize these facts and produce a verdict. It won't replace a human for complex incidents, but it handles the 80% that are obviously benign or obviously high-priority.\n\nThe pipeline has four steps:\n\nWe'll use Python with the `openai`\n\nSDK (works with any OpenAI-compatible endpoint, including local models), and the output schema is enforced via structured outputs so downstream code doesn't have to parse free text.\n\nBefore touching the LLM, build good context. Garbage in, garbage out.\n\n``` python\nimport json\nimport ipaddress\nfrom dataclasses import dataclass, asdict\nfrom typing import Optional\n\n@dataclass\nclass AlertContext:\n    raw_alert: dict\n    user_role: Optional[str]\n    user_dept: Optional[str]\n    asset_criticality: str  # \"low\", \"medium\", \"high\", \"critical\"\n    ip_reputation: Optional[str]\n    related_alerts_24h: int\n    is_business_hours: bool\n\ndef enrich_alert(alert: dict, user_db: dict, asset_db: dict) -> AlertContext:\n    user = user_db.get(alert.get(\"username\", \"\"), {})\n    asset = asset_db.get(alert.get(\"hostname\", \"\"), {})\n\n    src_ip = alert.get(\"src_ip\", \"\")\n    ip_rep = None\n    try:\n        addr = ipaddress.ip_address(src_ip)\n        if addr.is_private:\n            ip_rep = \"internal\"\n        else:\n            # In production: call AbuseIPDB, VirusTotal, Shodan, etc.\n            ip_rep = \"unknown_external\"\n    except ValueError:\n        pass\n\n    return AlertContext(\n        raw_alert=alert,\n        user_role=user.get(\"role\"),\n        user_dept=user.get(\"department\"),\n        asset_criticality=asset.get(\"criticality\", \"medium\"),\n        ip_reputation=ip_rep,\n        related_alerts_24h=count_related_alerts(alert, window_hours=24),\n        is_business_hours=is_business_hours(alert.get(\"timestamp\")),\n    )\n\ndef count_related_alerts(alert: dict, window_hours: int) -> int:\n    # Query your SIEM or local alert cache here\n    return 0\n\ndef is_business_hours(ts: Optional[str]) -> bool:\n    if not ts:\n        return False\n    from datetime import datetime, timezone\n    dt = datetime.fromisoformat(ts).astimezone(timezone.utc)\n    return 8 <= dt.hour < 18 and dt.weekday() < 5\n```\n\nThe `AlertContext`\n\nobject gives the model everything it needs without dumping a 10 KB log blob into the prompt.\n\nNow the core piece: sending the enriched alert to a language model and getting a structured verdict back.\n\n``` python\nfrom openai import OpenAI\nfrom pydantic import BaseModel\nfrom enum import Enum\n\nclass Severity(str, Enum):\n    CRITICAL = \"critical\"\n    HIGH = \"high\"\n    MEDIUM = \"medium\"\n    LOW = \"low\"\n    INFO = \"informational\"\n\nclass Disposition(str, Enum):\n    ESCALATE = \"escalate\"\n    MONITOR = \"monitor\"\n    CLOSE = \"close\"\n\nclass TriageResult(BaseModel):\n    severity: Severity\n    disposition: Disposition\n    confidence: float          # 0.0 to 1.0\n    reasoning: str             # one concise paragraph\n    recommended_actions: list[str]\n    false_positive_indicators: list[str]\n\nTRIAGE_SYSTEM_PROMPT = \"\"\"You are a senior SOC analyst performing first-pass alert triage.\nGiven an enriched security alert, output a structured triage verdict.\n\nRules:\n- If asset_criticality is \"critical\", escalate unless you have strong FP evidence.\n- Off-hours activity on privileged accounts increases severity by one level.\n- Known internal IPs with a single failed login are almost always false positives.\n- confidence reflects certainty, not severity.\n- reasoning: one paragraph, max 80 words, no bullet points.\n- recommended_actions: max 3 concrete next steps for an analyst.\n- false_positive_indicators: specific facts suggesting benign activity.\"\"\"\n\ndef triage_alert(ctx: AlertContext, client: OpenAI, model: str = \"gpt-4o-mini\") -> TriageResult:\n    payload = json.dumps(asdict(ctx), indent=2, default=str)\n\n    response = client.beta.chat.completions.parse(\n        model=model,\n        messages=[\n            {\"role\": \"system\", \"content\": TRIAGE_SYSTEM_PROMPT},\n            {\"role\": \"user\", \"content\": f\"Triage this alert:\\n\\n{payload}\"}\n        ],\n        response_format=TriageResult,\n        temperature=0.1,\n    )\n\n    return response.choices[0].message.parsed\n\n# Point at a local model to avoid sending alert data externally\nclient = OpenAI(base_url=\"http://localhost:11434/v1\", api_key=\"ollama\")\n\nsample_alert = {\n    \"alert_id\": \"A-2026-04421\",\n    \"rule\": \"Multiple Failed SSH Logins\",\n    \"username\": \"j.martin\",\n    \"hostname\": \"prod-db-01\",\n    \"src_ip\": \"185.220.101.47\",\n    \"timestamp\": \"2026-07-28T02:14:33+00:00\",\n    \"failed_attempts\": 12,\n}\n\nuser_db = {\"j.martin\": {\"role\": \"DBA\", \"department\": \"Engineering\"}}\nasset_db = {\"prod-db-01\": {\"criticality\": \"critical\"}}\n\nctx = enrich_alert(sample_alert, user_db, asset_db)\nresult = triage_alert(ctx, client)\n\nprint(f\"[{result.severity.upper()}] → {result.disposition}\")\nprint(f\"Confidence: {result.confidence:.0%}\")\nprint(f\"Reasoning: {result.reasoning}\")\nfor action in result.recommended_actions:\n    print(f\"  • {action}\")\n```\n\nFor this sample alert — Tor exit node, DBA account, production database, 2 AM — a properly prompted model outputs `severity=critical, disposition=escalate`\n\nwith clear reasoning about the combination of off-hours timing, privileged account, and known malicious IP source.\n\nIndividual triage is useful; batch processing is where it delivers ROI. Process the overnight alert queue before the morning shift arrives:\n\n``` python\nimport asyncio\n\nasync def batch_triage(\n    alerts: list[dict],\n    client: OpenAI,\n    user_db: dict,\n    asset_db: dict,\n    concurrency: int = 5,\n) -> list[tuple[dict, TriageResult]]:\n    semaphore = asyncio.Semaphore(concurrency)\n\n    async def triage_one(alert: dict) -> tuple[dict, TriageResult]:\n        async with semaphore:\n            ctx = enrich_alert(alert, user_db, asset_db)\n            loop = asyncio.get_event_loop()\n            result = await loop.run_in_executor(None, triage_alert, ctx, client)\n            return alert, result\n\n    return await asyncio.gather(*[triage_one(a) for a in alerts])\n\nasync def main():\n    alerts = load_alerts_from_siem()  # Your SIEM API or export\n    results = await batch_triage(alerts, client, user_db, asset_db)\n\n    to_escalate = [(a, r) for a, r in results if r.disposition == Disposition.ESCALATE]\n    to_close    = [(a, r) for a, r in results if r.disposition == Disposition.CLOSE]\n\n    print(f\"Total:      {len(alerts)}\")\n    print(f\"Escalate:   {len(to_escalate)} ({len(to_escalate)/len(alerts):.0%})\")\n    print(f\"Auto-close: {len(to_close)} ({len(to_close)/len(alerts):.0%})\")\n\n    for alert, result in to_escalate:\n        create_ticket(alert, result)\n\n    for alert, result in to_close:\n        auto_close_alert(alert, result)\n\nasyncio.run(main())\n```\n\nThe `concurrency=5`\n\ncap prevents rate limiting on the LLM API. Tune it based on your provider's limits or the hardware running your local model.\n\nDon't deploy this without a feedback loop. Track three numbers:\n\nFeed disagreements back into your system prompt as concrete examples. After two weeks of tuning on your environment's specific noise profile, 80% auto-close rates with under 1% false negative rate are realistic — this matches results from teams running similar pipelines in production.\n\nFor a baseline of what \"good\" triage looks like per alert category, the [security hardening checklists](https://ayinedjimi-consultants.fr/checklists) we publish include SOC triage playbooks you can adapt directly as prompt templates.\n\nLLM-powered alert triage is one of the highest-ROI applications of language models in security operations right now. The reason is straightforward: alert triage is fundamentally a classification and reasoning task over structured context, which is exactly what these models do well.\n\nThe implementation here runs entirely on a local model if you can't send alert data to an external API — just point `base_url`\n\nat your Ollama or vLLM endpoint. Start with a pilot on a single rule category like failed logins, measure results for two weeks, then expand.\n\nThe hard part isn't the code. It's building the enrichment layer with meaningful context, writing a system prompt that reflects your actual triage playbooks, and creating the feedback loop that catches model mistakes before they become missed incidents.\n\n*I run AYI NEDJIMI Consultants, a cybersecurity consulting firm. We publish free security hardening checklists — PDF and Excel.*", "url": "https://wpnews.pro/news/llm-powered-siem-alert-triage-reduce-noise-by-80", "canonical_source": "https://dev.to/ayinedjimi-consultants/llm-powered-siem-alert-triage-reduce-noise-by-80-22h7", "published_at": "2026-07-28 10:02:46+00:00", "updated_at": "2026-07-28 10:36:24.148659+00:00", "lang": "en", "topics": ["artificial-intelligence", "large-language-models", "ai-agents", "developer-tools"], "entities": ["OpenAI", "AbuseIPDB", "VirusTotal", "Shodan"], "alternates": {"html": "https://wpnews.pro/news/llm-powered-siem-alert-triage-reduce-noise-by-80", "markdown": "https://wpnews.pro/news/llm-powered-siem-alert-triage-reduce-noise-by-80.md", "text": "https://wpnews.pro/news/llm-powered-siem-alert-triage-reduce-noise-by-80.txt", "jsonld": "https://wpnews.pro/news/llm-powered-siem-alert-triage-reduce-noise-by-80.jsonld"}}